Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle credential offer deep links #431

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f41555d
feat: handle credential offer deep link in android
phoebus-84 Jul 31, 2024
bddeef6
fix: latest version of mobile zencode
phoebus-84 Jul 31, 2024
515a642
fix: try to fix ci
phoebus-84 Jul 31, 2024
db756cb
Merge branch 'main' into intent-url
phoebus-84 Aug 26, 2024
579a18c
Merge branch 'main' into intent-url
puria Aug 27, 2024
5764024
Update AndroidManifest.xml
puria Aug 27, 2024
beab92a
chore: prefer the hardcoded string over variable string
puria Aug 27, 2024
ac3a9ad
credential offer deep link should work with app scanner
phoebus-84 Aug 27, 2024
9bee996
fix handling deep link
phoebus-84 Aug 27, 2024
833c13e
Merge branch 'main' into intent-url
phoebus-84 Sep 4, 2024
94d9944
feat: intent url for both verification and credential-offer
phoebus-84 Sep 13, 2024
fdc163a
test: fix credential=offer test
phoebus-84 Sep 13, 2024
b8435eb
test: skip scan credential-offer test
phoebus-84 Sep 13, 2024
1633356
fix: revert scanner modal error handling
phoebus-84 Sep 13, 2024
ac7e107
fix: scan qr function
phoebus-84 Sep 13, 2024
5bff450
fix: typo
phoebus-84 Sep 13, 2024
a9c9f5d
fix: chacth intents url errors in logs
phoebus-84 Sep 13, 2024
83cf9a0
fix: credential-offer external redirection
phoebus-84 Sep 13, 2024
0ebe061
fix: internal scanner redirection
phoebus-84 Sep 13, 2024
356727f
Merge branch 'main' into intent-url
phoebus-84 Sep 13, 2024
c12b474
fix: return if fails verification qr to well known
phoebus-84 Sep 13, 2024
2a14f4a
fix: verification intent-url parsing
phoebus-84 Sep 13, 2024
ee87584
refactor: get parmas from url
phoebus-84 Sep 14, 2024
b2dc84b
feat: verification errors handling
phoebus-84 Sep 15, 2024
c41de2b
fix: fallback for bad intent-url
phoebus-84 Sep 15, 2024
1919085
feat: intent url for ios
phoebus-84 Sep 16, 2024
fbf7deb
fix: protocol should be openid4vp"
phoebus-84 Sep 16, 2024
dbdce08
Merge branch 'main' into intent-url
phoebus-84 Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="openid4vp" />
</intent-filter>
</activity>
<provider android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true" android:name="androidx.core.content.FileProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
Expand Down
1 change: 0 additions & 1 deletion android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
<string name="app_name">DIDroom Wallet</string>
<string name="title_activity_main">DIDroom Wallet</string>
<string name="package_name">com.didroom.wallet</string>
<string name="custom_url_scheme">openid-credential-offer</string>
</resources>
11 changes: 11 additions & 0 deletions ios/App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,16 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.getcapacitor.capacitor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>openid4vp</string>
</array>
</dict>
</array>
</dict>
</plist>
165 changes: 112 additions & 53 deletions src/lib/components/organisms/scanner/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ 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 { goto } from '$app/navigation';
import { verificationStore } from '$lib/verificationStore';
import { credentialOfferStore } from '$lib/credentialOfferStore';
import type { Feedback } from '$lib/utils/types';
import { m } 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 = {
Expand Down Expand Up @@ -43,17 +48,11 @@ export type Body = {
vp: string;
};

export type ParseQrResults =
| {
result: 'error';
message: string;
}
| {
result: 'ok';
data: Data;
};
export type ParseQrError = {
message: string;
};

const credentialSchema = z.object({
export const credentialSchema = z.object({
rp: z.string().url(),
t: z.string(),
m: z.literal('f'),
Expand All @@ -63,7 +62,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()
});
Expand All @@ -79,43 +78,6 @@ export type Data =
service: Service;
};

export const parseQr = async (value: string): Promise<ParseQrResults> => {
const notValidQr = 'not valid qr';
let parsedValue: Record<string, unknown>;
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
Expand All @@ -137,13 +99,110 @@ 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 parseBarcodeErrors = (barcodeResultMessage?: string) => {
if (!barcodeResultMessage) return;
if (!(typeof barcodeResultMessage === 'string')) return;
if (barcodeResultMessage.includes('QR code is expired')) {
return m.QR_code_is_expired();
}
if (
barcodeResultMessage.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 barcodeResultMessage;
};

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
};
} catch (err: { message: unknown }) {
return {
success: false,
feedback: {
type: 'error',
feedback: 'Verification failed',
message: parseBarcodeErrors(err.message)
}
};
}
};

export const gotoQrResult = async (url: string) => {
const urlParams = new URLSearchParams(url.split('://?')[1]);
const getUrlParams = (params: (string | [string, 'number' | 'array'])[]) =>
params.reduce((object, value) => {
const isValueString = typeof value === 'string';
const key = isValueString ? value : value[0];
const type = isValueString ? 'string' : value[1];

return {
...object,
[key]:
type === 'string'
? urlParams.get(key)?.trim()
: type === 'array'
? [urlParams.get(key)]
: Number(urlParams.get(key)?.trim())
};
}, {});

const parsedVerification = credentialSchema.safeParse(
getUrlParams(['rp', 't', 'm', ['exp', 'number'], 'ru', 'sid', 'id'])
);

if (parsedVerification.success) {
const info = await infoFromVerificationData(parsedVerification.data);
if (info.success) {
verificationStore.set(info.info);
return await goto('/verification');
} else {
verificationResultsStore.set({
feedback: info.feedback,
date: new Date().toISOString(),
id: parsedVerification.data.sid,
success: false
});
return await goto('/verification/results');
}
}

const parsedService = serviceSchema.safeParse(
getUrlParams([['credential_configuration_ids', 'array'], 'credential_issuer'])
);

if (parsedService.success) {
credentialOfferStore.set(parsedService.data);
return await goto('/credential-offer');
}
return await goto('/unlock');
};
11 changes: 11 additions & 0 deletions src/lib/verificationResultsStore.ts
Original file line number Diff line number Diff line change
@@ -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<VerificationResults>();
8 changes: 7 additions & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -36,6 +38,10 @@
},
{ signal }
);

App.addListener('appUrlOpen', async (data) => {
await gotoQrResult(data.url);
});
});
onDestroy(() => {
controller.abort();
Expand All @@ -48,7 +54,7 @@
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"
/>
<!-- uncomment to test didroom-components locally -->
<!-- <script
<!-- <script
type="module"
src="http://localhost:3333/build/didroom-components.esm.js"
></script>
Expand Down
4 changes: 1 addition & 3 deletions src/routes/[[lang]]/(protected)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import { goto, m } from '$lib/i18n';
import { onDestroy, onMount } from 'svelte';
import type { PluginListenerHandle } from '@capacitor/core';
import { getHomeFeedbackPreference } from '$lib/homeFeedbackPreferences.js';

export let data;
const { notReadedActivities, hasHomeFeedback } = data;
let appStateChange: PluginListenerHandle;


//

onMount(async () => {
Expand All @@ -23,7 +21,7 @@
});

onDestroy(() => {
appStateChange.remove();
if (appStateChange) appStateChange?.remove();
});

let tabs: TabProps[] = [
Expand Down
42 changes: 9 additions & 33 deletions src/routes/[[lang]]/(protected)/scan/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,14 @@
<script lang="ts">
import Modal from '$lib/components/molecules/Modal.svelte';
import Scanner from '$lib/components/organisms/scanner/Scanner.svelte';
import { parseQr, type ParseQrResults } from '$lib/components/organisms/scanner/tools';
import { credentialOfferStore } from '$lib/credentialOfferStore';
import { goto, m } from '$lib/i18n';
import { verificationStore } from '$lib/verificationStore';
import { gotoQrResult, type ParseQrError } from '$lib/components/organisms/scanner/tools';
import { Capacitor } from '@capacitor/core';
import { pushState } from '$app/navigation';
import { page } from '$app/stores';

let barcodeResult: ParseQrResults;
let isModalOpen: boolean;
let barcodeResult: ParseQrError | void;
const isWeb = Capacitor.getPlatform() == 'web';

const parseBareCodeResultErrors = (barcodeResultMessage: string) => {
console.log(barcodeResultMessage);
if (barcodeResultMessage.includes('QR code is expired')) {
return m.QR_code_is_expired();
}
if (
barcodeResultMessage.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 barcodeResultMessage;
};
function showModal() {
pushState('', {
isModalOpen: true
Expand All @@ -37,17 +19,13 @@
<Scanner
let:scan
on:success={async (e) => {
barcodeResult = await parseQr(e.detail.qr);
if (barcodeResult.result === 'ok' && barcodeResult.data.type === 'service') {
credentialOfferStore.set(barcodeResult.data.service);
return await goto('/credential-offer');
}
if (barcodeResult.result === 'ok' && barcodeResult.data.type === 'credential') {
verificationStore.set(barcodeResult.data.credential);
return await goto('/verification');
const qr = e.detail.qr;
if (!qr.startsWith('openid4vp://')) {
console.log('failed:', e.detail.qr);
showModal();
return;
}
showModal();
return (isModalOpen = true);
return await gotoQrResult(qr);
}}
>
<Modal
Expand All @@ -58,8 +36,6 @@
}}
textToCopy={barcodeResult?.message}
>
{#if !(barcodeResult?.result === 'ok')}
<d-text size="m">{parseBareCodeResultErrors(barcodeResult?.message || 'error')}</d-text>
{/if}
<d-text size="m">{barcodeResult?.message || 'error'}</d-text>
</Modal>
</Scanner>
19 changes: 19 additions & 0 deletions src/routes/[[lang]]/(protected)/verification/results/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { m } from '$lib/i18n';
import HeaderWithBackButton from '$lib/components/molecules/HeaderWithBackButton.svelte';
import { verificationResultsStore } from '$lib/verificationResultsStore';

const { feedback, date, id, success } = $verificationResultsStore;
</script>

<HeaderWithBackButton>
{success ? 'verification started' : 'verification failed'}
</HeaderWithBackButton>

<ion-content fullscreen class="ion-padding">
<d-feedback {...feedback} class="break-all" />

<div class="flex w-full justify-around">
<d-session-card sid={id} {date} {success} />
</div>
</ion-content>
Loading
Loading