fix(iap): acknowledge Google Play purchases only after the entitlement is persisted
This commit is contained in:
parent
1eb22af1f3
commit
957e136789
6 changed files with 667 additions and 144 deletions
|
|
@ -2,11 +2,14 @@ import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
|
|||
import { authErrorResponse, requireUser, type AuthError } from '../_shared/auth.ts'
|
||||
import { createServiceRoleClient } from '../_shared/quota.ts'
|
||||
import {
|
||||
acknowledgeGooglePlaySubscription,
|
||||
createGooglePlayPurchaseApi,
|
||||
GooglePlayVerificationError,
|
||||
sha256Hex,
|
||||
verifyGooglePlaySubscription,
|
||||
} from '../_shared/google-play.ts'
|
||||
import { applyGooglePlayPurchase } from '../_shared/google-play-apply.ts'
|
||||
import {
|
||||
createSupabaseGooglePlayPurchaseStore,
|
||||
GooglePlayPurchasePersistenceError,
|
||||
} from '../_shared/google-play-purchase-store.ts'
|
||||
|
||||
interface VerifyPurchaseRequest {
|
||||
platform?: unknown
|
||||
|
|
@ -40,69 +43,20 @@ Deno.serve(async (req: Request) => {
|
|||
return jsonResponse({ error: 'invalid_request' }, 400)
|
||||
}
|
||||
|
||||
const serviceClient = createServiceRoleClient()
|
||||
const ownsExpiredPurchaseToken = async (expiredToken: string): Promise<boolean> => {
|
||||
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,
|
||||
const { purchase, stored } = await applyGooglePlayPurchase(
|
||||
{
|
||||
playApi: createGooglePlayPurchaseApi(fetch),
|
||||
store: createSupabaseGooglePlayPurchaseStore(createServiceRoleClient()),
|
||||
},
|
||||
{
|
||||
userId: user.id,
|
||||
productId: body.productId,
|
||||
purchaseToken: body.purchaseToken,
|
||||
},
|
||||
)
|
||||
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,
|
||||
purchase: stored,
|
||||
verification: {
|
||||
product_id: purchase.productId,
|
||||
purchase_state: purchase.purchaseState,
|
||||
|
|
@ -122,6 +76,9 @@ Deno.serve(async (req: Request) => {
|
|||
return authErrorResponse(error as AuthError, corsHeaders)
|
||||
}
|
||||
}
|
||||
if (error instanceof GooglePlayPurchasePersistenceError && error.code !== 'purchase_persistence_failed') {
|
||||
return jsonResponse({ error: error.code }, 409)
|
||||
}
|
||||
if (error instanceof GooglePlayVerificationError) {
|
||||
return jsonResponse({ error: error.code }, error.status)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue