diff --git a/assets/images/orbs-world.png b/assets/images/orbs-world.png new file mode 100644 index 0000000..b5d27dc Binary files /dev/null and b/assets/images/orbs-world.png differ diff --git a/assets/images/view-of-earth.png b/assets/images/view-of-earth.png new file mode 100644 index 0000000..488f699 Binary files /dev/null and b/assets/images/view-of-earth.png differ diff --git a/src/app/(app)/intro.tsx b/src/app/(app)/intro.tsx new file mode 100644 index 0000000..8846881 --- /dev/null +++ b/src/app/(app)/intro.tsx @@ -0,0 +1,207 @@ +import { + Blur, + Canvas, + Circle, + ColorMatrix, + Group, + Image, + ImageSVG, + Paint, + RadialGradient, + Shadow, + fitbox, + rect, + useImage, + useSVG, + vec, +} from "@shopify/react-native-skia"; +import { router } from "expo-router"; +import React from "react"; +import { View, useWindowDimensions } from "react-native"; +import { + Easing, + runOnJS, + useDerivedValue, + useSharedValue, + withDelay, + withSequence, + withSpring, + withTiming, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { palette, sharedStyles as ss } from "@/src/styles"; + +function Orb({ cx, cy, r }: { cx: number; cy: number; r: number }) { + const win = useWindowDimensions(); + + const fullHeight = win.height; // + insets.top + insets.bottom; + + const radius = useSharedValue(0); + const shadowPos = useDerivedValue(() => { + return radius.value / 5; + }); + const shadowNeg = useDerivedValue(() => { + return -radius.value / 5; + }); + const shadowBlur = (r / 80) * 10; + + const computedelayMs = fullHeight - cy + cx * 4; + + React.useEffect(() => { + radius.value = withDelay( + computedelayMs, + withSpring(r, { + mass: 1.2, + damping: 10, + stiffness: 100, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }), + ); + }, []); + + return ( + + + + + + + ); +} + +export default function IntroScreen() { + const win = useWindowDimensions(); + const insets = useSafeAreaInsets(); + + const fullHeight = win.height + insets.top + insets.bottom; + const fullWidth = win.width + insets.left + insets.right; + + const opacity = useSharedValue(0); + + const backgroundEarthPng = useImage( + require("@/assets/images/view-of-earth.png"), + ); + const backgroundPanX = useSharedValue(0); + const orbsPanTransform = useDerivedValue(() => { + return [{ translateX: backgroundPanX.value }]; + }); + + const conduitWordMarkSvg = useSVG( + require("@/assets/images/psiphon-conduit-wordmark.svg"), + ); + const originalWordMarkWidth = 141; + const originalWordMarkHeight = 44; + const wordMarkSrc = rect( + 0, + 0, + originalWordMarkWidth, + originalWordMarkHeight, + ); + const wordMarkDst = rect( + fullWidth * 0.1, + fullHeight * 0.1, + fullWidth * 0.8, + fullHeight * 0.2, + ); + const wordMarkResizeTransform = fitbox("contain", wordMarkSrc, wordMarkDst); + + React.useEffect(() => { + opacity.value = withSequence( + withTiming(1, { duration: 1000 }), + withDelay( + 3000, + withTiming(0, { duration: 1000 }, () => { + runOnJS(router.replace)("/(app)/onboarding"); + }), + ), + ); + backgroundPanX.value = withTiming(-win.width, { + duration: 5000, + easing: Easing.inOut(Easing.ease), + }); + }, []); + + const opacityMatrix = useDerivedValue(() => { + // prettier-ignore + return [ + // R, G, B, A, Bias + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, opacity.value, 0, + ]; + }); + + if (!backgroundEarthPng) { + return null; + } + + const bgWidth = win.width * 2; + const bgHeight = + (bgWidth / backgroundEarthPng.width()) * backgroundEarthPng.height(); + + return ( + + + + + + + } + > + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(app)/onboarding.tsx b/src/app/(app)/onboarding.tsx index 093d3b9..2dbc29c 100644 --- a/src/app/(app)/onboarding.tsx +++ b/src/app/(app)/onboarding.tsx @@ -1,47 +1,45 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; import { Canvas, Circle, ColorMatrix, Fill, Group, - Image, LinearGradient, Paint, Paragraph, RoundedRect, - SkImage, SkParagraphStyle, SkTextStyle, Skia, TextAlign, useFonts, - useImage, vec, } from "@shopify/react-native-skia"; import * as Notifications from "expo-notifications"; +import { router } from "expo-router"; +import React from "react"; import { useTranslation } from "react-i18next"; import { BackHandler, useWindowDimensions } from "react-native"; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, +} from "react-native-gesture-handler"; import Animated, { runOnJS, useDerivedValue, useSharedValue, withTiming, } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { drawBigFont } from "@/src/common/utils"; import { useNotificationsPermissions } from "@/src/components/NotificationsStatus"; import { PrivacyPolicyLink } from "@/src/components/PrivacyPolicyLink"; import { SafeAreaView } from "@/src/components/SafeAreaView"; +import { OnboardingScene } from "@/src/components/canvas/OnboardingScene"; import { fonts, palette, sharedStyles as ss } from "@/src/styles"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { router } from "expo-router"; -import React from "react"; -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from "react-native-gesture-handler"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function OnboardingScreen() { const win = useWindowDimensions(); @@ -71,7 +69,6 @@ export default function OnboardingScreen() { { // WELCOME headerText: t("ONBOARDING_WELCOME_HEADER_I18N.string"), - image: useImage(require("@/assets/images/onboarding-welcome.png")), bodyText: t("ONBOARDING_WELCOME_BODY_I18N.string"), buttonText: t("ONBOARDING_WELCOME_BUTTON_I18N.string"), beforeNext: undefined, @@ -79,9 +76,6 @@ export default function OnboardingScreen() { { // INFO_1 headerText: t("ONBOARDING_INFO_1_HEADER_I18N.string"), - image: useImage( - require("@/assets/images/onboarding-instructions.png"), - ), bodyText: t("ONBOARDING_INFO_1_BODY_I18N.string"), buttonText: t("ONBOARDING_INFO_1_BUTTON_I18N.string"), beforeNext: undefined, @@ -89,9 +83,6 @@ export default function OnboardingScreen() { { // PERMISSIONS headerText: t("ONBOARDING_PERMISSIONS_HEADER_I18N.string"), - image: useImage( - require("@/assets/images/onboarding-permissions.png"), - ), bodyText: t("ONBOARDING_PERMISSIONS_BODY_I18N.string"), buttonText: shouldAskForNotifications ? t("ONBOARDING_ENABLE_NOTIFICATIONS_BUTTON_I18N.string") @@ -105,9 +96,6 @@ export default function OnboardingScreen() { { // PRIVACY POLICY headerText: t("ONBOARDING_PRIVACY_POLICY_HEADER_I18N.string"), - image: useImage( - require("@/assets/images/onboarding-privacy-policy.png"), - ), bodyText: t("ONBOARDING_PRIVACY_POLICY_BODY_I18N.string"), buttonText: t("ONBOARDING_PRIVACY_POLICY_BUTTON_I18N.string"), beforeNext: undefined, @@ -138,12 +126,12 @@ export default function OnboardingScreen() { width: usableWidth * 0.96, }; // image takes up the next 33% of usableHeight (48% total) - const imageTransform = [ + const sceneTransform = [ { translateY: usableHeight * 0.15 }, - { translateX: usableWidth * 0.18 }, + //{ translateX: usableWidth * 0.18 }, ]; - const imageSize = { - width: usableWidth * 0.66, + const sceneSize = { + width: usableWidth, height: usableHeight * 0.25, }; // body takes up the next 31% of usableHeight (79% total) @@ -171,6 +159,7 @@ export default function OnboardingScreen() { height: usableHeight * 0.08, }; const buttonBorderRadius = 15; + const privacyPolicyHeight = usableHeight * 0.05; // 10% of usable height is left for the Privacy Policy link to appear in const fontMgr = useFonts({ @@ -204,10 +193,6 @@ export default function OnboardingScreen() { .build(); }); - const image = useDerivedValue(() => { - return views[currentView.value].image; - }); - const bodyP = useDerivedValue(() => { if (!fontMgr) { return null; @@ -396,11 +381,11 @@ export default function OnboardingScreen() { width={headerSize.width} /> - - + @@ -511,6 +496,7 @@ export default function OnboardingScreen() { diff --git a/src/app/index.tsx b/src/app/index.tsx index 25640f9..48fa640 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -34,7 +34,7 @@ export default function Index() { if (hasOnboarded !== null) { runOnJS(router.replace)("/(app)/"); } else { - runOnJS(router.replace)("/(app)/onboarding"); + runOnJS(router.replace)("/(app)/intro"); } }); } diff --git a/src/components/ConduitOrbToggle.tsx b/src/components/ConduitOrbToggle.tsx index 599e065..2ef2278 100644 --- a/src/components/ConduitOrbToggle.tsx +++ b/src/components/ConduitOrbToggle.tsx @@ -425,8 +425,19 @@ export function ConduitOrbToggle({ } canvasWidth={width} orbRadius={finalOrbRadius} - orbCenterY={orbCenterY} - psiphonLogoSize={psiphonLogoSize} + midPoint={vec(0, 0)} + secondLastPoint={vec( + 0, + -finalOrbRadius, + )} + endPoint={vec( + 0, + -( + orbCenterY - + psiphonLogoSize / 2 + ), + )} // land in P logo + randomize={true} /> ); }, diff --git a/src/components/ConduitSettings.tsx b/src/components/ConduitSettings.tsx index fe5f2e5..3edd857 100644 --- a/src/components/ConduitSettings.tsx +++ b/src/components/ConduitSettings.tsx @@ -285,6 +285,26 @@ export function ConduitSettings() { > + + + { + setModalOpen(false); + router.push("/(app)/intro"); + }} + > + + {t("REPLAY_INTRO_I18N.string")} + + + - + diff --git a/src/components/PrivacyPolicyLink.tsx b/src/components/PrivacyPolicyLink.tsx index 0387c1a..beb95ab 100644 --- a/src/components/PrivacyPolicyLink.tsx +++ b/src/components/PrivacyPolicyLink.tsx @@ -7,7 +7,13 @@ import { PRIVACY_POLICY_URL } from "@/src/constants"; import { sharedStyles as ss } from "@/src/styles"; import { Icon } from "./Icon"; -export function PrivacyPolicyLink({ textStyle }: { textStyle: TextStyle }) { +export function PrivacyPolicyLink({ + containerHeight, + textStyle, +}: { + containerHeight: number; + textStyle: TextStyle; +}) { const { t } = useTranslation(); const style = { @@ -24,8 +30,9 @@ export function PrivacyPolicyLink({ textStyle }: { textStyle: TextStyle }) { ss.absolute, ss.row, ss.justifyCenter, + ss.alignCenter, ss.fullWidth, - { bottom: 0 }, + { bottom: 0, height: containerHeight }, ]} onPress={() => { Linking.openURL(PRIVACY_POLICY_URL); diff --git a/src/components/canvas/Chains.tsx b/src/components/canvas/Chains.tsx new file mode 100644 index 0000000..a4cbcbe --- /dev/null +++ b/src/components/canvas/Chains.tsx @@ -0,0 +1,209 @@ +import { Group, Path, fitbox, rect } from "@shopify/react-native-skia"; +import React from "react"; +import { + Easing, + SharedValue, + cancelAnimation, + useAnimatedReaction, + useDerivedValue, + useSharedValue, + withDelay, + withRepeat, + withSequence, + withTiming, +} from "react-native-reanimated"; + +import { palette } from "@/src/styles"; + +export function Chains({ + size, + sceneWidth, + sceneHeight, + currentView, +}: { + size: number; + sceneWidth: number; + sceneHeight: number; + currentView: SharedValue; +}) { + const chainOriginalWidth = 101; + const chainOriginalHeight = 62; + const chainDestWidth = size; + const chainDestHeight = + (chainDestWidth / chainOriginalWidth) * chainOriginalHeight; + const chainResizeTransform = fitbox( + "contain", + rect(0, 0, chainOriginalWidth, chainOriginalHeight), + rect(0, 0, chainDestWidth, chainDestHeight), + ); + + const breaker = useSharedValue(0); + const opacity = useSharedValue(1); + + const notchOriginalWidth = 20; + const notchOriginalHeight = 15; + const notchDestWidth = size / 6; + const notchDestHeight = + (notchDestWidth / notchOriginalWidth) * notchOriginalHeight; + const notchResizeTransform = fitbox( + "contain", + rect(0, 0, notchOriginalWidth, notchOriginalHeight), + rect(0, 0, notchDestWidth, notchDestHeight), + ); + const notchesOpacity = useSharedValue(0); + const notchesSlider = useSharedValue(0); + + useAnimatedReaction( + () => { + return currentView.value; + }, + (current, previous) => { + if (previous === 0) { + cancelAnimation(breaker); + } + if (previous === 1) { + breaker.value = 0; + opacity.value = 0; + } + if (current === 0) { + breaker.value = withDelay( + 1000, + withRepeat( + withSequence( + withTiming(0.05, { + duration: 300, + easing: Easing.in(Easing.bounce), + }), + withTiming(0, { + duration: 300, + easing: Easing.in(Easing.bounce), + }), + withTiming(0, { duration: 1100 }), + ), + -1, + true, + ), + ); + //breaker.value = withTiming(0); + opacity.value = withTiming(1); + notchesOpacity.value = 0; + notchesSlider.value = 0; + } else if (current === 1) { + breaker.value = withTiming(1, { + duration: 1000, + easing: Easing.in(Easing.bezierFn(0.25, 0.11, 0.9, -0.44)), + }); + opacity.value = withTiming(0, { + duration: 1050, + easing: Easing.circle, + }); + if (previous === 0) { + notchesOpacity.value = withSequence( + withDelay(500, withTiming(1)), + withTiming(0, { duration: 500 }), + ); + notchesSlider.value = withDelay( + 500, + withTiming(1, { duration: 500 }), + ); + } + } + }, + ); + + const animatedTransformL = useDerivedValue(() => { + return [ + { + rotate: -0.42 + breaker.value / 2, + }, + { + skewX: 0.5 * breaker.value, + }, + { + skewY: -0.2 * breaker.value, + }, + ]; + }); + const animatedTransformR = useDerivedValue(() => { + return [ + { + rotate: 0.4 - breaker.value / 2, + }, + { + translateX: size - size / 7 + (breaker.value * size) / 3, + }, + { + translateY: -(size - size / 6.4) + breaker.value * size * 1.3, + }, + ]; + }); + + const animatedTransformNotches = useDerivedValue(() => { + return [ + { translateX: sceneWidth / 2 + 12 }, + { + translateY: + sceneHeight / 5 + (sceneHeight / 5) * notchesSlider.value, + }, + ]; + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/canvas/ConduitConnectionLight.tsx b/src/components/canvas/ConduitConnectionLight.tsx index f945512..c31020a 100644 --- a/src/components/canvas/ConduitConnectionLight.tsx +++ b/src/components/canvas/ConduitConnectionLight.tsx @@ -3,6 +3,7 @@ import { Blur, Circle, Group, + SkPoint, interpolateColors, interpolateVector, vec, @@ -27,14 +28,22 @@ export function ConduitConnectionLight({ active, canvasWidth, orbRadius, - orbCenterY, - psiphonLogoSize, + midPoint, + secondLastPoint, + endPoint, + randomize, + x0init = 0, + y0init = 0, }: { active: boolean; canvasWidth: number; orbRadius: number; - orbCenterY: number; - psiphonLogoSize: number; + midPoint: SkPoint; + secondLastPoint: SkPoint; + endPoint: SkPoint; + randomize: boolean; + x0init?: number; + y0init?: number; }) { // A connection light will be rendered for every connection to the Conduit. // Each light will start at a random position horizontally off-screen, fly @@ -45,11 +54,10 @@ export function ConduitConnectionLight({ const lfo = React.useRef(useSharedValue(-1)); const periodMs = 5000; - const y0 = useSharedValue(0); - const x0 = useSharedValue(0); + const y0 = useSharedValue(y0init); + const x0 = useSharedValue(x0init); // interpolate trajectory between semi-random vectors - const endY = -(orbCenterY - psiphonLogoSize / 2); // land in P logo const trajectory = useDerivedValue(() => { return interpolateVector( lfo.current.value, @@ -60,9 +68,9 @@ export function ConduitConnectionLight({ (x0.value / canvasWidth) * orbRadius, (y0.value / canvasWidth) * orbRadius, ), - vec(0, 0), - vec(0, -orbRadius), - vec(0, endY), + midPoint, + secondLastPoint, + endPoint, ], ); }); @@ -108,7 +116,17 @@ export function ConduitConnectionLight({ if (active) { lfo.current.value = -1; lightOpacity.value = withTiming(1, { duration: 1000 }); - randomizeXYSpin(); + if (randomize) { + randomizeXYSpin(); + } else { + lfo.current.value = withRepeat( + withTiming(1, { + duration: periodMs, + }), + -1, + true, + ); + } } else { lightOpacity.value = withTiming(0, { duration: 1000 }, () => { cancelAnimation(lfo.current); diff --git a/src/components/canvas/FlexibleOrb.tsx b/src/components/canvas/FlexibleOrb.tsx new file mode 100644 index 0000000..17e74cc --- /dev/null +++ b/src/components/canvas/FlexibleOrb.tsx @@ -0,0 +1,184 @@ +import { + Circle, + Group, + Image, + RadialGradient, + Shadow, + useImage, + vec, +} from "@shopify/react-native-skia"; +import { + SharedValue, + cancelAnimation, + useAnimatedReaction, + useDerivedValue, + useSharedValue, + withDelay, + withRepeat, + withSequence, + withSpring, + withTiming, +} from "react-native-reanimated"; + +import { palette } from "@/src/styles"; + +interface FlexibleOrbProps { + currentView: SharedValue; + sceneWidth: number; + sceneHeight: number; +} +export function FlexibleOrb({ + currentView, + sceneHeight, + sceneWidth, +}: FlexibleOrbProps) { + const initialRadius = sceneHeight / 4; + const radius = useSharedValue(initialRadius); + const cx = useSharedValue(sceneWidth); + const cy = sceneHeight / 2; + + const orbsWorldPng = useImage(require("@/assets/images/orbs-world.png")); + const notificationsPng = useImage( + require("@/assets/images/onboarding-permissions.png"), + ); + const backgroundOpacity = useSharedValue(0); + + const privacyPolicyPng = useImage( + require("@/assets/images/onboarding-privacy-policy.png"), + ); + const privacyPolicyOpacity = useSharedValue(0); + + const radialGradientC = useDerivedValue(() => { + return vec(cx.value, cy); + }); + + const notificationY = useDerivedValue(() => { + return cy - radius.value * 1.2; + }); + + useAnimatedReaction( + () => { + return currentView.value; + }, + (current, previous) => { + if (previous === 0) { + cancelAnimation(radius); + } + if (previous === 1) { + radius.value = initialRadius; + } + if (current === 0) { + cx.value = withTiming(sceneWidth * 0.5); + radius.value = withDelay( + 1000, + withRepeat( + withSequence( + withTiming(initialRadius * 1.2, { duration: 300 }), + withSpring(initialRadius, { + duration: 1400, + dampingRatio: 0.3, + stiffness: 100, + restDisplacementThreshold: 0.01, + //restSpeedThreshold: 2, + }), + ), + -1, + false, + ), + ); + } else if (current === 1) { + radius.value = withSpring(sceneHeight / 2.5, { + mass: 5.2, + damping: 10, + stiffness: 100, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }); + cx.value = withDelay( + 500, + withSpring(sceneWidth * 0.6, { + mass: 5.2, + damping: 10, + stiffness: 100, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }), + ); + backgroundOpacity.value = withTiming(0, { duration: 300 }); + } else if (current === 2) { + cx.value = withSpring(sceneWidth * 0.3, { + mass: 3.2, + damping: 10, + stiffness: 100, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }); + radius.value = withSpring(sceneHeight / 3.5, { + mass: 2.2, + damping: 20, + stiffness: 100, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }); + backgroundOpacity.value = withTiming(1, { duration: 1000 }); + privacyPolicyOpacity.value = withTiming(0); + } else if (current === 3) { + backgroundOpacity.value = withTiming(0); + privacyPolicyOpacity.value = withTiming(1, { duration: 1000 }); + } + }, + ); + + return ( + + + + + + + + + + + ); +} diff --git a/src/components/canvas/OnboardingScene.tsx b/src/components/canvas/OnboardingScene.tsx new file mode 100644 index 0000000..49b418b --- /dev/null +++ b/src/components/canvas/OnboardingScene.tsx @@ -0,0 +1,30 @@ +import { Group } from "@shopify/react-native-skia"; +import { SharedValue } from "react-native-reanimated"; + +import { Chains } from "@/src/components/canvas/Chains"; +import { FlexibleOrb } from "@/src/components/canvas/FlexibleOrb"; +import { Phone } from "@/src/components/canvas/Phone"; + +interface OnboardingSceneProps { + currentView: SharedValue; + sceneWidth: number; + sceneHeight: number; +} +export function OnboardingScene({ + currentView, + sceneWidth, + sceneHeight, +}: OnboardingSceneProps) { + const commonProps = { currentView, sceneWidth, sceneHeight }; + return ( + + + + + + + + ); +} diff --git a/src/components/canvas/Phone.tsx b/src/components/canvas/Phone.tsx new file mode 100644 index 0000000..a5a5a11 --- /dev/null +++ b/src/components/canvas/Phone.tsx @@ -0,0 +1,165 @@ +import { palette } from "@/src/styles"; +import { + Fill, + Group, + Image, + LinearGradient, + Path, + Rect, + RoundedRect, + Turbulence, + fitbox, + rect, + useImage, + vec, +} from "@shopify/react-native-skia"; +import React from "react"; +import { + Easing, + cancelAnimation, + useAnimatedReaction, + useSharedValue, + withDelay, + withRepeat, + withTiming, +} from "react-native-reanimated"; +import { SharedValue } from "react-native-reanimated/lib/typescript/Animated"; + +import { ConduitConnectionLight } from "@/src/components/canvas/ConduitConnectionLight"; + +interface PhoneProps { + currentView: SharedValue; + sceneWidth: number; + sceneHeight: number; +} +export function Phone({ currentView, sceneWidth, sceneHeight }: PhoneProps) { + const phoneOriginalWidth = 50; + const phoneOriginalHeight = 85; + const phoneDestWidth = sceneWidth * 0.1; + const phoneDestHeight = + (phoneDestWidth / phoneOriginalWidth) * phoneOriginalHeight; + const phoneResizeTransform = fitbox( + "contain", + rect(0, 0, phoneOriginalWidth, phoneOriginalHeight), + rect(0, 0, phoneDestWidth, phoneDestHeight), + ); + + const psiphonLogoPng = useImage( + require("@/assets/images/psiphon-logo.png"), + ); + + const phoneOpacity = useSharedValue(0); + const turbulenceSeed = useSharedValue(0); + + useAnimatedReaction( + () => { + return currentView.value; + }, + (current, previous) => { + if (previous === 1) { + cancelAnimation(turbulenceSeed); + } + if (current === 0) { + phoneOpacity.value = withTiming(0); + turbulenceSeed.value = 0; + } else if (current === 1) { + phoneOpacity.value = withDelay( + previous === 0 ? 1400 : 0, + withTiming(1, { duration: 800 }), + ); + turbulenceSeed.value = withRepeat( + withTiming(10, { duration: 1000, easing: Easing.steps(5) }), + -1, + true, + ); + } else if (current === 2) { + phoneOpacity.value = withTiming(0, { duration: 500 }); + } + }, + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 990788c..8e0fc45 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -115,7 +115,7 @@ "string": "Welcome to Conduit!" }, "ONBOARDING_WELCOME_BODY_I18N": { - "string": "Turn your phone into an Internet Freedom badass.\n\nParticipate and make a difference around the world by running your own Conduit Station." + "string": "Turn your phone into an Internet Freedom badass.\n\nYou can help people around the world break through restrictive networks and access the global internet securely through the Psiphon Network." }, "ONBOARDING_WELCOME_BUTTON_I18N": { "string": "Next" @@ -124,7 +124,7 @@ "string": "What is Conduit?" }, "ONBOARDING_INFO_1_BODY_I18N": { - "string": "When running a Conduit Station, your phone acts as a beacon of Internet Freedom, bouncing people in censored regions of the world through your phone and into the Psiphon Network proper." + "string": "When you run a Conduit Station, your phone acts as a beacon of Internet Freedom.\n\nPeople in censored regions will bounce through your phone and into the Psiphon Network proper, creating a more open internet." }, "ONBOARDING_INFO_1_BUTTON_I18N": { "string": "Next" @@ -133,21 +133,24 @@ "string": "Allow Notifications" }, "ONBOARDING_PERMISSIONS_BODY_I18N": { - "string": "Conduit recommends you turn on notifications so we can provide critical updates about your Conduit Station.\n\nThese will only appear in your status bar, we have no time for spam." + "string": "Conduit recommends you enable notifications so we can provide critical updates about your Conduit Station.\n\nThese notifications will only appear in your status bar." }, "ONBOARDING_PERMISSIONS_BUTTON_I18N": { "string": "Next" }, "ONBOARDING_ENABLE_NOTIFICATIONS_BUTTON_I18N": { - "string": "Enable Notifications" + "string": "Next" }, "ONBOARDING_PRIVACY_POLICY_HEADER_I18N": { "string": "Privacy Policy" }, "ONBOARDING_PRIVACY_POLICY_BODY_I18N": { - "string": "We do not collect any personally identifiable information (PII) from users who run Conduit Stations.\n\nBy using Conduit, you accept the entire privacy policy, available below." + "string": "Before we begin, some ground rules: We do not collect any personally identifiable information (PII) from users who run Conduit Stations.\n\nBy using Conduit, you accept the entire privacy policy, available below." }, "ONBOARDING_PRIVACY_POLICY_BUTTON_I18N": { - "string": "Accept" + "string": "Accept & Begin" + }, + "REPLAY_INTRO_I18N": { + "string": "Replay Intro" } }