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"
}
}