mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
chore: add redirect.self.xyz to app link domains (#415)
* chore: add redirect.self.xyz to app link domains * chore: add redirect.self.xyz to android manifest * add self in the universal link path * set gestureEnabled false on Launch screen * improve ProveScreen UX * improve ProveScreen UX * implement deeplinking in mobile app and sdk * start app listener in ViewFinder * support WS connexion with complex qrcodes * remove /self path from android deeplink support --------- Co-authored-by: turboblitz <florent.tavernier@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f501bf47a4
commit
36a29c9a21
@@ -26,12 +26,14 @@ import ConfirmBelongingScreen from './screens/Onboarding/ConfirmBelongingScreen'
|
||||
import LoadingScreen from './screens/Onboarding/LoadingScreen';
|
||||
import PassportCameraScreen from './screens/Onboarding/PassportCameraScreen';
|
||||
import PassportCameraTrouble from './screens/Onboarding/PassportCameraTrouble';
|
||||
import PassportDataNotFound from './screens/Onboarding/PassportDataNotFound';
|
||||
import PassportNFCScanScreen from './screens/Onboarding/PassportNFCScanScreen';
|
||||
import PassportNFCTrouble from './screens/Onboarding/PassportNFCTrouble';
|
||||
import PassportOnboardingScreen from './screens/Onboarding/PassportOnboardingScreen';
|
||||
import UnsupportedPassportScreen from './screens/Onboarding/UnsupportedPassport';
|
||||
import ProofRequestStatusScreen from './screens/ProveFlow/ProofRequestStatusScreen';
|
||||
import ProveScreen from './screens/ProveFlow/ProveScreen';
|
||||
import QRCodeTroubleScreen from './screens/ProveFlow/QRCodeTrouble';
|
||||
import QRCodeViewFinderScreen from './screens/ProveFlow/ViewFinder';
|
||||
import CloudBackupScreen from './screens/Settings/CloudBackupScreen';
|
||||
import DevSettingsScreen from './screens/Settings/DevSettingsScreen';
|
||||
@@ -40,6 +42,7 @@ import PassportDataInfoScreen from './screens/Settings/PassportDataInfoScreen';
|
||||
import ShowRecoveryPhraseScreen from './screens/Settings/ShowRecoveryPhraseScreen';
|
||||
import SettingsScreen from './screens/SettingsScreen';
|
||||
import SplashScreen from './screens/SplashScreen';
|
||||
import { ProofProvider } from './stores/proofProvider';
|
||||
import analytics from './utils/analytics';
|
||||
import { black, slate300, white } from './utils/colors';
|
||||
|
||||
@@ -68,6 +71,7 @@ const AppNavigation = createNativeStackNavigator({
|
||||
screen: LaunchScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
gestureEnabled: false,
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
@@ -179,6 +183,23 @@ const AppNavigation = createNativeStackNavigator({
|
||||
// presentation: 'modal',
|
||||
},
|
||||
},
|
||||
QRCodeTrouble: {
|
||||
screen: QRCodeTroubleScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
animation: 'slide_from_bottom',
|
||||
presentation: 'modal',
|
||||
},
|
||||
},
|
||||
PassportDataNotFound: {
|
||||
screen: PassportDataNotFound,
|
||||
options: {
|
||||
headerShown: false,
|
||||
gestureEnabled: false,
|
||||
animation: 'slide_from_bottom',
|
||||
// presentation: 'modal',
|
||||
},
|
||||
},
|
||||
ProveScreen: {
|
||||
screen: ProveScreen,
|
||||
options: {
|
||||
@@ -330,7 +351,9 @@ const NavigationWithTracking = () => {
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView>
|
||||
<Navigation ref={navigationRef} onStateChange={trackScreen} />
|
||||
<ProofProvider>
|
||||
<Navigation ref={navigationRef} onStateChange={trackScreen} />
|
||||
</ProofProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PrimaryButton } from './PrimaryButton';
|
||||
|
||||
type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`;
|
||||
|
||||
const ACTION_TIMER = 1000; // time in ms
|
||||
const ACTION_TIMER = 600; // time in ms
|
||||
//slate400 to slate800 but in rgb
|
||||
const COLORS: RGBA[] = ['rgba(30, 41, 59, 0.3)', 'rgba(30, 41, 59, 1)'];
|
||||
export function HeldPrimaryButton({
|
||||
@@ -33,12 +33,13 @@ export function HeldPrimaryButton({
|
||||
};
|
||||
|
||||
const onPressOut = () => {
|
||||
setHasTriggered(false);
|
||||
Animated.timing(animation, {
|
||||
toValue: 0,
|
||||
duration: ACTION_TIMER,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
if (!hasTriggered) {
|
||||
Animated.timing(animation, {
|
||||
toValue: 0,
|
||||
duration: ACTION_TIMER,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonSize = (e: LayoutChangeEvent) => {
|
||||
|
||||
34
app/src/screens/Onboarding/PassportDataNotFound.tsx
Normal file
34
app/src/screens/Onboarding/PassportDataNotFound.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
|
||||
import Description from '../../components/typography/Description';
|
||||
import { Title } from '../../components/typography/Title';
|
||||
import useHapticNavigation from '../../hooks/useHapticNavigation';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { black, slate200, white } from '../../utils/colors';
|
||||
|
||||
const PassportDataNotFound: React.FC = () => {
|
||||
const onPress = useHapticNavigation('Launch');
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black}>
|
||||
<Title textAlign="center" style={{ color: white }}>
|
||||
✨ Are you new here?
|
||||
</Title>
|
||||
<Description mt={8} textAlign="center" style={{ color: slate200 }}>
|
||||
It seems like you need to go through the registration flow first.
|
||||
</Description>
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
gap={20}
|
||||
height={150}
|
||||
backgroundColor={white}
|
||||
>
|
||||
<PrimaryButton onPress={onPress}>Go to Registration</PrimaryButton>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default PassportDataNotFound;
|
||||
@@ -83,12 +83,21 @@ const ProveScreen: React.FC = () => {
|
||||
return (selectedApp?.disclosures as SelfAppDisclosureConfig) || [];
|
||||
}, [selectedApp?.disclosures]);
|
||||
|
||||
// Format the base64 image string correctly
|
||||
// Format the logo source based on whether it's a URL or base64 string
|
||||
const logoSource = useMemo(() => {
|
||||
if (!selectedApp?.logoBase64) {
|
||||
return null;
|
||||
}
|
||||
// Ensure the base64 string has the correct data URI prefix
|
||||
|
||||
// Check if the logo is already a URL
|
||||
if (
|
||||
selectedApp.logoBase64.startsWith('http://') ||
|
||||
selectedApp.logoBase64.startsWith('https://')
|
||||
) {
|
||||
return { uri: selectedApp.logoBase64 };
|
||||
}
|
||||
|
||||
// Otherwise handle as base64 as before
|
||||
const base64String = selectedApp.logoBase64.startsWith('data:image')
|
||||
? selectedApp.logoBase64
|
||||
: `data:image/png;base64,${selectedApp.logoBase64}`;
|
||||
@@ -122,11 +131,14 @@ const ProveScreen: React.FC = () => {
|
||||
|
||||
timeToNavigateToStatusScreen = setTimeout(() => {
|
||||
navigate('ProofRequestStatusScreen');
|
||||
}, 1000);
|
||||
}, 200);
|
||||
|
||||
if (!passportData || !secret) {
|
||||
console.log('No passport data or secret');
|
||||
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
|
||||
setTimeout(() => {
|
||||
navigate('PassportDataNotFound');
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,6 +198,7 @@ const ProveScreen: React.FC = () => {
|
||||
contentSize.height - paddingToBottom;
|
||||
if (isCloseToBottom && !hasScrolledToBottom) {
|
||||
setHasScrolledToBottom(true);
|
||||
buttonTap();
|
||||
}
|
||||
},
|
||||
[hasScrolledToBottom, isContentShorterThanScrollView],
|
||||
@@ -221,7 +234,13 @@ const ProveScreen: React.FC = () => {
|
||||
) : (
|
||||
<YStack alignItems="center" justifyContent="center">
|
||||
{logoSource && (
|
||||
<Image mb={20} source={logoSource} width={100} height={100} />
|
||||
<Image
|
||||
mb={20}
|
||||
source={logoSource}
|
||||
width={100}
|
||||
height={100}
|
||||
objectFit="contain"
|
||||
/>
|
||||
)}
|
||||
<BodyText fontSize={12} color={slate300} mb={20}>
|
||||
{url}
|
||||
|
||||
55
app/src/screens/ProveFlow/QRCodeTrouble.tsx
Normal file
55
app/src/screens/ProveFlow/QRCodeTrouble.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import Tips, { TipProps } from '../../components/Tips';
|
||||
import { Caption } from '../../components/typography/Caption';
|
||||
import useHapticNavigation from '../../hooks/useHapticNavigation';
|
||||
import SimpleScrolledTitleLayout from '../../layouts/SimpleScrolledTitleLayout';
|
||||
import { slate500 } from '../../utils/colors';
|
||||
|
||||
const tips: TipProps[] = [
|
||||
{
|
||||
title: 'Ensure Valid QR Code',
|
||||
body: "Make sure you're scanning a QR code from a supported Self partner application.",
|
||||
},
|
||||
{
|
||||
title: 'Try Different Distances',
|
||||
body: 'If scanning fails, try moving your device closer to the QR code or increase the size of the QR code on the screen.',
|
||||
},
|
||||
{
|
||||
title: 'Proper Lighting',
|
||||
body: "Ensure there's adequate lighting in the room. QR codes need good contrast to be read properly.",
|
||||
},
|
||||
{
|
||||
title: 'Hold Steady',
|
||||
body: 'Keep your device steady while scanning to prevent blurry images that might not scan correctly.',
|
||||
},
|
||||
{
|
||||
title: 'Clean Lens',
|
||||
body: 'Make sure your camera lens is clean and free of smudges or debris that could interfere with scanning.',
|
||||
},
|
||||
];
|
||||
|
||||
const tipsDeeplink: TipProps[] = [
|
||||
{
|
||||
title: 'Coming from another app/website?',
|
||||
body: 'Please contact the support, a telegram group is available in the options menu.',
|
||||
},
|
||||
];
|
||||
|
||||
const QRCodeTrouble: React.FC = () => {
|
||||
const go = useHapticNavigation('Home', { action: 'cancel' });
|
||||
return (
|
||||
<SimpleScrolledTitleLayout
|
||||
title="Having trouble scanning the QR code?"
|
||||
onDismiss={go}
|
||||
>
|
||||
<Caption size="large" color={slate500}>
|
||||
Here are some tips to help you successfully scan the QR code:
|
||||
</Caption>
|
||||
<Tips items={tips} />
|
||||
<Tips items={tipsDeeplink} />
|
||||
</SimpleScrolledTitleLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRCodeTrouble;
|
||||
@@ -49,6 +49,8 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
|
||||
const { setSelectedApp, cleanSelfApp } = useProofInfo();
|
||||
const [doneScanningQR, setDoneScanningQR] = useState(false);
|
||||
const { startAppListener } = useApp();
|
||||
const navigateToProveScreen = useHapticNavigation('ProveScreen');
|
||||
const onCancelPress = useHapticNavigation('Home');
|
||||
|
||||
// This resets to the default state when we navigate back to this screen
|
||||
useFocusEffect(
|
||||
@@ -64,26 +66,31 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
navigation.navigate('QRCodeTrouble');
|
||||
} else {
|
||||
setDoneScanningQR(true);
|
||||
const encodedData = parseUrlParams(uri!);
|
||||
const sessionId = encodedData.get('sessionId');
|
||||
if (!sessionId) {
|
||||
console.error('No sessionId found in QR code');
|
||||
const selfApp = encodedData.get('selfApp');
|
||||
if (selfApp) {
|
||||
const selfAppJson = JSON.parse(selfApp);
|
||||
setSelectedApp(selfAppJson);
|
||||
startAppListener(selfAppJson.sessionId, setSelectedApp);
|
||||
setTimeout(() => {
|
||||
navigateToProveScreen();
|
||||
}, 100);
|
||||
} else if (sessionId) {
|
||||
cleanSelfApp();
|
||||
startAppListener(sessionId, setSelectedApp);
|
||||
setTimeout(() => {
|
||||
navigateToProveScreen();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('No sessionId or selfApp found in QR code');
|
||||
setDoneScanningQR(false); // Reset to allow another scan attempt
|
||||
navigation.navigate('QRCodeTrouble');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO (_): cleaning here makes sense, clean app should set the disclosure states to default too
|
||||
// Clean up first
|
||||
cleanSelfApp();
|
||||
|
||||
// Start the app listener and wait a moment for the connection
|
||||
startAppListener(sessionId, setSelectedApp);
|
||||
|
||||
// Small delay to ensure the websocket connection is established
|
||||
setTimeout(() => {
|
||||
navigation.navigate('ProveScreen');
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -92,9 +99,9 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
|
||||
startAppListener,
|
||||
cleanSelfApp,
|
||||
setSelectedApp,
|
||||
navigateToProveScreen,
|
||||
],
|
||||
);
|
||||
const onCancelPress = useHapticNavigation('Home', { action: 'cancel' });
|
||||
|
||||
const shouldRenderCamera = !connectionModalVisible && !doneScanningQR;
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import React, {
|
||||
} from 'react';
|
||||
|
||||
import { SelfApp } from '../../../common/src/utils/appType';
|
||||
import { navigationRef } from '../Navigation';
|
||||
import { useApp } from '../stores/appProvider';
|
||||
import { setupUniversalLinkListener } from '../utils/qrCodeNew';
|
||||
import { usePassport } from './passportDataProvider';
|
||||
|
||||
@@ -73,6 +75,8 @@ export function ProofProvider({ children }: PropsWithChildren<{}>) {
|
||||
defaults.selectedApp,
|
||||
);
|
||||
|
||||
const { startAppListener } = useApp();
|
||||
|
||||
const setSelectedApp = useCallback((app: SelfApp) => {
|
||||
if (!app || Object.keys(app).length === 0) {
|
||||
return;
|
||||
@@ -92,6 +96,24 @@ export function ProofProvider({ children }: PropsWithChildren<{}>) {
|
||||
setDisclosureStatus(ProofStatusEnum.PENDING);
|
||||
}, []);
|
||||
|
||||
const handleNavigateToProveScreen = useCallback(() => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('ProveScreen');
|
||||
} else {
|
||||
console.log("Navigation not ready yet, couldn't navigate to ProveScreen");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigateToQRCodeTrouble = useCallback(() => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.navigate('QRCodeTrouble');
|
||||
} else {
|
||||
console.log(
|
||||
"Navigation not ready yet, couldn't navigate to QRCodeTrouble",
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
globalSetRegistrationStatus = setRegistrationStatus;
|
||||
globalSetDisclosureStatus = setDisclosureStatus;
|
||||
@@ -103,12 +125,24 @@ export function ProofProvider({ children }: PropsWithChildren<{}>) {
|
||||
|
||||
useEffect(() => {
|
||||
if (passportData && secret) {
|
||||
const universalLinkCleanup = setupUniversalLinkListener(setSelectedApp);
|
||||
const universalLinkCleanup = setupUniversalLinkListener(
|
||||
setSelectedApp,
|
||||
cleanSelfApp,
|
||||
startAppListener,
|
||||
handleNavigateToProveScreen,
|
||||
handleNavigateToQRCodeTrouble,
|
||||
);
|
||||
return () => {
|
||||
universalLinkCleanup();
|
||||
};
|
||||
}
|
||||
}, [passportData, secret, setSelectedApp]);
|
||||
}, [
|
||||
setSelectedApp,
|
||||
cleanSelfApp,
|
||||
startAppListener,
|
||||
handleNavigateToProveScreen,
|
||||
handleNavigateToQRCodeTrouble,
|
||||
]);
|
||||
|
||||
const publicApi: IProofContext = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,60 +1,101 @@
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { decode } from 'msgpack-lite';
|
||||
import { inflate } from 'pako';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { SelfApp } from '../../../common/src/utils/appType';
|
||||
|
||||
export default async function handleQRCodeScan(
|
||||
result: string,
|
||||
setApp: (app: SelfApp) => void,
|
||||
) {
|
||||
/**
|
||||
* Decodes a URL-encoded string.
|
||||
* @param {string} encodedUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
const decodeUrl = (encodedUrl: string): string => {
|
||||
try {
|
||||
const decodedResult = atob(result);
|
||||
const uint8Array = new Uint8Array(
|
||||
decodedResult.split('').map(char => char.charCodeAt(0)),
|
||||
);
|
||||
const decompressedData = inflate(uint8Array);
|
||||
const unpackedData = decode(decompressedData);
|
||||
const openPassportApp: SelfApp = unpackedData;
|
||||
|
||||
setApp(openPassportApp);
|
||||
console.log('✅', {
|
||||
message: 'QR code scanned',
|
||||
customData: {
|
||||
type: 'success',
|
||||
},
|
||||
});
|
||||
return decodeURIComponent(encodedUrl);
|
||||
} catch (error) {
|
||||
console.error('Error parsing QR code result:', error);
|
||||
console.log('Try again', {
|
||||
message: 'Error reading QR code: ' + (error as Error).message,
|
||||
customData: {
|
||||
type: 'error',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleUniversalLink = (url: string, setApp: (app: SelfApp) => void) => {
|
||||
const encodedData = new URL(url).searchParams.get('data');
|
||||
console.log('Encoded data:', encodedData);
|
||||
if (encodedData) {
|
||||
handleQRCodeScan(encodedData, setApp);
|
||||
} else {
|
||||
console.error('No data found in the Universal Link');
|
||||
console.error('Error decoding URL:', error);
|
||||
return encodedUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const setupUniversalLinkListener = (setApp: (app: SelfApp) => void) => {
|
||||
const handleQRCodeData = (
|
||||
uri: string,
|
||||
setApp: (app: SelfApp) => void,
|
||||
cleanSelfApp: () => void,
|
||||
startAppListener: (sessionId: string, setApp: (app: SelfApp) => void) => void,
|
||||
onNavigationNeeded?: () => void,
|
||||
onErrorCallback?: () => void,
|
||||
) => {
|
||||
const decodedUri = decodeUrl(uri);
|
||||
const encodedData = queryString.parseUrl(decodedUri).query;
|
||||
const sessionId = encodedData.sessionId;
|
||||
const selfAppStr = encodedData.selfApp as string | undefined;
|
||||
|
||||
if (selfAppStr) {
|
||||
try {
|
||||
const selfAppJson = JSON.parse(selfAppStr);
|
||||
setApp(selfAppJson);
|
||||
|
||||
if (onNavigationNeeded) {
|
||||
setTimeout(() => {
|
||||
onNavigationNeeded();
|
||||
}, 100);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error parsing selfApp:', error);
|
||||
if (onErrorCallback) {
|
||||
onErrorCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionId && typeof sessionId === 'string') {
|
||||
cleanSelfApp();
|
||||
startAppListener(sessionId, setApp);
|
||||
|
||||
if (onNavigationNeeded) {
|
||||
setTimeout(() => {
|
||||
onNavigationNeeded();
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
console.error('No sessionId or selfApp found in the data');
|
||||
if (onErrorCallback) {
|
||||
onErrorCallback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setupUniversalLinkListener = (
|
||||
setApp: (app: SelfApp) => void,
|
||||
cleanSelfApp: () => void,
|
||||
startAppListener: (sessionId: string, setApp: (app: SelfApp) => void) => void,
|
||||
onNavigationNeeded?: () => void,
|
||||
onErrorCallback?: () => void,
|
||||
) => {
|
||||
Linking.getInitialURL().then(url => {
|
||||
if (url) {
|
||||
handleUniversalLink(url, setApp);
|
||||
handleQRCodeData(
|
||||
url,
|
||||
setApp,
|
||||
cleanSelfApp,
|
||||
startAppListener,
|
||||
onNavigationNeeded,
|
||||
onErrorCallback,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const linkingEventListener = Linking.addEventListener('url', ({ url }) => {
|
||||
handleUniversalLink(url, setApp);
|
||||
handleQRCodeData(
|
||||
url,
|
||||
setApp,
|
||||
cleanSelfApp,
|
||||
startAppListener,
|
||||
onNavigationNeeded,
|
||||
onErrorCallback,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user