Prepares app for Euclid (#1473)

* setup IS_EUCLID build variable for conditionally using euclid desgins
create a headless header that only handles the status bar, for the new screens since they manage their own
make sure new screens get proper insets
add recoveryphrase 3.0
fix country picker



* this lint runs twice. once in repo wide lint and once here. so lets just run once to save resources



Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Aaron DeRuvo
2025-12-09 12:56:05 +01:00
committed by GitHub
parent 8587182778
commit fc82b6b2b3
21 changed files with 273 additions and 37 deletions

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { SystemBars } from 'react-native-edge-to-edge';
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
export const HeadlessNavForEuclid = (props: NativeStackHeaderProps) => {
return (
<>
<SystemBars
style={props.options.statusBarStyle}
hidden={props.options.statusBarHidden}
/>
</>
);
};

View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform } from 'react-native';
import { useSafeAreaInsets as useSafeAreaInsetsOriginal } from 'react-native-safe-area-context';
// gives bare minimums in case safe area doesnt provide for example space for status bar icons.
export function useSafeAreaInsets() {
const insets = useSafeAreaInsetsOriginal();
const minimum = Platform.select({ ios: 54, android: 26, web: 48 });
return {
...insets,
top: Math.max(insets.top, minimum || 0),
};
}

View File

@@ -10,6 +10,7 @@ import {
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen';
@@ -17,6 +18,7 @@ import RecoverWithPhraseScreen from '@/screens/account/recovery/RecoverWithPhras
import CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
import SettingsScreen from '@/screens/account/settings/SettingsScreen';
import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen';
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
const accountScreens = {
AccountRecovery: {
@@ -79,14 +81,22 @@ const accountScreens = {
screens: {},
},
},
ShowRecoveryPhrase: {
screen: ShowRecoveryPhraseScreen,
options: {
title: 'Recovery Phrase',
headerStyle: {
backgroundColor: white,
},
} as NativeStackNavigationOptions,
options: IS_EUCLID_ENABLED
? ({
headerShown: true,
header: HeadlessNavForEuclid,
statusBarStyle: ShowRecoveryPhraseScreen.statusBarStyle,
statusBarHidden: ShowRecoveryPhraseScreen.statusBarHidden,
} as NativeStackNavigationOptions)
: ({
title: 'Recovery Phrase',
headerStyle: {
backgroundColor: white,
},
} as NativeStackNavigationOptions),
},
};

View File

@@ -7,6 +7,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar';
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen';
import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen';
import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen';
@@ -22,6 +23,7 @@ import ConfirmBelongingScreen from '@/screens/documents/selection/ConfirmBelongi
import CountryPickerScreen from '@/screens/documents/selection/CountryPickerScreen';
import DocumentOnboardingScreen from '@/screens/documents/selection/DocumentOnboardingScreen';
import IDPickerScreen from '@/screens/documents/selection/IDPickerScreen';
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
const documentsScreens = {
DocumentCamera: {
@@ -75,9 +77,16 @@ const documentsScreens = {
},
CountryPicker: {
screen: CountryPickerScreen,
options: {
headerShown: false,
} as NativeStackNavigationOptions,
options: IS_EUCLID_ENABLED
? ({
header: HeadlessNavForEuclid,
statusBarHidden: CountryPickerScreen.statusBar?.hidden,
statusBarStyle: CountryPickerScreen.statusBar?.style,
headerShown: true,
} as NativeStackNavigationOptions)
: {
headerShown: false,
},
},
IDPicker: {
screen: IDPickerScreen,

View File

@@ -2,21 +2,85 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import Clipboard from '@react-native-clipboard/clipboard';
import type { RecoveryPhraseVariant } from '@selfxyz/euclid';
import { RecoveryPhraseScreen } from '@selfxyz/euclid';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { Description } from '@selfxyz/mobile-sdk-alpha/components';
import Mnemonic from '@/components/Mnemonic';
import useMnemonic from '@/hooks/useMnemonic';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { useSettingStore } from '@/stores/settingStore';
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
const ShowRecoveryPhraseScreen: React.FC = () => {
function useCopyRecoveryPhrase(mnemonic: string[] | undefined) {
const [copied, setCopied] = React.useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const onCopy = useCallback(() => {
if (!mnemonic) return;
Clipboard.setString(mnemonic.join(' '));
setCopied(true);
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout and store its ID
timeoutRef.current = setTimeout(() => setCopied(false), 2500);
}, [mnemonic]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return { copied, onCopy };
}
const ShowRecoveryPhraseScreen: React.FC & {
statusBarStyle: string;
statusBarHidden: boolean;
} = () => {
const { mnemonic, loadMnemonic } = useMnemonic();
const self = useSelfClient();
const { copied, onCopy } = useCopyRecoveryPhrase(mnemonic);
const { setHasViewedRecoveryPhrase } = useSettingStore();
const onRevealWords = useCallback(async () => {
const onReveal = useCallback(async () => {
await loadMnemonic();
}, [loadMnemonic]);
setHasViewedRecoveryPhrase(true);
}, [loadMnemonic, setHasViewedRecoveryPhrase]);
const insets = useSafeAreaInsets();
if (IS_EUCLID_ENABLED) {
const variant: RecoveryPhraseVariant = !mnemonic
? 'hidden'
: copied
? 'copied'
: 'revealed';
return (
<>
<RecoveryPhraseScreen
insets={insets}
onReveal={onReveal}
words={mnemonic}
onBack={self.goBack}
variant={variant}
onCopy={onCopy}
/>
</>
);
}
return (
<ExpandableBottomLayout.Layout backgroundColor="white">
<ExpandableBottomLayout.BottomSection
@@ -24,7 +88,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
justifyContent="center"
gap={20}
>
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} />
<Mnemonic words={mnemonic} onRevealWords={loadMnemonic} />
<Description>
This phrase is the only way to recover your account. Keep it secret,
keep it safe.
@@ -35,3 +99,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
};
export default ShowRecoveryPhraseScreen;
ShowRecoveryPhraseScreen.statusBarHidden =
RecoveryPhraseScreen.statusBar.hidden;
ShowRecoveryPhraseScreen.statusBarStyle = RecoveryPhraseScreen.statusBar.style;

View File

@@ -2,8 +2,21 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type React from 'react';
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
export default function CountryPickerScreen() {
return <SDKCountryPickerScreen />;
}
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
type CountryPickerScreenComponent = React.FC & {
statusBar: typeof SDKCountryPickerScreen.statusBar;
};
const CountryPickerScreen: CountryPickerScreenComponent = () => {
const insets = useSafeAreaInsets();
return <SDKCountryPickerScreen insets={insets} />;
};
CountryPickerScreen.statusBar = SDKCountryPickerScreen.statusBar;
export default CountryPickerScreen;

View File

@@ -8,3 +8,4 @@
* Use this constant instead of checking __DEV__ directly throughout the codebase.
*/
export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__;
export const IS_EUCLID_ENABLED = IS_DEV_MODE; // just in case we forgot to turn it off before pushing to prod.