[Fixed] How to get provider data before Angular application startup

Issue

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 {}

Solution

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.

Leave a Reply

(*) Required, Your email will not be published