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"