From 23958772201afa40c0a4c5190b4dab47c7126d42 Mon Sep 17 00:00:00 2001 From: turnoffthiscomputer Date: Mon, 21 Oct 2024 18:38:06 +0200 Subject: [PATCH] implement iOS deeplink --- app/App.tsx | 8 +- .../OpenPassport.xcodeproj/project.pbxproj | 4 +- app/ios/OpenPassport/AppDelegate.mm | 13 +- app/ios/OpenPassport/Info.plist | 9 + .../OpenPassport/OpenPassport.entitlements | 1 + .../OpenPassportDebug.entitlements | 23 +++ app/src/utils/qrCode.ts | 162 ++++++++++++------ sdk/src/QRcode/OpenPassportQRcode.tsx | 9 +- 8 files changed, 172 insertions(+), 57 deletions(-) create mode 100644 app/ios/OpenPassport/OpenPassportDebug.entitlements diff --git a/app/App.tsx b/app/App.tsx index c71be00c..8a6edf70 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -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 { @@ -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 ( diff --git a/app/ios/OpenPassport.xcodeproj/project.pbxproj b/app/ios/OpenPassport.xcodeproj/project.pbxproj index 9c57b98e..354bbb20 100644 --- a/app/ios/OpenPassport.xcodeproj/project.pbxproj +++ b/app/ios/OpenPassport.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ 1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerBridge.swift; sourceTree = ""; }; 1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = ""; }; 1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QRScannerBridge.m; sourceTree = ""; }; + 169349842CC694DA00166F21 /* OpenPassportDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = OpenPassportDebug.entitlements; path = OpenPassport/OpenPassportDebug.entitlements; sourceTree = ""; }; 16E6646D2B8D292500FDD6A0 /* QKMRZScannerViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QKMRZScannerViewRepresentable.swift; sourceTree = ""; }; 16E884A42C5BD764003B7125 /* passport.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = passport.json; sourceTree = ""; }; 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 = ""; }; @@ -181,6 +182,7 @@ 13B07FAE1A68108700A75B9A /* OpenPassport */ = { isa = PBXGroup; children = ( + 169349842CC694DA00166F21 /* OpenPassportDebug.entitlements */, 16E884A42C5BD764003B7125 /* passport.json */, 05EDEDC42B52D25D00AA51AD /* Prover.m */, 05EDEDC52B52D25D00AA51AD /* Prover.swift */, @@ -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; diff --git a/app/ios/OpenPassport/AppDelegate.mm b/app/ios/OpenPassport/AppDelegate.mm index 7dcbbf93..74f966d6 100644 --- a/app/ios/OpenPassport/AppDelegate.mm +++ b/app/ios/OpenPassport/AppDelegate.mm @@ -1,8 +1,8 @@ #import "AppDelegate.h" - #import #import #import +#import @implementation AppDelegate @@ -25,4 +25,13 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge #endif } -@end \ No newline at end of file +- (BOOL)application:(UIApplication *)application +continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +@end diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist index 7cc10a93..2f3ab8c8 100644 --- a/app/ios/OpenPassport/Info.plist +++ b/app/ios/OpenPassport/Info.plist @@ -64,5 +64,14 @@ A0000002472001 00000000000000 + CFBundleURLTypes + + + CFBundleURLSchemes + + proofofpassport + + + diff --git a/app/ios/OpenPassport/OpenPassport.entitlements b/app/ios/OpenPassport/OpenPassport.entitlements index 1c1da77f..c86eca21 100644 --- a/app/ios/OpenPassport/OpenPassport.entitlements +++ b/app/ios/OpenPassport/OpenPassport.entitlements @@ -11,6 +11,7 @@ appclips:openpassport.app appclips:staging.openpassport.app appclips:appclip.openpassport.app + applinks:proofofpassport-merkle-tree.xyz com.apple.developer.nfc.readersession.formats diff --git a/app/ios/OpenPassport/OpenPassportDebug.entitlements b/app/ios/OpenPassport/OpenPassportDebug.entitlements new file mode 100644 index 00000000..c86eca21 --- /dev/null +++ b/app/ios/OpenPassport/OpenPassportDebug.entitlements @@ -0,0 +1,23 @@ + + + + + com.apple.developer.associated-appclip-app-identifiers + + 5B29R5LYHQ.com.warroom.proofofpassport.Clip + + com.apple.developer.associated-domains + + appclips:openpassport.app + appclips:staging.openpassport.app + appclips:appclip.openpassport.app + applinks:proofofpassport-merkle-tree.xyz + + com.apple.developer.nfc.readersession.formats + + TAG + + com.apple.security.device.camera + + + diff --git a/app/src/utils/qrCode.ts b/app/src/utils/qrCode.ts index 10638bba..b612b4c2 100644 --- a/app/src/utils/qrCode.ts +++ b/app/src/utils/qrCode.ts @@ -1,4 +1,4 @@ -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"; @@ -6,89 +6,147 @@ 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 => { + const [, queryString] = url.split('?'); + const params = new Map(); + 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', + }); } -}; \ No newline at end of file +}; + +export const setupUniversalLinkListener = () => { + Linking.getInitialURL().then((url) => { + if (url) { + handleUniversalLink(url); + } + }); + + const linkingEventListener = Linking.addEventListener('url', ({ url }) => { + handleUniversalLink(url); + }); + + return () => { + linkingEventListener.remove(); + }; +}; diff --git a/sdk/src/QRcode/OpenPassportQRcode.tsx b/sdk/src/QRcode/OpenPassportQRcode.tsx index 3236da23..8cdaa7eb 100644 --- a/sdk/src/QRcode/OpenPassportQRcode.tsx +++ b/sdk/src/QRcode/OpenPassportQRcode.tsx @@ -51,6 +51,13 @@ const OpenPassportQRcode: React.FC = ({ ); }, [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 = () => (
@@ -89,7 +96,7 @@ const OpenPassportQRcode: React.FC = ({ default: return ( );