Question

App crashes on reopen from homescreen due to PurhcasesHybridCommon error

  • 12 February 2024
  • 9 replies
  • 159 views

Badge +1

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

9 replies

Userlevel 2
Badge +3

Hi @cian-robertson-2bf7fd, thanks for reaching out.

Looking at the stack trace, I believe you might be calling `getCustomerInfo` or `restorePurchases` before calling `Purchases.configure`. Is it possible in one of your paths, you’re calling one of those methods before actually configuring the SDK?

 

Badge +1

I noticed i was using a static variable to avoid calling the configure method multiple times but it wont hold if the app is stopped so i might be calling the configure multiple times. I’m trying to see if using shared preferences will stop the error. Could this be the cause of the error?

Badge +1

Nevermind it doesn’t work, this is what I tried: 

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

Badge +1

this is the error log i get now in the xcode logs: 

```

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)

```

Userlevel 2
Badge +3

Hi @cian-robertson-2bf7fd,

Calling configure multiple times is not recommended and could result in some undetermined behavior, it should only be called once on app open (not app foreground or other situations where memory is not released).

As for the latest trace, I’m not sure, since that crash is not symbolicated… But it could still be the same issue… Can you add some logs to make sure the configure is called only once on this scenario? (If you force close the app, it should be called again upon reopening it).

when i close it and reopen it from the home screen it crashes on TestFlight.

Do you mean when you force-close the app, or when you background it, then foreground it?

Also, a couple more questions:

  • What version of the SDK are you using?
  • Have you been able to test/reproduce in Android?

 

Badge +1

I mean when i force close the app and then open it again, opening it once backgrounded all works. I changed it so that the configuration is only called once within the apps lifecycle, however that did not resolve the problem, it got rid of the crash and error reports but the white screen still persists.

  • I have not been able to replicate it on android, it seems like its only an ios problem. 
  • I’m using the latest SDK:  purchases_flutter: ^6.21.0
Userlevel 2
Badge +3

Hi again @cian-robertson-2bf7fd,

Thanks for the info! After more research, it still seems to me that you are calling a method of the SDK BEFORE calling `Purchases.configure`.

Could you add some breakpoints/prints BEFORE calling all methods of RevenueCat’s SDK and reproduce again? You should make sure `configure` is called before any others. 

If you’re not able to find the issue, could you share the pieces of code where you call each method of the SDK and when you’re calling those?

Thanks again and sorry for the problems you’re having.

 

Badge +1

in my main.dart Future<void> main async{} method: 

```

try {

    PurchasesConfiguration configuration =

        PurchasesConfiguration(StoreConfig.instance.apiKey);

    await Purchases.configure(configuration);

    await PurchaseApi.identifyUser();

  } catch (e) {

    FirebaseCrashlytics.instance.log(e.toString());

  }

```

```

static Future<void> identifyUser() async {

    try {

      Auth auth = Auth();

      String userId = auth.userId?.toString() ?? '';

      if (userId.isNotEmpty) {

        await Purchases.logIn(userId);

      }

    } catch (e) {

      print(e);

    }

  }

```

I use this multiple times throughout screens to determine if users are subscribed

```

void initState() {

    super.initState();

    fetchSubscriptionStatus();

  }

 

  void fetchSubscriptionStatus() async {

    final PurchaseApi purchaseApi = PurchaseApi();

    bool subscriptionStatus = await purchaseApi.isPremium();

    setState(() {

      _isSubscribed = subscriptionStatus;

      //_isSubscribed = true;

      //pTesting

    });

  }

```

 

In my screen where user can purchase premium: 

```

Future fetchOffers(context) async {

    final offerings = await PurchaseApi.fetchOffers();

    Purchases.getOfferings();

    String price = await _premiumPrice;

 

    if (offerings.isEmpty) {

      ScaffoldMessenger.of(context).showSnackBar(

        SnackBar(

          showCloseIcon: true,

          content: Text(

              AppLocalizations.of(context)?.noPlansFound ?? 'No Plans Found'),

        ),

      );

      AnalyticsService().logEvent('error_in_retrieving_ffers');

    } else {

      final packages = offerings

          .map((offer) => offer.availablePackages)

          .expand((pair) => pair)

          .toList();

 

      AnalyticsService().logEvent('offers_retrieved');

 

      Utils.showSheet(

        context,

        (context) => PaywallWidget(

          packages: packages,

          title: AppLocalizations.of(context)?.joinPremium ??

              "Join PresentPal Premium",

          desciption: AppLocalizations.of(context)?.pricingMessage(price) ??

              'Unlock all premium features for just $price/month.',

          onClickedPackage: (package) async {

            await PurchaseApi.purchasePackage(package);

            if (!context.mounted) return;

            Navigator.pop(context);

          },

          footer: AppLocalizations.of(context)?.paywallFooter ??

              "Cancelations should be made at least 24 hours before the end of the current period. You can manage and cancel your subscriptions by going to your account settings. ",

          footerLink: AppLocalizations.of(context)?.paywallFooterLink ??

              'View Terms and Conditions',

        ),

      );

    }

  }

 

  Future<String> getPrice() async {

    try {

      final offerings = await PurchaseApi.fetchOffers();

      final packages = offerings

          .map((offer) => offer.availablePackages)

          .expand((pair) => pair)

          .toList();

 

      return packages[0].storeProduct.priceString;

    } catch (e) {

      return "\$4.99";

    }

  }

```

 

When a user logs in on login and sign up:

```

Future<void> logInPurchases() async {

    PurchaseApi.identifyUser();

  }

```

 

in settings:

```

Future<void> setCancelSubscriptionUrl() async {

    PurchaseApi purchaseApi = PurchaseApi();

    String? url = await purchaseApi.getSubscriptionManagementURL();

    setState(() {

      cancelSubscriptionURL = url.toString();

    });

  }

```

 

```

Future<String?> getSubscriptionManagementURL() async {

    try {

      final customerInfo = await Purchases.getCustomerInfo();

      if (customerInfo.managementURL != null) {

        return customerInfo.managementURL;

      } else {

        return '';

      }

    } catch (error) {

      print('Error fetching customer info: $error');

      return '';

    }

  }

```

 

```

Future<bool> isPremium() async {

    try {

      CustomerInfo customerInfo = await Purchases.getCustomerInfo();

      if (customerInfo.entitlements.all[entitlementID] != null &&

          customerInfo.entitlements.all[entitlementID]!.isActive == true) {

        return true;

      } else {

        return false;

      }

    } on PlatformException {

      return false;

    }

  }

```

Userlevel 2
Badge +3

Hi @cian-robertson-2bf7fd,

Is it possible the “await Purchases.configure(configuration);” is returning an error and getting swallowed by the try-catch around it? Can you confirm that is working correctly when you reproduce?

Reply