diff --git a/assets/icons/gift1_filled_corner0_rounded.svg b/assets/icons/gift1_filled_corner0_rounded.svg
new file mode 100644
index 0000000000..3b5c47abfc
--- /dev/null
+++ b/assets/icons/gift1_filled_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/gift1_stroke2_corner0_rounded.svg b/assets/icons/gift1_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..828de5df72
--- /dev/null
+++ b/assets/icons/gift1_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 19252f7654..d98ae21ee4 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -295,6 +295,9 @@ func serve(cctx *cli.Context) error {
e.GET("/starter-pack-short/:code", server.WebGeneric)
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
+ // subscription
+ e.GET("/subscriptions", server.WebGeneric)
+
// ipcc
e.GET("/ipcc", server.WebIpCC)
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 0b162d3634..d7d9d3a295 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -86,6 +86,7 @@ import {
StarterPackScreenShort,
} from '#/screens/StarterPack/StarterPackScreen'
import {Wizard} from '#/screens/StarterPack/Wizard'
+import {Subscriptions} from '#/screens/Subscriptions'
import {useTheme} from '#/alf'
import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
@@ -416,6 +417,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => Wizard}
options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
/>
+ Subscriptions}
+ options={{title: title(msg`Subscriptions`), requireAuth: true}}
+ />
>
)
}
diff --git a/src/components/icons/Gift1.tsx b/src/components/icons/Gift1.tsx
new file mode 100644
index 0000000000..874798af7d
--- /dev/null
+++ b/src/components/icons/Gift1.tsx
@@ -0,0 +1,9 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Gift1_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM10.333 7H11v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7ZM13 6.333V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333ZM11 9H5v2h6V9Zm2 2V9h6v2h-6Zm-2 2H6v6h5v-6Zm2 6v-6h5v6h-5Z',
+})
+
+export const Gift1_Filled_Corner0_Rounded = createSinglePathSVG({
+ path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-7V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333V7h-2v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7H11v4H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM11 13H4v7a1 1 0 0 0 1 1h6v-8Zm9 0h-7v8h6a1 1 0 0 0 1-1v-7Z',
+})
diff --git a/src/lib/currency.ts b/src/lib/currency.ts
new file mode 100644
index 0000000000..d1884b797d
--- /dev/null
+++ b/src/lib/currency.ts
@@ -0,0 +1,302 @@
+import React from 'react'
+
+import {deviceLocales} from '#/locale/deviceLocales'
+import {useGeolocation} from '#/state/geolocation'
+import {useLanguagePrefs} from '#/state/preferences'
+
+/**
+ * From react-native-localize
+ *
+ * MIT License
+ * Copyright (c) 2017-present, Mathieu Acthernoene
+ *
+ * @see https://github.com/zoontek/react-native-localize/blob/master/LICENSE
+ * @see https://github.com/zoontek/react-native-localize/blob/ee5bf25e0bb8f3b8e4f3fd055f67ad46269c81ea/src/constants.ts
+ */
+export const countryCodeToCurrency: Record = {
+ ad: 'eur',
+ ae: 'aed',
+ af: 'afn',
+ ag: 'xcd',
+ ai: 'xcd',
+ al: 'all',
+ am: 'amd',
+ an: 'ang',
+ ao: 'aoa',
+ ar: 'ars',
+ as: 'usd',
+ at: 'eur',
+ au: 'aud',
+ aw: 'awg',
+ ax: 'eur',
+ az: 'azn',
+ ba: 'bam',
+ bb: 'bbd',
+ bd: 'bdt',
+ be: 'eur',
+ bf: 'xof',
+ bg: 'bgn',
+ bh: 'bhd',
+ bi: 'bif',
+ bj: 'xof',
+ bl: 'eur',
+ bm: 'bmd',
+ bn: 'bnd',
+ bo: 'bob',
+ bq: 'usd',
+ br: 'brl',
+ bs: 'bsd',
+ bt: 'btn',
+ bv: 'nok',
+ bw: 'bwp',
+ by: 'byn',
+ bz: 'bzd',
+ ca: 'cad',
+ cc: 'aud',
+ cd: 'cdf',
+ cf: 'xaf',
+ cg: 'xaf',
+ ch: 'chf',
+ ci: 'xof',
+ ck: 'nzd',
+ cl: 'clp',
+ cm: 'xaf',
+ cn: 'cny',
+ co: 'cop',
+ cr: 'crc',
+ cu: 'cup',
+ cv: 'cve',
+ cw: 'ang',
+ cx: 'aud',
+ cy: 'eur',
+ cz: 'czk',
+ de: 'eur',
+ dj: 'djf',
+ dk: 'dkk',
+ dm: 'xcd',
+ do: 'dop',
+ dz: 'dzd',
+ ec: 'usd',
+ ee: 'eur',
+ eg: 'egp',
+ eh: 'mad',
+ er: 'ern',
+ es: 'eur',
+ et: 'etb',
+ fi: 'eur',
+ fj: 'fjd',
+ fk: 'fkp',
+ fm: 'usd',
+ fo: 'dkk',
+ fr: 'eur',
+ ga: 'xaf',
+ gb: 'gbp',
+ gd: 'xcd',
+ ge: 'gel',
+ gf: 'eur',
+ gg: 'gbp',
+ gh: 'ghs',
+ gi: 'gip',
+ gl: 'dkk',
+ gm: 'gmd',
+ gn: 'gnf',
+ gp: 'eur',
+ gq: 'xaf',
+ gr: 'eur',
+ gs: 'gbp',
+ gt: 'gtq',
+ gu: 'usd',
+ gw: 'xof',
+ gy: 'gyd',
+ hk: 'hkd',
+ hm: 'aud',
+ hn: 'hnl',
+ hr: 'hrk',
+ ht: 'htg',
+ hu: 'huf',
+ id: 'idr',
+ ie: 'eur',
+ il: 'ils',
+ im: 'gbp',
+ in: 'inr',
+ io: 'usd',
+ iq: 'iqd',
+ ir: 'irr',
+ is: 'isk',
+ it: 'eur',
+ je: 'gbp',
+ jm: 'jmd',
+ jo: 'jod',
+ jp: 'jpy',
+ ke: 'kes',
+ kg: 'kgs',
+ kh: 'khr',
+ ki: 'aud',
+ km: 'kmf',
+ kn: 'xcd',
+ kp: 'kpw',
+ kr: 'krw',
+ kw: 'kwd',
+ ky: 'kyd',
+ kz: 'kzt',
+ la: 'lak',
+ lb: 'lbp',
+ lc: 'xcd',
+ li: 'chf',
+ lk: 'lkr',
+ lr: 'lrd',
+ ls: 'lsl',
+ lt: 'eur',
+ lu: 'eur',
+ lv: 'eur',
+ ly: 'lyd',
+ ma: 'mad',
+ mc: 'eur',
+ md: 'mdl',
+ me: 'eur',
+ mf: 'eur',
+ mg: 'mga',
+ mh: 'usd',
+ mk: 'mkd',
+ ml: 'xof',
+ mm: 'mmk',
+ mn: 'mnt',
+ mo: 'mop',
+ mp: 'usd',
+ mq: 'eur',
+ mr: 'mro',
+ ms: 'xcd',
+ mt: 'eur',
+ mu: 'mur',
+ mv: 'mvr',
+ mw: 'mwk',
+ mx: 'mxn',
+ my: 'myr',
+ mz: 'mzn',
+ na: 'nad',
+ nc: 'xpf',
+ ne: 'xof',
+ nf: 'aud',
+ ng: 'ngn',
+ ni: 'nio',
+ nl: 'eur',
+ no: 'nok',
+ np: 'npr',
+ nr: 'aud',
+ nu: 'nzd',
+ nz: 'nzd',
+ om: 'omr',
+ pa: 'pab',
+ pe: 'pen',
+ pf: 'xpf',
+ pg: 'pgk',
+ ph: 'php',
+ pk: 'pkr',
+ pl: 'pln',
+ pm: 'eur',
+ pn: 'nzd',
+ pr: 'usd',
+ ps: 'ils',
+ pt: 'eur',
+ pw: 'usd',
+ py: 'pyg',
+ qa: 'qar',
+ re: 'eur',
+ ro: 'ron',
+ rs: 'rsd',
+ ru: 'rub',
+ rw: 'rwf',
+ sa: 'sar',
+ sb: 'sbd',
+ sc: 'scr',
+ sd: 'sdg',
+ se: 'sek',
+ sg: 'sgd',
+ sh: 'shp',
+ si: 'eur',
+ sj: 'nok',
+ sk: 'eur',
+ sl: 'sll',
+ sm: 'eur',
+ sn: 'xof',
+ so: 'sos',
+ sr: 'srd',
+ ss: 'ssp',
+ st: 'std',
+ sv: 'svc',
+ sx: 'ang',
+ sy: 'syp',
+ sz: 'szl',
+ tc: 'usd',
+ td: 'xaf',
+ tf: 'eur',
+ tg: 'xof',
+ th: 'thb',
+ tj: 'tjs',
+ tk: 'nzd',
+ tl: 'usd',
+ tm: 'tmt',
+ tn: 'tnd',
+ to: 'top',
+ tr: 'try',
+ tt: 'ttd',
+ tv: 'aud',
+ tw: 'twd',
+ tz: 'tzs',
+ ua: 'uah',
+ ug: 'ugx',
+ um: 'usd',
+ us: 'usd',
+ uy: 'uyu',
+ uz: 'uzs',
+ va: 'eur',
+ vc: 'xcd',
+ ve: 'vef',
+ vg: 'usd',
+ vi: 'usd',
+ vn: 'vnd',
+ vu: 'vuv',
+ wf: 'xpf',
+ ws: 'wst',
+ ye: 'yer',
+ yt: 'eur',
+ za: 'zar',
+ zm: 'zmw',
+ zw: 'zwl',
+}
+
+/**
+ * Best-guess currency formatting.
+ *
+ * Attempts to use `getLocales` from `expo-localization` if available,
+ * otherwise falls back to the `persisted.appLanguage` setting, and geolocation
+ * API for region.
+ */
+export function useCurrencyFormatter(
+ options?: Parameters[1],
+) {
+ const {geolocation} = useGeolocation()
+ const {appLanguage} = useLanguagePrefs()
+ return React.useMemo(() => {
+ const locale = deviceLocales.at(0)
+ const languageTag = locale?.languageTag || appLanguage || 'en-US'
+ const countryCode = (
+ locale?.regionCode ||
+ geolocation?.countryCode ||
+ 'us'
+ ).toLowerCase()
+ const currency = countryCodeToCurrency[countryCode]
+ const format = new Intl.NumberFormat(languageTag, {
+ ...(options || {}),
+ style: 'currency',
+ currency: currency,
+ }).format
+
+ return {
+ format,
+ currency,
+ countryCode,
+ languageTag,
+ }
+ }, [geolocation, appLanguage, options])
+}
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 9e3407261d..123b33ed98 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -56,6 +56,7 @@ export type CommonNavigatorParams = {
StarterPackShort: {code: string}
StarterPackWizard: undefined
StarterPackEdit: {rkey?: string}
+ Subscriptions: undefined
}
export type BottomTabNavigatorParams = CommonNavigatorParams & {
diff --git a/src/routes.ts b/src/routes.ts
index 188665d849..494a6f7bb7 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -63,4 +63,5 @@ export const router = new Router({
StarterPack: '/starter-pack/:name/:rkey',
StarterPackShort: '/starter-pack-short/:code',
StarterPackWizard: '/starter-pack/create',
+ Subscriptions: '/subscriptions',
})
diff --git a/src/screens/Subscriptions/index.tsx b/src/screens/Subscriptions/index.tsx
new file mode 100644
index 0000000000..1fc6dfec20
--- /dev/null
+++ b/src/screens/Subscriptions/index.tsx
@@ -0,0 +1,51 @@
+import React from 'react'
+import {View} 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 * as Layout from '#/components/Layout'
+// import {Loader} from '#/components/Loader'
+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'
+
+export type ScreenProps = NativeStackScreenProps<
+ CommonNavigatorParams,
+ 'Subscriptions'
+>
+
+export function Subscriptions(_props: ScreenProps) {
+ const {_} = useLingui()
+
+ // const currentPlatform = isAndroid ? 'android' : isIOS ? 'ios' : 'web'
+ // const activeSubscriptionPlatform =
+ // activeSubscription?.subscription?.state?.platform
+ // const isManageable =
+ // !activeSubscriptionPlatform ||
+ // activeSubscriptionPlatform === currentPlatform
+
+ return (
+
+
+
+
+
+ Subs
+
+
+
+ )
+}
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index 8323960924..3183ca889d 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -16,6 +16,7 @@ import {atoms} from '#/alf'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
+import {Gift1_Stroke2_Corner0_Rounded as Gift} from '#/components/icons/Gift1'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {Link} from '#/components/Link'
@@ -72,6 +73,9 @@ export function HomeHeaderLayoutMobile({
]}>
{IS_DEV && (
<>
+
+
+
): React.ReactNode => {
setDrawerOpen(false)
}, [navigation, setDrawerOpen])
+ const onPressSubscriptions = React.useCallback(() => {
+ navigation.navigate('Subscriptions')
+ setDrawerOpen(false)
+ }, [navigation, setDrawerOpen])
+
const onPressFeedback = React.useCallback(() => {
Linking.openURL(
FEEDBACK_FORM_URL({
@@ -264,6 +273,10 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
onPress={onPressProfile}
/>
+
>
) : (
<>
@@ -533,6 +546,31 @@ let SettingsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => {
}
SettingsMenuItem = React.memo(SettingsMenuItem)
+let SubscriptionsMenuItem = ({
+ isActive,
+ onPress,
+}: {
+ isActive: boolean
+ onPress: () => void
+}): React.ReactNode => {
+ const {_} = useLingui()
+ const t = useTheme()
+ return (
+
+ ) : (
+
+ )
+ }
+ label={_(msg`Support Bluesky`)}
+ onPress={onPress}
+ />
+ )
+}
+SubscriptionsMenuItem = React.memo(SubscriptionsMenuItem)
+
function MenuItem({icon, label, count, bold, onPress}: MenuItemProps) {
const t = useTheme()
return (