From 06b000af6811a543f79a25f651f68dc82476bfbb Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 11 Jun 2026 17:39:38 +0300 Subject: [PATCH] Add wallet and access management to React example Add collapsible React example sections for wallet switching/creation and access grant review/revocation. Keep management controls aligned with the existing example styling and badge treatment. Verification: pnpm exec tsc --noEmit; pnpm build:example; git diff --check --- examples/react/src/main.tsx | 222 ++++++++++++++++++++++++++++++++-- examples/react/src/styles.css | 139 ++++++++++++++++++++- 2 files changed, 350 insertions(+), 11 deletions(-) diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx index f4f3921..7fd00cf 100644 --- a/examples/react/src/main.tsx +++ b/examples/react/src/main.tsx @@ -5,6 +5,7 @@ import { supportedNetworks, type FeeOptionSelection, type FeeOptionWithBalance, + type AccessGrant, type Network, type OMSClientSessionExpiredEvent, type OMSClientSessionLoginType, @@ -41,12 +42,17 @@ function App() { const [lastTransactionHash, setLastTransactionHash] = useState('') const [lastTransactionExplorerUrl, setLastTransactionExplorerUrl] = useState('') const [feeOptions, setFeeOptions] = useState([]) + const [managedWallets, setManagedWallets] = useState([]) + const [newWalletReference, setNewWalletReference] = useState('') + const [accessGrants, setAccessGrants] = useState([]) const [useManualWalletSelection, setUseManualWalletSelection] = useState(readManualWalletSelectionPreference) const [sessionLifetimeSeconds, setSessionLifetimeSeconds] = useState(readSessionLifetimePreference) const [pendingWalletSelection, setPendingWalletSelection] = useState(null) const [emailAuthStatus, setEmailAuthStatus] = useState('Enter an email to start.') const [redirectStatus, setRedirectStatus] = useState('') const [walletStatus, setWalletStatus] = useState('') + const [activeWalletStatus, setActiveWalletStatus] = useState('') + const [accessStatus, setAccessStatus] = useState('') const [sessionExpiredPrompt, setSessionExpiredPrompt] = useState(null) const [isBusy, setIsBusy] = useState(false) const oidcCallbackStarted = useRef(false) @@ -180,6 +186,7 @@ function App() { setPendingWalletSelection(null) setLastIdToken('') + clearManagementState() setWalletAddress(result.walletAddress) setStep('wallet') setWalletStatus(status) @@ -195,6 +202,7 @@ function App() { setLastIdToken('') setLastTransactionHash('') setLastTransactionExplorerUrl('') + clearManagementState() setCode('') setStep('email') setEmailAuthStatus( @@ -316,6 +324,68 @@ function App() { }) } + async function loadManagedWallets() { + await run('Loading wallets...', setActiveWalletStatus, async () => { + const wallets = await oms.wallet.listWallets() + setManagedWallets(wallets) + setActiveWalletStatus(`Loaded ${formatCount(wallets.length, 'wallet')}.`) + }) + } + + async function useManagedWallet(wallet: OmsWallet) { + await run('Switching wallet...', setActiveWalletStatus, async () => { + const result = await oms.wallet.useWallet({ walletId: wallet.id }) + setWalletAddress(result.walletAddress) + clearWalletOperationResults() + setAccessGrants([]) + setAccessStatus('') + setManagedWallets(current => + current.map(item => item.id === result.wallet.id ? result.wallet : item), + ) + setActiveWalletStatus(`Using ${result.wallet.reference ?? formatWalletType(result.wallet.type)}.`) + }) + } + + async function createManagedWallet() { + await run('Creating wallet...', setActiveWalletStatus, async () => { + const reference = newWalletReference.trim() + const result = await oms.wallet.createWallet({ + reference: reference || undefined, + }) + setWalletAddress(result.walletAddress) + clearWalletOperationResults() + setAccessGrants([]) + setAccessStatus('') + setManagedWallets(current => { + const withoutCreated = current.filter(wallet => wallet.id !== result.wallet.id) + return [...withoutCreated, result.wallet] + }) + setNewWalletReference('') + setActiveWalletStatus(`Created and activated ${result.wallet.reference ?? formatWalletType(result.wallet.type)}.`) + }) + } + + async function loadAccess() { + await run('Loading access...', setAccessStatus, async () => { + const grants = await oms.wallet.listAccess() + setAccessGrants(grants) + setAccessStatus(`Loaded ${formatCount(grants.length, 'access grant')}.`) + }) + } + + async function revokeAccess(grant: AccessGrant) { + if (grant.isCaller) { + setAccessStatus('The current session access grant cannot be revoked here.') + return + } + + await run('Revoking access...', setAccessStatus, async () => { + await oms.wallet.revokeAccess({ targetCredentialId: grant.credentialId }) + setAccessGrants(current => current.filter(item => item.credentialId !== grant.credentialId)) + setAccessStatus('Access grant revoked.') + }) + } + async function getIdToken() { await run('Getting ID token...', setWalletStatus, async () => { const idToken = await oms.wallet.getIdToken() @@ -361,6 +431,7 @@ function App() { setLastTransactionHash('') setLastTransactionExplorerUrl('') setFeeOptions([]) + clearManagementState() setStep('email') setEmailAuthStatus('Enter an email to start.') setRedirectStatus('') @@ -368,6 +439,24 @@ function App() { }) } + function clearWalletOperationResults() { + feeSelection.current?.reject(new Error('Active wallet changed')) + feeSelection.current = null + setFeeOptions([]) + setLastSignature('') + setLastIdToken('') + setLastTransactionHash('') + setLastTransactionExplorerUrl('') + setWalletStatus('') + } + + function clearManagementState() { + setManagedWallets([]) + setAccessGrants([]) + setActiveWalletStatus('') + setAccessStatus('') + } + return (
@@ -625,6 +714,122 @@ function App() { )}
+ {selectedNetwork.id === Networks.amoy.id && ( +
+ ERC20 example +
+ +
+
+ )} + +
+ Wallet management +
+
+ +
+
+ + +
+ + {managedWallets.length > 0 ? ( +
+ {managedWallets.map(wallet => { + const isActiveWallet = sameAddress(wallet.address, walletAddress) + + return ( +
+
+ + {wallet.reference ?? `${formatWalletType(wallet.type)} wallet`} + {wallet.id} + + {isActiveWallet ? ( + Active + ) : ( + + )} +
+ {wallet.address} +
+ ) + })} +
+ ) : ( +

Load wallets to switch or create another wallet for this account.

+ )} + {activeWalletStatus && {activeWalletStatus}} +
+
+ +
+ Access management +
+
+ +
+ + {accessGrants.length > 0 ? ( +
+ {accessGrants.map(grant => ( +
+
+ + {grant.isCaller ? 'Current session grant' : 'Access grant'} + Credential ID: {grant.credentialId} + + {grant.isCaller ? ( + Caller + ) : ( + + )} +
+
+
+
Expires
+
{formatSessionExpiry(grant.expiresAt)}
+
+
+
+ ))} +
+ ) : ( +

Show access grants to review or revoke grants for other credentials.

+ )} + {accessStatus && {accessStatus}} +
+
+
Other operations
@@ -635,15 +840,6 @@ function App() {
- {selectedNetwork.id === Networks.amoy.id && ( -
- ERC20 example -
- -
-
- )} - @@ -779,6 +975,14 @@ function formatWalletType(walletType: string): string { .join(' ') } +function formatCount(count: number, singular: string): string { + return `${count} ${singular}${count === 1 ? '' : 's'}` +} + +function sameAddress(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase() +} + function isPendingWalletSelection( result: PendingWalletSelection | WalletActivationResult, ): result is PendingWalletSelection { diff --git a/examples/react/src/styles.css b/examples/react/src/styles.css index f7a6637..f3b8a4c 100644 --- a/examples/react/src/styles.css +++ b/examples/react/src/styles.css @@ -253,6 +253,23 @@ button.subtle:hover:not(:disabled) { border-color: var(--oms-slate-400); } +button.danger { + color: var(--oms-red-700); + background: var(--oms-red-50); + border-color: var(--oms-red-200); +} + +button.danger:hover:not(:disabled) { + color: var(--oms-red-800); + background: var(--oms-red-100); + border-color: var(--oms-red-400); +} + +button.danger:active:not(:disabled) { + background: var(--oms-red-200); + border-color: var(--oms-red-500); +} + button:disabled { cursor: not-allowed; color: var(--oms-slate-400); @@ -453,16 +470,21 @@ button:disabled { } .metadata-pill { + display: inline-flex; + align-items: center; + justify-content: center; min-width: 48px; - padding: 4px 8px; + min-height: 24px; + padding: 3px 9px; border: 1px solid var(--oms-slate-300); border-radius: 16px; color: var(--oms-slate-800); background: var(--oms-slate-100); font-size: 12px; font-weight: 800; - line-height: 1.2; + line-height: 1; text-align: center; + white-space: nowrap; } select:disabled { @@ -571,6 +593,115 @@ select:disabled { gap: 8px; } +.inline-field-action { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; + gap: 10px; +} + +.management-list { + display: grid; + gap: 8px; +} + +.management-card { + display: grid; + gap: 8px; + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--oms-slate-200); + border-radius: var(--oms-radius-input); + background: var(--oms-surface); +} + +.management-card-active { + border-color: var(--oms-purple-200); + background: var(--oms-purple-50); +} + +.management-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + min-width: 0; +} + +.management-card-header > span { + display: grid; + gap: 3px; + min-width: 0; +} + +.management-card-header strong, +.management-card-header small { + display: block; + min-width: 0; + overflow-wrap: anywhere; +} + +.management-card-header strong { + color: var(--oms-ink); + font-size: 14px; + line-height: 1.25; +} + +.management-card-header small { + color: var(--oms-muted-ink); + font-size: 12px; + font-weight: 700; +} + +.management-card-header button { + min-height: 36px; + padding: 7px 10px; + border-radius: var(--oms-radius-button); +} + +.management-card code { + display: block; + min-width: 0; + overflow-wrap: anywhere; + padding: 8px 10px; + border-radius: var(--oms-radius-button); + color: var(--oms-slate-800); + background: var(--oms-slate-100); + font-family: var(--oms-font-mono); + font-size: 12px; +} + +.management-meta { + display: grid; + gap: 8px; + margin: 0; +} + +.management-meta div { + display: grid; + gap: 3px; + min-width: 0; +} + +.management-meta dt, +.management-meta dd { + min-width: 0; + overflow-wrap: anywhere; +} + +.management-meta dt { + color: var(--oms-muted-ink); + font-size: 12px; + font-weight: 700; +} + +.management-meta dd { + margin: 0; + color: var(--oms-ink); + font-size: 13px; + font-weight: 700; +} + output { min-height: 42px; padding: 11px 12px; @@ -759,6 +890,10 @@ output { grid-template-columns: 1fr; } + .inline-field-action { + grid-template-columns: 1fr; + } + .modal-actions { grid-template-columns: 1fr; }