WIP rails

This commit is contained in:
Eric Bailey
2024-11-18 09:17:56 -06:00
parent a284df0cd5
commit 9be5761885
8 changed files with 213 additions and 50 deletions
+1
View File
@@ -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",
+25 -3
View File
@@ -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>
)
}
+89 -22
View File
@@ -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>
+5
View File
@@ -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
},
})
}
+30 -25
View File
@@ -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,34 +64,36 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
)} )}
{/* TODO NUX this */} {/* TODO NUX this */}
<View style={[a.pb_lg]}> {!isSubscribed && (
<Link <View style={[a.pb_lg]}>
to="/subscriptions" <Link
label={_(msg`Subscribe to Bluesky Plus`)} to="/subscriptions"
style={[a.p_lg, a.overflow_hidden, a.rounded_md]}> label={_(msg`Subscribe to Bluesky Plus`)}
<GradientFill gradient={tokens.gradients.nordic} rotate="270deg" /> style={[a.p_lg, a.overflow_hidden, a.rounded_md]}>
<View <GradientFill gradient={tokens.gradients.nordic} rotate="270deg" />
style={[ <View
a.absolute, style={[
a.inset_0, a.absolute,
a.rounded_md, a.inset_0,
{borderWidth: 2, borderColor: 'white', opacity: 0.1}, a.rounded_md,
]} {borderWidth: 2, borderColor: 'white', opacity: 0.1},
/> ]}
/>
<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'}]}>
<Trans> <Trans>
Support Bluesky and get access to exclusive features. Support Bluesky and get access to exclusive features.
</Trans> </Trans>
</Text> </Text>
</View>
</View> </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 && (
+5
View File
@@ -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"