Refactor for new API contract
This commit is contained in:
@@ -28,7 +28,11 @@ export function Subscriptions(_props: ScreenProps) {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
// const {data: entitlements} = useEntitlements()
|
// const {data: entitlements} = useEntitlements()
|
||||||
const {data: subscriptions, isError} = useMainSubscriptions()
|
const {data: subscriptions, isError, error} = useMainSubscriptions()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -96,17 +100,21 @@ export function MainSubscription({
|
|||||||
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
|
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
|
||||||
<View style={[a.gap_sm, a.pb_xl]}>
|
<View style={[a.gap_sm, a.pb_xl]}>
|
||||||
{subscriptions.available.monthly.map(s => (
|
{subscriptions.available.monthly.map(s => (
|
||||||
<View key={s.id}>
|
<View key={s.subscription.id}>
|
||||||
<Button
|
<Button
|
||||||
label={s.id}
|
label={s.subscription.id}
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color={s.status === 'active' ? 'primary' : 'secondary'}
|
color={
|
||||||
|
s.subscription.state?.status === 'active'
|
||||||
|
? 'primary'
|
||||||
|
: 'secondary'
|
||||||
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
purchaseSubscription(s.product)
|
purchaseSubscription(s)
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{s.id} ({s.price.formatted})
|
{s.subscription.id} ({s.price?.formatted})
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
@@ -114,17 +122,21 @@ export function MainSubscription({
|
|||||||
</View>
|
</View>
|
||||||
<View style={[a.gap_sm]}>
|
<View style={[a.gap_sm]}>
|
||||||
{subscriptions.available.annual.map(s => (
|
{subscriptions.available.annual.map(s => (
|
||||||
<View key={s.id}>
|
<View key={s.subscription.id}>
|
||||||
<Button
|
<Button
|
||||||
label={s.id}
|
label={s.subscription.id}
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color={s.status === 'active' ? 'primary' : 'secondary'}
|
color={
|
||||||
|
s.subscription.state?.status === 'active'
|
||||||
|
? 'primary'
|
||||||
|
: 'secondary'
|
||||||
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
purchaseSubscription(s.product)
|
purchaseSubscription(s)
|
||||||
}}>
|
}}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{s.id} ({s.price.formatted})
|
{s.subscription.id} ({s.price?.formatted})
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {RawSubscriptionObject} from '#/state/purchases/subscriptions/api/types'
|
import {Subscription} from '#/state/purchases/subscriptions/api/types'
|
||||||
import {EntitlementId} from '#/state/purchases/subscriptions/types'
|
import {Entitlement} from '#/state/purchases/subscriptions/types'
|
||||||
|
|
||||||
export async function getMainSubscriptions({
|
export async function getMainSubscriptions({
|
||||||
did,
|
did,
|
||||||
@@ -23,8 +23,8 @@ export async function getMainSubscriptions({
|
|||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
return json as {
|
return json as {
|
||||||
active: RawSubscriptionObject[]
|
active: Subscription[]
|
||||||
available: RawSubscriptionObject[]
|
available: Subscription[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ export async function getEntitlements({did}: {did: string}) {
|
|||||||
|
|
||||||
const {entitlements} = await res.json()
|
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 & {
|
export type SubscriptionState = {
|
||||||
id: SubscriptionId
|
|
||||||
storeId: string
|
|
||||||
lookupKey: string
|
|
||||||
checkoutId: string
|
|
||||||
interval: 'monthly' | 'annual'
|
|
||||||
active: boolean
|
active: boolean
|
||||||
autoRenewStatus:
|
entitlements: {id: EntitlementId}[]
|
||||||
| 'will_renew'
|
|
||||||
| 'will_not_renew'
|
|
||||||
| 'will_change_product'
|
|
||||||
| 'will_pause'
|
|
||||||
| 'requires_price_increase_consent'
|
|
||||||
| 'has_already_renewed'
|
|
||||||
| null
|
|
||||||
status:
|
status:
|
||||||
| 'trialing'
|
| 'trialing'
|
||||||
| 'active'
|
| 'active'
|
||||||
@@ -24,31 +15,56 @@ export type RawSubscriptionObjectBase<T> = T & {
|
|||||||
| 'paused'
|
| 'paused'
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
| 'incomplete'
|
| 'incomplete'
|
||||||
| null
|
renewalStatus:
|
||||||
entitlements: {
|
| 'will_renew'
|
||||||
created_at: number
|
| 'will_not_renew'
|
||||||
display_name: string
|
| 'will_change_product'
|
||||||
id: string
|
| 'will_pause'
|
||||||
lookup_key: string
|
| 'requires_price_increase_consent'
|
||||||
object: 'entitlement'
|
| 'has_already_renewed'
|
||||||
project_id: string
|
startedAt: number
|
||||||
}[]
|
periodStart: number
|
||||||
startedAt: number | null
|
periodEnd: number
|
||||||
periodStart: number | null
|
|
||||||
periodEnd: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RawSubscriptionObject =
|
/**
|
||||||
| RawSubscriptionObjectBase<{
|
* Response from our API
|
||||||
platform: 'android'
|
*/
|
||||||
provider: 'play_store'
|
export type Subscription =
|
||||||
}>
|
| {
|
||||||
| RawSubscriptionObjectBase<{
|
id: SubscriptionId
|
||||||
platform: 'ios'
|
platform: 'android' | 'ios'
|
||||||
provider: 'app_store'
|
interval: 'monthly' | 'annual'
|
||||||
}>
|
state?: SubscriptionState
|
||||||
| RawSubscriptionObjectBase<{
|
store: {
|
||||||
|
type: 'play_store' | 'app_store'
|
||||||
|
productId: string
|
||||||
|
productLookupKey: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: SubscriptionId
|
||||||
platform: 'web'
|
platform: 'web'
|
||||||
provider: 'stripe'
|
interval: 'monthly' | 'annual'
|
||||||
price: number
|
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 Purchases from 'react-native-purchases'
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useCurrencyFormatter} from '#/lib/currency'
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {getMainSubscriptions} from '#/state/purchases/subscriptions/api'
|
import {getMainSubscriptions} from '#/state/purchases/subscriptions/api'
|
||||||
|
import {Subscription as APISubscription} from '#/state/purchases/subscriptions/api/types'
|
||||||
import {
|
import {
|
||||||
Subscription,
|
Subscription,
|
||||||
Subscriptions,
|
Subscriptions,
|
||||||
@@ -13,6 +15,7 @@ import {useSession} from '#/state/session'
|
|||||||
export function useMainSubscriptions() {
|
export function useMainSubscriptions() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const did = currentAccount!.did
|
const did = currentAccount!.did
|
||||||
|
const currencyFormatter = useCurrencyFormatter()
|
||||||
|
|
||||||
return useQuery<Subscriptions>({
|
return useQuery<Subscriptions>({
|
||||||
queryKey: ['availableSubscriptions', did],
|
queryKey: ['availableSubscriptions', did],
|
||||||
@@ -26,30 +29,54 @@ export function useMainSubscriptions() {
|
|||||||
.filter(s => s.platform !== 'web')
|
.filter(s => s.platform !== 'web')
|
||||||
.filter(s => s.platform === platform)
|
.filter(s => s.platform === platform)
|
||||||
const lookupKeys = Array.from(
|
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 products = await Purchases.getProducts(lookupKeys)
|
||||||
const subscriptions: Subscription[] = platformSubscriptions
|
|
||||||
.map(sub => {
|
function decorateSubscription(
|
||||||
const productData = products.find(p => p.identifier === sub.storeId)
|
sub: APISubscription,
|
||||||
if (!productData) return undefined
|
): Subscription | undefined {
|
||||||
const subscription = {
|
if (sub.platform === 'web') {
|
||||||
...sub,
|
return {
|
||||||
|
id: sub.id,
|
||||||
|
platform: sub.platform,
|
||||||
|
interval: sub.interval,
|
||||||
|
subscription: sub,
|
||||||
price: {
|
price: {
|
||||||
value: productData.price * 100, // convert to cents
|
value: sub.store.price, // convert to cents
|
||||||
formatted: productData.priceString,
|
formatted: currencyFormatter.format(sub.store.price / 100),
|
||||||
},
|
|
||||||
product: {
|
|
||||||
platform,
|
|
||||||
data: productData,
|
|
||||||
},
|
},
|
||||||
|
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[]
|
.filter(Boolean) as Subscription[]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: rawSubscriptions.active,
|
active: active,
|
||||||
available: organizeMainSubscriptionsByTier(subscriptions),
|
available: organizeMainSubscriptionsByTier(subscriptions),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,17 +88,23 @@ export function usePurchaseSubscription() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
async mutationFn(product: Subscription['product']) {
|
async mutationFn(subscription: Subscription) {
|
||||||
if (product.platform === 'web') {
|
if (subscription.platform === 'web') {
|
||||||
throw new Error('Cannot purchase web subscription on native')
|
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
|
// TODO don't do this here
|
||||||
Purchases.addCustomerInfoUpdateListener(_info => {
|
Purchases.addCustomerInfoUpdateListener(_info => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['availableSubscriptions', currentAccount!.did],
|
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(
|
const platformSubscriptions = rawSubscriptions.available.filter(
|
||||||
s => s.platform === 'web',
|
s => s.platform === 'web',
|
||||||
)
|
)
|
||||||
const subscriptions = platformSubscriptions.map(sub => {
|
const subscriptions: Subscription[] = platformSubscriptions.map(sub => {
|
||||||
const subscription: Subscription = {
|
return {
|
||||||
...sub,
|
id: sub.id,
|
||||||
|
platform: 'web',
|
||||||
|
interval: sub.interval,
|
||||||
|
subscription: sub,
|
||||||
price: {
|
price: {
|
||||||
value: sub.price,
|
value: sub.store.price,
|
||||||
formatted: currencyFormatter.format(sub.price / 100),
|
formatted: currencyFormatter.format(sub.store.price / 100),
|
||||||
},
|
|
||||||
product: {
|
|
||||||
platform: 'web',
|
|
||||||
data: sub.checkoutId,
|
|
||||||
},
|
},
|
||||||
|
// 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 {
|
return {
|
||||||
active: rawSubscriptions.active,
|
/**
|
||||||
|
* TODO decorate this too
|
||||||
|
*/
|
||||||
|
active: active,
|
||||||
available: organizeMainSubscriptionsByTier(subscriptions),
|
available: organizeMainSubscriptionsByTier(subscriptions),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,10 +85,11 @@ export function useMainSubscriptions() {
|
|||||||
export function usePurchaseSubscription() {
|
export function usePurchaseSubscription() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
async mutationFn(product: Subscription['product']) {
|
async mutationFn(subscription: Subscription) {
|
||||||
if (product.platform !== 'web') {
|
if (subscription.platform !== 'web') {
|
||||||
throw new Error('Cannot purchase native subscription on web')
|
throw new Error('Cannot purchase native subscription on web')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentAccount || !currentAccount.email) {
|
if (!currentAccount || !currentAccount.email) {
|
||||||
throw new Error('No account or email')
|
throw new Error('No account or email')
|
||||||
}
|
}
|
||||||
@@ -64,7 +99,7 @@ export function usePurchaseSubscription() {
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
price: product.data,
|
price: subscription.product,
|
||||||
// TODO should NOT use query, use auth and our db state
|
// TODO should NOT use query, use auth and our db state
|
||||||
user: currentAccount.did,
|
user: currentAccount.did,
|
||||||
email: currentAccount.email,
|
email: currentAccount.email,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {PurchasesStoreProduct} from 'react-native-purchases'
|
import {PurchasesStoreProduct} from 'react-native-purchases'
|
||||||
|
|
||||||
import {
|
import {Subscription as APISubscription} from '#/state/purchases/subscriptions/api/types'
|
||||||
RawSubscriptionObject,
|
|
||||||
RawSubscriptionObjectBase,
|
|
||||||
} from '#/state/purchases/subscriptions/api/types'
|
|
||||||
|
|
||||||
export enum SubscriptionId {
|
export enum SubscriptionId {
|
||||||
Main0MonthlyAuto = 'main:0:monthly:auto',
|
Main0MonthlyAuto = 'main:0:monthly:auto',
|
||||||
@@ -20,46 +17,56 @@ export enum EntitlementId {
|
|||||||
Main2 = 'main:2',
|
Main2 = 'main:2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Entitlement = {
|
||||||
|
id: EntitlementId
|
||||||
|
}
|
||||||
|
|
||||||
export type Subscription =
|
export type Subscription =
|
||||||
| RawSubscriptionObjectBase<{
|
| {
|
||||||
|
id: SubscriptionId
|
||||||
platform: 'android'
|
platform: 'android'
|
||||||
provider: 'play_store'
|
interval: 'monthly' | 'annual'
|
||||||
price: {
|
/**
|
||||||
|
* Response from our API
|
||||||
|
*/
|
||||||
|
subscription: APISubscription
|
||||||
|
price?: {
|
||||||
value: number
|
value: number
|
||||||
formatted: string
|
formatted: string
|
||||||
}
|
}
|
||||||
product: {
|
product?: PurchasesStoreProduct
|
||||||
platform: 'android'
|
}
|
||||||
data: PurchasesStoreProduct
|
| {
|
||||||
}
|
id: SubscriptionId
|
||||||
}>
|
|
||||||
| RawSubscriptionObjectBase<{
|
|
||||||
platform: 'ios'
|
platform: 'ios'
|
||||||
provider: 'app_store'
|
interval: 'monthly' | 'annual'
|
||||||
price: {
|
/**
|
||||||
|
* Response from our API
|
||||||
|
*/
|
||||||
|
subscription: APISubscription
|
||||||
|
price?: {
|
||||||
value: number
|
value: number
|
||||||
formatted: string
|
formatted: string
|
||||||
}
|
}
|
||||||
product: {
|
product?: PurchasesStoreProduct
|
||||||
platform: 'ios'
|
}
|
||||||
data: PurchasesStoreProduct
|
| {
|
||||||
}
|
id: SubscriptionId
|
||||||
}>
|
|
||||||
| RawSubscriptionObjectBase<{
|
|
||||||
platform: 'web'
|
platform: 'web'
|
||||||
provider: 'stripe'
|
interval: 'monthly' | 'annual'
|
||||||
|
/**
|
||||||
|
* Response from our API
|
||||||
|
*/
|
||||||
|
subscription: APISubscription
|
||||||
price: {
|
price: {
|
||||||
value: number
|
value: number
|
||||||
formatted: string
|
formatted: string
|
||||||
}
|
}
|
||||||
product: {
|
product: string
|
||||||
platform: 'web'
|
}
|
||||||
data: string
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type Subscriptions = {
|
export type Subscriptions = {
|
||||||
active: RawSubscriptionObject[]
|
active: Subscription[]
|
||||||
available: {
|
available: {
|
||||||
monthly: Subscription[]
|
monthly: Subscription[]
|
||||||
annual: Subscription[]
|
annual: Subscription[]
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import {Subscription, Subscriptions} from './types'
|
|||||||
|
|
||||||
export function organizeMainSubscriptionsByTier(
|
export function organizeMainSubscriptionsByTier(
|
||||||
subscriptions: Subscription[],
|
subscriptions: Subscription[],
|
||||||
): Subscriptions['subscriptions'] {
|
): Subscriptions['available'] {
|
||||||
const result: Subscriptions['subscriptions'] = {
|
const result: Subscriptions['available'] = {
|
||||||
monthly: [],
|
monthly: [],
|
||||||
annual: [],
|
annual: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const sub of subscriptions) {
|
||||||
const {interval} = subscription
|
result[sub.interval].push(sub)
|
||||||
result[interval].push(subscription)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.monthly = result.monthly.sort((a, b) => {
|
result.monthly = result.monthly.sort((a, b) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user