Some design

This commit is contained in:
Eric Bailey
2024-12-08 17:49:42 -06:00
parent 9e65f7151c
commit be2606ea9d
7 changed files with 173 additions and 141 deletions
+4 -2
View File
@@ -16,8 +16,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' 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 {QueryProvider} from '#/lib/react-query'
import { import {
initialize, initialize,
@@ -28,6 +26,7 @@ import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider' import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid,isIOS} from '#/platform/detection'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' 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 IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {AppProfiler} from '#/AppProfiler' import {AppProfiler} from '#/AppProfiler'
import {RC_APPLE_PUBLIC_KEY, RC_GOOGLE_PUBLIC_KEY} from '#/env'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet' import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
@@ -187,6 +187,8 @@ function App() {
}, },
) )
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG)
if (isIOS) { if (isIOS) {
Purchases.configure({apiKey: RC_APPLE_PUBLIC_KEY}) Purchases.configure({apiKey: RC_APPLE_PUBLIC_KEY})
} else if (isAndroid) { } else if (isAndroid) {
+1 -1
View File
@@ -108,7 +108,7 @@ export const gradients = {
nordic: { nordic: {
values: [ values: [
[0, '#083367'], [0, '#083367'],
[0.4, '#386D84'], // [0.4, '#386D84'],
[1, '#9EE8C1'], [1, '#9EE8C1'],
], ],
hover_value: '#3A7085', hover_value: '#3A7085',
+4 -3
View File
@@ -1,12 +1,13 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens} from '#/alf' import {atoms as a, tokens, ViewStyleProp} from '#/alf'
export function GradientFill({ export function GradientFill({
gradient, gradient,
rotate = '0deg', rotate = '0deg',
}: { style,
}: ViewStyleProp & {
gradient: gradient:
| typeof tokens.gradients.primary | typeof tokens.gradients.primary
| typeof tokens.gradients.sky | typeof tokens.gradients.sky
@@ -75,7 +76,7 @@ export function GradientFill({
} }
start={start} start={start}
end={end} 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 {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {InlineLinkText, Link} from '#/components/Link' import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {SubduedFill} from '#/components/purchases/BlueskyPlusGradients'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function BlueskyPlusCore({ export function BlueskyPlusCore({
@@ -151,17 +152,43 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
a.overflow_hidden, a.overflow_hidden,
gtMobile ? {width: '100%', maxWidth: 400, minWidth: 200} : a.w_full, 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]}> <BlueskyPlusLogo width={80} fill="white" style={[a.z_10]} />
<Trans>Let's build the social web.</Trans>
</Text> <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 <Text
style={[ style={[
a.text_md, a.text_md,
a.leading_snug, a.leading_snug,
a.pt_xs, a.pt_lg,
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
]}> ]}>
<Trans> <Trans>
@@ -196,10 +223,7 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
: t.atoms.border_contrast_low, : t.atoms.border_contrast_low,
]}> ]}>
{Boolean(selected || hovered) && ( {Boolean(selected || hovered) && (
<GradientFill <SubduedFill style={{opacity: 0.2}} />
gradient={tokens.gradients.nordic}
style={{opacity: 0.1}}
/>
)} )}
<View <View
style={[ style={[
@@ -258,12 +282,14 @@ function DialogInner({control}: {control: Dialog.DialogControlProps}) {
a.align_center, a.align_center,
a.justify_center, a.justify_center,
{height: 24, width: 24}, {height: 24, width: 24},
{
backgroundColor: t.atoms.bg.backgroundColor,
},
]}> ]}>
{selected ? ( {selected ? (
<> <>
<GradientFill gradient={tokens.gradients.nordic} />
<CheckThink <CheckThink
fill="white" fill={t.atoms.text_contrast_high.color}
size="xs" size="xs"
style={[a.z_10]} 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>
)
}
+94 -124
View File
@@ -19,7 +19,7 @@ import {
APISubscription, APISubscription,
NativePurchaseRestricted, NativePurchaseRestricted,
} from '#/state/purchases/types' } 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 {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' 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 * as Layout from '#/components/Layout'
import {createStaticClick, InlineLinkText, Link} from '#/components/Link' import {createStaticClick, InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {SubduedFill} from '#/components/purchases/BlueskyPlusGradients'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export type ScreenProps = NativeStackScreenProps< export type ScreenProps = NativeStackScreenProps<
@@ -46,6 +47,8 @@ export type ScreenProps = NativeStackScreenProps<
> >
export function Subscriptions(_props: ScreenProps) { export function Subscriptions(_props: ScreenProps) {
const t = useTheme()
const isDark = t.name !== 'light'
const gutters = useGutters(['base', 'base']) const gutters = useGutters(['base', 'base'])
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const purchases = usePurchases() const purchases = usePurchases()
@@ -64,12 +67,16 @@ export function Subscriptions(_props: ScreenProps) {
return ( return (
<Layout.Screen noPaddingTop> <Layout.Screen noPaddingTop>
<Layout.Center> <Layout.Center>
<GradientFill gradient={tokens.gradients.nordic} /> <SubduedFill />
<View style={{height: insets.top}} /> <View style={{height: insets.top}} />
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer>
<Layout.Header.BackButton /> <Layout.Header.BackButton />
<Layout.Header.Content> <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.Content>
<Layout.Header.Slot /> <Layout.Header.Slot />
</Layout.Header.Outer> </Layout.Header.Outer>
@@ -114,17 +121,17 @@ function Core({
const coreSubscriptions = state.subscriptions.filter( const coreSubscriptions = state.subscriptions.filter(
s => s.group === SubscriptionGroupId.Core, s => s.group === SubscriptionGroupId.Core,
) )
const activeCoreSubscriptions = coreSubscriptions.filter( const activeSubscriptions = coreSubscriptions.filter(
s => s.status === 'active' || s.status === 'paused', s => s.status === 'active' || s.status === 'paused',
) )
const inactiveCoreSubscriptions = coreSubscriptions.filter( const inactiveSubscriptions = coreSubscriptions.filter(
s => s.status !== 'active' && s.status !== 'paused', s => s.status !== 'active' && s.status !== 'paused',
) )
const hasActiveSubscriptions = activeCoreSubscriptions.length > 0 const hasActiveSubscriptions = activeSubscriptions.length > 0
const hasInactiveCoreSubscriptions = inactiveCoreSubscriptions.length > 0 const hasInactiveSubscriptions = inactiveSubscriptions.length > 0
return ( return (
<View style={[a.gap_md]}> <View style={[isWeb ? a.gap_lg : a.gap_md]}>
{restricted === 'yes' ? ( {restricted === 'yes' ? (
<Admonition type="info"> <Admonition type="info">
<Trans> <Trans>
@@ -132,45 +139,60 @@ function Core({
Please subscribe additional accounts through our web application. Please subscribe additional accounts through our web application.
</Trans> </Trans>
</Admonition> </Admonition>
) : hasActiveSubscriptions ? null : hasInactiveCoreSubscriptions ? ( ) : hasActiveSubscriptions ? null : hasInactiveSubscriptions ? (
<Repurchase /> <PurchaseHeader />
) : ( ) : (
<Purchase /> <PurchaseHeader />
)} )}
<Divider /> {hasActiveSubscriptions ||
(hasInactiveSubscriptions && (
<>
<Divider />
{hasActiveSubscriptions && ( {hasActiveSubscriptions && (
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> <Text
<Trans>My subscriptions</Trans> style={[
</Text> 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_lg]}>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
{activeCoreSubscriptions.map(sub => ( {activeSubscriptions.map(sub => (
<Subscription key={sub.purchasedAt} subscription={sub} /> <Subscription key={sub.purchasedAt} subscription={sub} />
))} ))}
</View> </View>
</View> </View>
</View> </View>
)} )}
{hasInactiveCoreSubscriptions && ( {hasInactiveSubscriptions && (
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> <Text
<Trans>Past subscriptions</Trans> style={[
</Text> 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_lg]}>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
{inactiveCoreSubscriptions.map(sub => ( {inactiveSubscriptions.map(sub => (
<Subscription key={sub.purchasedAt} subscription={sub} /> <Subscription key={sub.purchasedAt} subscription={sub} />
))} ))}
</View> </View>
</View> </View>
</View> </View>
)} )}
</>
))}
<Divider /> <Divider />
@@ -219,6 +241,9 @@ function InfoCards() {
a.p_lg, a.p_lg,
a.rounded_md, a.rounded_md,
a.gap_sm, a.gap_sm,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<Text <Text
@@ -233,7 +258,7 @@ function InfoCards() {
<View style={[a.py_xs, a.gap_sm]}> <View style={[a.py_xs, a.gap_sm]}>
{activeFeatures.map(f => ( {activeFeatures.map(f => (
<View key={f.text} style={[a.flex_row, a.align_center, a.gap_xs]}> <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> <Text style={[a.text_sm]}>{f.text}</Text>
</View> </View>
))} ))}
@@ -283,14 +308,23 @@ function InfoCards() {
) )
} }
function Repurchase() { function PurchaseHeader() {
const t = useTheme()
const isDark = t.name !== 'light'
const {_} = useLingui() const {_} = useLingui()
const control = useDialogControl() const control = useDialogControl()
const {gtMobile} = useBreakpoints()
const large = gtMobile
return ( return (
<View style={[a.gap_sm]}> <View style={[large ? [a.py_sm, a.gap_md] : a.gap_sm]}>
<Text style={[a.text_3xl, a.font_heavy]}> <Text
<Trans>You're part of something bigger.</Trans> 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> </Text>
<View style={[a.gap_xs]}> <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({ export function useSubscriptionUI({
subscription: sub, subscription: sub,
}: { }: {
@@ -464,12 +423,13 @@ export function Subscription({
const {_} = useLingui() const {_} = useLingui()
const {mutateAsync: manageSubscription} = useManageSubscription() const {mutateAsync: manageSubscription} = useManageSubscription()
const ui = useSubscriptionUI({subscription: sub}) const ui = useSubscriptionUI({subscription: sub})
const isExpired = sub.status !== 'active' && sub.status !== 'paused'
const canManage = const canManage =
(isWeb && sub.platform === PlatformId.Web) || (isWeb && sub.platform === PlatformId.Web) ||
(isIOS && sub.platform === PlatformId.Ios) || (isIOS && sub.platform === PlatformId.Ios) ||
(isAndroid && sub.platform === PlatformId.Android) (isAndroid && sub.platform === PlatformId.Android)
const showManage = canManage && sub.status !== 'expired' const showManage = canManage && !isExpired
const statusStyles = React.useMemo<TextStyle>(() => { const statusStyles = React.useMemo<TextStyle>(() => {
return { return {
@@ -503,8 +463,13 @@ export function Subscription({
return ( return (
<View <View
key={sub.purchasedAt} key={sub.purchasedAt}
style={[a.px_lg, a.rounded_md, a.overflow_hidden]}> style={[
<GradientFill gradient={tokens.gradients.nordic} /> a.px_lg,
a.rounded_md,
a.overflow_hidden,
isExpired && [a.border, t.atoms.border_contrast_low],
]}>
{!isExpired && <GradientFill gradient={tokens.gradients.nordic} />}
<View <View
style={[ style={[
a.absolute, a.absolute,
@@ -613,6 +578,11 @@ function useFeatures() {
icon: Check, icon: Check,
text: _(msg`Custom app icons`), text: _(msg`Custom app icons`),
}, },
{
available: true,
icon: Check,
text: _(msg`Lorem ipsum dolor sit amet`),
},
{ {
available: false, available: false,
icon: Clock, icon: Clock,
@@ -20,6 +20,7 @@ export function usePurchaseOffering() {
return useMutation<void, Error | ReceiptAlreadyInUseError, Params>({ return useMutation<void, Error | ReceiptAlreadyInUseError, Params>({
async mutationFn({did, email, offering}: Params) { async mutationFn({did, email, offering}: Params) {
console.log(offering)
if (offering.platform === PlatformId.Web) { if (offering.platform === PlatformId.Web) {
throw new Error('Unsupported platform') throw new Error('Unsupported platform')
} }