Refactor for new API contract
This commit is contained in:
@@ -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 (
|
||||
<Layout.Screen>
|
||||
@@ -96,17 +100,21 @@ export function MainSubscription({
|
||||
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
|
||||
<View style={[a.gap_sm, a.pb_xl]}>
|
||||
{subscriptions.available.monthly.map(s => (
|
||||
<View key={s.id}>
|
||||
<View key={s.subscription.id}>
|
||||
<Button
|
||||
label={s.id}
|
||||
label={s.subscription.id}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color={s.status === 'active' ? 'primary' : 'secondary'}
|
||||
color={
|
||||
s.subscription.state?.status === 'active'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
}
|
||||
onPress={() => {
|
||||
purchaseSubscription(s.product)
|
||||
purchaseSubscription(s)
|
||||
}}>
|
||||
<ButtonText>
|
||||
{s.id} ({s.price.formatted})
|
||||
{s.subscription.id} ({s.price?.formatted})
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
@@ -114,17 +122,21 @@ export function MainSubscription({
|
||||
</View>
|
||||
<View style={[a.gap_sm]}>
|
||||
{subscriptions.available.annual.map(s => (
|
||||
<View key={s.id}>
|
||||
<View key={s.subscription.id}>
|
||||
<Button
|
||||
label={s.id}
|
||||
label={s.subscription.id}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color={s.status === 'active' ? 'primary' : 'secondary'}
|
||||
color={
|
||||
s.subscription.state?.status === 'active'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
}
|
||||
onPress={() => {
|
||||
purchaseSubscription(s.product)
|
||||
purchaseSubscription(s)
|
||||
}}>
|
||||
<ButtonText>
|
||||
{s.id} ({s.price.formatted})
|
||||
{s.subscription.id} ({s.price?.formatted})
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import {SubscriptionId} from '#/state/purchases/subscriptions/types'
|
||||
import {
|
||||
EntitlementId,
|
||||
SubscriptionId,
|
||||
} from '#/state/purchases/subscriptions/types'
|
||||
|
||||
export type RawSubscriptionObjectBase<T> = 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> = 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Subscriptions>({
|
||||
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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user