-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
377 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { AmountFormat } from '@/components/common/amount-format' | ||
import { Marquee } from '@/components/common/marquee' | ||
import { PaywallIllustration } from '@/components/svg-assets/paywall-illustration' | ||
import { Button } from '@/components/ui/button' | ||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' | ||
import { Text } from '@/components/ui/text' | ||
import { t } from '@lingui/macro' | ||
import { useLingui } from '@lingui/react' | ||
import { BlurView } from 'expo-blur' | ||
import { Link } from 'expo-router' | ||
import { CheckCircleIcon } from 'lucide-react-native' | ||
import { cssInterop } from 'nativewind' | ||
import { useState } from 'react' | ||
import { | ||
Image, | ||
type ImageSourcePropType, | ||
Pressable, | ||
ScrollView, | ||
View, | ||
} from 'react-native' | ||
|
||
cssInterop(BlurView, { | ||
className: { | ||
target: 'style', | ||
}, | ||
}) | ||
|
||
type FeatureCardProps = { | ||
source: ImageSourcePropType | ||
title: string | ||
className?: string | ||
} | ||
function FeatureCard({ source, title, className }: FeatureCardProps) { | ||
return ( | ||
<View className={className}> | ||
<Image | ||
source={source} | ||
className="h-32 w-44 rounded-xl bg-primary dark:bg-muted" | ||
resizeMode="contain" | ||
/> | ||
<BlurView | ||
intensity={20} | ||
className="absolute right-2 bottom-2 left-2 overflow-hidden rounded-lg p-2 dark:border dark:border-white/10" | ||
> | ||
<Text className="text-center font-medium text-primary-foreground text-sm dark:text-primary"> | ||
{title} | ||
</Text> | ||
</BlurView> | ||
</View> | ||
) | ||
} | ||
|
||
export default function PaywallScreen() { | ||
const { i18n } = useLingui() | ||
const [plan, setPlan] = useState<'growth' | 'wealth'>('growth') | ||
|
||
const proFeatures = [ | ||
{ | ||
source: require('../../assets/images/paywall-images/2.png'), | ||
title: t(i18n)`Unlimited`, | ||
}, | ||
{ | ||
source: require('../../assets/images/paywall-images/3.png'), | ||
title: t(i18n)`AI Insights`, | ||
}, | ||
{ | ||
source: require('../../assets/images/paywall-images/4.png'), | ||
title: t(i18n)`Multi-currencies`, | ||
}, | ||
{ | ||
source: require('../../assets/images/paywall-images/1.png'), | ||
title: t(i18n)`Security`, | ||
}, | ||
] | ||
|
||
const plans = { | ||
growth: [ | ||
t(i18n)`Up to 10 AI transactions per day`, | ||
t(i18n)`Unlimited transactions & categories`, | ||
t(i18n)`6 budgets & 6 accounts`, | ||
t(i18n)`Multi-currencies & customizations`, | ||
], | ||
wealth: [ | ||
t(i18n)`Up to 25 AI transactions per day`, | ||
t(i18n)`All features of growth plan`, | ||
t(i18n)`Unlimited budgets & accounts`, | ||
t(i18n)`Most advanced financial AI assistant`, | ||
], | ||
} | ||
|
||
return ( | ||
<ScrollView | ||
className="bg-card" | ||
contentContainerClassName="gap-3" | ||
automaticallyAdjustKeyboardInsets | ||
keyboardShouldPersistTaps="handled" | ||
> | ||
<Text className="mx-8 my-2 font-sans font-semibold text-3xl text-primary"> | ||
{t(i18n)`Complete control over your finances`} | ||
</Text> | ||
<Marquee spacing={20} speed={0.5}> | ||
<View className="flex-row gap-4 py-8"> | ||
{proFeatures.map((item, index) => ( | ||
<FeatureCard | ||
key={item.title} | ||
source={item.source} | ||
title={item.title} | ||
className={index % 2 === 0 ? '-translate-y-4' : 'translate-y-4'} | ||
/> | ||
))} | ||
</View> | ||
</Marquee> | ||
<Tabs | ||
value={plan} | ||
className="mx-8" | ||
onValueChange={(value) => { | ||
setPlan(value as 'growth' | 'wealth') | ||
}} | ||
> | ||
<TabsList> | ||
<TabsTrigger value="growth"> | ||
<Text>{t(i18n)`Growth`}</Text> | ||
</TabsTrigger> | ||
<TabsTrigger value="wealth"> | ||
<Text>{t(i18n)`Wealth`}</Text> | ||
</TabsTrigger> | ||
</TabsList> | ||
</Tabs> | ||
<View className="gap-3 py-2"> | ||
{plans[plan].map((item) => ( | ||
<View className="mx-8 flex-row gap-3" key={item}> | ||
<CheckCircleIcon className="size-6 text-amount-positive" /> | ||
<Text className="text-primary">{item}</Text> | ||
</View> | ||
))} | ||
</View> | ||
<View className="mx-8 flex-row items-end gap-6"> | ||
<Pressable className="h-36 flex-1 rounded-lg border-2 border-border"> | ||
<View className="flex-1 items-center justify-center bg-muted/40 p-4"> | ||
<Text className="font-bold text-4xl">1</Text> | ||
<Text className="mb-4 text-center text-muted-foreground text-sm uppercase"> | ||
month | ||
</Text> | ||
<AmountFormat amount={99000} currency="VND" /> | ||
</View> | ||
</Pressable> | ||
<Pressable className="h-44 flex-1 overflow-hidden rounded-lg bg-primary p-0.5"> | ||
<View className="h-8 items-center justify-center bg-primary"> | ||
<Text className="text-center font-semibold text-primary-foreground text-sm uppercase"> | ||
{t(i18n)`Best value`} | ||
</Text> | ||
</View> | ||
<View className="flex-1 items-center justify-center rounded-md bg-background p-4"> | ||
<Text className="font-bold text-4xl">12</Text> | ||
<Text className="mb-4 text-center text-muted-foreground text-sm uppercase"> | ||
months | ||
</Text> | ||
<AmountFormat amount={999000} currency="VND" /> | ||
</View> | ||
</Pressable> | ||
</View> | ||
<Button className="mx-8 mt-2"> | ||
<Text>{t(i18n)`Unlock 6pm Pro`}</Text> | ||
</Button> | ||
<Link href="/privacy-policy"> | ||
<Text className="mx-auto text-center text-muted-foreground text-sm"> | ||
Privacy Policy | ||
</Text> | ||
</Link> | ||
<PaywallIllustration className="mx-auto h-[566px] w-[200px] text-primary" /> | ||
</ScrollView> | ||
) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import * as React from 'react' | ||
import type { ViewStyle } from 'react-native' | ||
import { StyleSheet } from 'react-native' | ||
import { View } from 'react-native' | ||
import type { SharedValue } from 'react-native-reanimated' | ||
import Animated, { | ||
runOnJS, | ||
useAnimatedReaction, | ||
useAnimatedStyle, | ||
useFrameCallback, | ||
useSharedValue, | ||
} from 'react-native-reanimated' | ||
|
||
const AnimatedChild = ({ | ||
index, | ||
children, | ||
anim, | ||
textWidth, | ||
spacing, | ||
}: React.PropsWithChildren<{ | ||
index: number | ||
anim: SharedValue<number> | ||
textWidth: SharedValue<number> | ||
spacing: number | ||
}>) => { | ||
const stylez = useAnimatedStyle(() => { | ||
return { | ||
position: 'absolute', | ||
left: index * (textWidth.value + spacing), | ||
transform: [ | ||
{ | ||
translateX: -(anim.value % (textWidth.value + spacing)), | ||
}, | ||
], | ||
} | ||
}, [index, spacing, textWidth]) | ||
return <Animated.View style={stylez}>{children}</Animated.View> | ||
} | ||
|
||
export type MarqueeProps = React.PropsWithChildren<{ | ||
speed?: number | ||
spacing?: number | ||
style?: ViewStyle | ||
}> | ||
|
||
/** | ||
* Used to animate the given children in a horizontal manner. | ||
*/ | ||
export const Marquee = React.memo( | ||
({ speed = 1, children, spacing = 0, style }: MarqueeProps) => { | ||
const parentWidth = useSharedValue(0) | ||
const textWidth = useSharedValue(0) | ||
const [cloneTimes, setCloneTimes] = React.useState(0) | ||
const anim = useSharedValue(0) | ||
|
||
useFrameCallback(() => { | ||
anim.value += speed | ||
}, true) | ||
|
||
useAnimatedReaction( | ||
() => { | ||
if (textWidth.value === 0 || parentWidth.value === 0) { | ||
return 0 | ||
} | ||
return Math.round(parentWidth.value / textWidth.value) + 1 | ||
}, | ||
(v) => { | ||
if (v === 0) { | ||
return | ||
} | ||
// This is going to cover the case when the text/element size | ||
// is greater than the actual parent size | ||
// Double this to cover the entire screen twice, in this way we can | ||
// reset the position of the first element when its going to move out | ||
// of the screen without any noticible glitch | ||
runOnJS(setCloneTimes)(v * 2) | ||
}, | ||
[], | ||
) | ||
return ( | ||
<Animated.View | ||
style={style} | ||
onLayout={(ev) => { | ||
parentWidth.value = ev.nativeEvent.layout.width | ||
}} | ||
pointerEvents="box-none" | ||
> | ||
<Animated.View style={styles.row} pointerEvents="box-none"> | ||
{ | ||
// We are adding the text inside a ScrollView because in this way we | ||
// ensure that its not going to "wrap". | ||
} | ||
<Animated.ScrollView | ||
horizontal | ||
style={styles.hidden} | ||
pointerEvents="box-none" | ||
> | ||
<View | ||
onLayout={(ev) => { | ||
textWidth.value = ev.nativeEvent.layout.width | ||
}} | ||
> | ||
{children} | ||
</View> | ||
</Animated.ScrollView> | ||
{cloneTimes > 0 && | ||
[...Array(cloneTimes).keys()].map((index) => { | ||
return ( | ||
<AnimatedChild | ||
key={`clone-${index}`} | ||
index={index} | ||
anim={anim} | ||
textWidth={textWidth} | ||
spacing={spacing} | ||
> | ||
{children} | ||
</AnimatedChild> | ||
) | ||
})} | ||
</Animated.View> | ||
</Animated.View> | ||
) | ||
}, | ||
) | ||
|
||
const styles = StyleSheet.create({ | ||
hidden: { opacity: 0, zIndex: -9999 }, | ||
row: { flexDirection: 'row', overflow: 'hidden' }, | ||
}) |
49 changes: 49 additions & 0 deletions
49
apps/mobile/components/svg-assets/paywall-illustration.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters