|
| 1 | +import { |
| 2 | + NotificationTypeV2, |
| 3 | + Subtype, |
| 4 | + type Environment, |
| 5 | + type JWSRenewalInfoDecodedPayload, |
| 6 | + type ResponseBodyV2DecodedPayload, |
| 7 | +} from '@apple/app-store-server-library'; |
| 8 | +import { logger } from '../../logger'; |
| 9 | +import { |
| 10 | + SubscriptionProvider, |
| 11 | + UserSubscriptionStatus, |
| 12 | + type User, |
| 13 | + type UserSubscriptionFlags, |
| 14 | +} from '../../entity/user/User'; |
| 15 | +import { convertCurrencyToUSD } from '../../integrations/openExchangeRates'; |
| 16 | +import { updateStoreKitUserSubscription } from '../../plusSubscription'; |
| 17 | +import { isNullOrUndefined } from '../object'; |
| 18 | +import { |
| 19 | + getAnalyticsEventFromAppleNotification, |
| 20 | + logAppleAnalyticsEvent, |
| 21 | +} from './utils'; |
| 22 | +import { notifyNewStoreKitSubscription } from '../../routes/webhooks/apple'; |
| 23 | +import { productIdToCycle } from './types'; |
| 24 | + |
| 25 | +const getSubscriptionStatus = ( |
| 26 | + notificationType: ResponseBodyV2DecodedPayload['notificationType'], |
| 27 | + subtype?: ResponseBodyV2DecodedPayload['subtype'], |
| 28 | +): UserSubscriptionStatus => { |
| 29 | + switch (notificationType) { |
| 30 | + case NotificationTypeV2.SUBSCRIBED: |
| 31 | + case NotificationTypeV2.DID_RENEW: |
| 32 | + case NotificationTypeV2.DID_CHANGE_RENEWAL_PREF: // Upgrade/Downgrade |
| 33 | + case NotificationTypeV2.REFUND_REVERSED: |
| 34 | + return UserSubscriptionStatus.Active; |
| 35 | + case NotificationTypeV2.DID_CHANGE_RENEWAL_STATUS: // Disable/Enable Auto-Renew |
| 36 | + return subtype === Subtype.AUTO_RENEW_ENABLED |
| 37 | + ? UserSubscriptionStatus.Active |
| 38 | + : UserSubscriptionStatus.Cancelled; |
| 39 | + case NotificationTypeV2.DID_FAIL_TO_RENEW: |
| 40 | + // When user fails to renew and there is no grace period |
| 41 | + if (isNullOrUndefined(subtype)) { |
| 42 | + return UserSubscriptionStatus.Cancelled; |
| 43 | + } |
| 44 | + case NotificationTypeV2.GRACE_PERIOD_EXPIRED: |
| 45 | + return UserSubscriptionStatus.Cancelled; |
| 46 | + case NotificationTypeV2.REFUND: |
| 47 | + case NotificationTypeV2.EXPIRED: |
| 48 | + case NotificationTypeV2.REVOKE: // We don't support Family Sharing, but to be on the safe side |
| 49 | + return UserSubscriptionStatus.Expired; |
| 50 | + default: |
| 51 | + logger.error( |
| 52 | + { |
| 53 | + notificationType, |
| 54 | + subtype, |
| 55 | + provider: SubscriptionProvider.AppleStoreKit, |
| 56 | + }, |
| 57 | + 'Unknown notification type', |
| 58 | + ); |
| 59 | + throw new Error('Unknown notification type'); |
| 60 | + } |
| 61 | +}; |
| 62 | + |
| 63 | +const renewalInfoToSubscriptionFlags = ( |
| 64 | + data: JWSRenewalInfoDecodedPayload, |
| 65 | +): UserSubscriptionFlags => { |
| 66 | + const cycle = |
| 67 | + productIdToCycle[data.autoRenewProductId as keyof typeof productIdToCycle]; |
| 68 | + |
| 69 | + if (isNullOrUndefined(cycle)) { |
| 70 | + logger.error( |
| 71 | + { data, provider: SubscriptionProvider.AppleStoreKit }, |
| 72 | + 'Invalid auto renew product ID', |
| 73 | + ); |
| 74 | + throw new Error('Invalid auto renew product ID'); |
| 75 | + } |
| 76 | + |
| 77 | + return { |
| 78 | + cycle, |
| 79 | + subscriptionId: data.originalTransactionId, |
| 80 | + createdAt: new Date(data.recentSubscriptionStartDate!), |
| 81 | + expiresAt: new Date(data.renewalDate!), |
| 82 | + provider: SubscriptionProvider.AppleStoreKit, |
| 83 | + }; |
| 84 | +}; |
| 85 | + |
| 86 | +export const handleAppleSubscription = async ({ |
| 87 | + decodedInfo, |
| 88 | + user, |
| 89 | + environment, |
| 90 | + notification, |
| 91 | +}: { |
| 92 | + decodedInfo: JWSRenewalInfoDecodedPayload; |
| 93 | + user: User; |
| 94 | + environment: Environment; |
| 95 | + notification: ResponseBodyV2DecodedPayload; |
| 96 | +}) => { |
| 97 | + // Prevent double subscription |
| 98 | + if (user.subscriptionFlags?.provider === SubscriptionProvider.Paddle) { |
| 99 | + logger.error( |
| 100 | + { |
| 101 | + user, |
| 102 | + environment, |
| 103 | + notification, |
| 104 | + provider: SubscriptionProvider.AppleStoreKit, |
| 105 | + }, |
| 106 | + 'User already has a Paddle subscription', |
| 107 | + ); |
| 108 | + throw new Error('User already has a Paddle subscription'); |
| 109 | + } |
| 110 | + |
| 111 | + const subscriptionStatus = getSubscriptionStatus( |
| 112 | + notification.notificationType, |
| 113 | + notification.subtype, |
| 114 | + ); |
| 115 | + |
| 116 | + const subscriptionFlags = renewalInfoToSubscriptionFlags(decodedInfo); |
| 117 | + |
| 118 | + await updateStoreKitUserSubscription({ |
| 119 | + userId: user.id, |
| 120 | + status: subscriptionStatus, |
| 121 | + data: subscriptionFlags, |
| 122 | + }); |
| 123 | + |
| 124 | + const currencyInUSD = await convertCurrencyToUSD( |
| 125 | + (decodedInfo.renewalPrice || 0) / 1000, |
| 126 | + decodedInfo.currency || 'USD', |
| 127 | + ); |
| 128 | + |
| 129 | + const eventName = getAnalyticsEventFromAppleNotification( |
| 130 | + notification.notificationType, |
| 131 | + notification.subtype, |
| 132 | + ); |
| 133 | + |
| 134 | + if (eventName) { |
| 135 | + await logAppleAnalyticsEvent(decodedInfo, eventName, user, currencyInUSD); |
| 136 | + } |
| 137 | + |
| 138 | + if (notification.notificationType === NotificationTypeV2.SUBSCRIBED) { |
| 139 | + await notifyNewStoreKitSubscription(decodedInfo, user, currencyInUSD); |
| 140 | + } |
| 141 | +}; |
0 commit comments