convertToPurchase function

Purchase convertToPurchase(
  1. Map<String, dynamic> itemJson, {
  2. required bool platformIsAndroid,
  3. required bool platformIsIOS,
  4. required Map<String, bool> acknowledgedAndroidPurchaseTokens,
  5. Map<String, dynamic>? originalJson,
})

Implementation

gentype.Purchase convertToPurchase(
  Map<String, dynamic> itemJson, {
  required bool platformIsAndroid,
  required bool platformIsIOS,
  required Map<String, bool> acknowledgedAndroidPurchaseTokens,
  Map<String, dynamic>? originalJson,
}) {
  // Native SDKs serialize the generated Purchase types directly. Preserve that
  // canonical payload as the base so newly generated optional fields are not
  // silently dropped by this legacy compatibility adapter.
  //
  // The fallback wire keys below are accepted through
  // flutter_inapp_purchase 9.x only and are scheduled for removal in 10.0.0.
  // New native bridges, fixtures, and custom MethodChannel producers must emit
  // the canonical OpenIAP keys.
  final sourcePayload = normalizeDynamicMap(<String, dynamic>{
    if (originalJson != null) ...originalJson,
    ...itemJson,
  })!;

  final productId = sourcePayload['productId']?.toString() ?? '';
  final hasSourceId = sourcePayload.containsKey('id');
  final sourceId = sourcePayload['id']?.toString();
  final transactionIdSelection = _transactionIdFrom(sourcePayload);
  final sourceTransactionId = transactionIdSelection.value;
  final dynamic quantityValue = sourcePayload['quantity'];
  int quantity = 1;
  if (quantityValue is num) {
    quantity = quantityValue.toInt();
  } else if (quantityValue is String) {
    final parsedQuantity = int.tryParse(quantityValue.trim());
    if (parsedQuantity != null) {
      quantity = parsedQuantity;
    }
  }

  final String? purchaseId = hasSourceId
      ? ((sourceId?.isNotEmpty ?? false) ? sourceId : null)
      : (sourceTransactionId?.isNotEmpty ?? false)
          ? sourceTransactionId
          : (productId.isNotEmpty ? productId : null);

  if (purchaseId == null || purchaseId.isEmpty) {
    debugPrint(
      '[flutter_inapp_purchase] Skipping purchase with missing identifiers',
    );
    throw const FormatException('Missing purchase identifier');
  }

  final transactionDate =
      _parseTimestampMilliseconds(sourcePayload['transactionDate']) ?? 0;

  if (platformIsAndroid) {
    final hasSelectedPurchaseState =
        sourcePayload.containsKey('purchaseState') ||
            sourcePayload.containsKey('purchaseStateAndroid');
    final stateValue = _coerceAndroidPurchaseState(
      _canonicalOrLegacy(
        sourcePayload,
        canonicalKey: 'purchaseState',
        legacyKey: 'purchaseStateAndroid',
        warningKey: 'purchase.purchaseStateAndroid',
        message: 'The purchase `purchaseStateAndroid` field is deprecated and '
            'will be removed in flutter_inapp_purchase 10.0.0. Use '
            '`purchaseState` instead.',
      ),
      hasSelectedState: hasSelectedPurchaseState,
    );
    final purchaseState = _mapAndroidPurchaseState(stateValue).toJson();

    // Determine store from input or default based on platform
    final storeValue = sourcePayload['store']?.toString() ?? 'google';
    final androidTransactionId = _resolveAndroidTransactionId(
      hasCanonicalTransactionId: transactionIdSelection.isPresent,
      canonicalTransactionId: sourceTransactionId,
      id: sourceId,
      purchaseToken: sourcePayload['purchaseToken']?.toString(),
      store: storeValue,
    );
    if (!transactionIdSelection.isPresent &&
        androidTransactionId != null &&
        androidTransactionId == sourceId) {
      warnLegacyOnce(
        'purchase.id-transaction-id',
        'Using purchase `id` as `transactionId` is deprecated and will be '
            'removed in flutter_inapp_purchase 10.0.0. Emit `transactionId` '
            'directly instead.',
      );
    }

    final map = <String, dynamic>{
      ...sourcePayload,
      'id': purchaseId,
      'productId': productId,
      'platform': gentype.IapPlatform.Android.toJson(),
      'store': storeValue,
      'isAutoRenewing': sourcePayload['isAutoRenewing'] as bool? ??
          sourcePayload['autoRenewingAndroid'] as bool? ??
          false,
      'purchaseState': purchaseState,
      'quantity': quantity,
      'transactionDate': transactionDate,
      'purchaseToken': sourcePayload['purchaseToken']?.toString(),
      'autoRenewingAndroid': sourcePayload['autoRenewingAndroid'] as bool?,
      'currentPlanId': sourcePayload['currentPlanId']?.toString(),
      'dataAndroid': _canonicalOrLegacy(
        sourcePayload,
        canonicalKey: 'dataAndroid',
        legacyKey: 'originalJsonAndroid',
        warningKey: 'purchase.originalJsonAndroid',
        message: 'The purchase `originalJsonAndroid` field is deprecated and '
            'will be removed in flutter_inapp_purchase 10.0.0. Use '
            '`dataAndroid` instead.',
      )?.toString(),
      'developerPayloadAndroid':
          sourcePayload['developerPayloadAndroid']?.toString(),
      'ids': _toStringList(sourcePayload['ids']),
      'isAcknowledgedAndroid': sourcePayload['isAcknowledgedAndroid'] as bool?,
      'obfuscatedAccountIdAndroid':
          sourcePayload['obfuscatedAccountIdAndroid']?.toString(),
      'obfuscatedProfileIdAndroid':
          sourcePayload['obfuscatedProfileIdAndroid']?.toString(),
      'packageNameAndroid': sourcePayload['packageNameAndroid']?.toString(),
      'signatureAndroid': sourcePayload['signatureAndroid']?.toString(),
      // Pending/orderless Play purchases legitimately have no order ID.
      // Legacy native payloads only exposed it through `id`, so recover that
      // value only when it is distinguishable from a Google purchase token.
      // The `id` transaction fallback ends in 10.0.0.
      'transactionId': androidTransactionId,
    };

    final purchaseToken = sourcePayload['purchaseToken']?.toString();
    if (purchaseToken != null && purchaseToken.isNotEmpty) {
      acknowledgedAndroidPurchaseTokens[purchaseToken] =
          sourcePayload['isAcknowledgedAndroid'] as bool? ?? false;
    }

    return gentype.PurchaseAndroid.fromJson(map);
  }

  if (platformIsIOS) {
    final stateIOS = _parsePurchaseStateIOS(
      _canonicalOrLegacy(
        sourcePayload,
        canonicalKey: 'purchaseState',
        legacyKey: 'transactionStateIOS',
        warningKey: 'purchase.transactionStateIOS',
        message: 'The purchase `transactionStateIOS` field is deprecated and '
            'will be removed in flutter_inapp_purchase 10.0.0. Use '
            '`purchaseState` instead.',
      ),
    ).toJson();

    final originalTransactionDateIOS = _parseTimestampMilliseconds(
      sourcePayload['originalTransactionDateIOS'],
    );

    // Determine store from input or default based on platform
    final storeValueIOS = sourcePayload['store']?.toString() ?? 'apple';
    if (!transactionIdSelection.isPresent && (sourceId?.isNotEmpty ?? false)) {
      warnLegacyOnce(
        'purchase.id-transaction-id',
        'Using purchase `id` as `transactionId` is deprecated and will be '
            'removed in flutter_inapp_purchase 10.0.0. Emit `transactionId` '
            'directly instead.',
      );
    }

    final map = <String, dynamic>{
      ...sourcePayload,
      'id': purchaseId,
      'productId': productId,
      'platform': gentype.IapPlatform.IOS.toJson(),
      'store': storeValueIOS,
      'isAutoRenewing': sourcePayload['isAutoRenewing'] as bool? ?? false,
      'purchaseState': stateIOS,
      'quantity': quantity,
      'transactionDate': transactionDate,
      'purchaseToken': _canonicalOrLegacy(
        sourcePayload,
        canonicalKey: 'purchaseToken',
        legacyKey: 'transactionReceipt',
        warningKey: 'purchase.transactionReceipt',
        message: 'The purchase `transactionReceipt` field is deprecated and '
            'will be removed in flutter_inapp_purchase 10.0.0. Use '
            '`purchaseToken` instead.',
      )?.toString(),
      'ids': _toStringList(sourcePayload['ids']),
      'appAccountToken': sourcePayload['appAccountToken']?.toString(),
      'appBundleIdIOS': sourcePayload['appBundleIdIOS']?.toString(),
      'countryCodeIOS': sourcePayload['countryCodeIOS']?.toString(),
      'currencyCodeIOS': sourcePayload['currencyCodeIOS']?.toString(),
      'currencySymbolIOS': sourcePayload['currencySymbolIOS']?.toString(),
      'environmentIOS': sourcePayload['environmentIOS']?.toString(),
      'expirationDateIOS':
          _parseTimestampMilliseconds(sourcePayload['expirationDateIOS']),
      'originalTransactionIdentifierIOS':
          sourcePayload['originalTransactionIdentifierIOS']?.toString(),
      'originalTransactionDateIOS': originalTransactionDateIOS,
      'subscriptionGroupIdIOS':
          sourcePayload['subscriptionGroupIdIOS']?.toString(),
      'transactionId': transactionIdSelection.isPresent
          ? sourceTransactionId ?? ''
          // The legacy `id` transaction fallback ends in 10.0.0.
          : purchaseId,
      'transactionReasonIOS': sourcePayload['transactionReasonIOS']?.toString(),
      'webOrderLineItemIdIOS':
          sourcePayload['webOrderLineItemIdIOS']?.toString(),
      'revocationDateIOS':
          _parseTimestampMilliseconds(sourcePayload['revocationDateIOS']),
      'revocationReasonIOS': sourcePayload['revocationReasonIOS']?.toString(),
    };

    return gentype.PurchaseIOS.fromJson(map);
  }

  throw const FormatException('Unsupported platform for legacy purchase');
}