RevenueCat / purchases-flutter

Flutter plugin for in-app purchases and subscriptions. Supports iOS, macOS and Android.
https://www.revenuecat.com/
MIT License
608 stars 170 forks source link

App crashes on reopen from homescreen due to PurhcasesHybridCommon error #980

Open crobertson247 opened 9 months ago

crobertson247 commented 9 months ago

When I open the app everything works fine but when i close it and reopen it from the home screen it crashes on TestFlight.

Here's the crash log of the thread its crashing on:

Thread 0 name:
Thread 0 Crashed:
0   libswiftCore.dylib              0x00000001847363fc _assertionFailure(_:_:file:line:flags:) + 264 (AssertCommon.swift:144)
1   PurchasesHybridCommon           0x00000001019af1a4 closure #1 in variable initialization expression of static FatalErrorUtil.defaultFatalErrorClosure + 64 (FatalErrorUtil.swift:15)
2   PurchasesHybridCommon           0x00000001019a73a0 fatalError(_:file:line:) + 60 (FatalErrorUtil.swift:27)
3   PurchasesHybridCommon           0x00000001019a73a0 static CommonFunctionality.sharedInstance.getter + 84 (CommonFunctionality.swift:21)
4   PurchasesHybridCommon           0x00000001019a73a0 static CommonFunctionality.customerInfo(fetchPolicy:completion:) + 84 (<compiler-generated>:405)
5   PurchasesHybridCommon           0x00000001019a73a0 specialized static CommonFunctionality.customerInfo(completion:) + 372 (CommonFunctionality.swift:398)
6   PurchasesHybridCommon           0x00000001019a3964 @objc static CommonFunctionality.restorePurchases(completion:) + 76
7   purchases_flutter               0x0000000102cf5f2c -[PurchasesFlutterPlugin getCustomerInfoWithResult:] + 48 (PurchasesFlutterPlugin.m:347)
8   purchases_flutter               0x0000000102cf493c -[PurchasesFlutterPlugin handleMethodCall:result:] + 2048 (PurchasesFlutterPlugin.m:100)
9   Flutter                         0x000000010342196c 0x102e40000 + 6166892
10  Flutter                         0x0000000102e83c00 0x102e40000 + 277504
11  libdispatch.dylib               0x00000001933106a8 _dispatch_call_block_and_release + 32 (init.c:1530)
12  libdispatch.dylib               0x0000000193312300 _dispatch_client_callout + 20 (object.m:561)
13  libdispatch.dylib               0x0000000193320998 _dispatch_main_queue_drain + 984 (queue.c:7813)
14  libdispatch.dylib               0x00000001933205b0 _dispatch_main_queue_callback_4CF + 44 (queue.c:7973)
15  CoreFoundation                  0x000000018b34cf9c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16 (CFRunLoop.c:1780)
16  CoreFoundation                  0x000000018b349ca8 __CFRunLoopRun + 1996 (CFRunLoop.c:3149)
17  CoreFoundation                  0x000000018b3493f8 CFRunLoopRunSpecific + 608 (CFRunLoop.c:3420)
18  GraphicsServices                0x00000001ce8d74f8 GSEventRunModal + 164 (GSEvent.c:2196)
19  UIKitCore                       0x000000018d76f8a0 -[UIApplication _run] + 888 (UIApplication.m:3685)
20  UIKitCore                       0x000000018d76eedc UIApplicationMain + 340 (UIApplication.m:5270)
21  Runner                          0x000000010030877c main + 64 (AppDelegate.swift:7)
22  dyld                            0x00000001ae09edcc start + 2240 (dyldMain.cpp:1269)

this is my main.dart code:

import 'dart:io';

import 'package:awesome_notifications/awesome_notifications.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:presentpal/api/constants.dart';
import 'package:presentpal/api/firebase_api.dart';
import 'package:presentpal/api/purchases_api.dart';
import 'package:presentpal/pages/gift_chose.dart';
import 'package:presentpal/pages/home.dart';
import 'package:presentpal/provider/locale_provider.dart';
import 'package:presentpal/services/analytics_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'provider/theme_provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'services/preferences_service.dart';
import 'pages/change_notifier.dart' as presentpal;
import 'package:provider/provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'pages/onboarding.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'store_config.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:presentpal/services/set_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;

final navigatorKey = GlobalKey<NavigatorState>();

Future<void> configureLocalTimeZone() async {
  tz.initializeTimeZones();
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  presentpal.AppState appState = presentpal.AppState();
  try {
    MobileAds.instance.initialize();

    //get user data
    await Firebase.initializeApp();

    FlutterError.onError = (errorDetails) {
      FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
    };

    await FirebaseApi().initNotifications();

    await setNotifications.initializeNotifications();

    await configureLocalTimeZone();

    String? initialLocale = await getLocale();
    if (initialLocale != null && initialLocale.isNotEmpty) {
      appState.setLocale(initialLocale);
    }
    try {
      await dotenv.load(fileName: ".env");
    } catch (e) {
      print(e.toString());
    }
    if (Platform.isIOS) {
      StoreConfig(
        store: Store.appStore,
        apiKey: appleApiKey,
      );
    } else if (Platform.isAndroid) {
      StoreConfig(
        store: Store.playStore,
        apiKey: googleApiKey,
      );
    }
    await PurchaseApi.init();
  } catch (e) {
    print(e.toString());
    AnalyticsService analyticsService = AnalyticsService();
    analyticsService.logEvent('error: ${e.toString()}');
  }

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => LocaleModel()),
      ],
      child: MyApp(appState: appState),
    ),
  );
}

class MyApp extends StatefulWidget {
  final presentpal.AppState appState;

  const MyApp({super.key, required this.appState});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool hasSeenOnboarding = false;

  @override
  void initState() {
    super.initState();
    _populateFields();
    checkOnboardingStatus();
    AwesomeNotifications().isNotificationAllowed().then((isAllowed) {
      if (!isAllowed) {
        AwesomeNotifications().requestPermissionToSendNotifications();
      }
    });
  }

  Future<void> checkOnboardingStatus() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final bool seenOnboarding = prefs.getBool('hasSeenOnboarding') ?? false;
    setState(() {
      hasSeenOnboarding = seenOnboarding;
    });
  }

  final _preferenceServices = PreferencesService();

  String selectedLanguage = 'en';

  void _populateFields() async {
    final settings = await _preferenceServices.getSettings();
    String? storedLanguage = settings.languageCode;
    if (storedLanguage.isNotEmpty) {
      setState(() {
        selectedLanguage = storedLanguage;
      });
      widget.appState.setLocale(selectedLanguage);
    } else {
      String languageCode;
      try {
        //get locale from settings
        Locale userLocale = WidgetsBinding.instance.window.locale;
        languageCode = userLocale.languageCode;
        if ([
          'en',
          'ar',
          'bn',
          'de',
          'es',
          'fr',
          'hi',
          'it',
          'ko',
          'pt',
          'ru',
          'zh',
          'zh_HK'
        ].contains(languageCode)) {
          setState(() {
            selectedLanguage = languageCode;
          });
          widget.appState.setLocale(selectedLanguage);
        }
      } catch (e) {
        languageCode = 'en';
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => LocaleProvider(),
        builder: (context, child) {
          final provider = Provider.of<LocaleProvider>(context);
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            title: 'PresentPal',
            themeMode: ThemeMode.system,
            theme: MyThemes.lightTheme,
            navigatorObservers: [AnalyticsService().getAnalyticsObserver()],
            navigatorKey: navigatorKey,
            locale: provider.locale,
            supportedLocales: const [
              Locale('en'), // English
              Locale('ar'), //Arabic
              Locale('bn'), //Bengali
              Locale('de'), //German
              Locale('es'), //Spanish
              Locale('fr'), //French
              Locale('hi'), //Hindi
              Locale('it'), //Italian
              Locale('ko'), //Korean
              Locale('pt'), //Portuguese
              Locale('ru'), //Russian
              Locale('zh'), //Chinese
              Locale('zh_HK') //Chinese (Hong Kong)
            ],
            localizationsDelegates: const [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            darkTheme: MyThemes.darkTheme,
            routes: {
              '/home': (context) => const HomePage(),
              '/onboarding': (context) => const OnboardingScreen(),
              '/giftChoose': (context) => const GiftChoose(),
            },
            home:
                hasSeenOnboarding ? const HomePage() : const OnboardingScreen(),
          );
        }
        );
  }
}

Future<void> setLocale(String languageCode) async {
  final SharedPreferences preferences = await SharedPreferences.getInstance();
  preferences.setString('languageCode', languageCode);
}

Future<String?> getLocale() async {
  WidgetsFlutterBinding.ensureInitialized();
  final SharedPreferences preferences = await SharedPreferences.getInstance();
  return preferences.getString('languageCode');
}

class LocaleModel extends ChangeNotifier {
  Locale? _locale;

  Locale? get locale => _locale;

  void set(Locale locale) {
    _locale = locale;
    notifyListeners();
  }
}
RCGitBot commented 9 months ago

👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

NachoSoto commented 9 months ago

Thanks for the report!

That error happens if you try to use Purchases before initializing it.

In your initialization code it looks like you have many calls inside of a try. If any of the calls prior to PurchaseApi.init(); throw an exception, you're simply logging the error and moving on, which means that Purchases will end up being uninitialized.

I recommend separating each of those initializations so you can isolate what's throwing an exception, and so you don't inadvertently leave Purchases uninitialized.

crobertson247 commented 9 months ago

I added the try catch as a suggestion from someone on the flutter form, however, before it I didn't use the try catch and didn't get any errors on the initialization of the app. I'm able to access all the subscription information up until i close and reopen the app, I noticed i was using a static variable to try and not call configure multiple times, but even after changing it to shared preferences i still get the same error.

I call the init method multiple times to ensure it is initialised before using.

static bool isConfigured = false;

  static Future init() async {
    try {
      PreferencesService preferencesService = PreferencesService();
      isConfigured = await preferencesService.getConfigurationStatus();
      await Purchases.setLogLevel(LogLevel.debug);
      if (isConfigured == false) {
        await Purchases.configure(
            PurchasesConfiguration(StoreConfig.instance.apiKey));
        isConfigured = true;
        await preferencesService.setConfigurationStatus(true);
      }
    } catch (e) {
      print(e);
    }
  }
crobertson247 commented 9 months ago

This is the error log i get now:

Thread 0 name:
Thread 0 Crashed:
0   libswiftCore.dylib              0x00000001847363fc _assertionFailure(_:_:file:line:flags:) + 264 (AssertCommon.swift:144)
1   PurchasesHybridCommon           0x0000000106477d0c 0x106464000 + 81164
2   PurchasesHybridCommon           0x000000010646f3b4 0x106464000 + 46004
3   PurchasesHybridCommon           0x000000010646b980 0x106464000 + 31104
4   purchases_flutter               0x00000001077f9f2c 0x1077f0000 + 40748
5   purchases_flutter               0x00000001077f893c 0x1077f0000 + 35132
6   Flutter                         0x0000000107f2596c 0x107944000 + 6166892
7   Flutter                         0x0000000107987c00 0x107944000 + 277504
8   libdispatch.dylib               0x00000001933106a8 _dispatch_call_block_and_release + 32 (init.c:1530)
9   libdispatch.dylib               0x0000000193312300 _dispatch_client_callout + 20 (object.m:561)
10  libdispatch.dylib               0x0000000193320998 _dispatch_main_queue_drain + 984 (queue.c:7813)
11  libdispatch.dylib               0x00000001933205b0 _dispatch_main_queue_callback_4CF + 44 (queue.c:7973)
12  CoreFoundation                  0x000000018b34cf9c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16 (CFRunLoop.c:1780)
13  CoreFoundation                  0x000000018b349ca8 __CFRunLoopRun + 1996 (CFRunLoop.c:3149)
14  CoreFoundation                  0x000000018b3493f8 CFRunLoopRunSpecific + 608 (CFRunLoop.c:3420)
15  GraphicsServices                0x00000001ce8d74f8 GSEventRunModal + 164 (GSEvent.c:2196)
16  UIKitCore                       0x000000018d76f8a0 -[UIApplication _run] + 888 (UIApplication.m:3685)
17  UIKitCore                       0x000000018d76eedc UIApplicationMain + 340 (UIApplication.m:5270)
18  Runner                          0x0000000104dd077c 0x104dc8000 + 34684
19  dyld                            0x00000001ae09edcc start + 2240 (dyldMain.cpp:1269)
vegaro commented 8 months ago

It looks like the stacktrace doesn't have symbols, so it's hard to know what's going on, although it looks pretty much the same as the original.

As a suggestion that I think might help you figure this out. There's a isConfigured property (https://pub.dev/documentation/purchases_flutter/latest/purchases_flutter/Purchases/isConfigured.html) you can use instead of the code you shared that uses PreferencesService. That function will check if there's an instance of Purchases, and your crash means there's no instance, so checking isConfigured before interacting with the SDK will prevent the exception.