diff --git a/src/utils/proUnlock.ts b/src/utils/proUnlock.ts index 778b55a..5a56233 100644 --- a/src/utils/proUnlock.ts +++ b/src/utils/proUnlock.ts @@ -50,6 +50,11 @@ const grant = async () => { await AsyncStorage.setItem(CACHE_KEY, '1').catch(() => {}); }; +const revoke = async () => { + set({ owned: false, error: null }); + await AsyncStorage.removeItem(CACHE_KEY).catch(() => {}); +}; + // Play hands the purchase to the listener. Grant it, then finalise: an Android // purchase left unacknowledged for three days is automatically refunded. // ponytail: the grant is client-side. Server-side verification of @@ -80,7 +85,19 @@ export async function initProUnlock() { ]); const listed = (Array.isArray(products) ? products : []).find((p) => p.id === PRO_SKU); set({ price: listed?.displayPrice ?? null }); - if (purchases.some((p) => p.productId === PRO_SKU)) await grant(); + const bought = purchases.find((p) => p.productId === PRO_SKU); + if (bought) { + await grant(); + // A purchase Play never got from us finalised (the app was killed between + // the two) is refunded automatically after three days. Harmless to repeat. + await finishTransaction({ purchase: bought as never, isConsumable: false }).catch(() => {}); + } else if (listed) { + // The store is the authority: a refunded purchase is no longer in the + // list, so the cache must not outlive it. Only when the query ANSWERED and + // the product is still on sale — an offline or broken query also returns + // nothing, and that must never revoke a paying user. + await revoke(); + } } catch (e) { set({ error: e instanceof Error ? e.message : 'Store unavailable' }); }