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