152 lines
5 KiB
TypeScript
152 lines
5 KiB
TypeScript
import React from 'react'
|
|
import ReactTestRenderer, { act } from 'react-test-renderer'
|
|
import { Platform } from 'react-native'
|
|
|
|
jest.mock('../src/lib/auth-context', () => ({ useAuth: jest.fn() }))
|
|
jest.mock('../src/lib/entitlement-context', () => ({ useEntitlement: jest.fn() }))
|
|
jest.mock('../src/lib/native-config', () => ({ getMobileRuntimeConfig: jest.fn() }))
|
|
jest.mock('../src/lib/supabase', () => ({
|
|
supabase: { functions: { invoke: jest.fn() } },
|
|
}))
|
|
|
|
const mockedAuth = jest.requireMock('../src/lib/auth-context') as { useAuth: jest.Mock }
|
|
const mockedEntitlement = jest.requireMock('../src/lib/entitlement-context') as {
|
|
useEntitlement: jest.Mock
|
|
}
|
|
const mockedConfig = jest.requireMock('../src/lib/native-config') as {
|
|
getMobileRuntimeConfig: jest.Mock
|
|
}
|
|
const mockedSupabase = jest.requireMock('../src/lib/supabase').supabase as {
|
|
functions: { invoke: jest.Mock }
|
|
}
|
|
const mockedIap = jest.requireActual('react-native-iap') as {
|
|
getAvailablePurchases: jest.Mock
|
|
useIAP: jest.Mock
|
|
__stableUseIapResult: Record<string, unknown> & { restorePurchases: jest.Mock }
|
|
}
|
|
|
|
import { BillingProvider, useBilling } from '../src/lib/billing-context'
|
|
|
|
const USER_ID = '11111111-1111-4111-8111-111111111111'
|
|
const PURCHASE_ID = '22222222-2222-4222-8222-222222222222'
|
|
const PRODUCT_ID = 'd3ro_voice_pro_monthly'
|
|
const TOKEN = `play_${'a'.repeat(32)}`
|
|
const originalPlatform = Platform.OS
|
|
let latestBilling: ReturnType<typeof useBilling> | null = null
|
|
|
|
function Probe(): null {
|
|
latestBilling = useBilling()
|
|
return null
|
|
}
|
|
|
|
describe('BillingProvider explicit restore flow', () => {
|
|
beforeAll(() => {
|
|
Object.defineProperty(Platform, 'OS', { configurable: true, value: 'android' })
|
|
})
|
|
|
|
afterAll(() => {
|
|
Object.defineProperty(Platform, 'OS', { configurable: true, value: originalPlatform })
|
|
})
|
|
|
|
beforeEach(() => {
|
|
latestBilling = null
|
|
mockedAuth.useAuth.mockReturnValue({ user: { id: USER_ID } })
|
|
mockedConfig.getMobileRuntimeConfig.mockReturnValue({ installedFromPlayStore: true })
|
|
mockedIap.__stableUseIapResult.restorePurchases.mockClear()
|
|
mockedIap.__stableUseIapResult.connected = true
|
|
mockedIap.__stableUseIapResult.subscriptions = []
|
|
mockedIap.useIAP.mockImplementation(() => mockedIap.__stableUseIapResult)
|
|
mockedIap.getAvailablePurchases.mockReset().mockResolvedValue([{
|
|
id: 'order-1',
|
|
productId: PRODUCT_ID,
|
|
purchaseToken: TOKEN,
|
|
purchaseState: 'purchased',
|
|
store: 'google',
|
|
transactionDate: Date.parse('2026-08-21T00:00:00.000Z'),
|
|
quantity: 1,
|
|
isAutoRenewing: true,
|
|
packageNameAndroid: 'com.d3ro.voice',
|
|
}])
|
|
mockedSupabase.functions.invoke.mockReset().mockResolvedValue({
|
|
data: {
|
|
purchase: { purchase_id: PURCHASE_ID, acknowledged: true },
|
|
verification: {
|
|
product_id: PRODUCT_ID,
|
|
purchase_state: 'purchased',
|
|
entitled: true,
|
|
acknowledged: true,
|
|
},
|
|
finish_transaction: false,
|
|
server_acknowledged: true,
|
|
},
|
|
error: null,
|
|
})
|
|
mockedEntitlement.useEntitlement.mockReturnValue({
|
|
snapshot: {
|
|
tier: 'free',
|
|
provider: 'none',
|
|
storeProductId: null,
|
|
},
|
|
refresh: jest.fn(async () => ({
|
|
tier: 'pro',
|
|
status: 'active',
|
|
provider: 'google_play',
|
|
paymentProvider: 'google_play',
|
|
currentPeriodStart: '2026-08-21T00:00:00.000Z',
|
|
currentPeriodEnd: '2026-09-21T00:00:00.000Z',
|
|
cancelAt: null,
|
|
autoRenewing: true,
|
|
storeProductId: PRODUCT_ID,
|
|
overageCredits: 0,
|
|
usageToday: {},
|
|
purchases: [{
|
|
id: PURCHASE_ID,
|
|
platform: 'google_play',
|
|
productId: PRODUCT_ID,
|
|
state: 'purchased',
|
|
purchaseAt: '2026-08-21T00:00:00.000Z',
|
|
expiresAt: '2026-09-21T00:00:00.000Z',
|
|
autoRenewing: true,
|
|
verifiedAt: '2026-08-21T00:00:01.000Z',
|
|
}],
|
|
adFree: true,
|
|
refreshedAt: '2026-08-21T00:00:02.000Z',
|
|
})),
|
|
})
|
|
})
|
|
|
|
test('awaits the direct store list, server verification, and refreshed entitlement', async () => {
|
|
let renderer: ReactTestRenderer.ReactTestRenderer
|
|
await act(async () => {
|
|
renderer = ReactTestRenderer.create(
|
|
<BillingProvider><Probe /></BillingProvider>,
|
|
)
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await act(async () => {
|
|
await latestBilling!.restore()
|
|
})
|
|
|
|
expect(latestBilling).toMatchObject({
|
|
connected: true,
|
|
storeAvailable: true,
|
|
operation: 'succeeded',
|
|
errorCode: null,
|
|
})
|
|
expect(mockedIap.getAvailablePurchases).toHaveBeenCalledWith({
|
|
includeSuspendedAndroid: true,
|
|
})
|
|
expect(mockedIap.__stableUseIapResult.restorePurchases).not.toHaveBeenCalled()
|
|
expect(mockedSupabase.functions.invoke).toHaveBeenCalledWith('iap-verify', {
|
|
body: {
|
|
platform: 'google_play',
|
|
productId: PRODUCT_ID,
|
|
purchaseToken: TOKEN,
|
|
},
|
|
})
|
|
expect(mockedEntitlement.useEntitlement.mock.results[0].value.refresh)
|
|
.toHaveBeenCalledTimes(1)
|
|
act(() => renderer!.unmount())
|
|
})
|
|
})
|