Hello,
I am currently implementing server-side handling for in-app subscription payments and using App Store Server Notifications V2 to receive notifications with a TestFlight account.
However, I am not receiving EXPIRED
notifications, although I am successfully receiving other notifications such as SUBSCRIBED
, DID_RENEW
, and DID_CHANGE_RENEWAL_PREF
.
Here are some details of my observations:
- Until a certain point, I was receiving EXPIRED notifications without any issues, but they stopped coming in after that point.
- Each subscription renews every 3 minutes in TestFlight account, and I receive DID_RENEW notifications 7 to 12 times. According to the official documentation, the subscriptions should renew up to 12 times, but I am not receiving exactly 12 DID_RENEW notifications. This inconsistency might be related to the issue.
- Other notifications (
SUBSCRIBED
,DID_RENEW
,DID_CHANGE_RENEWAL_PREF
) are received without any issues.
Could anyone provide insight into why this might be happening and suggest any alternative methods to handle subscription expirations in case the EXPIRED
notifications are not reliable?
Thank you for your assistance.
Relevant Official Documentation
App Store Server Notifications V2
App Store Server Notifications V2_notificationType
Testing in-app purchases with sandbox
Current Server Implementation
Below is the Kotlin Spring Boot server code currently implemented for handling App Store Server Notifications V2:
@RestController
class ProductIosController(
private val productIosService: ProductIosService,
private val appStoreNotificationService: AppStoreNotificationService,
) : BaseController {
@PostMapping("/api/v1/ios-products/app-store-notifications-v2")
fun handleNotification(@RequestBody @Valid notification: AppStoreNotificationRequest): CustomResponse {
appStoreNotificationService.processNotification(notification)
return CustomResponse.ok()
}
}
@Service
class AppStoreNotificationService(
@Qualifier("appStoreClient") private val appStoreServerAPIClient: AppStoreServerAPIClient,
@Qualifier("signedVerifier") private val signedDataVerifier: SignedDataVerifier,
) {
@Transactional
fun processNotification(notification: AppStoreNotificationRequest) {
logger.info("signedPayload: ${notification.signedPayload}")
val decodedPayload = verifyAndDecodeSignedPayload(notification.signedPayload)
val notificationType = decodedPayload.notificationType
val signedTransactionInfo = decodedPayload.data.signedTransactionInfo
val transaction = signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo)
val (user, product) = fetchUserAndProduct(transaction)
when (notificationType) {
SUBSCRIBED -> processSubscriptionPurchase(user, product, decodedPayload, transaction)
DID_CHANGE_RENEWAL_PREF -> processSubscriptionGradeChange(user, product, decodedPayload, transaction)
DID_CHANGE_RENEWAL_STATUS -> processRenewalStatusChange(transaction)
OFFER_REDEEMED -> processOfferRedeemed(transaction)
DID_RENEW -> processSubscriptionRenewal(user, product, decodedPayload, transaction)
EXPIRED -> processSubscriptionExpiration(user, product)
DID_FAIL_TO_RENEW -> processFailedRenewal(transaction)
GRACE_PERIOD_EXPIRED -> processSubscriptionGracePeriodExpiration(transaction)
PRICE_INCREASE -> processPriceIncrease(transaction)
REFUND -> processSubscriptionRefund(transaction)
REFUND_DECLINED -> processRefundDeclined(transaction)
CONSUMPTION_REQUEST -> processConsumptionRequest(transaction)
RENEWAL_EXTENDED -> processRenewalExtension(transaction)
REVOKE -> processSubscriptionRevocation(transaction)
TEST -> processTestNotification(transaction)
RENEWAL_EXTENSION -> processRenewalExtension(transaction)
REFUND_REVERSED -> processRefundReversed(transaction)
EXTERNAL_PURCHASE_TOKEN -> processExternalPurchaseToken(transaction)
else -> logger.warn("Unsupported notification type: ${notificationType.value}")
}
}