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"
/>
-