d3ro-voice/apps/mobile-rn/__tests__/billing-restore-provider.test.tsx
2026-08-29 18:33:45 +09:00

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())
})
})