purchases-flutter icon indicating copy to clipboard operation
purchases-flutter copied to clipboard

App crashes on reopen from homescreen due to PurhcasesHybridCommon error

Open crobertson247 opened this issue 6 months ago • 5 comments

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();
  }
}

crobertson247 avatar Feb 12 '24 18:02 crobertson247

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

RCGitBot avatar Feb 12 '24 18:02 RCGitBot

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.

NachoSoto avatar Feb 12 '24 18:02 NachoSoto

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 avatar Feb 12 '24 19:02 crobertson247

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)

crobertson247 avatar Feb 12 '24 19:02 crobertson247

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.

vegaro avatar Feb 22 '24 11:02 vegaro