Bop it
This commit is contained in:
@@ -5,10 +5,8 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigationState} from '@react-navigation/native'
|
import {useNavigationState} from '@react-navigation/native'
|
||||||
|
|
||||||
import {
|
import {usePurchaseOffering} from '#/state/purchases/hooks/usePurchaseOffering'
|
||||||
usePurchaseOffering,
|
import {useSubscriptionGroup} from '#/state/purchases/hooks/useSubscriptionGroup'
|
||||||
useSubscriptionGroup,
|
|
||||||
} from '#/state/purchases/hooks/useSubscriptionGroup'
|
|
||||||
import {
|
import {
|
||||||
SubscriptionGroupId,
|
SubscriptionGroupId,
|
||||||
SubscriptionOfferingId,
|
SubscriptionOfferingId,
|
||||||
@@ -53,6 +51,8 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
|
|||||||
const {data: coreOffering} = useSubscriptionGroup(SubscriptionGroupId.Core)
|
const {data: coreOffering} = useSubscriptionGroup(SubscriptionGroupId.Core)
|
||||||
const {mutateAsync: purchaseOffering, isPending} = usePurchaseOffering()
|
const {mutateAsync: purchaseOffering, isPending} = usePurchaseOffering()
|
||||||
|
|
||||||
|
console.log({coreOffering})
|
||||||
|
|
||||||
const onPressSubscribe = async () => {
|
const onPressSubscribe = async () => {
|
||||||
try {
|
try {
|
||||||
if (!currentAccount) return
|
if (!currentAccount) return
|
||||||
|
|||||||
+183
-159
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {TextStyle,View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
@@ -8,12 +8,16 @@ import {CommonNavigatorParams} from '#/lib/routes/types'
|
|||||||
import {isAndroid, isIOS, isWeb} from '#/platform/detection'
|
import {isAndroid, isIOS, isWeb} from '#/platform/detection'
|
||||||
import {PurchasesState, usePurchases} from '#/state/purchases'
|
import {PurchasesState, usePurchases} from '#/state/purchases'
|
||||||
import {useManageSubscription} from '#/state/purchases/hooks/useManageSubscription'
|
import {useManageSubscription} from '#/state/purchases/hooks/useManageSubscription'
|
||||||
|
import {useNativeUserState} from '#/state/purchases/hooks/useNativeUserState'
|
||||||
import {
|
import {
|
||||||
PlatformId,
|
PlatformId,
|
||||||
SubscriptionGroupId,
|
SubscriptionGroupId,
|
||||||
SubscriptionOfferingId,
|
SubscriptionOfferingId,
|
||||||
} from '#/state/purchases/types'
|
} from '#/state/purchases/types'
|
||||||
import {APISubscription} from '#/state/purchases/types'
|
import {
|
||||||
|
APISubscription,
|
||||||
|
NativePurchaseRestricted,
|
||||||
|
} from '#/state/purchases/types'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
@@ -25,7 +29,7 @@ import {GradientFill} from '#/components/GradientFill'
|
|||||||
import {AndroidLogo} from '#/components/icons/AndroidLogo'
|
import {AndroidLogo} from '#/components/icons/AndroidLogo'
|
||||||
import {AppleLogo} from '#/components/icons/AppleLogo'
|
import {AppleLogo} from '#/components/icons/AppleLogo'
|
||||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Rotate} from '#/components/icons/ArrowRotateCounterClockwise'
|
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Rotate} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||||
import {Full as BlueskyPlusLogo, Logotype} from '#/components/icons/BlueskyPlus'
|
import {Full as BlueskyPlusLogo} from '#/components/icons/BlueskyPlus'
|
||||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
@@ -43,6 +47,7 @@ export type ScreenProps = NativeStackScreenProps<
|
|||||||
export function Subscriptions(_props: ScreenProps) {
|
export function Subscriptions(_props: ScreenProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const purchases = usePurchases()
|
const purchases = usePurchases()
|
||||||
|
const {loading, restricted} = useNativeUserState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -51,12 +56,12 @@ export function Subscriptions(_props: ScreenProps) {
|
|||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
<CenteredView style={[a.util_screen_outer]}>
|
<CenteredView style={[a.util_screen_outer]}>
|
||||||
<View style={[a.px_xl, a.py_xl]}>
|
<View style={[a.px_xl, a.py_xl]}>
|
||||||
{purchases.status === 'loading' ? (
|
{purchases.status === 'loading' || loading ? (
|
||||||
<Loader />
|
<Loader />
|
||||||
) : purchases.status === 'error' ? (
|
) : purchases.status === 'error' ? (
|
||||||
<View />
|
<View />
|
||||||
) : (
|
) : (
|
||||||
<Core state={purchases} />
|
<Core state={purchases} restricted={restricted} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
@@ -67,16 +72,48 @@ export function Subscriptions(_props: ScreenProps) {
|
|||||||
|
|
||||||
function Core({
|
function Core({
|
||||||
state,
|
state,
|
||||||
|
restricted,
|
||||||
}: {
|
}: {
|
||||||
state: Exclude<PurchasesState, {status: 'loading' | 'error'}>
|
state: Exclude<PurchasesState, {status: 'loading' | 'error'}>
|
||||||
|
restricted: NativePurchaseRestricted
|
||||||
}) {
|
}) {
|
||||||
|
const coreSubscriptions = state.subscriptions
|
||||||
|
.filter(s => s.group === SubscriptionGroupId.Core)
|
||||||
|
.filter(s => {
|
||||||
|
return ['active', 'paused'].includes(s.status)
|
||||||
|
})
|
||||||
|
const isSubscribedToCore = coreSubscriptions.length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<BlueskyPlusLogo width={130} gradient="midnight" />
|
||||||
|
|
||||||
|
{restricted === 'yes' ? (
|
||||||
|
<Admonition type="info">
|
||||||
|
<Trans>
|
||||||
|
Another account on this device is already subscribed to Bluesky+.
|
||||||
|
Please subscribe additional accounts through our web application.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
) : isSubscribedToCore ? null : (
|
||||||
|
<Purchase />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSubscribedToCore ? (
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
{coreSubscriptions.map(sub => (
|
||||||
|
<Subscription key={sub.purchasedAt} subscription={sub} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Purchase() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const coreSubscriptions = state.subscriptions.filter(
|
|
||||||
s => s.group === SubscriptionGroupId.Core,
|
|
||||||
)
|
|
||||||
const isSubscribedToCore = !!coreSubscriptions.length
|
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
@@ -122,24 +159,6 @@ function Core({
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[]}>
|
|
||||||
{isSubscribedToCore ? (
|
|
||||||
<CoreSubscriptions subscriptions={state.subscriptions} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isSubscribedToCore && (
|
|
||||||
<>
|
|
||||||
{state.config.nativePurchaseRestricted === 'yes' ? (
|
|
||||||
<View style={[a.pt_md]}>
|
|
||||||
<Admonition type="info">
|
|
||||||
<Trans>
|
|
||||||
Another account on this device is already subscribed to
|
|
||||||
Bluesky+. Please subscribe additional accounts through our web
|
|
||||||
application.
|
|
||||||
</Trans>
|
|
||||||
</Admonition>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<BlueskyPlusLogo width={130} gradient="nordic" />
|
<BlueskyPlusLogo width={130} gradient="nordic" />
|
||||||
|
|
||||||
@@ -156,8 +175,8 @@ function Core({
|
|||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
]}>
|
]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
Subscribing to Bluesky+ helps ensure that our work to build
|
Subscribing to Bluesky+ helps ensure that our work to build an open,
|
||||||
an open, secure, and user-first internet can continue.
|
secure, and user-first internet can continue.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -195,9 +214,7 @@ function Core({
|
|||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
f.available
|
f.available ? t.atoms.text : t.atoms.text_contrast_medium,
|
||||||
? t.atoms.text
|
|
||||||
: t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
]}>
|
||||||
{f.text}
|
{f.text}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -216,163 +233,143 @@ function Core({
|
|||||||
<ButtonText style={[t.atoms.text]}>
|
<ButtonText style={[t.atoms.text]}>
|
||||||
<Trans>Subscribe</Trans>
|
<Trans>Subscribe</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
<ButtonIcon
|
<ButtonIcon icon={Plus} position="right" style={[t.atoms.text]} />
|
||||||
icon={Plus}
|
|
||||||
position="right"
|
|
||||||
style={[t.atoms.text]}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<BlueskyPlus control={control} />
|
<BlueskyPlus control={control} />
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/*
|
|
||||||
<View style={[a.pt_md]}>
|
|
||||||
<Text style={[a.mb_md, a.text_xs]}>
|
|
||||||
<InlineLinkText
|
|
||||||
to="#"
|
|
||||||
label="TODO REPLACE"
|
|
||||||
style={[a.text_xs, t.atoms.text_contrast_low]}>
|
|
||||||
Terms and Conditions
|
|
||||||
</InlineLinkText>{' '}
|
|
||||||
·{' '}
|
|
||||||
<InlineLinkText
|
|
||||||
to="#"
|
|
||||||
label="TODO REPLACE"
|
|
||||||
style={[a.text_xs, t.atoms.text_contrast_low]}>
|
|
||||||
Privacy Policy
|
|
||||||
</InlineLinkText>{' '}
|
|
||||||
·{' '}
|
|
||||||
<InlineLinkText
|
|
||||||
to="#"
|
|
||||||
label="TODO REPLACE"
|
|
||||||
style={[a.text_xs, t.atoms.text_contrast_low]}>
|
|
||||||
EULA
|
|
||||||
</InlineLinkText>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
*/}
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoreSubscriptions(props: {subscriptions: APISubscription[]}) {
|
export function useSubscriptionUI({
|
||||||
const t = useTheme()
|
subscription: sub,
|
||||||
|
}: {
|
||||||
|
subscription: APISubscription
|
||||||
|
}) {
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {mutateAsync: manageSubscription} = useManageSubscription()
|
|
||||||
|
|
||||||
const copy = React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
return {
|
const periodEndsAt = i18n.date(new Date(sub.periodEndsAt), {
|
||||||
[SubscriptionOfferingId.CoreMonthly]: {
|
dateStyle: 'short',
|
||||||
title: _(msg`Monthly`),
|
})
|
||||||
},
|
const title = {
|
||||||
[SubscriptionOfferingId.CoreAnnual]: {
|
[SubscriptionOfferingId.CoreMonthly]: _(msg`Monthly`),
|
||||||
title: _(msg`Annual`),
|
[SubscriptionOfferingId.CoreAnnual]: _(msg`Annual`),
|
||||||
},
|
}[sub.offering]
|
||||||
}
|
const PlatformLogo = {
|
||||||
}, [_])
|
|
||||||
|
|
||||||
return props.subscriptions.map(sub => {
|
|
||||||
const endDate = i18n.date(new Date(sub.periodEndsAt), {dateStyle: 'medium'})
|
|
||||||
const StatusIcon = sub.renews ? Rotate : Clock
|
|
||||||
const c = copy[sub.offering]
|
|
||||||
const canManage =
|
|
||||||
(isWeb && sub.platform === PlatformId.Web) ||
|
|
||||||
(isIOS && sub.platform === PlatformId.Ios) ||
|
|
||||||
(isAndroid && sub.platform === PlatformId.Android)
|
|
||||||
const PlatformIcon = {
|
|
||||||
[PlatformId.Ios]: AppleLogo,
|
[PlatformId.Ios]: AppleLogo,
|
||||||
[PlatformId.Android]: AndroidLogo,
|
[PlatformId.Android]: AndroidLogo,
|
||||||
[PlatformId.Web]: Globe,
|
[PlatformId.Web]: Globe,
|
||||||
}[sub.platform]
|
}[sub.platform]
|
||||||
|
const platformName = {
|
||||||
|
[PlatformId.Ios]: _('iOS'),
|
||||||
|
[PlatformId.Android]: _('Android'),
|
||||||
|
[PlatformId.Web]: _('Web'),
|
||||||
|
}[sub.platform]
|
||||||
|
const status = {
|
||||||
|
active: _(msg`Active`),
|
||||||
|
expired: _(msg`Expired`),
|
||||||
|
paused: _(msg`Paused`),
|
||||||
|
unknown: _(msg`Unknown`),
|
||||||
|
}[sub.status]
|
||||||
|
const renewalStatus = {
|
||||||
|
will_renew: _(msg`Renews ${periodEndsAt}`),
|
||||||
|
will_not_renew: {
|
||||||
|
active: _(msg`Expires ${periodEndsAt}`),
|
||||||
|
expired: _(msg`Expired ${periodEndsAt}`),
|
||||||
|
paused: _(msg`Expires ${periodEndsAt}`),
|
||||||
|
unknown: _(msg`Unknown`),
|
||||||
|
}[sub.status],
|
||||||
|
will_pause: _(msg`Pauses ${periodEndsAt}`),
|
||||||
|
unknown: _(msg`Unknown`),
|
||||||
|
}[sub.renewalStatus]
|
||||||
|
const RenewalStatusIcon = {
|
||||||
|
will_renew: Rotate,
|
||||||
|
will_not_renew: Clock,
|
||||||
|
will_pause: Clock,
|
||||||
|
unknown: Clock,
|
||||||
|
}[sub.renewalStatus]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
renewalStatus,
|
||||||
|
platformName,
|
||||||
|
PlatformLogo,
|
||||||
|
RenewalStatusIcon,
|
||||||
|
}
|
||||||
|
}, [_, i18n, sub])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Subscription({
|
||||||
|
subscription: sub,
|
||||||
|
}: {
|
||||||
|
subscription: APISubscription
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {mutateAsync: manageSubscription} = useManageSubscription()
|
||||||
|
const ui = useSubscriptionUI({subscription: sub})
|
||||||
|
|
||||||
|
const canManage =
|
||||||
|
(isWeb && sub.platform === PlatformId.Web) ||
|
||||||
|
(isIOS && sub.platform === PlatformId.Ios) ||
|
||||||
|
(isAndroid && sub.platform === PlatformId.Android)
|
||||||
|
|
||||||
|
const statusStyles = React.useMemo<TextStyle>(() => {
|
||||||
|
return {
|
||||||
|
active: {
|
||||||
|
color: tokens.blueskyPlus.dark,
|
||||||
|
backgroundColor: tokens.blueskyPlus.light,
|
||||||
|
borderWidth: undefined,
|
||||||
|
borderColor: undefined,
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
color: t.atoms.text_contrast_medium.color,
|
||||||
|
backgroundColor: t.atoms.bg.backgroundColor,
|
||||||
|
borderWidth: a.border.borderWidth,
|
||||||
|
borderColor: t.atoms.border_contrast_high.borderColor,
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
color: t.atoms.bg.backgroundColor,
|
||||||
|
backgroundColor: t.atoms.text_contrast_medium.color,
|
||||||
|
borderWidth: undefined,
|
||||||
|
borderColor: undefined,
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
color: t.atoms.text_contrast_medium.color,
|
||||||
|
backgroundColor: t.atoms.bg.backgroundColor,
|
||||||
|
borderWidth: a.border.borderWidth,
|
||||||
|
borderColor: t.atoms.border_contrast_high.borderColor,
|
||||||
|
},
|
||||||
|
}[sub.status]
|
||||||
|
}, [t, sub.status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={sub.purchasedAt}
|
key={sub.purchasedAt}
|
||||||
style={[
|
style={[
|
||||||
a.p_md,
|
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.border,
|
a.border,
|
||||||
a.gap_xs,
|
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
<View
|
<View style={[a.flex_row, a.justify_between, a.align_center, a.py_sm]}>
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
{paddingBottom: 6},
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
<Logotype width={75} fill={t.atoms.text.color} />
|
<Text style={[a.text_lg, a.font_heavy]}>{ui.title}</Text>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_xs,
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
{
|
{
|
||||||
paddingVertical: 3,
|
paddingVertical: 2,
|
||||||
paddingHorizontal: 5,
|
paddingHorizontal: 6,
|
||||||
backgroundColor: tokens.blueskyPlus.dark,
|
...statusStyles,
|
||||||
color: tokens.blueskyPlus.light,
|
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{c.title}
|
{ui.status}
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_sm,
|
|
||||||
a.rounded_full,
|
|
||||||
a.font_bold,
|
|
||||||
{
|
|
||||||
paddingVertical: 3,
|
|
||||||
paddingHorizontal: 5,
|
|
||||||
borderWidth: sub.renews ? 0 : a.border.borderWidth,
|
|
||||||
borderColor: t.palette.negative_500,
|
|
||||||
backgroundColor: sub.renews
|
|
||||||
? tokens.blueskyPlus.light
|
|
||||||
: t.atoms.bg.backgroundColor,
|
|
||||||
color: sub.renews
|
|
||||||
? tokens.blueskyPlus.dark
|
|
||||||
: t.atoms.text.color,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{sub.renews ? <Trans>Active</Trans> : <Trans>Cancelled</Trans>}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
|
||||||
<StatusIcon size="sm" fill={t.atoms.text_contrast_low.color} />
|
|
||||||
<Text style={[a.text_sm, a.font_bold]}>{endDate}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
{paddingTop: 6},
|
|
||||||
]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_xs,
|
|
||||||
{left: -1},
|
|
||||||
]}>
|
|
||||||
<PlatformIcon size="md" fill={t.atoms.text_contrast_low.color} />
|
|
||||||
<Text
|
|
||||||
style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Managed via web</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -392,7 +389,34 @@ function CoreSubscriptions(props: {subscriptions: APISubscription[]}) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<View style={[a.py_sm, a.flex_row, a.justify_between, a.align_center]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_xs,
|
||||||
|
{left: -1},
|
||||||
|
]}>
|
||||||
|
<ui.PlatformLogo size="md" fill={t.atoms.text_contrast_low.color} />
|
||||||
|
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
|
||||||
|
{ui.platformName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<ui.RenewalStatusIcon
|
||||||
|
size="sm"
|
||||||
|
fill={t.atoms.text_contrast_low.color}
|
||||||
|
/>
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
{ui.renewalStatus}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import {APIEntitlement,APISubscription} from '#/state/purchases/types'
|
import {APIEntitlement, APISubscription} from '#/state/purchases/types'
|
||||||
|
|
||||||
export type NativePurchaseRestricted = 'yes' | 'no' | 'unknown'
|
|
||||||
|
|
||||||
export type PurchasesState =
|
export type PurchasesState =
|
||||||
| {
|
| {
|
||||||
@@ -17,9 +15,7 @@ export type PurchasesState =
|
|||||||
email?: string
|
email?: string
|
||||||
subscriptions: APISubscription[]
|
subscriptions: APISubscription[]
|
||||||
entitlements: APIEntitlement[]
|
entitlements: APIEntitlement[]
|
||||||
config: {
|
config: {}
|
||||||
nativePurchaseRestricted: NativePurchaseRestricted
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = React.createContext<PurchasesState>({
|
export const Context = React.createContext<PurchasesState>({
|
||||||
|
|||||||
@@ -1,40 +1,57 @@
|
|||||||
import React from 'react'
|
|
||||||
import Purchases, {PURCHASES_ERROR_CODE} from 'react-native-purchases'
|
import Purchases, {PURCHASES_ERROR_CODE} from 'react-native-purchases'
|
||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {NativePurchaseRestricted} from '#/state/purchases/context'
|
import {NativePurchaseRestricted} from '#/state/purchases/types'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
export function useNativeUserState() {
|
export function useNativeUserState() {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const [restricted, setRestricted] =
|
|
||||||
React.useState<NativePurchaseRestricted>('unknown')
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const {data, isFetching} = useQuery<{
|
||||||
async function check() {
|
restricted: NativePurchaseRestricted
|
||||||
if (!currentAccount?.did) return
|
}>({
|
||||||
|
queryKey: ['purchases-native-user-state', currentAccount?.did],
|
||||||
// reset on each run
|
placeholderData: {restricted: 'unknown'},
|
||||||
setRestricted('unknown')
|
/**
|
||||||
|
* Once we fetch this, no need to do so again for the current session. We
|
||||||
|
* don't do any transfer logic between accounts, so this doesn't change
|
||||||
|
* during the course of a user session.
|
||||||
|
*/
|
||||||
|
staleTime: Infinity,
|
||||||
|
async queryFn() {
|
||||||
|
if (!currentAccount) {
|
||||||
|
return {restricted: 'unknown'}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// MUST ensure we're the correct user first
|
// MUST ensure we're the correct user first
|
||||||
await Purchases.logIn(currentAccount?.did)
|
await Purchases.logIn(currentAccount.did)
|
||||||
await Purchases.restorePurchases()
|
await Purchases.restorePurchases()
|
||||||
setRestricted('no')
|
return {restricted: 'no'}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
switch (e.code) {
|
||||||
/**
|
/**
|
||||||
|
* Indicates there are active subscriptions for another account on
|
||||||
|
* this device. User cannot make additional purchases on this device.
|
||||||
|
*
|
||||||
* @see https://www.revenuecat.com/docs/test-and-launch/errors#-receipt_already_in_use
|
* @see https://www.revenuecat.com/docs/test-and-launch/errors#-receipt_already_in_use
|
||||||
*/
|
*/
|
||||||
if (e.code === PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR) {
|
case PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR:
|
||||||
setRestricted('yes')
|
return {restricted: 'yes'}
|
||||||
|
default:
|
||||||
|
/**
|
||||||
|
* Any other error is considered unknown. Let the user proceed.
|
||||||
|
* Additional errors can occur at time of purchase and should be
|
||||||
|
* handled there.
|
||||||
|
*/
|
||||||
|
return {restricted: 'no'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
})
|
||||||
check()
|
|
||||||
}, [currentAccount?.did])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
restricted,
|
loading: isFetching,
|
||||||
|
restricted: data?.restricted ?? 'unknown',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export function useNativeUserState() {
|
import {NativePurchaseRestricted} from '#/state/purchases/types'
|
||||||
|
|
||||||
|
export function useNativeUserState(): {
|
||||||
|
loading: boolean
|
||||||
|
restricted: NativePurchaseRestricted
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
restricted: false,
|
loading: false,
|
||||||
|
restricted: 'no',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Purchases, {PURCHASES_ERROR_CODE} from 'react-native-purchases'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {
|
||||||
|
PlatformId,
|
||||||
|
ReceiptAlreadyInUseError,
|
||||||
|
SubscriptionOffering,
|
||||||
|
} from '#/state/purchases/types'
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
did: string
|
||||||
|
email: string
|
||||||
|
offering: SubscriptionOffering
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePurchaseOffering() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return useMutation<void, Error | ReceiptAlreadyInUseError, Params>({
|
||||||
|
async mutationFn({did, email, offering}: Params) {
|
||||||
|
if (offering.platform === PlatformId.Web) {
|
||||||
|
throw new Error('Unsupported platform')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Purchases.logIn(did)
|
||||||
|
await Purchases.setEmail(email)
|
||||||
|
await Purchases.purchaseStoreProduct(offering.package)
|
||||||
|
} catch (e: any) {
|
||||||
|
/**
|
||||||
|
* @see https://www.revenuecat.com/docs/test-and-launch/errors#-receipt_already_in_use
|
||||||
|
*/
|
||||||
|
if (e.code === PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR) {
|
||||||
|
throw new ReceiptAlreadyInUseError(
|
||||||
|
_(
|
||||||
|
msg`This device has existing active subscriptions for a different account. Multiple account subscriptions are not possible on native platforms at this time.`,
|
||||||
|
),
|
||||||
|
// @ts-ignore
|
||||||
|
{cause: e},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {Linking} from 'react-native'
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {api} from '#/state/purchases/api'
|
||||||
|
import {PlatformId, SubscriptionOffering} from '#/state/purchases/types'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
|
export function usePurchaseOffering() {
|
||||||
|
return useMutation({
|
||||||
|
async mutationFn({
|
||||||
|
did,
|
||||||
|
email,
|
||||||
|
offering,
|
||||||
|
}: {
|
||||||
|
did: string
|
||||||
|
email: string
|
||||||
|
offering: SubscriptionOffering
|
||||||
|
}) {
|
||||||
|
if (offering.platform !== PlatformId.Web) {
|
||||||
|
throw new Error('Unsupported platform')
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data, error} = await api<{
|
||||||
|
checkoutUrl: string
|
||||||
|
}>('/checkout/create', {
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
did,
|
||||||
|
email,
|
||||||
|
price: offering.package.priceId,
|
||||||
|
redirectUrl: IS_DEV
|
||||||
|
? `http://localhost:19006/subscriptions`
|
||||||
|
: `https://bsky.app/subscriptions`,
|
||||||
|
},
|
||||||
|
}).json()
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
throw new Error(`Failed to create checkout URL`)
|
||||||
|
}
|
||||||
|
|
||||||
|
Linking.openURL(data.checkoutUrl)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import Purchases, {PURCHASES_ERROR_CODE} from 'react-native-purchases'
|
import Purchases from 'react-native-purchases'
|
||||||
import {useMutation,useQuery} from '@tanstack/react-query'
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {api} from '#/state/purchases/api'
|
import {api} from '#/state/purchases/api'
|
||||||
import {
|
import {
|
||||||
parseOfferingId,
|
APIOffering,
|
||||||
PlatformId,
|
PlatformId,
|
||||||
SubscriptionGroupId,
|
SubscriptionGroupId,
|
||||||
SubscriptionOffering,
|
SubscriptionOffering,
|
||||||
@@ -14,67 +14,56 @@ export function useSubscriptionGroup(group: SubscriptionGroupId) {
|
|||||||
return useQuery<{offerings: SubscriptionOffering[]}>({
|
return useQuery<{offerings: SubscriptionOffering[]}>({
|
||||||
queryKey: ['subscription-group', group],
|
queryKey: ['subscription-group', group],
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
const platform = isIOS ? 'ios' : 'android'
|
const platform = isAndroid ? 'android' : 'ios'
|
||||||
const {data, error, response} = await api(
|
const {data, error} = await api<{
|
||||||
`/subscriptions/${group}?platform=${platform}`,
|
offerings: APIOffering[]
|
||||||
).json()
|
}>(`/subscriptions/${group}?platform=${platform}`).json()
|
||||||
|
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
console.log(error, response)
|
throw new Error(`Failed to fetch subscription group`)
|
||||||
throw new Error('Failed to fetch subscription group')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {offerings} = data
|
const {offerings} = data
|
||||||
const productIds = offerings.map((o: any) => {
|
const revenueCatIdentifiers = offerings.map(o =>
|
||||||
return isIOS ? o.productId : o.productId.split(':')[0]
|
isAndroid ? parseIdentifierFromAndroidProductId(o.product) : o.product,
|
||||||
})
|
|
||||||
// console.log({ offers: false })
|
|
||||||
// const offers = await Purchases.getOfferings()
|
|
||||||
// console.log({ offers })
|
|
||||||
const products = await Purchases.getProducts(productIds)
|
|
||||||
|
|
||||||
return {
|
|
||||||
offerings: offerings.map((o: any) => {
|
|
||||||
const product = products.find(
|
|
||||||
(p: any) => p.identifier === o.productId,
|
|
||||||
)
|
)
|
||||||
return {
|
const products = await Purchases.getProducts(revenueCatIdentifiers)
|
||||||
id: parseOfferingId(o.id),
|
|
||||||
platform: isIOS ? PlatformId.Ios : PlatformId.Android,
|
const parsed: SubscriptionOffering[] = []
|
||||||
price: product?.priceString,
|
for (const o of offerings) {
|
||||||
|
if (o.platform === PlatformId.Web) continue
|
||||||
|
|
||||||
|
const product = products.find(p => p.identifier === o.product)
|
||||||
|
if (!product) continue
|
||||||
|
|
||||||
|
parsed.push({
|
||||||
|
id: o.id,
|
||||||
|
platform: o.platform,
|
||||||
package: product,
|
package: product,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
|
return {
|
||||||
|
offerings: parsed,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePurchaseOffering() {
|
/**
|
||||||
return useMutation({
|
* Whereas iOS has separate product IDs for each subscription product, Android
|
||||||
async mutationFn({
|
* has a single ID with a suffixed payment plan e.g. `monthly` and `annual` for
|
||||||
did,
|
* our core offerings.
|
||||||
email,
|
*
|
||||||
offering,
|
* However, the full "identifier" is concatenated, so we just pass around the
|
||||||
}: {
|
* full thing and parse from there.
|
||||||
did: string
|
*/
|
||||||
email: string
|
function parseIdentifierFromAndroidProductId(productId: string) {
|
||||||
offering: SubscriptionOffering
|
if (!productId.includes(':')) {
|
||||||
}) {
|
throw new Error(
|
||||||
if (offering.platform === PlatformId.Web) {
|
`Expected Android product ID to contain a colon: ${productId}`,
|
||||||
throw new Error('Unsupported platform')
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return productId.split(':')[0]
|
||||||
await Purchases.logIn(did)
|
|
||||||
await Purchases.setEmail(email)
|
|
||||||
await Purchases.purchaseStoreProduct(offering.package)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.code === PURCHASES_ERROR_CODE.RECEIPT_ALREADY_IN_USE_ERROR) {
|
|
||||||
console.log('recipt error', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,43 @@
|
|||||||
import {Linking} from 'react-native'
|
import {useQuery} from '@tanstack/react-query'
|
||||||
import {useMutation,useQuery} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {api} from '#/state/purchases/api'
|
import {api} from '#/state/purchases/api'
|
||||||
import {
|
import {
|
||||||
parseOfferingId,
|
APIOffering,
|
||||||
PlatformId,
|
PlatformId,
|
||||||
SubscriptionGroupId,
|
SubscriptionGroupId,
|
||||||
SubscriptionOffering,
|
SubscriptionOffering,
|
||||||
} from '#/state/purchases/types'
|
} from '#/state/purchases/types'
|
||||||
import {IS_DEV} from '#/env'
|
|
||||||
|
|
||||||
export function useSubscriptionGroup(group: SubscriptionGroupId) {
|
export function useSubscriptionGroup(group: SubscriptionGroupId) {
|
||||||
return useQuery<{offerings: SubscriptionOffering[]}>({
|
return useQuery<{offerings: SubscriptionOffering[]}>({
|
||||||
queryKey: ['subscription-group', group],
|
queryKey: ['subscription-group', group],
|
||||||
async queryFn() {
|
async queryFn() {
|
||||||
const {data, error} = await api(
|
const {data, error} = await api<{
|
||||||
`/subscriptions/${group}?platform=web`,
|
offerings: APIOffering[]
|
||||||
).json()
|
}>(`/subscriptions/${group}?platform=web`).json()
|
||||||
if (error || !data) {
|
if (error || !data) {
|
||||||
throw new Error('Failed to fetch subscription group')
|
throw new Error('Failed to fetch subscription group')
|
||||||
}
|
}
|
||||||
|
|
||||||
const {offerings} = data
|
const {offerings} = data
|
||||||
|
|
||||||
return {
|
const parsed: SubscriptionOffering[] = []
|
||||||
offerings: offerings.map((o: any) => ({
|
|
||||||
id: parseOfferingId(o.id),
|
for (const o of offerings) {
|
||||||
platform: PlatformId.Web,
|
if (o.platform !== PlatformId.Web) continue
|
||||||
|
|
||||||
|
parsed.push({
|
||||||
|
id: o.id,
|
||||||
|
platform: o.platform,
|
||||||
package: {
|
package: {
|
||||||
priceId: o.productId,
|
priceId: o.product,
|
||||||
},
|
},
|
||||||
})),
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
offerings: parsed,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePurchaseOffering() {
|
|
||||||
return useMutation({
|
|
||||||
async mutationFn({
|
|
||||||
did,
|
|
||||||
email,
|
|
||||||
offering,
|
|
||||||
}: {
|
|
||||||
did: string
|
|
||||||
email: string
|
|
||||||
offering: SubscriptionOffering
|
|
||||||
}) {
|
|
||||||
if (offering.platform !== PlatformId.Web) {
|
|
||||||
throw new Error('Unsupported platform')
|
|
||||||
}
|
|
||||||
|
|
||||||
const {data, error} = await api<{
|
|
||||||
checkoutUrl: string
|
|
||||||
}>('/checkout/create', {
|
|
||||||
method: 'POST',
|
|
||||||
json: {
|
|
||||||
did,
|
|
||||||
email,
|
|
||||||
price: offering.package.priceId,
|
|
||||||
redirectUrl: IS_DEV
|
|
||||||
? `http://localhost:19006/subscriptions`
|
|
||||||
: `https://bsky.app/subscriptions`,
|
|
||||||
},
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if (error || !data) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
Linking.openURL(data.checkoutUrl)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react'
|
|||||||
|
|
||||||
import {Context, PurchasesState} from '#/state/purchases/context'
|
import {Context, PurchasesState} from '#/state/purchases/context'
|
||||||
import {useNativeEventsListener} from '#/state/purchases/hooks/useNativeEventsListener'
|
import {useNativeEventsListener} from '#/state/purchases/hooks/useNativeEventsListener'
|
||||||
import {useNativeUserState} from '#/state/purchases/hooks/useNativeUserState'
|
|
||||||
import {usePurchasesState} from '#/state/purchases/hooks/usePurchasesState'
|
import {usePurchasesState} from '#/state/purchases/hooks/usePurchasesState'
|
||||||
|
|
||||||
export type {PurchasesState} from '#/state/purchases/context'
|
export type {PurchasesState} from '#/state/purchases/context'
|
||||||
@@ -13,7 +12,6 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
error: purchasesStateError,
|
error: purchasesStateError,
|
||||||
refetch,
|
refetch,
|
||||||
} = usePurchasesState()
|
} = usePurchasesState()
|
||||||
const {restricted} = useNativeUserState()
|
|
||||||
const ctx = React.useMemo<PurchasesState>(() => {
|
const ctx = React.useMemo<PurchasesState>(() => {
|
||||||
if (purchasesStateError) {
|
if (purchasesStateError) {
|
||||||
return {
|
return {
|
||||||
@@ -30,12 +28,10 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
email: purchases?.email,
|
email: purchases?.email,
|
||||||
subscriptions: purchases?.subscriptions ?? [],
|
subscriptions: purchases?.subscriptions ?? [],
|
||||||
entitlements: purchases?.entitlements ?? [],
|
entitlements: purchases?.entitlements ?? [],
|
||||||
config: {
|
config: {},
|
||||||
nativePurchaseRestricted: restricted,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [purchases, purchasesStateError, restricted])
|
}, [purchases, purchasesStateError])
|
||||||
|
|
||||||
useNativeEventsListener({
|
useNativeEventsListener({
|
||||||
onCustomerInfoUpdated() {
|
onCustomerInfoUpdated() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {PurchasesStoreProduct} from 'react-native-purchases'
|
import type {PurchasesStoreProduct} from 'react-native-purchases'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primitives
|
* PRIMITIVES
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export enum EntitlementId {
|
export enum EntitlementId {
|
||||||
@@ -15,7 +15,7 @@ export enum PlatformId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription primitives
|
* SUBSCRIPTION PRIMITIVES
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export enum SubscriptionGroupId {
|
export enum SubscriptionGroupId {
|
||||||
@@ -41,15 +41,18 @@ export type SubscriptionOffering =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NativePurchaseRestricted = 'yes' | 'no' | 'unknown'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API types for our toy server
|
* API TYPES FROM OUR TOY SERVER
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type APISubscription = {
|
export type APISubscription = {
|
||||||
|
status: 'active' | 'paused' | 'expired' | 'unknown'
|
||||||
|
renewalStatus: 'will_renew' | 'will_not_renew' | 'will_pause' | 'unknown'
|
||||||
group: SubscriptionGroupId
|
group: SubscriptionGroupId
|
||||||
platform: PlatformId
|
platform: PlatformId
|
||||||
offering: SubscriptionOfferingId
|
offering: SubscriptionOfferingId
|
||||||
renews: boolean
|
|
||||||
periodDtartsAt: string
|
periodDtartsAt: string
|
||||||
periodEndsAt: string
|
periodEndsAt: string
|
||||||
purchasedAt: string
|
purchasedAt: string
|
||||||
@@ -59,8 +62,25 @@ export type APIEntitlement = {
|
|||||||
id: EntitlementId
|
id: EntitlementId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type APIOffering = {
|
||||||
|
id: SubscriptionOfferingId
|
||||||
|
platform: PlatformId
|
||||||
|
product: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parsers
|
* CUSTOM ERROR TYPES
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if the device already has active subscriptions for other accounts.
|
||||||
|
*
|
||||||
|
* @see https://www.revenuecat.com/docs/test-and-launch/errors#-receipt_already_in_use
|
||||||
|
*/
|
||||||
|
export class ReceiptAlreadyInUseError extends Error {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PARSERS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function parseOfferingId(id: string): SubscriptionOfferingId {
|
export function parseOfferingId(id: string): SubscriptionOfferingId {
|
||||||
|
|||||||
Reference in New Issue
Block a user