diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index 65547877..81a26bbb 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -94,17 +94,19 @@ export default function SettingsScreen() { className="bg-card" > - + + + {t(i18n)`General`} diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx index fd633ba3..c63e1d65 100644 --- a/apps/mobile/app/(app)/_layout.tsx +++ b/apps/mobile/app/(app)/_layout.tsx @@ -106,6 +106,13 @@ export default function AuthenticatedLayout() { headerTitle: t(i18n)`Feedback`, }} /> + + + + + {title} + + + + ) +} + +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 ( + + + {t(i18n)`Complete control over your finances`} + + + + {proFeatures.map((item, index) => ( + + ))} + + + { + setPlan(value as 'growth' | 'wealth') + }} + > + + + {t(i18n)`Growth`} + + + {t(i18n)`Wealth`} + + + + + {plans[plan].map((item) => ( + + + {item} + + ))} + + + + + 1 + + month + + + + + + + + {t(i18n)`Best value`} + + + + 12 + + months + + + + + + + + + Privacy Policy + + + + + ) +} diff --git a/apps/mobile/assets/images/paywall-images/1.png b/apps/mobile/assets/images/paywall-images/1.png new file mode 100644 index 00000000..e39fc985 Binary files /dev/null and b/apps/mobile/assets/images/paywall-images/1.png differ diff --git a/apps/mobile/assets/images/paywall-images/2.png b/apps/mobile/assets/images/paywall-images/2.png new file mode 100644 index 00000000..c6d90633 Binary files /dev/null and b/apps/mobile/assets/images/paywall-images/2.png differ diff --git a/apps/mobile/assets/images/paywall-images/3.png b/apps/mobile/assets/images/paywall-images/3.png new file mode 100644 index 00000000..5ac6e11c Binary files /dev/null and b/apps/mobile/assets/images/paywall-images/3.png differ diff --git a/apps/mobile/assets/images/paywall-images/4.png b/apps/mobile/assets/images/paywall-images/4.png new file mode 100644 index 00000000..1f1f79c0 Binary files /dev/null and b/apps/mobile/assets/images/paywall-images/4.png differ diff --git a/apps/mobile/components/common/marquee.tsx b/apps/mobile/components/common/marquee.tsx new file mode 100644 index 00000000..96a44aa8 --- /dev/null +++ b/apps/mobile/components/common/marquee.tsx @@ -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 + textWidth: SharedValue + spacing: number +}>) => { + const stylez = useAnimatedStyle(() => { + return { + position: 'absolute', + left: index * (textWidth.value + spacing), + transform: [ + { + translateX: -(anim.value % (textWidth.value + spacing)), + }, + ], + } + }, [index, spacing, textWidth]) + return {children} +} + +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 ( + { + parentWidth.value = ev.nativeEvent.layout.width + }} + pointerEvents="box-none" + > + + { + // We are adding the text inside a ScrollView because in this way we + // ensure that its not going to "wrap". + } + + { + textWidth.value = ev.nativeEvent.layout.width + }} + > + {children} + + + {cloneTimes > 0 && + [...Array(cloneTimes).keys()].map((index) => { + return ( + + {children} + + ) + })} + + + ) + }, +) + +const styles = StyleSheet.create({ + hidden: { opacity: 0, zIndex: -9999 }, + row: { flexDirection: 'row', overflow: 'hidden' }, +}) diff --git a/apps/mobile/components/svg-assets/paywall-illustration.tsx b/apps/mobile/components/svg-assets/paywall-illustration.tsx new file mode 100644 index 00000000..3ceadebc --- /dev/null +++ b/apps/mobile/components/svg-assets/paywall-illustration.tsx @@ -0,0 +1,49 @@ +import Svg, { type SvgProps, G, Path, Defs, ClipPath } from 'react-native-svg' +export const PaywallIllustration = (props: SvgProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) diff --git a/apps/mobile/components/ui/tabs.tsx b/apps/mobile/components/ui/tabs.tsx index 057e5df2..e69a4f37 100644 --- a/apps/mobile/components/ui/tabs.tsx +++ b/apps/mobile/components/ui/tabs.tsx @@ -12,7 +12,7 @@ const TabsList = React.forwardRef<