angularfire icon indicating copy to clipboard operation
angularfire copied to clipboard

AngularFire does not work on ios with ionic capacitor 3

Open moblizeitllc opened this issue 3 years ago • 49 comments

Version info

Angular: "@angular/common": "~12.1.1", "@angular/core": "~12.1.1",

"@angular/forms": "~12.1.1",
"@angular/platform-browser": "~12.1.1",
"@angular/platform-browser-dynamic": "~12.1.1",
"@angular/router": "~12.1.1",
"@capacitor/android": "^3.3.2",
"@capacitor/app": "^1.0.6",
"@capacitor/core": "3.3.2",
"@capacitor/ios": "^3.3.2",
"@ionic/angular": "^5.5.2",

"rxjs": "~6.6.0",
"tslib": "^2.2.0",
"xcode": "^3.0.1",
"xml-js": "^1.6.11",
"zone.js": "~0.11.4"

Firebase: "firebase": "^9.5.0", AngularFire: "@angular/fire": "^7.2.0",

Other (e.g. Ionic/Cordova, Node, browser, operating system): ionic info

Ionic:

Ionic CLI : 6.18.1 (/usr/local/lib/node_modules/@ionic/cli) Ionic Framework : @ionic/angular 5.9.1 @angular-devkit/build-angular : 12.1.4 @angular-devkit/schematics : 12.1.4 @angular/cli : 12.1.4 @ionic/angular-toolkit : 4.0.0

Capacitor:

Capacitor CLI : 3.3.2 @capacitor/android : 3.3.2 @capacitor/core : 3.3.2 @capacitor/ios : 3.3.2

Cordova:

Cordova CLI : 10.0.0 ([email protected]) Cordova Platforms : android broken, ios 5.1.1 Cordova Plugins : no whitelisted plugins (0 plugins total)

Utility:

cordova-res (update available: 0.15.4) : 0.15.3 native-run : 1.5.0

System:

ios-deploy : 1.9.4 ios-sim : ios-sim/9.0.0 darwin-x64 node-v14.17.0 NodeJS : v14.17.0 (/usr/local/bin/node) npm : 7.24.0 OS : macOS Monterey Xcode : Xcode 13.1 Build version 13A1030d

How to reproduce these conditions

Failing test unit, Stackblitz demonstrating the problem

Steps to set up and reproduce ionic build ionic cap copy ionic cap sync open xcode and either run on device or simulator

Sample data and security rules

Debug output

2021-12-08 13:25:36.266937-0600 App[2469:682932] KeyboardPlugin: resize mode - native 2021-12-08 13:25:36.280309-0600 App[2469:682932] InAppPurchase[objc] Initialized. ⚡️ Loading app at capacitor://localhost... ⚡️ [log] - onscript loading complete ⚡️ To Native -> Device getId 109505367 ⚡️ [log] - Ionic Native: deviceready event fired after 48 ms ⚡️ TO JS {"uuid":"14A77728-365E-4EDC-8F78-7CD1F6CC8CA0"} To Native Cordova -> AppVersion getVersionNumber AppVersion678940142 ["options": []] ⚡️ WebView loaded SplashScreen.hideSplash: SplashScreen was automatically hidden after default timeout. You should call SplashScreen.hide() as soon as your web app is loaded (or increase the timeout). Read more at https://capacitorjs.com/docs/apis/splash-screen#hiding-the-splash-screen ⚡️ To Native -> App addListener 109505368

** Errors in the JavaScript console ** above pasted nothing else

** Output from firebase.database().enableLogging(true); **

** Screenshots **

Expected behavior

should run fine as it runs on android

Actual behavior

does not run on ios but run on android

moblizeitllc avatar Dec 08 '21 19:12 moblizeitllc

some new observations. i can actually run on ios using

ionic capacitor run ios -l --external

but not directly from xcode. so there is something that xcode is doing while running it

moblizeitllc avatar Dec 09 '21 03:12 moblizeitllc

Same here. I also tried with modular. But issue still happens

tcns avatar Dec 09 '21 17:12 tcns

I got rid of angular/fire. And on vanilla firebase and rxfire works perfectly

"rxfire": "^6.0.3", "firebase": "^9.6.0"

tcns avatar Dec 09 '21 18:12 tcns

do u have code sample/example on how ur appmodule.ts and a sample call to firebase looks like?

moblizeitllc avatar Dec 09 '21 18:12 moblizeitllc

I was also having this issue when building for an iOS emulator and actual iOS device, but adding this to the app.component.ts worked for me.

import { Component } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { initializeApp } from 'firebase/app';
import { indexedDBLocalPersistence, initializeAuth } from 'firebase/auth';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor() {
    const app = initializeApp(environment.firebase);
    if (Capacitor.isNativePlatform) {
      initializeAuth(app, {
        persistence: indexedDBLocalPersistence
      });
    }
  }
}

This is where I found the fix and a sample code https://github.com/firebase/firebase-js-sdk/issues/5552#issuecomment-929580662

Original Issue https://forum.ionicframework.com/t/firebase-auth-in-sdk-9-does-not-work-on-ios-sim-or-devices/215362/9

ChristianGerardHizon avatar Dec 10 '21 04:12 ChristianGerardHizon

but i am not using auth module. does this still apply?

moblizeitllc avatar Dec 10 '21 06:12 moblizeitllc

I was also having this issue when building for an iOS emulator and actual iOS device, but adding this to the app.component.ts worked for me.

import { Component } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { initializeApp } from 'firebase/app';
import { indexedDBLocalPersistence, initializeAuth } from 'firebase/auth';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor() {
    const app = initializeApp(environment.firebase);
    if (Capacitor.isNativePlatform) {
      initializeAuth(app, {
        persistence: indexedDBLocalPersistence
      });
    }
  }
}

This is where I found the fix and a sample code firebase/firebase-js-sdk#5552 (comment)

Original Issue https://forum.ionicframework.com/t/firebase-auth-in-sdk-9-does-not-work-on-ios-sim-or-devices/215362/9

This did the trick for me, thanks

gianmd avatar Dec 10 '21 20:12 gianmd

as i am initilaizing the firebase in app.module.ts so curious how does your app.module.ts looks like?

moblizeit avatar Dec 11 '21 05:12 moblizeit

@moblizeit Sorry for late answer. Here app.module.ts. I just deleted all firebase initializers and providers.

@NgModule({
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    SwiperModule,
    IonicModule.forRoot(),
    IonicStorageModule.forRoot(),
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: environment.production
    })
  ],
  declarations: [AppComponent],
  providers: [InAppBrowser, StatusBar, Broadcaster],
  exports: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Instead of this I created small service

import {Injectable, OnInit} from '@angular/core';
import {environment} from '../../environments/environment';
import 'firebase/firestore';
import 'firebase/database';
import { initializeApp } from 'firebase/app';
import { initializeAuth, indexedDBLocalPersistence, Auth, browserLocalPersistence } from 'firebase/auth';
import { getDatabase, Database } from '@firebase/database';
import { FirebaseApp } from '@firebase/app';

@Injectable({
  providedIn: 'root'
})
export class FirebasecustomService implements OnInit {
  app: FirebaseApp;
  database: Database;
  auth: Auth;
  constructor() {
    this.app = initializeApp(environment.firebaseConfig);
    this.auth = initializeAuth(this.app, {
      persistence: indexedDBLocalPersistence
    });
    this.database = getDatabase(this.app);
  }

  ngOnInit(): void {

  }
}

And just use vanilla rxfire. Like this.

import {BehaviorSubject, forkJoin, from, Observable, of, pipe, Subject} from 'rxjs';
import {catchError, first, map, mergeMap, switchMap, tap} from 'rxjs/operators';
import {User} from '@firebase/auth';
import {Database, ref, set, remove} from 'firebase/database';
import {objectVal} from 'rxfire/database';

...

  constructor(
    public storage: Storage,
    public firebaseCustom: FirebasecustomService
  ) {
    this.db = firebaseCustom.database;
  }

...

  loadStories(): Observable<Story[]> {
    return objectVal<Story[]>(ref(this.db, '/stories'))
        .pipe(
            mergeMap(val => of(Object.values(val)))
        );
  }

I dont know if its normal code. Because I'm not js dev. Just my pet project. But its working both on sim and real device

tcns avatar Dec 17 '21 14:12 tcns

I have the same error. Launching angular fire app on real iOS device I get this error:

TypeError: undefined is not an object (evaluating 'gapi.iframes.getContext')

is it possible to solve this and continue using angular fire?

eeschiavo avatar Dec 25 '21 22:12 eeschiavo

@tcns solution is working. You must use AngularFire v7 (not with compatibility mode) to solve this issue.

eeschiavo avatar Jan 02 '22 16:01 eeschiavo

Yes, but the problem with AngularFire v7 in compatibility mode still exists!

mifo111 avatar Jan 04 '22 11:01 mifo111

some new observations. i can actually run on ios using

ionic capacitor run ios -l --external

but not directly from xcode. so there is something that xcode is doing while running it

ionic capacitor run ios --external (without livereload) also does not work.

mifo111 avatar Jan 04 '22 11:01 mifo111

Been struggling a lot with this issue the last days but I managed to fix it. For those who need help here's my code. Thanks to @tcns for the first solution which I added upon.

You can delete all Firebase related imports from app.module.ts since my solution only uses Firebase. The packages rxfire and @angular/fire can be removed from your package.json. The only dependency I have is "firebase": "^9.6.1". I used observables for the getObject and list functions since that's what I'm used to use and I didn't want to rewrite my original code.

Hope this can help somebody.

import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { environment } from '@environment';
import { initializeApp } from 'firebase/app';
import { Auth, getAuth, indexedDBLocalPersistence, initializeAuth, signInWithCustomToken } from 'firebase/auth';
import { Database, getDatabase, onValue, orderByChild, query, ref } from 'firebase/database';
import { Observable, Observer, from } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {
  private readonly database: Database;
  private readonly auth: Auth;

  constructor() {
    const firebaseApp = initializeApp(environment.firebase);

    if (Capacitor.isNativePlatform()) {
      initializeAuth(firebaseApp, {
        persistence: indexedDBLocalPersistence
      });
    }

    this.database = getDatabase(firebaseApp);
    this.auth = getAuth(firebaseApp);
  }

  connectFirebase(firebaseToken) {
    return from(signInWithCustomToken(this.auth, firebaseToken));
  }

  disconnectFirebase() {
    return from(this.auth.signOut());
  }

  getObject<T>(path: string): Observable<T> {
    return new Observable((observer: Observer<T>) => {
      const dbRef = ref(this.database, path);
      const listener = onValue(dbRef, snapshot => {
        const data = snapshot.val();
        observer.next(data);
      });

      return {
        unsubscribe() {
          listener();
        }
      };
    });
  }

  public list<T>(path: string, orderChildBy?: string): Observable<Array<T>> {
    return new Observable<Array<T>>((observer: Observer<Array<T>>) => {
      const dbRef = ref(this.database, path);
      const dbReference = !orderChildBy ? dbRef : query(dbRef, orderByChild(orderChildBy));

      const listener = onValue(dbReference, snapshot => {
        const data = Object.values<T>(snapshot.val() || {});
        console.log(path, data);
        observer.next(data);
      });

      return {
        unsubscribe() {
          listener();
        }
      };
    });
  }
}

SeppeD avatar Jan 05 '22 14:01 SeppeD

Came across the same issue and found a temporary solution. I was using "@angular/fire": "^7.2.0" and was getting the TypeError: undefined is not an object (evaluating 'gapi.iframes.getContext') when building for IOS using capacitor. By downgrading to "@angular/fire": "^6.1.5" I can get it working by just removing the /compat from the imports. In the end my app.module.ts looks like this:

import { AngularFireModule } from '@angular/fire';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { AngularFirestoreModule } from '@angular/fire/firestore';

@NgModule({
  entryComponents: [],
  imports: [
    BrowserModule,
    HammerModule,
    IonicModule.forRoot({ mode: "ios" }),
    AppRoutingModule,
    HttpClientModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
    IonicSelectableModule,
    AngularFireModule.initializeApp(environment.firebaseConfig),
    AngularFireAuthModule,
    AngularFireDatabaseModule,
    AngularFirestoreModule
  ],
  declarations: [AppComponent],
  providers: [InAppBrowser, StatusBar, Broadcaster],
  exports: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This required minimal code changes but not ideal I am using an old version of AngularFire.

tommy-neeld-rft avatar Jan 13 '22 09:01 tommy-neeld-rft

I'm having the same issue. Any solution instead of using the old version?

davidecampello avatar Jan 26 '22 11:01 davidecampello

Ok, so I got this working finally with the following dependencies:

    "@angular/fire": "^7.2.0",
    "firebase": "^9.6.4",
    "rxfire": "^6.0.3"

After reading this ticket and many others I decided to remove all references to compat but no luck, it still wouldn't connect on iOS. I was going to try vanilla like others described and create a service but didn't like that I'd lose DI.

Instead I did this:

import { provideFirebaseApp, getApp, initializeApp, FirebaseApp } from '@angular/fire/app';
import { provideAuth, initializeAuth } from '@angular/fire/auth';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';

import { indexedDBLocalPersistence } from 'firebase/auth';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideFirestore(() => getFirestore()),
    provideAuth(() => initializeAuth(getApp(), { persistence: indexedDBLocalPersistence })),
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    AuthService
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Notice I imported indexedDBLocalPersistence from 'firebase/auth' instead of @angular/fire/auth. I was getting compile time errors saying indexedDBLocalPersistence wasn't available. Something for the AngularFire team to look at.

Now I can use like so:

import { Component } from '@angular/core';
import { Auth, signInWithEmailAndPassword, UserCredential, User } from '@angular/fire/auth';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor(private afAuth: Auth) {
    signInWithEmailAndPassword(this.afAuth, email, password);
  }
}

Hope this saves someone the hours I wasted.

DeezCashews avatar Jan 26 '22 17:01 DeezCashews

I'm experiencing the same problem since I've upgraded to Capacitor 3, Angular 12, Firebase 9, AngularFire 7.2.0 and Ionic 6. It's clear from my logs that it gets stuck while authentifying with Firebase or loading data. I tried to initialize firebase differently as suggested in the previous posts but without success.

However I noticed very different behaviors and it's quite confusing:

  • android: OK on any device

  • ionic capacitor run ios -l --external: OK on any device (iPhone or iPad)

  • emulator via XCode on iPad: OK

  • emulator via XCode iPhone: KO

  • app from the store on iPad: OK

  • app from the store on iPhone: KO

Anyone facing the same issue ?

ThierryLib avatar Feb 04 '22 08:02 ThierryLib

Ok, so I got this working finally with the following dependencies:

    "@angular/fire": "^7.2.0",
    "firebase": "^9.6.4",
    "rxfire": "^6.0.3"

After reading this ticket and many others I decided to remove all references to compat but no luck, it still wouldn't connect on iOS. I was going to try vanilla like others described and create a service but didn't like that I'd lose DI.

Instead I did this:

import { provideFirebaseApp, getApp, initializeApp, FirebaseApp } from '@angular/fire/app';
import { provideAuth, initializeAuth } from '@angular/fire/auth';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';

import { indexedDBLocalPersistence } from 'firebase/auth';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideFirestore(() => getFirestore()),
    provideAuth(() => initializeAuth(getApp(), { persistence: indexedDBLocalPersistence })),
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    AuthService
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Notice I imported indexedDBLocalPersistence from 'firebase/auth' instead of @angular/fire/auth. I was getting compile time errors saying indexedDBLocalPersistence wasn't available. Something for the AngularFire team to look at.

Now I can use like so:

import { Component } from '@angular/core';
import { Auth, signInWithEmailAndPassword, UserCredential, User } from '@angular/fire/auth';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor(private afAuth: Auth) {
    signInWithEmailAndPassword(this.afAuth, email, password);
  }
}

Hope this saves someone the hours I wasted.

Work like a charm 👍 Thank you!

limes avatar Feb 06 '22 14:02 limes

I'm experiencing the same problem since I've upgraded to Capacitor 3, Angular 12, Firebase 9, AngularFire 7.2.0 and Ionic 6. It's clear from my logs that it gets stuck while authentifying with Firebase or loading data. I tried to initialize firebase differently as suggested in the previous posts but without success.

However I noticed very different behaviors and it's quite confusing:

  • android: OK on any device
  • ionic capacitor run ios -l --external: OK on any device (iPhone or iPad)
  • emulator via XCode on iPad: OK
  • emulator via XCode iPhone: KO
  • app from the store on iPad: OK
  • app from the store on iPhone: KO

Anyone facing the same issue ?

Yes, same issue in my project.

aogorek avatar Feb 15 '22 15:02 aogorek

Oh my! I also ran into this problem on iOS for iPhone devices and simulators.

What made it really hard to identify the cause of the problem is that the error seems to get swallowed. 😱

You don't see the message undefined is not an object (evaluating 'gapi.iframes.getContext) anywhere in the logs. Instead, you have to use Safari's Web Inspector and enable a breakpoint for uncaught exceptions to reveal it: AngularFire undefined is not an object (evaluating 'gapi iframes getContext)

I am using Capacitor 3 and the app is using the compatibility mode in @angular/fire@7 and firebase@9 for the AngularFireDatabase. I observed that the valueChanges() didn't emit anything, leading me to think that it's a network/configuration-related problem rather than a problem in one of the libraries...

I tested the simple workaround of downgrading to @angular/fire@6 and firebase@8 and can confirm that it resolves the issue.

What makes me a bit sad though is that if I had known that there are such unexpected differences with the compat mode, then I would have migrated to the modular approach right away instead of going the lazy way, which in the end consumed more time for the error identification than if I migrated right away 😅

In any case, a thousand thanks for the hints that the problem originates from the compat mode. 🙌

Mobiletainment avatar Feb 15 '22 21:02 Mobiletainment

unfortunatly using @angular/fire@6 is not always a solution because I'm using a library (@robingenz/capacitor-firebase-authentication) that require firebase 9 to work.

davidecampello avatar Feb 15 '22 22:02 davidecampello

Is there a solution to this problem that allows the use of AngularFire and doesn't involve downgrading? I've tried @DeezCashews approach but wasn't successful.

timstoute avatar Mar 10 '22 03:03 timstoute

@timstoute I switch to modular way just like @DeezCashews, and it works. Maybe you miss some injection or import.

mattlin4567 avatar Mar 10 '22 03:03 mattlin4567

This issue is causing a bit of headache for my app so I thought I'd do some research and post it here. I still don't have a solution but maybe an underlying cause.

As @Mobiletainment points out, there is an error which shows that "gapi.iframes.getContext" is missing from the window object. Following this research I found a StackOverflow answer suggesting that you mock this object and inject it into the window yourself (1). I could not get this to work, but this perhaps could be a viable solution.

I then questioned why I couldn't see the gapi object on iOS, but I could see it when I ran the same app in the browser or on Android. I believe this could come down to CORS. On Android and the browser, the Ionic app is run from http://localhost but when deployed to iOS it is run from capacitor://localhost (2). I had a look at the domains allowed on my firebase project and sure enough I can enable http:// domain but domains on other protocols. It is possible that the compat mode does not mitigate for this (as previous versions did) and therefore Firebase does not enable the creation of the gapi object.

Just an idea, sources below!

1: https://stackoverflow.com/questions/69778975/angular-ionic-mobile-app-ios-does-not-fetch-from-firebase-using-angularfire 2:https://ionicframework.com/docs/troubleshooting/cors

alistairheath avatar Mar 10 '22 08:03 alistairheath

Based on my comment above, I believe I have found a workaround. If calls from capacitor://localhost are being rejected by firebase which then prevents the creation of the "gapi" object on the browser window, what if we could use a different protocol instead of "capacitor://".

Fortunately capacitor.config.ts has a feature do exactly this, just set server .iosScheme to "ionic" like so: const config: CapacitorConfig = { ... server: { iosScheme: 'ionic' } };

I tried this and compat worked immediately on iOS! I'm still nervous to push this into production as I unsure what unintended consequences may arise from using the "ionic" protocol, which was used with Cordova, instead of the "capacitor" protocol.

alistairheath avatar Mar 10 '22 09:03 alistairheath

@alistairheath - amazing, great sleuthing! I confirm this worked in Xcode / Simulator and on local iOS device. Very much appreciated.

timstoute avatar Mar 10 '22 13:03 timstoute

Thank you @alistairheath!! That one line solved my days of debugging

markdgold avatar Mar 23 '22 00:03 markdgold

@alistairheath this is works but after changing iosScheme, analytics will stop function from what I had found out now.

dodomui avatar Apr 05 '22 11:04 dodomui

Hi. Just wanted to know, is there any fix for this instead of workaround?

farisfaisalthena avatar Apr 25 '22 03:04 farisfaisalthena