1- import { CheckIcon , CopyIcon } from '@radix-ui/react-icons'
1+ import { CheckIcon , CopyIcon , ExclamationTriangleIcon } from '@radix-ui/react-icons'
22import { RadioGroup } from '@radix-ui/react-radio-group'
33import Image from 'next/image'
44import Link from 'next/link'
55import { type ReadonlyURLSearchParams , useRouter } from 'next/navigation'
66import { type FormEvent , useEffect , useState } from 'react'
77import QRCode from 'react-qr-code'
8+ import { Alert , AlertDescription , AlertTitle } from '@/components/ui/alert'
89import { Button } from '@/components/ui/button'
910import { Card } from '@/components/ui/card'
1011import { Label } from '@/components/ui/label'
@@ -38,9 +39,12 @@ export function IssueTab({
3839
3940 const [ selectedAuthorization , setSelectedAuthorization ] = useState < string > ( 'none' )
4041 const [ selectedDeferBy , setDeferBy ] = useState < string > ( 'none' )
42+ const [ issuanceMethod , setIssuanceMethod ] = useState < 'qr' | 'dcApi' > ( 'qr' )
4143
4244 const [ credentialOfferUri , setCredentialOfferUri ] = useState < string > ( )
4345 const [ userPin , setUserPin ] = useState < string > ( )
46+ const [ issueStatus , setIssueStatus ] = useState < 'idle' | 'issuing' | 'issued' | 'error' > ( 'idle' )
47+ const [ issueError , setIssueError ] = useState < string > ( )
4448
4549 const selectedIssuer = issuers ?. find ( ( i ) => i . id === selectedIssuerId )
4650 const router = useRouter ( )
@@ -68,6 +72,9 @@ export function IssueTab({
6872 if ( query . dpop ) setRequireDpop ( query . dpop === 'true' )
6973 if ( query . walletAttestation ) setRequireWalletAttestation ( query . walletAttestation === 'true' )
7074 if ( query . keyAttestation ) setRequireKeyAttestation ( query . keyAttestation === 'true' )
75+ if ( query . initiationMethod === 'dcApi' || query . initiationMethod === 'qr' ) {
76+ setIssuanceMethod ( query . initiationMethod )
77+ }
7178 } )
7279 } , [ issuers , searchParams ] )
7380
@@ -84,6 +91,7 @@ export function IssueTab({
8491 params . set ( 'dpop' , `${ requireDpop } ` )
8592 params . set ( 'keyAttestation' , `${ requireKeyAttestation } ` )
8693 params . set ( 'walletAttestation' , `${ requireWalletAttestation } ` )
94+ if ( issuanceMethod ) params . set ( 'initiationMethod' , issuanceMethod )
8795 if ( credentialType !== undefined ) params . set ( 'credentialType' , `${ credentialType } ` )
8896
8997 const existingSearchParams = new URLSearchParams ( searchParams . toString ( ) )
@@ -101,6 +109,7 @@ export function IssueTab({
101109 selectedFormat ,
102110 selectedAuthorization ,
103111 selectedDeferBy ,
112+ issuanceMethod ,
104113 credentialType ,
105114 router ,
106115 searchParams ,
@@ -109,6 +118,54 @@ export function IssueTab({
109118 requireWalletAttestation ,
110119 ] )
111120
121+ useEffect ( ( ) => {
122+ setIssueStatus ( 'idle' )
123+ setIssueError ( undefined )
124+ } , [ issuanceMethod ] )
125+
126+ const buildDcApiRequestData = ( offerUri : string ) => {
127+ const queryIndex = offerUri . indexOf ( '?' )
128+ if ( queryIndex >= 0 ) {
129+ const params = new URLSearchParams ( offerUri . slice ( queryIndex + 1 ) )
130+ const offerUriParam = params . get ( 'credential_offer_uri' )
131+ if ( offerUriParam ) {
132+ return { credential_offer_uri : offerUriParam }
133+ }
134+ const inlineOffer = params . get ( 'credential_offer' )
135+ if ( inlineOffer ) {
136+ try {
137+ return { credential_offer : JSON . parse ( inlineOffer ) }
138+ } catch {
139+ return { credential_offer : inlineOffer }
140+ }
141+ }
142+ }
143+
144+ try {
145+ return { credential_offer : JSON . parse ( offerUri ) }
146+ } catch {
147+ return { credential_offer_uri : offerUri }
148+ }
149+ }
150+
151+ const initiateDcIssuance = async ( offerUri : string ) => {
152+ if ( ! ( 'credentials' in navigator ) || typeof navigator . credentials . create !== 'function' ) {
153+ throw new Error ( 'Digital Credentials API is not supported in this browser.' )
154+ }
155+ const requestData = buildDcApiRequestData ( offerUri )
156+ await navigator . credentials . create ( {
157+ // @ts -expect-error Digital Credentials API typings
158+ digital : {
159+ requests : [
160+ {
161+ protocol : 'openid4vci1.0' ,
162+ data : requestData ,
163+ } ,
164+ ] ,
165+ } ,
166+ } )
167+ }
168+
112169 async function onSubmitIssueCredential ( e : FormEvent ) {
113170 e . preventDefault ( )
114171 console . log ( selectedIssuer , credentialType , selectedFormat , selectedAuthorization )
@@ -128,6 +185,19 @@ export function IssueTab({
128185 } )
129186 setCredentialOfferUri ( offer . credentialOffer )
130187 setUserPin ( offer . issuanceSession . userPin )
188+ setIssueError ( undefined )
189+ setIssueStatus ( 'idle' )
190+
191+ if ( issuanceMethod === 'dcApi' ) {
192+ setIssueStatus ( 'issuing' )
193+ try {
194+ await initiateDcIssuance ( offer . credentialOffer )
195+ setIssueStatus ( 'issued' )
196+ } catch ( error ) {
197+ setIssueStatus ( 'error' )
198+ setIssueError ( error instanceof Error ? error . message : 'Unknown error while calling Digital Credentials API' )
199+ }
200+ }
131201 }
132202
133203 const copyConfiguration = async ( ) => {
@@ -252,6 +322,19 @@ export function IssueTab({
252322 ) ) }
253323 </ RadioGroup >
254324 </ div >
325+ < div className = "space-y-2" >
326+ < Label htmlFor = "initiation-method" > Initiation Method</ Label >
327+ < RadioGroup
328+ name = "initiation-method"
329+ required
330+ className = "flex flex-col gap-2 md:gap-4 md:flex-row"
331+ onValueChange = { ( v ) => setIssuanceMethod ( v as 'qr' | 'dcApi' ) }
332+ value = { issuanceMethod }
333+ >
334+ < MiniRadioItem value = "qr" label = "QR / Deeplink" />
335+ < MiniRadioItem value = "dcApi" label = "Digital Credentials API" />
336+ </ RadioGroup >
337+ </ div >
255338 < div className = "space-y-2" >
256339 < div >
257340 < Label htmlFor = "format" > Authentication</ Label >
@@ -327,7 +410,11 @@ export function IssueTab({
327410 />
328411 </ div >
329412 < div className = "flex justify-center items-center bg-gray-200 min-h-64 w-full rounded-md" >
330- { credentialOfferUri ? (
413+ { issuanceMethod === 'dcApi' ? (
414+ < p className = "text-gray-500 text-center px-6" >
415+ Digital Credentials API is selected. The credential offer will be sent directly.
416+ </ p >
417+ ) : credentialOfferUri ? (
331418 < TooltipProvider >
332419 < Tooltip >
333420 < div className = "flex flex-col p-5 gap-2 justify-center items-center gap-6" >
@@ -372,6 +459,25 @@ export function IssueTab({
372459 < p className = "text-gray-500 break-all" > Credential offer will be displayed here</ p >
373460 ) }
374461 </ div >
462+ { issuanceMethod === 'dcApi' && issueStatus !== 'idle' && (
463+ < Alert variant = { issueStatus === 'issued' ? 'success' : issueStatus === 'error' ? 'destructive' : 'warning' } >
464+ { issueStatus === 'error' ? (
465+ < ExclamationTriangleIcon className = "h-4 w-4" />
466+ ) : issueStatus === 'issued' ? (
467+ < CheckIcon className = "h-5 w-5" />
468+ ) : null }
469+ < AlertTitle >
470+ { issueStatus === 'issuing'
471+ ? 'Requesting Credential'
472+ : issueStatus === 'issued'
473+ ? 'Credential Request Sent'
474+ : 'Issuance Failed' }
475+ </ AlertTitle >
476+ { issueStatus === 'error' && issueError && (
477+ < AlertDescription className = "mt-2" > { issueError } </ AlertDescription >
478+ ) }
479+ </ Alert >
480+ ) }
375481 < Button
376482 onClick = { onSubmitIssueCredential }
377483 disabled = { disabled }
0 commit comments