WIP rails
This commit is contained in:
@@ -146,6 +146,7 @@
|
|||||||
"expo-updates": "~0.25.14",
|
"expo-updates": "~0.25.14",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
|
"gretchen": "^1.5.1",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"hls.js": "^1.5.11",
|
"hls.js": "^1.5.11",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, TextProps} from 'react-native'
|
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'
|
import {useTheme} from '#/alf'
|
||||||
|
|
||||||
@@ -15,8 +22,9 @@ type Props = {
|
|||||||
export function Full(props: Props) {
|
export function Full(props: Props) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {fill, ...rest} = props
|
const {fill, ...rest} = props
|
||||||
|
const gradient = fill === 'nordic'
|
||||||
const styles = StyleSheet.flatten(props.style)
|
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
|
// @ts-ignore it's fiiiiine
|
||||||
const size = parseInt(rest.width || 100)
|
const size = parseInt(rest.width || 100)
|
||||||
const ratio = 90 / 448
|
const ratio = 90 / 448
|
||||||
@@ -27,6 +35,7 @@ export function Full(props: Props) {
|
|||||||
viewBox="0 0 448 90"
|
viewBox="0 0 448 90"
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[styles, {width: size, height: size * ratio}]}>
|
style={[styles, {width: size, height: size * ratio}]}>
|
||||||
|
{gradient && <Gradients />}
|
||||||
<Path
|
<Path
|
||||||
fill={_fill}
|
fill={_fill}
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@@ -43,8 +52,9 @@ export function Full(props: Props) {
|
|||||||
export function Logotype(props: Props) {
|
export function Logotype(props: Props) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {fill, ...rest} = props
|
const {fill, ...rest} = props
|
||||||
|
const gradient = fill === 'nordic'
|
||||||
const styles = StyleSheet.flatten(props.style)
|
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
|
// @ts-ignore it's fiiiiine
|
||||||
const size = parseInt(rest.width || 100)
|
const size = parseInt(rest.width || 100)
|
||||||
const ratio = 78 / 330
|
const ratio = 78 / 330
|
||||||
@@ -55,6 +65,7 @@ export function Logotype(props: Props) {
|
|||||||
viewBox="0 0 330 78"
|
viewBox="0 0 330 78"
|
||||||
{...rest}
|
{...rest}
|
||||||
style={[styles, {width: size, height: size * ratio}]}>
|
style={[styles, {width: size, height: size * ratio}]}>
|
||||||
|
{gradient && <Gradients />}
|
||||||
<Path
|
<Path
|
||||||
fill={_fill}
|
fill={_fill}
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@@ -64,3 +75,14 @@ export function Logotype(props: Props) {
|
|||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Gradients() {
|
||||||
|
return (
|
||||||
|
<Defs>
|
||||||
|
<LinearGradient id="nordic" x1="0" y1="1" x2="1" y2="0">
|
||||||
|
<Stop offset="0" stopColor="#083367" stopOpacity="1" />
|
||||||
|
<Stop offset="1" stopColor="#9EE8C1" stopOpacity="1" />
|
||||||
|
</LinearGradient>
|
||||||
|
</Defs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View, Linking} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} 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'
|
||||||
|
|
||||||
// import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|
||||||
// import {isAndroid, isIOS} from '#/platform/detection'
|
// import {isAndroid, isIOS} from '#/platform/detection'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
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 {CenteredView} from '#/view/com/util/Views'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
// import {Button, ButtonText, ButtonIcon} from '#/components/Button'
|
|
||||||
import * as Layout from '#/components/Layout'
|
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 {Text} from '#/components/Typography'
|
||||||
// import {GradientFill} from '#/components/GradientFill'
|
|
||||||
// import {Divider} from '#/components/Divider'
|
// import {Divider} from '#/components/Divider'
|
||||||
// import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
// import * as Dialog from '#/components/Dialog'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
// import * as Toggle from '#/components/forms/Toggle'
|
import {Logotype} from '#/components/icons/BlueskyPlus'
|
||||||
// import {Admonition} from '#/components/Admonition'
|
import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements'
|
||||||
|
import {useCreateCheckout} from '#/state/purchases/subscriptions/useCreateCheckout'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
export type ScreenProps = NativeStackScreenProps<
|
export type ScreenProps = NativeStackScreenProps<
|
||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
@@ -28,22 +26,91 @@ export type ScreenProps = NativeStackScreenProps<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function Subscriptions(_props: ScreenProps) {
|
export function Subscriptions(_props: ScreenProps) {
|
||||||
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
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 (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
<CenteredView sideBorders={true}>
|
<CenteredView sideBorders={true} style={[a.util_screen_outer]}>
|
||||||
<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>
|
<Logotype width={200} fill='nordic' />
|
||||||
|
|
||||||
|
<View style={[a.pt_xl]}>
|
||||||
|
{isEntitlementsLoading ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isSubscribed ? (
|
||||||
|
<View>
|
||||||
|
<Text>You're subscribed!</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={[a.flex_row, a.gap_md]}>
|
||||||
|
<Button
|
||||||
|
label={_('Subscribe')}
|
||||||
|
onPress={onPressMonthly}
|
||||||
|
size='large'
|
||||||
|
variant='solid'
|
||||||
|
color='primary'
|
||||||
|
>
|
||||||
|
<ButtonText>Subscribe ($6/month)</ButtonText>
|
||||||
|
<ButtonIcon icon={isPending ? Loader : Plus} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_('Subscribe')}
|
||||||
|
onPress={onPressAnnual}
|
||||||
|
size='large'
|
||||||
|
variant='solid'
|
||||||
|
color='secondary'
|
||||||
|
>
|
||||||
|
<ButtonText>Subscribe ($60/year)</ButtonText>
|
||||||
|
<ButtonIcon icon={isPending ? Loader : Plus} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {create} from 'gretchen'
|
||||||
|
|
||||||
|
export const api = create({
|
||||||
|
baseURL: process.env.PLUS_SERVICE_URL,
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,11 +15,14 @@ import {Logotype} from '#/components/icons/BlueskyPlus'
|
|||||||
import {InlineLinkText, Link} from '#/components/Link'
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useEntitlements} from '#/state/purchases/subscriptions/useEntitlements'
|
||||||
|
|
||||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
|
const {data: entitlements, isLoading: isEntitlementsLoading} = useEntitlements()
|
||||||
|
const isSubscribed = entitlements?.some(e => e.id === 'core')
|
||||||
|
|
||||||
const kawaii = useKawaiiMode()
|
const kawaii = useKawaiiMode()
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TODO NUX this */}
|
{/* TODO NUX this */}
|
||||||
|
{!isSubscribed && (
|
||||||
<View style={[a.pb_lg]}>
|
<View style={[a.pb_lg]}>
|
||||||
<Link
|
<Link
|
||||||
to="/subscriptions"
|
to="/subscriptions"
|
||||||
@@ -77,7 +81,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={[a.flex_1, a.relative, a.z_10]}>
|
<View style={[a.flex_1, a.relative, a.z_10]}>
|
||||||
<Logotype width={100} fill={t.atoms.text.color} />
|
<Logotype width={100} fill='white' />
|
||||||
|
|
||||||
<View style={[a.pt_xs]}>
|
<View style={[a.pt_xs]}>
|
||||||
<Text style={[a.text_sm, a.leading_tight, {color: 'white'}]}>
|
<Text style={[a.text_sm, a.leading_tight, {color: 'white'}]}>
|
||||||
@@ -89,6 +93,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
|
|||||||
@@ -11560,6 +11560,11 @@ graphql@15.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
|
||||||
integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==
|
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:
|
gzip-size@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
|
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
|
||||||
|
|||||||
Reference in New Issue
Block a user