import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { authErrorResponse, requireUser, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { acknowledgeGooglePlaySubscription, GooglePlayVerificationError, sha256Hex, verifyGooglePlaySubscription, } from '../_shared/google-play.ts' interface VerifyPurchaseRequest { platform?: unknown productId?: unknown purchaseToken?: unknown } function jsonResponse(body: Record, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return jsonResponse({ error: 'method_not_allowed' }, 405) } try { const user = await requireUser(req) const body = await req.json() as VerifyPurchaseRequest if ( body.platform !== 'google_play' || typeof body.productId !== 'string' || typeof body.purchaseToken !== 'string' ) { return jsonResponse({ error: 'invalid_request' }, 400) } const serviceClient = createServiceRoleClient() const ownsExpiredPurchaseToken = async (expiredToken: string): Promise => { const { data: previous, error: previousError } = await serviceClient .from('iap_purchases') .select('id') .eq('platform', 'google_play') .eq('user_id', user.id) .eq('token_hash', await sha256Hex(expiredToken)) .maybeSingle() if (previousError) throw new Error('expired_purchase_lookup_failed') return previous !== null } let purchase = await verifyGooglePlaySubscription( user.id, body.productId, body.purchaseToken, fetch, ownsExpiredPurchaseToken, ) if (purchase.entitled && !purchase.acknowledged) { await acknowledgeGooglePlaySubscription(purchase.productId, body.purchaseToken) purchase = { ...purchase, acknowledged: true, verification: { ...purchase.verification, acknowledgementState: 'ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED', }, } } const { data, error } = await serviceClient.rpc('apply_verified_google_play_purchase', { p_user_id: user.id, p_platform: purchase.platform, p_product_id: purchase.productId, p_store_transaction_id: purchase.storeTransactionId, p_token_hash: await sha256Hex(body.purchaseToken), p_linked_token_hash: purchase.linkedPurchaseToken ? await sha256Hex(purchase.linkedPurchaseToken) : null, p_purchase_token: body.purchaseToken, p_purchase_state: purchase.purchaseState, p_purchase_at: purchase.purchaseAt, p_expires_at: purchase.expiresAt, p_auto_renewing: purchase.autoRenewing, p_acknowledged: purchase.acknowledged, p_tier: purchase.tier, p_entitled: purchase.entitled, p_verification: purchase.verification, }) if (error) { if (error.message.includes('purchase_owned_by_other_user')) { return jsonResponse({ error: 'purchase_owned_by_other_user' }, 409) } if (error.message.includes('active_subscription_other_provider')) { return jsonResponse({ error: 'active_subscription_other_provider' }, 409) } throw new Error('purchase_persistence_failed') } return jsonResponse({ purchase: data, verification: { product_id: purchase.productId, purchase_state: purchase.purchaseState, entitled: purchase.entitled, acknowledged: purchase.acknowledged, }, finish_transaction: false, server_acknowledged: purchase.acknowledged, }) } catch (error) { if (error && typeof error === 'object' && 'status' in error && 'message' in error) { const candidate = error as { status: unknown; message: unknown } if ( (candidate.status === 401 || candidate.status === 403) && typeof candidate.message === 'string' ) { return authErrorResponse(error as AuthError, corsHeaders) } } if (error instanceof GooglePlayVerificationError) { return jsonResponse({ error: error.code }, error.status) } return jsonResponse({ error: 'purchase_verification_failed' }, 500) } })