Skip to content

Commit

Permalink
implement iOS deeplink
Browse files Browse the repository at this point in the history
  • Loading branch information
remicolin committed Oct 21, 2024
1 parent 73de3c0 commit 2395877
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 57 deletions.
8 changes: 7 additions & 1 deletion app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AMPLITUDE_KEY } from '@env';
import * as amplitude from '@amplitude/analytics-react-native';
import useUserStore from './src/stores/userStore';
import { bgWhite } from './src/utils/colors';
import { setupUniversalLinkListener } from './src/utils/qrCode'; // Adjust the import path as needed
global.Buffer = Buffer;

function App(): JSX.Element {
Expand All @@ -29,13 +30,18 @@ function App(): JSX.Element {
useEffect(() => {
setSelectedTab('splash');
}, [setSelectedTab]);

useEffect(() => {
if (AMPLITUDE_KEY) {
amplitude.init(AMPLITUDE_KEY);
}
}, []);

useEffect(() => {
const cleanup = setupUniversalLinkListener();
return cleanup;
}, []);

return (
<YStack f={1} bc={bgWhite} h="100%" w="100%">
<YStack h="100%" w="100%">
Expand Down
4 changes: 3 additions & 1 deletion app/ios/OpenPassport.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerBridge.swift; sourceTree = "<group>"; };
1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = "<group>"; };
1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QRScannerBridge.m; sourceTree = "<group>"; };
169349842CC694DA00166F21 /* OpenPassportDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = OpenPassportDebug.entitlements; path = OpenPassport/OpenPassportDebug.entitlements; sourceTree = "<group>"; };
16E6646D2B8D292500FDD6A0 /* QKMRZScannerViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QKMRZScannerViewRepresentable.swift; sourceTree = "<group>"; };
16E884A42C5BD764003B7125 /* passport.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = passport.json; sourceTree = "<group>"; };
453D60E43CC0F08D884424E7 /* Pods-OpenPassport.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OpenPassport.debug.xcconfig"; path = "Target Support Files/Pods-OpenPassport/Pods-OpenPassport.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -181,6 +182,7 @@
13B07FAE1A68108700A75B9A /* OpenPassport */ = {
isa = PBXGroup;
children = (
169349842CC694DA00166F21 /* OpenPassportDebug.entitlements */,
16E884A42C5BD764003B7125 /* passport.json */,
05EDEDC42B52D25D00AA51AD /* Prover.m */,
05EDEDC52B52D25D00AA51AD /* Prover.swift */,
Expand Down Expand Up @@ -475,7 +477,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 63;
Expand Down
13 changes: 11 additions & 2 deletions app/ios/OpenPassport/AppDelegate.mm
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTBridge.h>
#import <React/RCTLinkingManager.h>

@implementation AppDelegate

Expand All @@ -25,4 +25,13 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
#endif
}

@end
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}

@end
9 changes: 9 additions & 0 deletions app/ios/OpenPassport/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,14 @@
<string>A0000002472001</string>
<string>00000000000000</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>proofofpassport</string>
</array>
</dict>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions app/ios/OpenPassport/OpenPassport.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<string>appclips:openpassport.app</string>
<string>appclips:staging.openpassport.app</string>
<string>appclips:appclip.openpassport.app</string>
<string>applinks:proofofpassport-merkle-tree.xyz</string>
</array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
Expand Down
23 changes: 23 additions & 0 deletions app/ios/OpenPassport/OpenPassportDebug.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-appclip-app-identifiers</key>
<array>
<string>5B29R5LYHQ.com.warroom.proofofpassport.Clip</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>appclips:openpassport.app</string>
<string>appclips:staging.openpassport.app</string>
<string>appclips:appclip.openpassport.app</string>
<string>applinks:proofofpassport-merkle-tree.xyz</string>
</array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
162 changes: 110 additions & 52 deletions app/src/utils/qrCode.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,152 @@
import { NativeModules, Platform } from "react-native";
import { NativeModules, Platform, Linking } from "react-native";
// import { AppType, reconstructAppType } from "../../../common/src/utils/appType";
import useNavigationStore from '../stores/navigationStore';
import { getCircuitName, parseDSC } from "../../../common/src/utils/certificates/handleCertificate";
import useUserStore from "../stores/userStore";
import { downloadZkey } from "./zkeyDownload";
import msgpack from "msgpack-lite";
import pako from "pako";
import { OpenPassportApp } from "../../../common/src/utils/appType";
import { Mode, OpenPassportApp } from "../../../common/src/utils/appType";

const parseUrlParams = (url: string): Map<string, string> => {
const [, queryString] = url.split('?');
const params = new Map<string, string>();
if (queryString) {
queryString.split('&').forEach(pair => {
const [key, value] = pair.split('=');
params.set(key, decodeURIComponent(value));
});
}
return params;
};

export const scanQRCode = () => {
const { toast, setSelectedApp, setSelectedTab } = useNavigationStore.getState();

if (Platform.OS === 'ios') {
if (NativeModules.QRScannerBridge && NativeModules.QRScannerBridge.scanQRCode) {
NativeModules.QRScannerBridge.scanQRCode()
.then((result: string) => {
handleQRCodeScan(result, toast, setSelectedApp, setSelectedTab);
})
.catch((error: any) => {
console.error('QR Scanner Error:', error);
Linking.getInitialURL().then((url) => {
if (url) {
handleUniversalLink(url);
} else {
if (Platform.OS === 'ios') {
console.log("Scanning QR code on iOS without Universal Link");

const qrScanner = NativeModules.QRScannerBridge;
if (qrScanner && qrScanner.scanQRCode) {
qrScanner.scanQRCode()
.then((result: string) => {
const params = parseUrlParams(result);
const encodedData = params.get('data');
handleQRCodeScan(encodedData as string, toast, setSelectedApp, setSelectedTab);
})
.catch((error: any) => {
console.error('QR Scanner Error:', error);
toast.show('Error', {
message: 'Failed to scan QR code',
type: 'error',
});
});
} else {
console.error('QR Scanner module not found for iOS');
toast.show('Error', {
message: 'Failed to scan QR code',
message: 'QR Scanner not available',
type: 'error',
});
});
} else {
console.error('QR Scanner module not found for iOS');
toast.show('Error', {
message: 'QR Scanner not available',
type: 'error',
});
}
} else if (Platform.OS === 'android') {
if (NativeModules.QRCodeScanner && NativeModules.QRCodeScanner.scanQRCode) {
NativeModules.QRCodeScanner.scanQRCode()
.then((result: string) => {
handleQRCodeScan(result, toast, setSelectedApp, setSelectedTab);
})
.catch((error: any) => {
console.error('QR Scanner Error:', error);
}
} else if (Platform.OS === 'android') {
const qrScanner = NativeModules.QRCodeScanner;
if (qrScanner && qrScanner.scanQRCode) {
qrScanner.scanQRCode()
.then((result: string) => {
handleQRCodeScan(result, toast, setSelectedApp, setSelectedTab);
})
.catch((error: any) => {
console.error('QR Scanner Error:', error);
toast.show('Error', {
message: 'Failed to scan QR code',
type: 'error',
});
});
} else {
console.error('QR Scanner module not found for Android');
toast.show('Error', {
message: 'Failed to scan QR code',
message: 'QR Scanner not available',
type: 'error',
});
});
} else {
console.error('QR Scanner module not found for Android');
toast.show('Error', {
message: 'QR Scanner not available',
type: 'error',
});
}
}
}
}
}).catch(err => {
console.error('An error occurred while getting initial URL', err);
toast.show('Error', {
message: 'Failed to process initial link',
type: 'error',
});
});
};

const handleQRCodeScan = (result: string, toast: any, setSelectedApp: any, setSelectedTab: any) => {
try {
console.log(result);
const decodedResult = atob(result);
const uint8Array = new Uint8Array(decodedResult.split('').map(char => char.charCodeAt(0)));
const decompressedData = pako.inflate(uint8Array);
const unpackedData = msgpack.decode(decompressedData);
console.log(unpackedData);
const openPassportApp: OpenPassportApp = unpackedData;
setSelectedApp(openPassportApp);

const dsc = useUserStore.getState().passportData.dsc;
const sigAlgName = parseDSC(dsc!);
if (openPassportApp.mode == 'vc_and_disclose') {
downloadZkey('vc_and_disclose');
}
else {
const circuitName = getCircuitName("prove", sigAlgName.signatureAlgorithm, sigAlgName.hashFunction);
downloadZkey(circuitName as any);
}

const circuitName = openPassportApp.mode === 'vc_and_disclose'
? 'vc_and_disclose'
: getCircuitName("prove" as Mode, sigAlgName.signatureAlgorithm, sigAlgName.hashFunction);
downloadZkey(circuitName as any);

setSelectedTab("prove");
toast.show('✅', {
message: "QR code scanned",
customData: {
type: "success",
},
})


});
} catch (error) {
console.error('Error parsing QR code result:', error);
toast.show('Try again', {
message: "Error reading QR code",
message: "Error reading QR code: " + (error as Error).message,
customData: {
type: "info",
type: "error",
},
})
});
}
};

const handleUniversalLink = (url: string) => {
const { toast, setSelectedApp, setSelectedTab } = useNavigationStore.getState();
const params = parseUrlParams(url);
const encodedData = params.get('data');
console.log("Encoded data:", encodedData);
if (encodedData) {
handleQRCodeScan(encodedData, toast, setSelectedApp, setSelectedTab);
} else {
console.error('No data found in the Universal Link');
toast.show('Error', {
message: 'Invalid link',
type: 'error',
});
}
};
};

export const setupUniversalLinkListener = () => {
Linking.getInitialURL().then((url) => {
if (url) {
handleUniversalLink(url);
}
});

const linkingEventListener = Linking.addEventListener('url', ({ url }) => {
handleUniversalLink(url);
});

return () => {
linkingEventListener.remove();
};
};
9 changes: 8 additions & 1 deletion sdk/src/QRcode/OpenPassportQRcode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ const OpenPassportQRcode: React.FC<OpenPassportQRcodeProps> = ({
);
}, [sessionId, websocketUrl]);

const generateUniversalLink = () => {
const baseUrl = 'https://proofofpassport-merkle-tree.xyz';
const path = '/open-passport';
const data = openPassportVerifier.getIntent(appName, userId, userIdType, sessionId);
return `${baseUrl}${path}?data=${data}`;
};

const renderProofStatus = () => (
<div style={containerStyle}>
<div style={ledContainerStyle}>
Expand Down Expand Up @@ -89,7 +96,7 @@ const OpenPassportQRcode: React.FC<OpenPassportQRcodeProps> = ({
default:
return (
<QRCodeSVG
value={openPassportVerifier.getIntent(appName, userId, userIdType, sessionId)}
value={generateUniversalLink()}
size={size}
/>
);
Expand Down

0 comments on commit 2395877

Please sign in to comment.