import { createServiceRoleClient } from '../_shared/quota.ts' import { createGooglePlayPurchaseApi, fetchGooglePlaySubscription, GOOGLE_PLAY_PRODUCT_TIERS, GooglePlayVerificationError, verifyGooglePlaySubscriptionPayload, type NormalizedGooglePlayPurchase, } from '../_shared/google-play.ts' import { applyGooglePlayPurchase } from '../_shared/google-play-apply.ts' import { createSupabaseGooglePlayPurchaseStore, type RegisteredGooglePlayPurchase, } from '../_shared/google-play-purchase-store.ts' import { GooglePubSubError, parseGooglePlayRtdn, verifyGooglePubSubIdentity, } from '../_shared/google-pubsub.ts' function jsonResponse(body: Record, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }) } Deno.serve(async (req: Request) => { if (req.method !== 'POST') return jsonResponse({ error: 'method_not_allowed' }, 405) let eventId: string | null = null const serviceClient = createServiceRoleClient() const purchaseStore = createSupabaseGooglePlayPurchaseStore(serviceClient) try { await verifyGooglePubSubIdentity(req) const notification = parseGooglePlayRtdn(await req.json()) if (notification.kind === 'test') { return jsonResponse({ success: true, test: true }) } let purchaseRecord: RegisteredGooglePlayPurchase | null = await purchaseStore.findPurchase( notification.purchaseToken, ) let preverifiedPurchase: NormalizedGooglePlayPurchase | null = null // Google Play subscriptions-center re-subscriptions can notify the server // before the app has registered the new token. Map those only through the // prior token that our database already owns; never guess a user. if (!purchaseRecord) { const verification = await fetchGooglePlaySubscription(notification.purchaseToken) const recognizedItems = (verification.lineItems ?? []).filter((item) => ( typeof item.productId === 'string' && GOOGLE_PLAY_PRODUCT_TIERS[item.productId] !== undefined )) const expiredToken = verification.outOfAppPurchaseContext?.expiredPurchaseToken if ( recognizedItems.length !== 1 || typeof expiredToken !== 'string' || expiredToken.length < 8 || expiredToken.length > 4096 ) { throw new GooglePubSubError('purchase_not_registered', 503) } const previous = await purchaseStore.findPurchase(expiredToken) if (!previous) { throw new GooglePubSubError('purchase_not_registered', 503) } const productId = recognizedItems[0].productId as string purchaseRecord = { userId: previous.userId, productId } preverifiedPurchase = await verifyGooglePlaySubscriptionPayload( previous.userId, productId, verification, (candidate) => Promise.resolve(candidate === expiredToken), ) } const { data: insertedEvent, error: insertError } = await serviceClient .from('store_notification_events') .insert({ platform: 'google_play', message_id: notification.messageId, event_type: `subscription:${notification.notificationType}`, }) .select('id') .single() if (insertError) { if (insertError.code !== '23505') throw new Error('notification_insert_failed') const { data: existingEvent, error: existingError } = await serviceClient .from('store_notification_events') .select('id, processed_at') .eq('platform', 'google_play') .eq('message_id', notification.messageId) .single() if (existingError || !existingEvent) throw new Error('notification_lookup_failed') const existingRecord = existingEvent as { id?: unknown; processed_at?: unknown } if (existingRecord.processed_at) return jsonResponse({ success: true, duplicate: true }) if (typeof existingRecord.id !== 'string') throw new Error('notification_lookup_failed') eventId = existingRecord.id } else { const insertedRecord = insertedEvent as { id?: unknown } if (typeof insertedRecord.id !== 'string') throw new Error('notification_insert_failed') eventId = insertedRecord.id } await applyGooglePlayPurchase( { playApi: createGooglePlayPurchaseApi(fetch), store: purchaseStore }, { userId: purchaseRecord.userId, productId: purchaseRecord.productId, purchaseToken: notification.purchaseToken, preverified: preverifiedPurchase, }, ) if (!eventId) throw new Error('notification_lookup_failed') const { error: completeError } = await serviceClient .from('store_notification_events') .update({ processed_at: new Date().toISOString(), processing_error: null }) .eq('id', eventId) if (completeError) throw new Error('notification_completion_failed') return jsonResponse({ success: true }) } catch (error) { if (eventId) { const errorCode = error instanceof GooglePlayVerificationError || error instanceof GooglePubSubError ? error.code : 'notification_processing_failed' await serviceClient .from('store_notification_events') .update({ processing_error: errorCode }) .eq('id', eventId) } if (error instanceof GooglePubSubError || error instanceof GooglePlayVerificationError) { return jsonResponse({ error: error.code }, error.status) } return jsonResponse({ error: 'notification_processing_failed' }, 500) } })