diff --git a/src/screens/Subscriptions/index.tsx b/src/screens/Subscriptions/index.tsx index 5230ff052e..c2950f98e4 100644 --- a/src/screens/Subscriptions/index.tsx +++ b/src/screens/Subscriptions/index.tsx @@ -28,7 +28,11 @@ export function Subscriptions(_props: ScreenProps) { const {_} = useLingui() // const {data: entitlements} = useEntitlements() - const {data: subscriptions, isError} = useMainSubscriptions() + const {data: subscriptions, isError, error} = useMainSubscriptions() + + if (error) { + console.log(error) + } return ( @@ -96,17 +100,21 @@ export function MainSubscription({ style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}> {subscriptions.available.monthly.map(s => ( - + @@ -114,17 +122,21 @@ export function MainSubscription({ {subscriptions.available.annual.map(s => ( - + diff --git a/src/state/purchases/subscriptions/api/index.ts b/src/state/purchases/subscriptions/api/index.ts index 9be0595454..c81da3f8ca 100644 --- a/src/state/purchases/subscriptions/api/index.ts +++ b/src/state/purchases/subscriptions/api/index.ts @@ -1,5 +1,5 @@ -import {RawSubscriptionObject} from '#/state/purchases/subscriptions/api/types' -import {EntitlementId} from '#/state/purchases/subscriptions/types' +import {Subscription} from '#/state/purchases/subscriptions/api/types' +import {Entitlement} from '#/state/purchases/subscriptions/types' export async function getMainSubscriptions({ did, @@ -23,8 +23,8 @@ export async function getMainSubscriptions({ const json = await res.json() return json as { - active: RawSubscriptionObject[] - available: RawSubscriptionObject[] + active: Subscription[] + available: Subscription[] } } @@ -40,5 +40,5 @@ export async function getEntitlements({did}: {did: string}) { const {entitlements} = await res.json() - return entitlements as {id: EntitlementId}[] + return entitlements as Entitlement[] } diff --git a/src/state/purchases/subscriptions/api/types.ts b/src/state/purchases/subscriptions/api/types.ts index e4e5f19012..780c10b068 100644 --- a/src/state/purchases/subscriptions/api/types.ts +++ b/src/state/purchases/subscriptions/api/types.ts @@ -1,20 +1,11 @@ -import {SubscriptionId} from '#/state/purchases/subscriptions/types' +import { + EntitlementId, + SubscriptionId, +} from '#/state/purchases/subscriptions/types' -export type RawSubscriptionObjectBase = T & { - id: SubscriptionId - storeId: string - lookupKey: string - checkoutId: string - interval: 'monthly' | 'annual' +export type SubscriptionState = { active: boolean - autoRenewStatus: - | 'will_renew' - | 'will_not_renew' - | 'will_change_product' - | 'will_pause' - | 'requires_price_increase_consent' - | 'has_already_renewed' - | null + entitlements: {id: EntitlementId}[] status: | 'trialing' | 'active' @@ -24,31 +15,56 @@ export type RawSubscriptionObjectBase = T & { | 'paused' | 'unknown' | 'incomplete' - | null - entitlements: { - created_at: number - display_name: string - id: string - lookup_key: string - object: 'entitlement' - project_id: string - }[] - startedAt: number | null - periodStart: number | null - periodEnd: number | null + renewalStatus: + | 'will_renew' + | 'will_not_renew' + | 'will_change_product' + | 'will_pause' + | 'requires_price_increase_consent' + | 'has_already_renewed' + startedAt: number + periodStart: number + periodEnd: number } -export type RawSubscriptionObject = - | RawSubscriptionObjectBase<{ - platform: 'android' - provider: 'play_store' - }> - | RawSubscriptionObjectBase<{ - platform: 'ios' - provider: 'app_store' - }> - | RawSubscriptionObjectBase<{ +/** + * Response from our API + */ +export type Subscription = + | { + id: SubscriptionId + platform: 'android' | 'ios' + interval: 'monthly' | 'annual' + state?: SubscriptionState + store: { + type: 'play_store' | 'app_store' + productId: string + productLookupKey: string + } + } + | { + id: SubscriptionId platform: 'web' - provider: 'stripe' - price: number - }> + interval: 'monthly' | 'annual' + state: undefined + store: { + type: 'stripe' + productId: string + price: number + priceId: string + subscriptionId: undefined + } + } + | { + id: SubscriptionId + platform: 'web' + interval: 'monthly' | 'annual' + state: SubscriptionState + store: { + type: 'stripe' + productId: string + price: number + priceId: string + subscriptionId: string + } + } diff --git a/src/state/purchases/subscriptions/index.ts b/src/state/purchases/subscriptions/index.ts index ed3d308c1f..d35cfc39d3 100644 --- a/src/state/purchases/subscriptions/index.ts +++ b/src/state/purchases/subscriptions/index.ts @@ -1,8 +1,10 @@ import Purchases from 'react-native-purchases' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' +import {useCurrencyFormatter} from '#/lib/currency' import {isAndroid} from '#/platform/detection' import {getMainSubscriptions} from '#/state/purchases/subscriptions/api' +import {Subscription as APISubscription} from '#/state/purchases/subscriptions/api/types' import { Subscription, Subscriptions, @@ -13,6 +15,7 @@ import {useSession} from '#/state/session' export function useMainSubscriptions() { const {currentAccount} = useSession() const did = currentAccount!.did + const currencyFormatter = useCurrencyFormatter() return useQuery({ queryKey: ['availableSubscriptions', did], @@ -26,30 +29,54 @@ export function useMainSubscriptions() { .filter(s => s.platform !== 'web') .filter(s => s.platform === platform) const lookupKeys = Array.from( - new Set(platformSubscriptions.map(s => s.lookupKey)), + new Set(platformSubscriptions.map(s => s.store.productLookupKey)), ) const products = await Purchases.getProducts(lookupKeys) - const subscriptions: Subscription[] = platformSubscriptions - .map(sub => { - const productData = products.find(p => p.identifier === sub.storeId) - if (!productData) return undefined - const subscription = { - ...sub, + + function decorateSubscription( + sub: APISubscription, + ): Subscription | undefined { + if (sub.platform === 'web') { + return { + id: sub.id, + platform: sub.platform, + interval: sub.interval, + subscription: sub, price: { - value: productData.price * 100, // convert to cents - formatted: productData.priceString, - }, - product: { - platform, - data: productData, + value: sub.store.price, // convert to cents + formatted: currencyFormatter.format(sub.store.price / 100), }, + product: sub.store.priceId, } - return subscription - }) + } + + const productData = products.find( + p => p.identifier === sub.store.productId, + ) + + if (!productData) return undefined + + return { + id: sub.id, + platform: sub.platform, + interval: sub.interval, + subscription: sub, + price: { + value: productData.price * 100, // convert to cents + formatted: productData.priceString, + }, + product: productData, + } + } + const subscriptions = platformSubscriptions + .map(decorateSubscription) + .filter(Boolean) as Subscription[] + const active = rawSubscriptions.active + .map(decorateSubscription) .filter(Boolean) as Subscription[] return { - active: rawSubscriptions.active, + active: active, available: organizeMainSubscriptionsByTier(subscriptions), } }, @@ -61,17 +88,23 @@ export function usePurchaseSubscription() { const queryClient = useQueryClient() return useMutation({ - async mutationFn(product: Subscription['product']) { - if (product.platform === 'web') { + async mutationFn(subscription: Subscription) { + if (subscription.platform === 'web') { throw new Error('Cannot purchase web subscription on native') } + + if (!subscription.product) { + throw new Error('Subscription product not found') + } + // TODO don't do this here Purchases.addCustomerInfoUpdateListener(_info => { queryClient.invalidateQueries({ queryKey: ['availableSubscriptions', currentAccount!.did], }) }) - return Purchases.purchaseStoreProduct(product.data) + + return Purchases.purchaseStoreProduct(subscription.product) }, }) } diff --git a/src/state/purchases/subscriptions/index.web.ts b/src/state/purchases/subscriptions/index.web.ts index 233ebedb45..8a16ef1071 100644 --- a/src/state/purchases/subscriptions/index.web.ts +++ b/src/state/purchases/subscriptions/index.web.ts @@ -25,23 +25,57 @@ export function useMainSubscriptions() { const platformSubscriptions = rawSubscriptions.available.filter( s => s.platform === 'web', ) - const subscriptions = platformSubscriptions.map(sub => { - const subscription: Subscription = { - ...sub, + const subscriptions: Subscription[] = platformSubscriptions.map(sub => { + return { + id: sub.id, + platform: 'web', + interval: sub.interval, + subscription: sub, price: { - value: sub.price, - formatted: currencyFormatter.format(sub.price / 100), - }, - product: { - platform: 'web', - data: sub.checkoutId, + value: sub.store.price, + formatted: currencyFormatter.format(sub.store.price / 100), }, + // TODO may need to have si_id too + product: sub.store.priceId, } - return subscription + }) + + const active: Subscription[] = rawSubscriptions.active.map(sub => { + if (sub.platform === 'web') { + const s: Subscription = { + id: sub.id, + platform: 'web', + interval: sub.interval, + subscription: sub, + price: { + value: sub.store.price, + formatted: currencyFormatter.format(sub.store.price / 100), + }, + // TODO may need to have si_id too + product: sub.store.priceId, + } + return s + } + + const s: Subscription = { + id: sub.id, + platform: sub.platform, + interval: sub.interval, + subscription: sub, + price: { + value: 0, + formatted: '', + }, + product: undefined, + } + return s }) return { - active: rawSubscriptions.active, + /** + * TODO decorate this too + */ + active: active, available: organizeMainSubscriptionsByTier(subscriptions), } }, @@ -51,10 +85,11 @@ export function useMainSubscriptions() { export function usePurchaseSubscription() { const {currentAccount} = useSession() return useMutation({ - async mutationFn(product: Subscription['product']) { - if (product.platform !== 'web') { + async mutationFn(subscription: Subscription) { + if (subscription.platform !== 'web') { throw new Error('Cannot purchase native subscription on web') } + if (!currentAccount || !currentAccount.email) { throw new Error('No account or email') } @@ -64,7 +99,7 @@ export function usePurchaseSubscription() { { method: 'POST', body: JSON.stringify({ - price: product.data, + price: subscription.product, // TODO should NOT use query, use auth and our db state user: currentAccount.did, email: currentAccount.email, diff --git a/src/state/purchases/subscriptions/types.ts b/src/state/purchases/subscriptions/types.ts index 31859490dd..46a7b8c7a0 100644 --- a/src/state/purchases/subscriptions/types.ts +++ b/src/state/purchases/subscriptions/types.ts @@ -1,9 +1,6 @@ import {PurchasesStoreProduct} from 'react-native-purchases' -import { - RawSubscriptionObject, - RawSubscriptionObjectBase, -} from '#/state/purchases/subscriptions/api/types' +import {Subscription as APISubscription} from '#/state/purchases/subscriptions/api/types' export enum SubscriptionId { Main0MonthlyAuto = 'main:0:monthly:auto', @@ -20,46 +17,56 @@ export enum EntitlementId { Main2 = 'main:2', } +export type Entitlement = { + id: EntitlementId +} + export type Subscription = - | RawSubscriptionObjectBase<{ + | { + id: SubscriptionId platform: 'android' - provider: 'play_store' - price: { + interval: 'monthly' | 'annual' + /** + * Response from our API + */ + subscription: APISubscription + price?: { value: number formatted: string } - product: { - platform: 'android' - data: PurchasesStoreProduct - } - }> - | RawSubscriptionObjectBase<{ + product?: PurchasesStoreProduct + } + | { + id: SubscriptionId platform: 'ios' - provider: 'app_store' - price: { + interval: 'monthly' | 'annual' + /** + * Response from our API + */ + subscription: APISubscription + price?: { value: number formatted: string } - product: { - platform: 'ios' - data: PurchasesStoreProduct - } - }> - | RawSubscriptionObjectBase<{ + product?: PurchasesStoreProduct + } + | { + id: SubscriptionId platform: 'web' - provider: 'stripe' + interval: 'monthly' | 'annual' + /** + * Response from our API + */ + subscription: APISubscription price: { value: number formatted: string } - product: { - platform: 'web' - data: string - } - }> + product: string + } export type Subscriptions = { - active: RawSubscriptionObject[] + active: Subscription[] available: { monthly: Subscription[] annual: Subscription[] diff --git a/src/state/purchases/subscriptions/util.ts b/src/state/purchases/subscriptions/util.ts index b01e5e7890..9fff0a1bfe 100644 --- a/src/state/purchases/subscriptions/util.ts +++ b/src/state/purchases/subscriptions/util.ts @@ -2,15 +2,14 @@ import {Subscription, Subscriptions} from './types' export function organizeMainSubscriptionsByTier( subscriptions: Subscription[], -): Subscriptions['subscriptions'] { - const result: Subscriptions['subscriptions'] = { +): Subscriptions['available'] { + const result: Subscriptions['available'] = { monthly: [], annual: [], } - for (const subscription of subscriptions) { - const {interval} = subscription - result[interval].push(subscription) + for (const sub of subscriptions) { + result[sub.interval].push(sub) } result.monthly = result.monthly.sort((a, b) => {