2

My goal is to get "clientId" asynchronously from the AppConfigService and use it as the "GoogleLoginProvider" function's "clientId" before the app starts.

I could put it in an environment variable but, in my specific case, it is not an option.

I'm using Angular 8.

import { APP_INITIALIZER } from '@angular/core';

export function getGoogleClientId(appConfigService: AppConfigService) {
    return () => appConfigService.getGoogleClientId().toPromise().then((clientId) => {
        // service reliably returns clientId here
    });
}

const getGoogleId: Provider = {
    provide: APP_INITIALIZER,
    useFactory: getGoogleClientId,
    deps: [AppConfigService],
    multi: true
}

@NgModule({
    providers: [
        {
            provide: 'SocialAuthServiceConfig',
            useValue: {
                autoLogin: false,
                providers: [{
                    id: GoogleLoginProvider.PROVIDER_ID,
                    provider: new GoogleLoginProvider(clientId), //<-- How do I get the service's "clientId" here?
                }],
            } as SocialAuthServiceConfig
        }
    ]
})
export class AppModule {}
Aaron Salazar
  • 4,467
  • 10
  • 39
  • 54
  • Hello, can [this solution](https://stackoverflow.com/a/57801277/8678900) help you ? – sohaieb azaiez Mar 09 '21 at 19:02
  • @sohaieb I think I'm already using this solution to get the clientId. My question is, once I have the clientId, how do I put it into the GoogleLoginProvider? – Aaron Salazar Mar 09 '21 at 19:10
  • what about this source: [Dynamic Dependency Injection](https://www.damirscorner.com/blog/posts/20170526-DynamicDependencyInjectionInAngular.html)? In this exeplanation you can inject http request for your clientId and get the result from the **apiFactory** method.. – sohaieb azaiez Mar 09 '21 at 19:18
  • @sohaieb I think this explanation puts me in the same situation. The explanation shows how to dynamically add classes and not providers themselves or portions of the provider. If you look at my code you'll see I'm not adding classes. I need to add an id a provider before it is injected or before the app is bootstrapped. – Aaron Salazar Mar 09 '21 at 21:15

2 Answers2

2

Your problem is that you're using useValue to inject an object but you need to use useFactory to create a changeable, dependent value based on information unavailable before run time which allows dependencies (API service, config service, etc).

Then I suggest modifying the library you're using at this moment (angularx-social-login) to allow the behavior that you want.

However, I was reading the library code and I realized that they accept an object and a promise!

You can check It here

Hence, I create an example to handle promise and fetch our config from our server (API).

app.module.ts

export function AppConfigServiceFactory(
  configService: AppConfigService
): () => void {
  return async () => await configService.load();
}

@NgModule({
  imports: [BrowserModule, FormsModule, SocialLoginModule, HttpClientModule],
  declarations: [AppComponent, HelloComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: AppConfigServiceFactory,
      deps: [AppConfigService],
      multi: true
    },
    {
      provide: "SocialAuthServiceConfig",
      useValue: new Promise(async resolve => {
        // await until the app config service is loaded
        const config = await AppConfigService.configFetched();

        resolve({
          autoLogin: false,
          providers: [
            {
              id: GoogleLoginProvider.PROVIDER_ID,
              provider: new GoogleLoginProvider(config.googleClientId)
            }
          ]
        } as SocialAuthServiceConfig);
      })
    }
  ]
})
export class AppModule {}


app.config.service

export class AppConfigService {
  static config: AppConfig | null = null;

  constructor(private api: ApiService) {}

  static configFetched(): Promise<AppConfig> {
    return new Promise(async resolve => {
      // wait for the app config service is loaded (after 3000 ms)
      const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
      const waitFor = async function waitFor(f) {
        // check each 500 ms
        while (!f()) await sleep(500);
        return f();
      };
      await waitFor(() => AppConfigService?.config); 

      resolve(AppConfigService.config);
    });
  }

  async load(): Promise<AppConfig> {
    try {
      // simulating HTTP request to obtain my config
      const promise = new Promise<AppConfig>(resolve => {

        // after 3000 ms our config will be available
        setTimeout(async () => {
          const config: AppConfig = await this.api.getConfig().toPromise();
          AppConfigService.config = config;
          resolve(config);
        }, 3000);

      }).then(config => config);

      return promise;
    } catch (error) {
      throw error;
    }
  }
}

My complete solution is here on stackblitz.

bjdose
  • 1,269
  • 8
  • 10
  • I thought this was working but it gives this error, "Missing required parameter 'client_id'" but only when the app first starts up with nothing in local storage. As far as I can tell, the "provide: "SocialAuthServiceConfig"" line runs before "AppConfigServiceFactory" returns. This also happens in your stackblitz code. – Aaron Salazar Mar 16 '21 at 20:10
  • I see, but the same way I explained. This library is using `useValue` to provide the object with the google key id. The correct way to achieve what you want is using a factory to provide the value. We can try to use another way to handle the asynchronous problem that we're facing. But can be a little bit awful solution. – bjdose Mar 17 '21 at 16:45
  • However, I edited and change the behavior to use `useValue` and handle the asynchronous problem we had. – bjdose Mar 17 '21 at 17:17
  • 1
    I tried to use the useFactory but I couldn't quite get it to work. I see what you mean by it being a bit awful but maybe it'll work in our situation. – Aaron Salazar Mar 18 '21 at 19:51
  • I just implemented your solution and I think it is working. Thank you! – Aaron Salazar Mar 18 '21 at 21:09
  • Is there no simpler way to achieve this? Hundreds of lines of additional code along with root project files seems messy – Ste Oct 31 '22 at 14:13
0

i developed another solucion using @bjdose answer

as angularx-social-login can receive a Promise<SocialAuthServiceConfig>

However, I was reading the library code and I realized that they accept an object and a promise!

make a function that returns a Promise with the config, first get the necessary data from your backend in my case the client id string and then return the config object

/**
 * return a Promise with *angularx-social-login* config object
 */
async function getGoogleLoginConfig(): Promise<SocialAuthServiceConfig> {
  const clientId = await fetchGoogleClientId()
  return Promise.resolve(
    {
      autoLogin: true,
      providers: [
        {
          id: GoogleLoginProvider.PROVIDER_ID,
          provider: new GoogleLoginProvider(clientId)
        }
      ],
      onError: (err) => {
        console.error(err);
      }
    } as SocialAuthServiceConfig)
}
/**
 * get google client id from backend
 */
async function fetchGoogleClientId(): Promise<string> {
  let url = `${environment.auth.googleId}`
  let response = await fetch(url,
    {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    })
  let data = await response.json()
  //change here to your backend response structure
  return data.client_id
}

lastly add angularx-social-login to your providers

  providers: [
    {
      provide: "SocialAuthServiceConfig",
      useValue: getGoogleLoginConfig()
    }
  ]