Some design
This commit is contained in:
+4
-2
@@ -16,8 +16,6 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {isIOS, isAndroid} from '#/platform/detection'
|
||||
import {RC_APPLE_PUBLIC_KEY, RC_GOOGLE_PUBLIC_KEY} from '#/env'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {
|
||||
initialize,
|
||||
@@ -28,6 +26,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'
|
||||
@@ -72,6 +71,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {AppProfiler} from '#/AppProfiler'
|
||||
import {RC_APPLE_PUBLIC_KEY, 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'
|
||||
@@ -187,6 +187,8 @@ function App() {
|
||||
},
|
||||
)
|
||||
|
||||
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG)
|
||||
|
||||
if (isIOS) {
|
||||
Purchases.configure({apiKey: RC_APPLE_PUBLIC_KEY})
|
||||
} else if (isAndroid) {
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export const gradients = {
|
||||
nordic: {
|
||||
values: [
|
||||
[0, '#083367'],
|
||||
[0.4, '#386D84'],
|
||||
// [0.4, '#386D84'],
|
||||
[1, '#9EE8C1'],
|
||||
],
|
||||
hover_value: '#3A7085',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {useMemo} from 'react'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
|
||||
|
||||
export function GradientFill({
|
||||
gradient,
|
||||
rotate = '0deg',
|
||||
}: {
|
||||
style,
|
||||
}: ViewStyleProp & {
|
||||
gradient:
|
||||
| typeof tokens.gradients.primary
|
||||
| typeof tokens.gradients.sky
|
||||
@@ -75,7 +76,7 @@ export function GradientFill({
|
||||
}
|
||||
start={start}
|
||||
end={end}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
style={[a.absolute, a.inset_0, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {CheckThick_Stroke2_Corner0_Rounded as CheckThink} from '#/components/ico
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {InlineLinkText, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {SubduedFill} from '#/components/purchases/BlueskyPlusGradients'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function BlueskyPlusCore({
|
||||
@@ -151,17 +152,43 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
|
||||
a.overflow_hidden,
|
||||
gtMobile ? {width: '100%', maxWidth: 400, minWidth: 200} : a.w_full,
|
||||
]}>
|
||||
<BlueskyPlusLogo width={100} gradient="nordic" />
|
||||
<View style={[a.relative, a.pb_md]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.overflow_hidden,
|
||||
isWeb
|
||||
? undefined
|
||||
: {borderTopLeftRadius: 20, borderTopRightRadius: 20},
|
||||
{
|
||||
top: (a.p_xl.padding + 4) * -1,
|
||||
bottom: 0,
|
||||
left: a.p_xl.padding * -1,
|
||||
right: a.p_xl.padding * -1,
|
||||
},
|
||||
]}>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
</View>
|
||||
|
||||
<Text style={[a.text_3xl, a.font_heavy, a.pt_lg]}>
|
||||
<Trans>Let's build the social web.</Trans>
|
||||
</Text>
|
||||
<BlueskyPlusLogo width={80} fill="white" style={[a.z_10]} />
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.text_5xl,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
a.pt_lg,
|
||||
{color: 'white'},
|
||||
]}>
|
||||
<Trans>Let's build the social web.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
a.pt_xs,
|
||||
a.pt_lg,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
@@ -196,10 +223,7 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
|
||||
: t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{Boolean(selected || hovered) && (
|
||||
<GradientFill
|
||||
gradient={tokens.gradients.nordic}
|
||||
style={{opacity: 0.1}}
|
||||
/>
|
||||
<SubduedFill style={{opacity: 0.2}} />
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
@@ -258,12 +282,14 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{height: 24, width: 24},
|
||||
{
|
||||
backgroundColor: t.atoms.bg.backgroundColor,
|
||||
},
|
||||
]}>
|
||||
{selected ? (
|
||||
<>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
<CheckThink
|
||||
fill="white"
|
||||
fill={t.atoms.text_contrast_high.color}
|
||||
size="xs"
|
||||
style={[a.z_10]}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens, useTheme, ViewStyleProp} from '#/alf'
|
||||
|
||||
const dark = tokens.gradients.nordic
|
||||
const light = {
|
||||
values: [
|
||||
[0, '#0F62C7'],
|
||||
[1, '#9EE8C1'],
|
||||
],
|
||||
} as const
|
||||
|
||||
export function SubduedFill({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
const isDark = t.name !== 'light'
|
||||
const gradient = isDark ? dark : light
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.inset_0, t.atoms.bg]}>
|
||||
<LinearGradient
|
||||
colors={gradient.values.map(c => c[1]) as [string, string, ...string[]]}
|
||||
locations={
|
||||
gradient.values.map(c => c[0]) as [number, number, ...number[]]
|
||||
}
|
||||
start={{x: 0, y: 1}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[a.absolute, a.inset_0, {opacity: 0.3}, style]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
APISubscription,
|
||||
NativePurchaseRestricted,
|
||||
} from '#/state/purchases/types'
|
||||
import {atoms as a, tokens, useBreakpoints, useGutters,useTheme} from '#/alf'
|
||||
import {atoms as a, tokens, useBreakpoints, useGutters, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
@@ -38,6 +38,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {createStaticClick, InlineLinkText, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {SubduedFill} from '#/components/purchases/BlueskyPlusGradients'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type ScreenProps = NativeStackScreenProps<
|
||||
@@ -46,6 +47,8 @@ export type ScreenProps = NativeStackScreenProps<
|
||||
>
|
||||
|
||||
export function Subscriptions(_props: ScreenProps) {
|
||||
const t = useTheme()
|
||||
const isDark = t.name !== 'light'
|
||||
const gutters = useGutters(['base', 'base'])
|
||||
const insets = useSafeAreaInsets()
|
||||
const purchases = usePurchases()
|
||||
@@ -64,12 +67,16 @@ export function Subscriptions(_props: ScreenProps) {
|
||||
return (
|
||||
<Layout.Screen noPaddingTop>
|
||||
<Layout.Center>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
<SubduedFill />
|
||||
|
||||
<View style={{height: insets.top}} />
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<BlueskyPlusLogo width={100} fill="white" />
|
||||
<BlueskyPlusLogo
|
||||
width={100}
|
||||
fill={isDark ? t.palette.white : t.atoms.text_contrast_high.color}
|
||||
/>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
@@ -114,17 +121,17 @@ function Core({
|
||||
const coreSubscriptions = state.subscriptions.filter(
|
||||
s => s.group === SubscriptionGroupId.Core,
|
||||
)
|
||||
const activeCoreSubscriptions = coreSubscriptions.filter(
|
||||
const activeSubscriptions = coreSubscriptions.filter(
|
||||
s => s.status === 'active' || s.status === 'paused',
|
||||
)
|
||||
const inactiveCoreSubscriptions = coreSubscriptions.filter(
|
||||
const inactiveSubscriptions = coreSubscriptions.filter(
|
||||
s => s.status !== 'active' && s.status !== 'paused',
|
||||
)
|
||||
const hasActiveSubscriptions = activeCoreSubscriptions.length > 0
|
||||
const hasInactiveCoreSubscriptions = inactiveCoreSubscriptions.length > 0
|
||||
const hasActiveSubscriptions = activeSubscriptions.length > 0
|
||||
const hasInactiveSubscriptions = inactiveSubscriptions.length > 0
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md]}>
|
||||
<View style={[isWeb ? a.gap_lg : a.gap_md]}>
|
||||
{restricted === 'yes' ? (
|
||||
<Admonition type="info">
|
||||
<Trans>
|
||||
@@ -132,45 +139,60 @@ function Core({
|
||||
Please subscribe additional accounts through our web application.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
) : hasActiveSubscriptions ? null : hasInactiveCoreSubscriptions ? (
|
||||
<Repurchase />
|
||||
) : hasActiveSubscriptions ? null : hasInactiveSubscriptions ? (
|
||||
<PurchaseHeader />
|
||||
) : (
|
||||
<Purchase />
|
||||
<PurchaseHeader />
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
{hasActiveSubscriptions ||
|
||||
(hasInactiveSubscriptions && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
{hasActiveSubscriptions && (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
|
||||
<Trans>My subscriptions</Trans>
|
||||
</Text>
|
||||
{hasActiveSubscriptions && (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>My subscriptions</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
{activeCoreSubscriptions.map(sub => (
|
||||
<Subscription key={sub.purchasedAt} subscription={sub} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
{activeSubscriptions.map(sub => (
|
||||
<Subscription key={sub.purchasedAt} subscription={sub} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{hasInactiveCoreSubscriptions && (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Past subscriptions</Trans>
|
||||
</Text>
|
||||
{hasInactiveSubscriptions && (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Past subscriptions</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
{inactiveCoreSubscriptions.map(sub => (
|
||||
<Subscription key={sub.purchasedAt} subscription={sub} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
{inactiveSubscriptions.map(sub => (
|
||||
<Subscription key={sub.purchasedAt} subscription={sub} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -219,6 +241,9 @@ function InfoCards() {
|
||||
a.p_lg,
|
||||
a.rounded_md,
|
||||
a.gap_sm,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text
|
||||
@@ -233,7 +258,7 @@ function InfoCards() {
|
||||
<View style={[a.py_xs, a.gap_sm]}>
|
||||
{activeFeatures.map(f => (
|
||||
<View key={f.text} style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<f.icon fill={t.atoms.text_contrast_low.color} size="xs" />
|
||||
<f.icon fill={tokens.blueskyPlus.mid} size="xs" />
|
||||
<Text style={[a.text_sm]}>{f.text}</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -283,14 +308,23 @@ function InfoCards() {
|
||||
)
|
||||
}
|
||||
|
||||
function Repurchase() {
|
||||
function PurchaseHeader() {
|
||||
const t = useTheme()
|
||||
const isDark = t.name !== 'light'
|
||||
const {_} = useLingui()
|
||||
const control = useDialogControl()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const large = gtMobile
|
||||
|
||||
return (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_3xl, a.font_heavy]}>
|
||||
<Trans>You're part of something bigger.</Trans>
|
||||
<View style={[large ? [a.py_sm, a.gap_md] : a.gap_sm]}>
|
||||
<Text
|
||||
style={[
|
||||
large ? a.text_5xl : a.text_3xl,
|
||||
a.font_heavy,
|
||||
isDark ? undefined : t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Building a better internet needs your support.</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_xs]}>
|
||||
@@ -320,81 +354,6 @@ function Repurchase() {
|
||||
)
|
||||
}
|
||||
|
||||
function Purchase() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = useDialogControl()
|
||||
const {features} = useFeatures()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={[a.text_3xl, a.font_heavy, a.pb_xs]}>
|
||||
<Trans>Building a better internet needs your support.</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.gap_xs]}>
|
||||
<Text style={[a.text_md, a.leading_snug, a.pt_xs]}>
|
||||
<Trans>
|
||||
Subscribing to Bluesky+ helps ensure that our work to build an open,
|
||||
secure, and user-first internet can continue.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug, a.pt_xs]}>
|
||||
<Trans>Plus, you'll get access to exclusive features!</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.my_md,
|
||||
a.p_lg,
|
||||
a.gap_sm,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{features.map(f => (
|
||||
<View key={f.text} style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={{paddingTop: 2}}>
|
||||
<f.icon
|
||||
fill={
|
||||
f.available
|
||||
? tokens.blueskyPlus.mid
|
||||
: t.atoms.text_contrast_low.color
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
f.available ? t.atoms.text : t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{f.text}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_('Subscribe')}
|
||||
onPress={() => control.open()}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
style={[a.overflow_hidden]}>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
<ButtonText style={[{color: 'white'}]}>
|
||||
<Trans>Subscribe</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Plus} position="right" style={[{color: 'white'}]} />
|
||||
</Button>
|
||||
<BlueskyPlusCore control={control} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSubscriptionUI({
|
||||
subscription: sub,
|
||||
}: {
|
||||
@@ -464,12 +423,13 @@ export function Subscription({
|
||||
const {_} = useLingui()
|
||||
const {mutateAsync: manageSubscription} = useManageSubscription()
|
||||
const ui = useSubscriptionUI({subscription: sub})
|
||||
const isExpired = sub.status !== 'active' && sub.status !== 'paused'
|
||||
|
||||
const canManage =
|
||||
(isWeb && sub.platform === PlatformId.Web) ||
|
||||
(isIOS && sub.platform === PlatformId.Ios) ||
|
||||
(isAndroid && sub.platform === PlatformId.Android)
|
||||
const showManage = canManage && sub.status !== 'expired'
|
||||
const showManage = canManage && !isExpired
|
||||
|
||||
const statusStyles = React.useMemo<TextStyle>(() => {
|
||||
return {
|
||||
@@ -503,8 +463,13 @@ export function Subscription({
|
||||
return (
|
||||
<View
|
||||
key={sub.purchasedAt}
|
||||
style={[a.px_lg, a.rounded_md, a.overflow_hidden]}>
|
||||
<GradientFill gradient={tokens.gradients.nordic} />
|
||||
style={[
|
||||
a.px_lg,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
isExpired && [a.border, t.atoms.border_contrast_low],
|
||||
]}>
|
||||
{!isExpired && <GradientFill gradient={tokens.gradients.nordic} />}
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
@@ -613,6 +578,11 @@ function useFeatures() {
|
||||
icon: Check,
|
||||
text: _(msg`Custom app icons`),
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
icon: Check,
|
||||
text: _(msg`Lorem ipsum dolor sit amet`),
|
||||
},
|
||||
{
|
||||
available: false,
|
||||
icon: Clock,
|
||||
|
||||
@@ -20,6 +20,7 @@ export function usePurchaseOffering() {
|
||||
|
||||
return useMutation<void, Error | ReceiptAlreadyInUseError, Params>({
|
||||
async mutationFn({did, email, offering}: Params) {
|
||||
console.log(offering)
|
||||
if (offering.platform === PlatformId.Web) {
|
||||
throw new Error('Unsupported platform')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user