8.4 C
New York
Thursday, April 10, 2025

ios – Sandbox Receipt Validation Fails with Error 21003 on Bodily Gadget (Firebase Cloud RUN Perform & App Retailer Join)


I am creating an iOS app that makes use of in-app purchases (IAP) configured by way of App Retailer Join. For receipt validation, I’m utilizing a Firebase Cloud Perform to ahead the receipt knowledge to Apple’s sandbox verification endpoint. My setup is as follows:

Testing on a bodily iOS gadget utilizing a sandbox check account.
I’m utilizing the IAP merchandise as outlined in App Retailer Join.
The consumer retrieves the receipt utilizing SKReceiptManager.retrieveReceiptData() (by way of my customized helper) and sends it—together with the product ID—to my Firebase Cloud Perform.
I’ve verified that my APPLE_SHARED_SECRET (handed within the payload beneath the “password” key) is ready by way of atmosphere variables.
Regardless of checking that the receipt isn’t empty and trimming any additional whitespace, I repeatedly obtain an error from Apple with standing code 21003 (“The receipt couldn’t be authenticated”).
Beneath are related code snippets from my implementation:

  cors(req, res, async () => {
    attempt {
      if (req.technique !== 'POST') {
        return res.standing(405).ship({ error: 'Technique not allowed. Use POST as an alternative.' });
      }

      // Validate request physique
      if (!req.physique.receipt) {
        return res.standing(400).ship({ error: 'Lacking receipt knowledge' });
      }
      if (!req.physique.subscriptionId) {
        return res.standing(400).ship({ error: 'Lacking subscription ID' });
      }

      // Authenticate consumer (utilizing Firebase Auth)
      const userId = await getUserIdFromAuth(req);
      if (!userId) {
        return res.standing(401).ship({ error: 'Authentication required' });
      }

      // Examine shared secret availability
      if (!course of.env.APPLE_SHARED_SECRET) {
        capabilities.logger.error('Lacking APPLE_SHARED_SECRET atmosphere variable');
        return res.standing(500).ship({ error: 'Server configuration error' });
      }

      // Clear receipt knowledge and set subscription ID
      const receiptData = req.physique.receipt.trim();
      const subscriptionId = req.physique.subscriptionId;

      // Create payload for Apple's validation API
      const requestData = {
        'receipt-data': receiptData,
        'password': course of.env.APPLE_SHARED_SECRET,
        'exclude-old-transactions': false,
      };

      // Try validation utilizing sandbox endpoint first
      let appleResponse;
      let appleStatus;
      attempt {
        capabilities.logger.information('Making an attempt sandbox validation first');
        appleResponse = await axios.publish(APPLE_SANDBOX_URL, requestData, {
          timeout: 10000,
          headers: { 'Content material-Kind': 'software/json' },
        });
        appleStatus = appleResponse.knowledge.standing;

        // If we get manufacturing error code 21008, attempt manufacturing as an alternative.
        if (appleStatus === 21008) {
          capabilities.logger.information('Receipt is from manufacturing atmosphere; retrying with manufacturing endpoint');
          appleResponse = await axios.publish(APPLE_PRODUCTION_URL, requestData, {
            timeout: 10000,
            headers: { 'Content material-Kind': 'software/json' },
          });
          appleStatus = appleResponse.knowledge.standing;
        }
      } catch (appleError) {
        capabilities.logger.error('Error contacting Apple servers:', appleError);
        throw new Error(`Did not contact Apple servers: ${appleError.message}`);
      }

      // Examine Apple's response standing
      if (appleStatus === 0) {
        // Course of legitimate receipt (omitting particulars for brevity)
        return res.standing(200).ship({
          verified: true,
          message: 'Subscription is lively or validated',
          // further fields...
        });
      } else if (appleStatus === 21003) {
        capabilities.logger.error('Receipt authentication failed (21003) - probably a shared secret mismatch');
        return res.standing(400).ship({
          verified: false,
          error: 'Receipt validation failed: The receipt couldn't be authenticated',
          code: 21003,
          description: getAppleStatusDescription(21003),
          resolution: 'Guarantee your shared secret is appropriate and matches the one in App Retailer Join',
        });
      } else {
        capabilities.logger.error(`Apple verification error: Standing ${appleStatus}`);
        return res.standing(400).ship({
          verified: false,
          error: `Receipt validation failed with standing: ${appleStatus}`,
          code: appleStatus,
          description: getAppleStatusDescription(appleStatus),
        });
      }
    } catch (error) {
      capabilities.logger.error('Error in validateIosPurchase:', error);
      return res.standing(500).ship({
        verified: false,
        error: `Validation error: ${error.message}`,
      });
    }
  });
});
  attempt {
    // Guarantee consumer is authenticated by way of Firebase
    last FirebaseAuth auth = FirebaseAuth.occasion;
    last Person? consumer = auth.currentUser;
    if (consumer == null) {
      throw Exception('No authenticated consumer discovered. Please log in first.');
    }

    // Retrieve and log Firebase ID token for debugging
    last idToken = await consumer.getIdToken(true);
    debugPrint('Firebase ID token (partial): ${idToken.substring(0, 20)}...');

    // Delay to let the token propagate
    await Future.delayed(const Period(milliseconds: 300));

    if (Platform.isIOS) {
      attempt {
        // Retrieve receipt knowledge utilizing customized SKReceiptManager technique
        last String receiptData = await SKReceiptManager.retrieveReceiptData();
        if (receiptData.isEmpty) {
          throw Exception('Receipt knowledge is empty');
        }
        debugPrint('Calling validateIosPurchase with receipt size: ${receiptData.size}');

        // Ship receipt to Firebase HTTP perform for validation
        last httpFirebaseService = HttpFirebaseService();
        last verificationData = await httpFirebaseService.validateIosPurchase(
          receiptData,
          buy.productID,
        );

        if (verificationData['verified'] == true) {
          // Replace consumer subscription state right here...
          await _updateUserPremiumStatus(
            userId: consumer.uid,
            productId: buy.productID,
            expiryDateString: verificationData['expiryDate'],
            transactionId: verificationData['transaction_id'],
            originalTransactionId: verificationData['original_transaction_id'],
          );
          debugPrint('Buy verified and premium standing up to date');
          await _inAppPurchase.completePurchase(buy);
        } else {
          debugPrint('Receipt verification failed: ${verificationData['error']}');
          throw Exception(verificationData['error']);
        }
      } catch (e) {
        debugPrint('Error throughout receipt verification: $e');
        await _inAppPurchase.completePurchase(buy);
        rethrow;
      }
    } else {
      throw UnimplementedError('Android buy stream is just not but supported.');
    }
  } catch (e) {
    debugPrint('Buy verification error: $e');
    rethrow;
  }
}

Has anybody skilled this concern and will assist me resolve my concern

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles