diff --git a/src/components/dialogs/SubscriptionsDialog.tsx b/src/components/dialogs/SubscriptionsDialog.tsx new file mode 100644 index 0000000000..eb54c760b6 --- /dev/null +++ b/src/components/dialogs/SubscriptionsDialog.tsx @@ -0,0 +1,269 @@ +import React from 'react' +import {Linking, View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCreateCheckout} from '#/state/purchases/subscriptions/useCreateCheckout' +import {useSession} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {GradientFill} from '#/components/GradientFill' +import {Full as BlueskyPlusLogo} from '#/components/icons/BlueskyPlus' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export function SubscriptionsDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + return ( + + + + ) +} + +function DialogInner() { + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + const t = useTheme() + const [plan, setPlan] = React.useState<'monthly' | 'annual'>('monthly') + + const {mutateAsync: createCheckout, isPending} = useCreateCheckout() + + const onPressSubscribe = async () => { + try { + if (!currentAccount) return + const data = await createCheckout({ + did: currentAccount.did, + email: currentAccount.email!, + price: + plan === 'monthly' + ? 'price_1QLsNLAwTlpRxHkAhuUwbnlL' + : 'price_1QLsOjAwTlpRxHkAcrV8DL2P', + }) + + if (!data?.checkoutUrl) { + throw new Error('No checkout URL') + } + + Linking.openURL(data.checkoutUrl) + } catch (e: any) { + Toast.show(_(msg`Could not take you to checkout`), 'xmark') + } + } + + return ( + + + + + + + + + + + Early bird discount! + + + Support Bluesky now and get a 25% discount for a year + + setPlan(values[0] as 'monthly' | 'annual')} + style={[a.w_full, a.gap_md, a.my_xl]}> + + {({selected}) => ( + + + + + + Monthly plan + + + $6/month + + + + + Billed monthly, cancel anytime. $8/month after 12 months + + + + + )} + + + {({selected}) => ( + + + + + + Annual plan + + + $60/year + + + + Billed annually, renews at $80/year + + + + )} + + + + + + Terms and Conditions + {' '} + ·{' '} + + Privacy Policy + {' '} + ·{' '} + + EULA + + + + + + + ) +} + +function MarketingBlurb() { + return ( + <> + + + + + Here's the thing: we don't have any premium features yet because all + our servers are on fire. Support us now so we can put the fires out + and get working on those features! + + + + + What you'll get now: + + + + {' '} + · A supporter badge on your profile + + + {' '} + · A warm fuzzy feeling knowing you helped us out + + + + + What you'll get soon: + + + + {' '} + · Custom profile colors + + + {' '} + · Longer and higher quality videos + + + {' '} + · Custom app icons + + + + ) +} diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 4e3695bbf2..9205e97632 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Pressable, View, ViewStyle} from 'react-native' +import {Pressable, StyleProp, View, ViewStyle} from 'react-native' import Animated, {LinearTransition} from 'react-native-reanimated' import {HITSLOP_10} from '#/lib/constants' @@ -57,6 +57,7 @@ export type GroupProps = React.PropsWithChildren<{ disabled?: boolean onChange: (value: string[]) => void label: string + style?: StyleProp }> export type ItemProps = ViewStyleProp & { @@ -82,6 +83,7 @@ export function Group({ type = 'checkbox', maxSelections, label, + style, }: GroupProps) { const groupRole = type === 'radio' ? 'radiogroup' : undefined const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues @@ -134,7 +136,7 @@ export function Group({ return ( {gradient && } @@ -54,15 +61,20 @@ export function Logotype(props: Props) { const {fill, ...rest} = props const gradient = fill === 'nordic' const styles = StyleSheet.flatten(props.style) - const _fill = gradient ? 'url(#nordic)' : fill || styles?.color || t.palette.primary_500 + const _fill = gradient + ? 'url(#nordic)' + : fill || styles?.color || t.palette.primary_500 // @ts-ignore it's fiiiiine const size = parseInt(rest.width || 100) const ratio = 78 / 330 + const {_} = useLingui() return ( {gradient && } diff --git a/src/screens/Subscriptions/index.tsx b/src/screens/Subscriptions/index.tsx index 23c5d7ceab..bac2692f1c 100644 --- a/src/screens/Subscriptions/index.tsx +++ b/src/screens/Subscriptions/index.tsx @@ -1,24 +1,20 @@ import React from 'react' -import {View, Linking} from 'react-native' -import {msg} from '@lingui/macro' +import {View} from 'react-native' import {useLingui} from '@lingui/react' import {NativeStackScreenProps} from '@react-navigation/native-stack' -// import {isAndroid, isIOS} from '#/platform/detection' import {CommonNavigatorParams} from '#/lib/routes/types' +import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements' import {CenteredView} from '#/view/com/util/Views' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {SubscriptionsDialog} from '#/components/dialogs/SubscriptionsDialog' +import {Logotype} from '#/components/icons/BlueskyPlus' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' -import {Button, ButtonText, ButtonIcon} from '#/components/Button' import {Text} from '#/components/Typography' -// import {Divider} from '#/components/Divider' -import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {Logotype} from '#/components/icons/BlueskyPlus' -import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements' -import {useCreateCheckout} from '#/state/purchases/subscriptions/useCreateCheckout' -import {useSession} from '#/state/session' export type ScreenProps = NativeStackScreenProps< CommonNavigatorParams, @@ -26,54 +22,17 @@ export type ScreenProps = NativeStackScreenProps< > export function Subscriptions(_props: ScreenProps) { - const t = useTheme() const {_} = useLingui() - const {currentAccount} = useSession() - const {data: entitlements, isLoading: isEntitlementsLoading} = useEntitlements() + const {data: entitlements, isLoading: isEntitlementsLoading} = + useEntitlements() const isSubscribed = entitlements?.some(e => e.id === 'core') - - const {mutateAsync: createCheckout} = useCreateCheckout() - const isPending = false - - const onPressMonthly = async () => { - try { - const data = await createCheckout({ - did: currentAccount!.did, - email: currentAccount!.email!, - price: 'price_1QLsNLAwTlpRxHkAhuUwbnlL', - }) - - if (!data?.checkoutUrl) { - throw new Error('No checkout URL') - } - - Linking.openURL(data.checkoutUrl) - } catch (e: any) { - } - } - const onPressAnnual = async () => { - try { - const data = await createCheckout({ - did: currentAccount!.did, - email: currentAccount!.email!, - price: 'price_1QLsOjAwTlpRxHkAcrV8DL2P', - }) - - if (!data?.checkoutUrl) { - throw new Error('No checkout URL') - } - - Linking.openURL(data.checkoutUrl) - } catch (e: any) { - } - } - + const control = useDialogControl() return ( - + {isEntitlementsLoading ? ( @@ -85,28 +44,18 @@ export function Subscriptions(_props: ScreenProps) { You're subscribed! ) : ( - + <> - - + + )} )} diff --git a/src/state/purchases/subscriptions/useCreateCheckout.ts b/src/state/purchases/subscriptions/useCreateCheckout.ts index a02bc5c6eb..775b2def18 100644 --- a/src/state/purchases/subscriptions/useCreateCheckout.ts +++ b/src/state/purchases/subscriptions/useCreateCheckout.ts @@ -1,23 +1,21 @@ import {useMutation} from '@tanstack/react-query' -import {IS_DEV} from '#/env' import {api} from '#/state/purchases/subscriptions/api' +import {IS_DEV} from '#/env' export function useCreateCheckout() { return useMutation({ - async mutationFn(props: { - price: string, - did: string, - email: string, - }) { + async mutationFn(props: {price: string; did: string; email: string}) { const {data, error} = await api<{ checkoutUrl: string }>('/checkout/create', { method: 'POST', json: { ...props, - redirectUrl: IS_DEV ? `http://localhost:19006/subscriptions` : `https://bsky.app/subscriptions`, - } + redirectUrl: IS_DEV + ? `http://localhost:19006/subscriptions` + : `https://bsky.app/subscriptions`, + }, }).json() if (error) { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 0ef03588d7..d497ec0e64 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -6,22 +6,26 @@ import {useLingui} from '@lingui/react' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useKawaiiMode} from '#/state/preferences/kawaii' +import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements' import {useSession} from '#/state/session' import {DesktopFeeds} from '#/view/shell/desktop/Feeds' import {DesktopSearch} from '#/view/shell/desktop/Search' -import {atoms as a, tokens,useTheme, web} from '#/alf' +import {atoms as a, tokens, useTheme, web} from '#/alf' +import {Button} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {SubscriptionsDialog} from '#/components/dialogs/SubscriptionsDialog' import {GradientFill} from '#/components/GradientFill' import {Logotype} from '#/components/icons/BlueskyPlus' -import {InlineLinkText, Link} from '#/components/Link' +import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' import {Text} from '#/components/Typography' -import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements' export function DesktopRightNav({routeName}: {routeName: string}) { const t = useTheme() const {_} = useLingui() const {hasSession, currentAccount} = useSession() - const {data: entitlements, isLoading: isEntitlementsLoading} = useEntitlements() + const subscriptionsDialogControl = useDialogControl() + const {data: entitlements} = useEntitlements() const isSubscribed = entitlements?.some(e => e.id === 'core') const kawaii = useKawaiiMode() @@ -66,11 +70,14 @@ export function DesktopRightNav({routeName}: {routeName: string}) { {/* TODO NUX this */} {!isSubscribed && ( - subscriptionsDialogControl.open()} label={_(msg`Subscribe to Bluesky Plus`)} style={[a.p_lg, a.overflow_hidden, a.rounded_md]}> - + - + @@ -91,7 +98,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { - + + )}