diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a12da685..7e3aa3f5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,18 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index aa7465b1..55430cce 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,5 +3,4 @@ DIDroom Wallet DIDroom Wallet com.didroom.wallet - openid-credential-offer diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 4eeb3399..eda13e20 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -46,5 +46,17 @@ UIViewControllerBasedStatusBarAppearance + CFBundleURLTypes + + + CFBundleURLName + com.getcapacitor.capacitor + CFBundleURLSchemes + + didroom4vp + openid-credential-offer + + + \ No newline at end of file diff --git a/src/lib/components/organisms/scanner/tools.ts b/src/lib/components/organisms/scanner/tools.ts index 505ef6ff..49b03f8d 100644 --- a/src/lib/components/organisms/scanner/tools.ts +++ b/src/lib/components/organisms/scanner/tools.ts @@ -8,8 +8,12 @@ import { http } from '@slangroom/http'; import verQrToInfo from '$lib/mobile_zencode/wallet/ver_qr_to_info.zen?raw'; import verQrToInfoKeys from '$lib/mobile_zencode/wallet/ver_qr_to_info.keys.json?raw'; import { log } from '$lib/log'; +import { verificationStore } from '$lib/verificationStore'; +import { credentialOfferStore } from '$lib/credentialOfferStore'; +import type { Feedback } from '$lib/utils/types'; +import { m, goto } from '$lib/i18n'; +import { verificationResultsStore } from '$lib/verificationResultsStore'; -//@ts-expect-error something is wrong in Slangroom types const slangroom = new Slangroom(helpers, zencode, pocketbase, http); export type QrToInfoResults = { @@ -43,17 +47,7 @@ export type Body = { vp: string; }; -export type ParseQrResults = - | { - result: 'error'; - message: string; - } - | { - result: 'ok'; - data: Data; - }; - -const credentialSchema = z.object({ +export const credentialSchema = z.object({ rp: z.string().url(), t: z.string(), m: z.literal('f'), @@ -63,7 +57,7 @@ const credentialSchema = z.object({ id: z.string() }); -const serviceSchema = z.object({ +export const serviceSchema = z.object({ credential_configuration_ids: z.array(z.string()), credential_issuer: z.string().url() }); @@ -79,43 +73,6 @@ export type Data = service: Service; }; -export const parseQr = async (value: string): Promise => { - const notValidQr = 'not valid qr'; - let parsedValue: Record; - let type: 'credential' | 'service'; - try { - parsedValue = JSON.parse(value); - } catch (e) { - return { result: 'error', message: notValidQr }; - } - if (credentialSchema.safeParse(parsedValue).success) { - type = 'credential'; - parsedValue.type = 'credential'; - } else if (serviceSchema.safeParse(parsedValue).success) { - type = 'service'; - parsedValue.type = 'service'; - } else { - return { result: 'error', message: notValidQr }; - } - - // if (type == 'credential' && !isUrlAllowed(parsedValue.url as string)) { - // return { result: 'error', message: 'not allowed verifier url' }; - // } - - //todo: validate service urls - if (type == 'service') { - delete parsedValue.type; - return { result: 'ok', data: { type, service: parsedValue as Service } }; - } else { - try { - const credential = await getCredentialQrInfo(parsedValue as Credential); - return { result: 'ok', data: { type, credential } }; - } catch (err) { - return { result: 'error', message: `error getting credential info: ${err}` }; - } - } -}; - export const verifyCredential = async (post: Post) => { const res = await slangroom.execute( `Rule unknown ignore @@ -137,13 +94,142 @@ export const getCredentialQrInfo = async (qrJSON: Credential) => { ...qrJSON, credential_array: myCredentials }; - log(JSON.stringify(data)) + log(JSON.stringify(data)); try { - const res = await slangroom.execute(verQrToInfo, { data, keys: JSON.parse(verQrToInfoKeys) }); - log(JSON.stringify(res)); - return res.result as QrToInfoResults; + const res = await slangroom.execute(verQrToInfo, { data, keys: JSON.parse(verQrToInfoKeys) }); + log(JSON.stringify(res)); + return res.result as QrToInfoResults; } catch (err) { log(JSON.stringify(err)); throw new Error(`error executing zencode: ${err}`); } }; + +const parseQrCodeErrors = (qrcodeResultMessage?: string) => { + if (!qrcodeResultMessage) return; + if (!(typeof qrcodeResultMessage === 'string')) return; + if (qrcodeResultMessage.includes('QR code is expired')) { + return m.QR_code_is_expired(); + } + if ( + qrcodeResultMessage.includes( + 'no_signed_selective_disclosure_found_that_matched_the_requested_claims' + ) + ) { + return m.You_have_no_signed_selective_disclosure_that_matched_the_requested_claims_or_your_credential_is_expired(); + } + return qrcodeResultMessage; +}; + +const infoFromVerificationData = async ( + data: Credential +): Promise< + | { + success: true; + info: QrToInfoResults; + } + | { + success: false; + feedback: Feedback; + } +> => { + try { + const credential = await getCredentialQrInfo(data); + verificationStore.set(credential); + return { + success: true, + info: credential + }; + //@ts-ignore + } catch (err: { message: unknown }) { + return { + success: false, + feedback: { + type: 'error', + feedback: 'Verification failed', + message: parseQrCodeErrors(err.message) + } + }; + } +}; + +const extractUrlParams = ( + params: { [key: string]: 'string' | 'number' | 'array' }, + urlSearchParams: URLSearchParams +) => + Object.entries(params).reduce((result, [key, type]) => { + const value = urlSearchParams.get(key)?.trim(); + let parsedValue; + + switch (type) { + case 'array': + parsedValue = value ? [value] : []; + break; + case 'number': + parsedValue = value ? Number(value) : undefined; + break; + default: + parsedValue = value; + } + + return { + ...result, + [key]: parsedValue + }; + }, {}); + +const parseParams = (urlParams: URLSearchParams, params: any, schema: any) => { + return schema.safeParse(extractUrlParams(params, urlParams)); +}; + +const handleVerificationSuccess = async (verificationData: any) => { + const info = await infoFromVerificationData(verificationData); + if (info.success) { + verificationStore.set(info.info); + return await goto('/verification'); + } else { + verificationResultsStore.set({ + feedback: info.feedback, + date: new Date().toISOString(), + id: verificationData.sid, + success: false + }); + return await goto('/verification/results'); + } +}; + +const handleServiceSuccess = async (serviceData: any) => { + credentialOfferStore.set(serviceData); + return await goto('/credential-offer'); +}; + +export const gotoQrResult = async (url: string) => { + const urlParams = new URLSearchParams(url.split('://?')[1]); + + const verificationParams = { + rp: 'string', + t: 'string', + m: 'string', + exp: 'number', + ru: 'string', + sid: 'string', + id: 'string' + }; + + const parsedVerification = parseParams(urlParams, verificationParams, credentialSchema); + if (parsedVerification.success) { + return handleVerificationSuccess(parsedVerification.data); + } + + const serviceParams = { + credential_configuration_ids: 'array', + credential_issuer: 'string' + }; + + const parsedService = parseParams(urlParams, serviceParams, serviceSchema); + if (parsedService.success) { + return handleServiceSuccess(parsedService.data); + } + + return await goto('/unlock'); +}; diff --git a/src/lib/verificationResultsStore.ts b/src/lib/verificationResultsStore.ts new file mode 100644 index 00000000..8e525fb4 --- /dev/null +++ b/src/lib/verificationResultsStore.ts @@ -0,0 +1,11 @@ +import type { Feedback } from './utils/types'; +import { writable } from 'svelte/store'; + +export type VerificationResults = { + feedback: Feedback; + date: string; + id: string; + success: boolean; +}; + +export const verificationResultsStore = writable(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4eee1b6b..87f7932a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,7 +15,9 @@ import { onDestroy, onMount } from 'svelte'; import { navigating } from '$app/stores'; import { App } from '@capacitor/app'; + import { gotoQrResult } from '$lib/components/organisms/scanner/tools'; import FingerPrint from '$lib/assets/lottieFingerPrint/FingerPrint.svelte'; + const controller = new AbortController(); const signal = controller.signal; @@ -36,6 +38,10 @@ }, { signal } ); + + App.addListener('appUrlOpen', async (data) => { + await gotoQrResult(data.url); + }); }); onDestroy(() => { controller.abort(); @@ -48,7 +54,7 @@ content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" /> -