Question

Error Fetching Offerings Post-Flutter Upgrade: Null Type Cast Exception

  • 15 March 2024
  • 4 replies
  • 105 views

Badge

After upgrading our Flutter environment from version 3.13.8 to 3.19.3, we encountered a persistent error when fetching offerings from RevenueCat using the purchases_flutter plugin. The application throws a type cast exception, specifically type 'Null' is not a subtype of type 'Map<dynamic, dynamic>' in type cast, during the offerings fetch operation.

 

Details

  • Flutter Version: Upgraded from 3.13.8 to 3.19.3.
  • purchases_flutter Version: Initially encountered on version 6.21.0; upgraded to 6.24.0 in an attempt to resolve the issue, but the error persists.
  • Error Message: type 'Null' is not a subtype of type 'Map<dynamic, dynamic>' in type cast
  • Error Location: The error occurs within the try-catch block around the Purchases.getOfferings() call, indicating an issue with handling the fetched offerings data.
  • Observations:
    • The debug logs indicate that the getOfferings call successfully contacts RevenueCat and receives a response, but fails during JSON deserialization or handling within the plugin.
    • The issue arose immediately following the Flutter upgrade.

 

Code

Future<void> updateAvailablePlans() async {
_logVerboseLevelMessage('Updating Available Plans');
purchasesUpdating.value = true;

Offerings? offerings;
try {
_logVerboseLevelMessage('Getting Offerings from RevenueCat');
offerings = await Purchases.getOfferings();
_logVerboseLevelMessage('Offerings: $offerings');
} on PlatformException catch (e) {
debugPrint('Offerings Error: $e');
Get.customSnackBar(
status: StatusType.error,
compact: true,
title: e.message ?? 'Unknown error',
);
allAvailablePlans.clear();
return;
} catch (e) {
_logErrorLevelMessage('Error getting Offerings: $e', e, null);
Get.customSnackBar(
status: StatusType.error,
compact: true,
title: e.toString(),
);
allAvailablePlans.clear();
return;
}

purchasesUpdating.value = false;

// .... Rest of the code
}


Logs

D/[Purchases] - DEBUG(25790): ℹ️ Debug logging enabled
D/[Purchases] - DEBUG(25790): ℹ️ SDK Version - 7.5.2
D/[Purchases] - DEBUG(25790): ℹ️ Package name - com.deepklarity.storia
D/[Purchases] - DEBUG(25790): 👤 Initial App User ID: {Omitted}
D/[Purchases] - DEBUG(25790): ℹ️ Purchases configured with response verification: DISABLED
D/[Purchases] - DEBUG(25790): 👤 Identifying App User ID: {Omitted}
D/[Purchases] - DEBUG(25790): ℹ️ Deleting old synced subscriber attributes that don't belong to {Omitted}
D/[Purchases] - DEBUG(25790): ℹ️ App foregrounded
D/[Purchases] - DEBUG(25790): ℹ️ CustomerInfo cache is stale, updating from network in foreground.
D/[Purchases] - DEBUG(25790): Retrieving customer info with policy: FETCH_CURRENT
D/[Purchases] - DEBUG(25790): ℹ️ Updating pending purchase queue
D/[Purchases] - DEBUG(25790): ℹ️ Offerings cache is stale, updating from network in foreground
D/[Purchases] - DEBUG(25790): 😻 Start Offerings update from network.
D/[Purchases] - DEBUG(25790): ℹ️ Querying purchases
D/[Purchases] - DEBUG(25790): Request already scheduled with jitter delay, adding existing callbacks to unjittered request with key: BackgroundAwareCallbackCacheKey(cacheKey=[/subscribers/{Omitted}/offerings], appInBackground=false)
D/[Purchases] - DEBUG(25790): ℹ️ Updating pending purchase queue
D/[Purchases] - DEBUG(25790): ℹ️ No subscriber attributes to synchronize.
D/[Purchases] - DEBUG(25790): ℹ️ Listener set
D/[Purchases] - DEBUG(25790): ℹ️ Sending latest CustomerInfo to listener.
D/[Purchases] - DEBUG(25790): ℹ️ Starting connection for com.android.billingclient.api.BillingClientImpl@af4104a
D/[Purchases] - DEBUG(25790): ℹ️ Ending connection for com.android.billingclient.api.BillingClientImpl@2285499
D/[Purchases] - DEBUG(25790): ℹ️ Billing Service Setup finished for com.android.billingclient.api.BillingClientImpl@af4104a
D/[Purchases] - DEBUG(25790): ℹ️ Updating pending purchase queue
D/[Purchases] - DEBUG(25790): Retrieving customer info with policy: CACHED_OR_FETCHED
D/[Purchases] - DEBUG(25790): ℹ️ Vending CustomerInfo from cache.
D/[Purchases] - DEBUG(25790): ℹ️ Checking if cache is stale AppInBackground false
D/[Purchases] - DEBUG(25790): ℹ️ Syncing purchases
D/[Purchases] - DEBUG(25790): ℹ️ Querying purchase history for type subs
D/[Purchases] - DEBUG(25790): ℹ️ Cleaning previously sent tokens
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ Saving tokens []
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ No pending purchases to sync
D/[Purchases] - DEBUG(25790): Request already scheduled with jitter delay, adding existing callbacks to unjittered request with key: BackgroundAwareCallbackCacheKey(cacheKey=[/subscribers/{Omitted}], appInBackground=false)
D/[Purchases] - DEBUG(25790): Retrieving customer info with policy: CACHED_OR_FETCHED
D/[Purchases] - DEBUG(25790): ℹ️ Vending CustomerInfo from cache.
D/[Purchases] - DEBUG(25790): ℹ️ Checking if cache is stale AppInBackground false
D/[Purchases] - DEBUG(25790): ℹ️ No cached Offerings, fetching from network
D/[Purchases] - DEBUG(25790): 😻 Start Offerings update from network.
D/[Purchases] - DEBUG(25790): Same call already in progress, adding to callbacks map with key: BackgroundAwareCallbackCacheKey(cacheKey=[/subscribers/{Omitted}/offerings], appInBackground=false)
D/[Purchases] - DEBUG(25790): Request already scheduled with jitter delay, adding existing callbacks to unjittered request with key: BackgroundAwareCallbackCacheKey(cacheKey=[/subscribers/{Omitted}/offerings], appInBackground=false)
D/[Purchases] - DEBUG(25790): Billing connected with country code: IN
D/TrafficStats(25790): tagSocket(225) with statsTag=0xffffffff, statsUid=-1
W/WindowOnBackDispatcher(25790): OnBackInvokedCallback is not enabled for the application.
W/WindowOnBackDispatcher(25790): Set 'android:enableOnBackInvokedCallback="true"' in the application manifest.
D/[Purchases] - DEBUG(25790): API request started: GET /subscribers/{Omitted}/offerings
D/[Purchases] - DEBUG(25790): API request completed with status: GET /subscribers/{Omitted}/offerings 304
2
D/[Purchases] - DEBUG(25790): ℹ️ Requesting products from the store with identifiers: storia_premium, storia_topup_con_199
2
D/[Purchases] - DEBUG(25790): ℹ️ Querying purchases
D/[Purchases] - DEBUG(25790): ℹ️ Cleaning previously sent tokens
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ Saving tokens []
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ No pending purchases to sync
D/[Purchases] - DEBUG(25790): ℹ️ Cleaning previously sent tokens
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ Saving tokens []
D/[Purchases] - DEBUG(25790): ℹ️ Tokens already posted: []
D/[Purchases] - DEBUG(25790): ℹ️ No pending purchases to sync
D/[Purchases] - DEBUG(25790): ℹ️ Products request finished for storia_premium, storia_topup_con_199
D/[Purchases] - DEBUG(25790): ℹ️ Requesting products from the store with identifiers: storia_topup_con_199
D/[Purchases] - DEBUG(25790): ℹ️ Products request finished for storia_topup_con_199
D/[Purchases] - DEBUG(25790): 💰 Retrieved productDetailsList: ProductDetails{jsonString='{"productId":"storia_topup_con_199","type":"inapp","title":"TopUp (Storia - AI generated stories)","name":"TopUp","description":"TopUp","localizedIn":["en-US"],"skuDetailsToken":"AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=","oneTimePurchaseOfferDetails":{"priceAmountMicros":180000000,"priceCurrencyCode":"INR","formattedPrice":"₹180.00"}}', parsedJson={"productId":"storia_topup_con_199","type":"inapp","title":"TopUp (Storia - AI generated stories)","name":"TopUp","description":"TopUp","localizedIn":["en-US"],"skuDetailsToken":"AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=","oneTimePurchaseOfferDetails":{"priceAmountMicros":180000000,"priceCurrencyCode":"INR","formattedPrice":"₹180.00"}}, productId='storia_topup_con_199', productType='inapp', title='TopUp (Storia - AI generated stories)', productDetailsToken='AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=', subscriptionOfferDetails=null}
2
D/[Purchases] - DEBUG(25790): 💰 storia_topup_con_199 - ProductDetails{jsonString='{"productId":"storia_topup_con_199","type":"inapp","title":"TopUp (Storia - AI generated stories)","name":"TopUp","description":"TopUp","localizedIn":["en-US"],"skuDetailsToken":"AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=","oneTimePurchaseOfferDetails":{"priceAmountMicros":180000000,"priceCurrencyCode":"INR","formattedPrice":"₹180.00"}}', parsedJson={"productId":"storia_topup_con_199","type":"inapp","title":"TopUp (Storia - AI generated stories)","name":"TopUp","description":"TopUp","localizedIn":["en-US"],"skuDetailsToken":"AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=","oneTimePurchaseOfferDetails":{"priceAmountMicros":180000000,"priceCurrencyCode":"INR","formattedPrice":"₹180.00"}}, productId='storia_topup_con_199', productType='inapp', title='TopUp (Storia - AI generated stories)', productDetailsToken='AEuhp4JGY4oM_SKIBDBFHhLlW741aOpnE0ho-4WBrg6sV2zuuAMgxsSjQ3rMm-iz0BM=', subscriptionOfferDetails=null}

D/[Purchases] - DEBUG(25790): ℹ️ Building offerings response with 2 products
I/flutter (25790): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (25790): │ type 'Null' is not a subtype of type 'Map<dynamic, dynamic>' in type cast
I/flutter (25790): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (25790): │ #0 LoggerHelper.logErrorLevelMessage (package:storia/logic/services/misc_helpers/logger_helper.dart:33:13)
I/flutter (25790): │ #1 PurchasesHelper._logErrorLevelMessage (package:storia/logic/services/purchases_helpers/purchases_helper.dart:386:10)
I/flutter (25790): │ #2 PurchasesHelper.updateAvailablePlans (package:storia/logic/services/purchases_helpers/purchases_helper.dart:309:7)
I/flutter (25790): │ #3 <asynchronous suspension>
I/flutter (25790): │ #4 PurchasesHelper._customerUpdateListener (package:storia/logic/services/purchases_helpers/purchases_helper.dart:63:5)
I/flutter (25790): │ #5 <asynchronous suspension>
I/flutter (25790): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (25790): │ ⛔ Error getting Offerings: type 'Null' is not a subtype of type 'Map<dynamic, dynamic>' in type cast
I/flutter (25790): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
D/[Purchases] - DEBUG(25790): API request started: GET /subscribers/6VhC9mt2AFYPWg7puz2nqnzrbqR2
D/[Purchases] - DEBUG(25790): API request completed with status: GET /subscribers/6VhC9mt2AFYPWg7puz2nqnzrbqR2 304
D/[Purchases] - DEBUG(25790): 😻 CustomerInfo updated from network.
D/[Purchases] - DEBUG(25790): ℹ️ Purchase history is empty.
D/[Purchases] - DEBUG(25790): ℹ️ Querying purchase history for type inapp
D/[Purchases] - DEBUG(25790): ℹ️ Purchase history is empty.
D/[Purchases] - DEBUG(25790): Retrieving customer info with policy: CACHED_OR_FETCHED
D/[Purchases] - DEBUG(25790): ℹ️ Vending CustomerInfo from cache.
D/[Purchases] - DEBUG(25790): ℹ️ Checking if cache is stale AppInBackground false
D/ScrollOptim [SceneManager](25790): updateCurrentActivity: mCurrentActivityName=null, isOptEnable=true, isAnimAheadEnable=true, isFrameInsertEnable=true, InsertNum=1, isEnabledForScrollChanged=false

 


4 replies

Badge

I have conducted further investigation to pinpoint the exact location and cause of the error we're experiencing when fetching offerings. The error occurs during the deserialization process in the Offerings.fromJson method, specifically when handling the availablePackages within our all.current offering.

 

Debugging Process and Findings:

  • Invocation of getOfferings Method: I added print statements to trace the execution flow and found that the error is emitted during the execution of Offerings.fromJson(Map<String, dynamic>.from(res)) in getOfferings method:
static Future<Offerings> getOfferings() async {
print('getOfferings Called');
final res = await _channel.invokeMethod('getOfferings');
final enc = json.encode(res);
log('enc: $enc');
return Offerings.fromJson(
Map<String, dynamic>.from(res),
);
}
  • Serialized Data Structure: The error occurs specifically when attempting to serialize the available packages inside our all.current offering. I observed the data structure of res right before serialization:
{
"all": {
"current": {
"identifier": "current",
"serverDescription": "The standard set of packages",
"metadata": {},
"availablePackages": [
{
"identifier": "$rc_annual",
"packagesType": "ANNUAL",
"product": {...},
"offeringIdentifier": "current"
},
{
"identifier": "$rc_monthly",
"packagesType": "MONTHLY",
"product": {...},
"offeringIdentifier": "current"
}
],
"lifetime": null,
"annual": {...},
"sixMonth": null,
"threeMonth": null,
"twoMonth": null,
"monthly": {...},
"weekly": null
}
},
"current": {...}
}
  • Mismatch in Expected Data Structure: The availablePackages key within res contains 4 keys: identifier, packagesType, product, and offeringIdentifier. However, the serializer expects the structure to include presentedOfferingContext instead of offeringIdentifier. This discrepancy between expected and received data structures is the root cause of the serialization error.
  • The serialization logic in our code attempts to create a PackageImpl object from JSON data. However, it fails because it expects a presentedOfferingContext key that is not present in the incoming JSON. Here's the relevant part of the serialization logic:
_$PackageImpl _$$PackageImplFromJson(Map json) => _$PackageImpl(
json['identifier'] as String,
$enumDecode(_$PackageTypeEnumMap, json['packageType'],
unknownValue: PackageType.unknown),
StoreProduct.fromJson(Map<String, dynamic>.from(json['product'] as Map)),
PresentedOfferingContext.fromJson(
Map<String, dynamic>.from(json['presentedOfferingContext'] as Map)),
);

Map<String, dynamic> _$$PackageImplToJson(_$PackageImpl instance) =>
<String, dynamic>{
'identifier': instance.identifier,
'packageType': _$PackageTypeEnumMap[instance.packageType]!,
'product': instance.storeProduct.toJson(),
'presentedOfferingContext': instance.presentedOfferingContext.toJson(),
};
  • The attempt to deserialize presentedOfferingContext from the JSON data fails because the actual JSON contains offeringIdentifier instead, leading to the error from:
    PresentedOfferingContext.fromJson(
    Map<String, dynamic>.from(json['presentedOfferingContext'] as Map)),

 


I have provided as many details as I could from my end. If anything else is required to solve this issue please let me know.

Userlevel 4
Badge +8

Hi, thank you for this detailed information! The team and I are investigating this and will let you know if we need any other info and what we find.

Badge

Hey Haley, thanks for checking into that issue for us. Just an update - we've gone ahead and switched back to using an older version of the package in our app, specifically 5.8.2, alongside the new Flutter SDK version 3.19.3 for the time being.

Userlevel 4
Badge +8

Hi, so sorry for the delay! Can you let us know the following:

  • Does the issue happens in iOS, Android or both (logs are from android so I imagine it happens on android at least)
  • Can you try with a clean build flutter clean and (assuming android) cd android && ./gradlew clean and to try to remove any caches you might be using?
  • Can you also try after updating to our newest Flutter 6.25.0 and let us know if the issue persists?  https://github.com/RevenueCat/purchases-flutter/releases/tag/6.25.0

 

Reply