diff --git a/package.json b/package.json index 6372dd5f5b..47cbe7f2bb 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "expo-updates": "~0.25.14", "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", + "gretchen": "^1.5.1", "history": "^5.3.0", "hls.js": "^1.5.11", "js-sha256": "^0.9.0", diff --git a/src/components/icons/BlueskyPlus.tsx b/src/components/icons/BlueskyPlus.tsx index c1384d5932..a4113b25d4 100644 --- a/src/components/icons/BlueskyPlus.tsx +++ b/src/components/icons/BlueskyPlus.tsx @@ -1,6 +1,13 @@ import React from 'react' import {StyleSheet, TextProps} from 'react-native' -import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' +import Svg, { + Defs, + LinearGradient, + Path, + PathProps, + Stop, + SvgProps, +} from 'react-native-svg' import {useTheme} from '#/alf' @@ -15,8 +22,9 @@ type Props = { export function Full(props: Props) { const t = useTheme() const {fill, ...rest} = props + const gradient = fill === 'nordic' const styles = StyleSheet.flatten(props.style) - const _fill = 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 = 90 / 448 @@ -27,6 +35,7 @@ export function Full(props: Props) { viewBox="0 0 448 90" {...rest} style={[styles, {width: size, height: size * ratio}]}> + {gradient && } + {gradient && } ) } + +function Gradients() { + return ( + + + + + + + ) +} diff --git a/src/screens/Subscriptions/index.tsx b/src/screens/Subscriptions/index.tsx index 1fc6dfec20..23c5d7ceab 100644 --- a/src/screens/Subscriptions/index.tsx +++ b/src/screens/Subscriptions/index.tsx @@ -1,26 +1,24 @@ import React from 'react' -import {View} from 'react-native' +import {View, Linking} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {NativeStackScreenProps} from '@react-navigation/native-stack' -// import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' // import {isAndroid, isIOS} from '#/platform/detection' import {CommonNavigatorParams} from '#/lib/routes/types' -// import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements' -import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' -import {atoms as a} from '#/alf' -// import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import {atoms as a, useTheme} from '#/alf' import * as Layout from '#/components/Layout' -// import {Loader} from '#/components/Loader' +import {Loader} from '#/components/Loader' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' import {Text} from '#/components/Typography' -// import {GradientFill} from '#/components/GradientFill' // import {Divider} from '#/components/Divider' -// import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -// import * as Dialog from '#/components/Dialog' -// import * as Toggle from '#/components/forms/Toggle' -// import {Admonition} from '#/components/Admonition' +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, @@ -28,22 +26,91 @@ export type ScreenProps = NativeStackScreenProps< > export function Subscriptions(_props: ScreenProps) { + const t = useTheme() const {_} = useLingui() + const {currentAccount} = useSession() + 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 currentPlatform = isAndroid ? 'android' : isIOS ? 'ios' : 'web' - // const activeSubscriptionPlatform = - // activeSubscription?.subscription?.state?.platform - // const isManageable = - // !activeSubscriptionPlatform || - // activeSubscriptionPlatform === currentPlatform return ( - - - + - Subs + + + + {isEntitlementsLoading ? ( + + ) : ( + <> + {isSubscribed ? ( + + You're subscribed! + + ) : ( + + + + + )} + + )} + diff --git a/src/state/purchases/subscriptions/api.ts b/src/state/purchases/subscriptions/api.ts new file mode 100644 index 0000000000..9d12b03357 --- /dev/null +++ b/src/state/purchases/subscriptions/api.ts @@ -0,0 +1,5 @@ +import {create} from 'gretchen' + +export const api = create({ + baseURL: process.env.PLUS_SERVICE_URL, +}) diff --git a/src/state/purchases/subscriptions/useCreateCheckout.ts b/src/state/purchases/subscriptions/useCreateCheckout.ts new file mode 100644 index 0000000000..a02bc5c6eb --- /dev/null +++ b/src/state/purchases/subscriptions/useCreateCheckout.ts @@ -0,0 +1,30 @@ +import {useMutation} from '@tanstack/react-query' + +import {IS_DEV} from '#/env' +import {api} from '#/state/purchases/subscriptions/api' + +export function useCreateCheckout() { + return useMutation({ + 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`, + } + }).json() + + if (error) { + throw error + } + + return data + }, + }) +} diff --git a/src/state/purchases/subscriptions/useEntitlements.ts b/src/state/purchases/subscriptions/useEntitlements.ts new file mode 100644 index 0000000000..35c3178fce --- /dev/null +++ b/src/state/purchases/subscriptions/useEntitlements.ts @@ -0,0 +1,28 @@ +import {useQuery} from '@tanstack/react-query' + +import {useSession} from '#/state/session' +import {api} from '#/state/purchases/subscriptions/api' + +export function useEntitlements() { + const {currentAccount} = useSession() + + return useQuery({ + enabled: !!currentAccount, + queryKey: ['entitlements', currentAccount?.did], + refetchOnWindowFocus: true, + async queryFn() { + const params = new URLSearchParams('/entitlements') + params.set('did', currentAccount!.did) + const url = `/entitlements?${params.toString()}` + const {data, error} = await api<{ + entitlements: {id: 'core'; platform: 'web'}[] + }>(url).json() + + if (error) { + return [] + } + + return data?.entitlements + }, + }) +} diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index b68342dd0c..0ef03588d7 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -15,11 +15,14 @@ import {Logotype} from '#/components/icons/BlueskyPlus' import {InlineLinkText, Link} 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 isSubscribed = entitlements?.some(e => e.id === 'core') const kawaii = useKawaiiMode() @@ -61,34 +64,36 @@ export function DesktopRightNav({routeName}: {routeName: string}) { )} {/* TODO NUX this */} - - - - + {!isSubscribed && ( + + + + - - + + - - - - Support Bluesky and get access to exclusive features. - - + + + + Support Bluesky and get access to exclusive features. + + + - - - + + + )} {hasSession && ( diff --git a/yarn.lock b/yarn.lock index 474ef2f8ba..74293833a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11560,6 +11560,11 @@ graphql@15.8.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== +gretchen@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/gretchen/-/gretchen-1.5.1.tgz#fbd5da351d8c5af356d85ca36555d78583ade4a4" + integrity sha512-tk/8z1+ZriI32zldoroZOfTmHpubAX7/bmvQpqdW6jpVPhS+tseBDRlqHsAOJ0HCnRTmuRFCcRrniCQzHQvmew== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"