From a6985a28ea6105736ff390ffd70a4f10d140bf2b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 25 Oct 2024 16:05:30 -0500 Subject: [PATCH] Init state, Android and web --- src/App.native.tsx | 8 + src/env.ts | 3 + src/lib/currency.ts | 302 ++++++++++++++++++ src/state/purchases/constants.ts | 260 +++++++++++++++ src/state/purchases/useSubscriptions/index.ts | 54 ++++ .../purchases/useSubscriptions/index.web.ts | 198 ++++++++++++ .../purchases/useSubscriptions/types/index.ts | 19 ++ .../useSubscriptions/types/stripe.ts | 60 ++++ src/state/purchases/useSubscriptions/util.ts | 53 +++ 9 files changed, 957 insertions(+) create mode 100644 src/lib/currency.ts create mode 100644 src/state/purchases/constants.ts create mode 100644 src/state/purchases/useSubscriptions/index.ts create mode 100644 src/state/purchases/useSubscriptions/index.web.ts create mode 100644 src/state/purchases/useSubscriptions/types/index.ts create mode 100644 src/state/purchases/useSubscriptions/types/stripe.ts create mode 100644 src/state/purchases/useSubscriptions/util.ts diff --git a/src/App.native.tsx b/src/App.native.tsx index 668fb91fcd..0be0bad8e3 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -5,6 +5,7 @@ import '#/view/icons' import React, {useEffect, useState} from 'react' import {GestureHandlerRootView} from 'react-native-gesture-handler' import {KeyboardProvider} from 'react-native-keyboard-controller' +import Purchases from 'react-native-purchases' import {RootSiblingParent} from 'react-native-root-siblings' import { initialWindowMetrics, @@ -24,6 +25,7 @@ import {s} from '#/lib/styles' import {ThemeProvider} from '#/lib/ThemeContext' import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' +import {isAndroid,isIOS} from '#/platform/detection' import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' @@ -66,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' +import {RC_GOOGLE_PUBLIC_KEY} from '#/env' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -173,6 +176,11 @@ function App() { Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() => setReady(true), ) + + if (isIOS) { + } else if (isAndroid) { + Purchases.configure({apiKey: RC_GOOGLE_PUBLIC_KEY}) + } }, []) if (!isReady) { diff --git a/src/env.ts b/src/env.ts index fbcc15f576..ea456cf76a 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,3 +7,6 @@ export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as | 'info' | 'warn' | 'error' + +export const RC_GOOGLE_PUBLIC_KEY = + process.env.EXPO_PUBLIC_RC_GOOGLE_PUBLIC_KEY || '' 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/state/purchases/constants.ts b/src/state/purchases/constants.ts new file mode 100644 index 0000000000..d2937d5e68 --- /dev/null +++ b/src/state/purchases/constants.ts @@ -0,0 +1,260 @@ +/** + * 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', +} diff --git a/src/state/purchases/useSubscriptions/index.ts b/src/state/purchases/useSubscriptions/index.ts new file mode 100644 index 0000000000..e90a268bf6 --- /dev/null +++ b/src/state/purchases/useSubscriptions/index.ts @@ -0,0 +1,54 @@ +import Purchases, {PurchasesPackage} from 'react-native-purchases' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation,useQuery} from '@tanstack/react-query' + +import {Subscription} from '#/state/purchases/useSubscriptions/types' +import {identifierToSubscriptionInfo} from '#/state/purchases/useSubscriptions/util' +import {useSession} from '#/state/session' + +export function useAvailableSubscriptions() { + const {_} = useLingui() + const {currentAccount} = useSession() + const did = currentAccount!.did + + return useQuery({ + queryKey: ['availableSubscriptions', did], + async queryFn() { + Purchases.logIn(did) + const offerings = await Purchases.getOfferings() + if ( + offerings.current !== null && + offerings.current.availablePackages.length !== 0 + ) { + return normalizePackages(offerings.current.availablePackages) + } + throw new Error(_(msg`Error loading subscriptions`)) + }, + }) +} + +export function usePurchaseSubscription() { + return useMutation({ + async mutationFn(pkg: PurchasesPackage) { + return Purchases.purchasePackage(pkg) + }, + }) +} + +function normalizePackages(pkgs: PurchasesPackage[]): Subscription[] { + return pkgs + .map(p => { + const info = identifierToSubscriptionInfo(p.product.identifier) + if (!info) return + return { + info, + pricing: { + formatted: p.product.priceString, + value: p.product.price, + }, + raw: p, + } + }) + .filter(Boolean) as Subscription[] +} diff --git a/src/state/purchases/useSubscriptions/index.web.ts b/src/state/purchases/useSubscriptions/index.web.ts new file mode 100644 index 0000000000..2cfef4357a --- /dev/null +++ b/src/state/purchases/useSubscriptions/index.web.ts @@ -0,0 +1,198 @@ +import {useMutation,useQuery} from '@tanstack/react-query' + +import {useCurrencyFormatter} from '#/lib/currency' +import {Subscription} from '#/state/purchases/useSubscriptions/types' +import { + StripePrice, + StripeProduct, +} from '#/state/purchases/useSubscriptions/types/stripe' +import {identifierToSubscriptionInfo} from '#/state/purchases/useSubscriptions/util' +import {useSession} from '#/state/session' + +export function useAvailableSubscriptions() { + const {currentAccount} = useSession() + const currencyFormatter = useCurrencyFormatter() + + return useQuery({ + queryKey: ['availableSubscriptions', currentAccount!.did], + async queryFn() { + return normalizeProducts(RESPONSE, { + currencyFormatter, + }) + }, + }) +} + +export function usePurchaseSubscription() { + return useMutation({ + async mutationFn(_priceObject: any) {}, + }) +} + +function normalizeProducts( + products: {product: StripeProduct; price: StripePrice}[], + options: { + currencyFormatter: ReturnType + }, +): Subscription[] { + return products + .map(({product, price}) => { + const info = identifierToSubscriptionInfo(product.name) + + if (!info) return + + const priceObj = + price.currency_options[options.currencyFormatter.currency] + const value = priceObj.unit_amount + const formatted = options.currencyFormatter.format(value / 100) + + return { + info, + pricing: { + value, + formatted, + }, + raw: { + price_id: price.id, + }, + } + }) + .filter(Boolean) as Subscription[] +} + +const RESPONSE = [ + { + product: { + id: 'prod_R2eNjNa6mB1Jlu', + object: 'product', + active: true, + attributes: [], + created: 1729092225, + default_price: 'price_1QAZ4fAwTlpRxHkARl1gtw5C', + description: null, + features: [], + images: [], + livemode: false, + marketing_features: [], + metadata: {}, + name: 'bsky_tier_0_monthly', + package_dimensions: null, + shippable: null, + statement_descriptor: null, + tax_code: 'txcd_10000000', + type: 'service', + unit_label: null, + updated: 1729869343, + url: null, + }, + price: { + id: 'price_1QAZ4fAwTlpRxHkARl1gtw5C', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1729092225, + currency: 'usd', + currency_options: { + gbp: { + custom_unit_amount: null, + tax_behavior: 'inclusive', + unit_amount: 250, + unit_amount_decimal: '250', + }, + usd: { + custom_unit_amount: null, + tax_behavior: 'inclusive', + unit_amount: 300, + unit_amount_decimal: '300', + }, + }, + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_R2eNjNa6mB1Jlu', + recurring: { + aggregate_usage: null, + interval: 'month', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: 'inclusive', + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 300, + unit_amount_decimal: '300', + }, + }, + { + product: { + id: 'prod_R37Zg28EeQ9XAz', + object: 'product', + active: true, + attributes: [], + created: 1729200807, + default_price: 'price_1QB1K0AwTlpRxHkA4Mdfxati', + description: null, + features: [], + images: [], + livemode: false, + marketing_features: [], + metadata: {}, + name: 'bsky_tier_0_annual', + package_dimensions: null, + shippable: null, + statement_descriptor: null, + tax_code: 'txcd_10000000', + type: 'service', + unit_label: null, + updated: 1729869329, + url: null, + }, + price: { + id: 'price_1QB1K0AwTlpRxHkA4Mdfxati', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1729200808, + currency: 'usd', + currency_options: { + gbp: { + custom_unit_amount: null, + tax_behavior: 'inclusive', + unit_amount: 2000, + unit_amount_decimal: '2000', + }, + usd: { + custom_unit_amount: null, + tax_behavior: 'inclusive', + unit_amount: 2400, + unit_amount_decimal: '2400', + }, + }, + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_R37Zg28EeQ9XAz', + recurring: { + aggregate_usage: null, + interval: 'year', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: 'inclusive', + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 2400, + unit_amount_decimal: '2400', + }, + }, +] diff --git a/src/state/purchases/useSubscriptions/types/index.ts b/src/state/purchases/useSubscriptions/types/index.ts new file mode 100644 index 0000000000..c6025aa449 --- /dev/null +++ b/src/state/purchases/useSubscriptions/types/index.ts @@ -0,0 +1,19 @@ +export enum SubscriptionIdentifier { + Tier0MonthlyAuto = 'Tier0MonthlyAuto', + Tier0AnnualAuto = 'Tier0AnnualAuto', + Tier1MonthlyAuto = 'Tier1MonthlyAuto', + Tier1AnnualAuto = 'Tier1AnnualAuto', + Tier2MonthlyAuto = 'Tier2MonthlyAuto', + Tier2AnnualAuto = 'Tier2AnnualAuto', +} + +export type SubscriptionInfo = { + identifier: SubscriptionIdentifier + interval: 'monthly' | 'annual' + autoRenew: boolean +} + +export type Subscription = { + info: SubscriptionInfo + raw: any +} diff --git a/src/state/purchases/useSubscriptions/types/stripe.ts b/src/state/purchases/useSubscriptions/types/stripe.ts new file mode 100644 index 0000000000..ea1f372f44 --- /dev/null +++ b/src/state/purchases/useSubscriptions/types/stripe.ts @@ -0,0 +1,60 @@ +export type StripeProduct = { + // main stuff + id: string + default_price: string // ID of Price obj + name: string + active: boolean + // other stuff + object: string + attributes: unknown[] + created: number + description: string | null + features: unknown[] + images: unknown[] + livemode: boolean + marketing_features: unknown[] + metadata: unknown + package_dimensions: unknown | null + shippable: unknown | null + statement_descriptor: unknown | null + tax_code: string + type: 'service' | string + unit_label: unknown | null + updated: number + url: string | null +} + +export type StripePrice = { + // main stuff + id: string + product: string // Product obj associated + currency_options: { + [key: string]: { + custom_unit_amount: number | null + tax_behavior: 'inclusive' | string + unit_amount: number + unit_amount_decimal: string + } + } + active: boolean + recurring: { + interval: 'month' | 'year' | string + interval_count: number + usage_type: 'licensed' | string + } + object: 'price' | string + type: 'recurring' | string + // other stuff + metadata: unknown + unit_amount: number // default Price in cents + currency: string // default currency + tax_behavior: 'inclusive' | string // default tax behavior + billing_scheme: 'per_unit' | string + created: number + custom_unit_amount: number | null + livemode: boolean + lookup_key: string | null + nickname: string | null + tiers_mode: string | null + transform_quantity: string | null +} diff --git a/src/state/purchases/useSubscriptions/util.ts b/src/state/purchases/useSubscriptions/util.ts new file mode 100644 index 0000000000..829af96dd0 --- /dev/null +++ b/src/state/purchases/useSubscriptions/util.ts @@ -0,0 +1,53 @@ +import { + SubscriptionIdentifier, + SubscriptionInfo, +} from '#/state/purchases/useSubscriptions/types' + +export function identifierToSubscriptionInfo( + identifier: string, +): SubscriptionInfo | undefined { + switch (identifier) { + /* + * Android + */ + case 'bsky_tier_0:monthly-auto': { + return { + identifier: SubscriptionIdentifier.Tier0MonthlyAuto, + interval: 'monthly', + autoRenew: true, + } + } + case 'bsky_tier_0:annual-auto': { + return { + identifier: SubscriptionIdentifier.Tier0AnnualAuto, + interval: 'annual', + autoRenew: true, + } + } + + /* + * Stripe + */ + case 'bsky_tier_0_monthly': { + return { + identifier: SubscriptionIdentifier.Tier0MonthlyAuto, + interval: 'monthly', + autoRenew: true, + } + } + case 'bsky_tier_0_annual': { + return { + identifier: SubscriptionIdentifier.Tier0MonthlyAuto, + interval: 'annual', + autoRenew: true, + } + } + + /* + * Fallback + */ + default: { + return undefined + } + } +}