Bunch of contract updates

This commit is contained in:
Eric Bailey
2024-10-28 18:14:36 -05:00
parent 6d0c5078e9
commit e9c6a8461c
9 changed files with 188 additions and 64 deletions
+99 -40
View File
@@ -9,11 +9,14 @@ import {
useMainSubscriptions, useMainSubscriptions,
usePurchaseSubscription, usePurchaseSubscription,
} from '#/state/purchases/subscriptions' } from '#/state/purchases/subscriptions'
// import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements'
import {Subscriptions as SubscriptionsResponse} from '#/state/purchases/subscriptions/types'
import {ViewHeader} from '#/view/com/util/ViewHeader' import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export type ScreenProps = NativeStackScreenProps< export type ScreenProps = NativeStackScreenProps<
@@ -24,54 +27,110 @@ export type ScreenProps = NativeStackScreenProps<
export function Subscriptions(_props: ScreenProps) { export function Subscriptions(_props: ScreenProps) {
const {_} = useLingui() const {_} = useLingui()
const {data: subscriptions} = useMainSubscriptions() // const {data: entitlements} = useEntitlements()
const {mutateAsync: purchaseSubscription} = usePurchaseSubscription() const {data: subscriptions, isError} = useMainSubscriptions()
return ( return (
<Layout.Screen> <Layout.Screen>
<CenteredView sideBorders={true}> <CenteredView sideBorders={true}>
<ViewHeader title={_(msg`Liked By`)} /> <ViewHeader title={_(msg`Subscriptions`)} />
<View style={[a.px_xl, a.py_xl, a.gap_lg]}> <View style={[a.px_xl, a.py_xl, a.gap_lg]}>
<Text style={[a.text_2xl]}>Subs</Text> <Text style={[a.text_2xl]}>Subs</Text>
<View style={[a.gap_sm]}> {isError ? (
{subscriptions?.monthly.map(s => ( <Text>Error</Text>
<View key={s.id}> ) : !subscriptions ? (
<Button <Loader />
label={s.id} ) : (
size="small" <MainSubscription subscriptions={subscriptions} />
variant="solid" )}
color={s.status === 'active' ? 'primary' : 'secondary'}
onPress={() => {
purchaseSubscription(s.product)
}}>
<ButtonText>
{s.id} ({s.price.formatted})
</ButtonText>
</Button>
</View>
))}
</View>
<View style={[a.gap_sm]}>
{subscriptions?.annual.map(s => (
<View key={s.id}>
<Button
label={s.id}
size="small"
variant="solid"
color={s.status === 'active' ? 'primary' : 'secondary'}
onPress={() => {
purchaseSubscription(s.product)
}}>
<ButtonText>
{s.id} ({s.price.formatted})
</ButtonText>
</Button>
</View>
))}
</View>
</View> </View>
</CenteredView> </CenteredView>
</Layout.Screen> </Layout.Screen>
) )
} }
export function MainSubscription({
subscriptions,
}: {
subscriptions: SubscriptionsResponse
}) {
const t = useTheme()
const active = subscriptions.active.at(0)
const {mutateAsync: purchaseSubscription} = usePurchaseSubscription()
if (active) {
const contents = (
<View style={[a.gap_sm]}>
<View style={[a.flex_row, a.align_center, a.justify_between]}>
<Text style={[a.text_xl, a.font_heavy]}>Bluesky Plus</Text>
<Text
style={[
a.text_md,
a.font_bold,
a.p_xs,
a.rounded_xs,
{
color: t.palette.white,
backgroundColor: t.palette.primary_500,
},
]}>
Supporter
</Text>
</View>
<Text style={[a.text_md]}>Managed via: {active?.platform}</Text>
</View>
)
return (
<View
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}>
{contents}
</View>
)
} else {
return (
<View
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}>
<Button
label={s.id}
size="small"
variant="solid"
color={s.status === 'active' ? 'primary' : 'secondary'}
onPress={() => {
purchaseSubscription(s.product)
}}>
<ButtonText>
{s.id} ({s.price.formatted})
</ButtonText>
</Button>
</View>
))}
</View>
<View style={[a.gap_sm]}>
{subscriptions.available.annual.map(s => (
<View key={s.id}>
<Button
label={s.id}
size="small"
variant="solid"
color={s.status === 'active' ? 'primary' : 'secondary'}
onPress={() => {
purchaseSubscription(s.product)
}}>
<ButtonText>
{s.id} ({s.price.formatted})
</ButtonText>
</Button>
</View>
))}
</View>
</View>
)
}
}
+24 -2
View File
@@ -1,4 +1,5 @@
import {RawSubscriptionObject} from '#/state/purchases/subscriptions/api/types' import {RawSubscriptionObject} from '#/state/purchases/subscriptions/api/types'
import {EntitlementId} from '#/state/purchases/subscriptions/types'
export async function getMainSubscriptions({ export async function getMainSubscriptions({
did, did,
@@ -13,10 +14,31 @@ export async function getMainSubscriptions({
if (!res.ok) { if (!res.ok) {
console.error('error fetching subscriptions', res.status, await res.text()) console.error('error fetching subscriptions', res.status, await res.text())
return {
active: [],
available: [],
}
}
const json = await res.json()
return json as {
active: RawSubscriptionObject[]
available: RawSubscriptionObject[]
}
}
export async function getEntitlements({did}: {did: string}) {
const res = await fetch(
`https://bsky-purchases.ngrok.io/entitlements?user=${did}`,
)
if (!res.ok) {
console.error('error fetching entitlements', res.status, await res.text())
return [] return []
} }
const {subscriptions} = await res.json() const {entitlements} = await res.json()
return subscriptions as RawSubscriptionObject[] return entitlements as {id: EntitlementId}[]
} }
@@ -6,7 +6,15 @@ export type RawSubscriptionObjectBase<T> = T & {
lookupKey: string lookupKey: string
checkoutId: string checkoutId: string
interval: 'monthly' | 'annual' interval: 'monthly' | 'annual'
autoRenew: boolean active: boolean
autoRenewStatus:
| 'will_renew'
| 'will_not_renew'
| 'will_change_product'
| 'will_pause'
| 'requires_price_increase_consent'
| 'has_already_renewed'
| null
status: status:
| 'trialing' | 'trialing'
| 'active' | 'active'
+16 -7
View File
@@ -1,5 +1,5 @@
import Purchases from 'react-native-purchases' import Purchases from 'react-native-purchases'
import {useMutation, useQuery} from '@tanstack/react-query' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {isAndroid} from '#/platform/detection' import {isAndroid} from '#/platform/detection'
import {getMainSubscriptions} from '#/state/purchases/subscriptions/api' import {getMainSubscriptions} from '#/state/purchases/subscriptions/api'
@@ -17,15 +17,12 @@ export function useMainSubscriptions() {
return useQuery<Subscriptions>({ return useQuery<Subscriptions>({
queryKey: ['availableSubscriptions', did], queryKey: ['availableSubscriptions', did],
async queryFn() { async queryFn() {
Purchases.logIn(did) const platform = isAndroid ? 'android' : 'ios'
Purchases.setEmail(currentAccount!.email!)
const platform = isAndroid ? 'android' : ('ios' as const)
const rawSubscriptions = await getMainSubscriptions({ const rawSubscriptions = await getMainSubscriptions({
did: currentAccount!.did, did: currentAccount!.did,
platform, platform,
}) })
const platformSubscriptions = rawSubscriptions const platformSubscriptions = rawSubscriptions.available
.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(
@@ -51,17 +48,29 @@ export function useMainSubscriptions() {
}) })
.filter(Boolean) as Subscription[] .filter(Boolean) as Subscription[]
return organizeMainSubscriptionsByTier(subscriptions) return {
active: rawSubscriptions.active,
available: organizeMainSubscriptionsByTier(subscriptions),
}
}, },
}) })
} }
export function usePurchaseSubscription() { export function usePurchaseSubscription() {
const {currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation({ return useMutation({
async mutationFn(product: Subscription['product']) { async mutationFn(product: Subscription['product']) {
if (product.platform === 'web') { if (product.platform === 'web') {
throw new Error('Cannot purchase web subscription on native') throw new Error('Cannot purchase web subscription on native')
} }
// TODO don't do this here
Purchases.addCustomerInfoUpdateListener(_info => {
queryClient.invalidateQueries({
queryKey: ['availableSubscriptions', currentAccount!.did],
})
})
return Purchases.purchaseStoreProduct(product.data) return Purchases.purchaseStoreProduct(product.data)
}, },
}) })
@@ -22,7 +22,7 @@ export function useMainSubscriptions() {
did: currentAccount!.did, did: currentAccount!.did,
platform: 'web', platform: 'web',
}) })
const platformSubscriptions = rawSubscriptions.filter( const platformSubscriptions = rawSubscriptions.available.filter(
s => s.platform === 'web', s => s.platform === 'web',
) )
const subscriptions = platformSubscriptions.map(sub => { const subscriptions = platformSubscriptions.map(sub => {
@@ -40,7 +40,10 @@ export function useMainSubscriptions() {
return subscription return subscription
}) })
return organizeMainSubscriptionsByTier(subscriptions) return {
active: rawSubscriptions.active,
available: organizeMainSubscriptionsByTier(subscriptions),
}
}, },
}) })
} }
+15 -3
View File
@@ -1,6 +1,9 @@
import {PurchasesStoreProduct} from 'react-native-purchases' import {PurchasesStoreProduct} from 'react-native-purchases'
import {RawSubscriptionObjectBase} from '#/state/purchases/subscriptions/api/types' import {
RawSubscriptionObject,
RawSubscriptionObjectBase,
} from '#/state/purchases/subscriptions/api/types'
export enum SubscriptionId { export enum SubscriptionId {
Main0MonthlyAuto = 'main:0:monthly:auto', Main0MonthlyAuto = 'main:0:monthly:auto',
@@ -11,6 +14,12 @@ export enum SubscriptionId {
Main2AnnualAuto = 'main:2:annual:auto', Main2AnnualAuto = 'main:2:annual:auto',
} }
export enum EntitlementId {
Main0 = 'main:0',
Main1 = 'main:1',
Main2 = 'main:2',
}
export type Subscription = export type Subscription =
| RawSubscriptionObjectBase<{ | RawSubscriptionObjectBase<{
platform: 'android' platform: 'android'
@@ -50,6 +59,9 @@ export type Subscription =
}> }>
export type Subscriptions = { export type Subscriptions = {
monthly: Subscription[] active: RawSubscriptionObject[]
annual: Subscription[] available: {
monthly: Subscription[]
annual: Subscription[]
}
} }
@@ -0,0 +1,14 @@
import {useQuery} from '@tanstack/react-query'
import {getEntitlements} from '#/state/purchases/subscriptions/api'
import {useSession} from '#/state/session'
export function useEntitlements() {
const {currentAccount} = useSession()
return useQuery({
queryKey: ['entitlements', currentAccount!.did],
async queryFn() {
return getEntitlements({did: currentAccount!.did})
},
})
}
+2 -2
View File
@@ -2,8 +2,8 @@ import {Subscription, Subscriptions} from './types'
export function organizeMainSubscriptionsByTier( export function organizeMainSubscriptionsByTier(
subscriptions: Subscription[], subscriptions: Subscription[],
): Subscriptions { ): Subscriptions['subscriptions'] {
const result: Subscriptions = { const result: Subscriptions['subscriptions'] = {
monthly: [], monthly: [],
annual: [], annual: [],
} }
+4 -7
View File
@@ -5,7 +5,6 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
import {PressableScale} from '#/lib/custom-animations/PressableScale' import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
@@ -276,12 +275,10 @@ let DrawerContent = ({}: {}): React.ReactNode => {
onPress={onPressProfile} onPress={onPressProfile}
/> />
<SettingsMenuItem onPress={onPressSettings} /> <SettingsMenuItem onPress={onPressSettings} />
{IS_INTERNAL && ( <SubscriptionsMenuItem
<SubscriptionsMenuItem isActive={false}
isActive={false} onPress={onPressSubscriptions}
onPress={onPressSubscriptions} />
/>
)}
</> </>
) : ( ) : (
<> <>