@me.jango/signa 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -2
- package/core/controller.d.ts +43 -0
- package/core/index.d.ts +79 -0
- package/core/index.js +347 -0
- package/index.d.ts +1607 -0
- package/index.js +3074 -0
- package/package.json +20 -4
- package/react/index.d.ts +34 -0
- package/react/index.js +195 -0
- package/wagmi/index.d.ts +26 -0
- package/wagmi/index.js +97 -0
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @me.jango/signa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sign in to your app with a Signa passkey account or an external wallet, then request exact payment
|
|
4
|
+
reviews. This is the browser-only slice of the Signa client. Bot access and the CLI ship separately as
|
|
5
|
+
the full client archive linked from https://api.signa.center/api/docs/client.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @me.jango/signa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Import | What it is | Needs |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `@me.jango/signa` | The wallet client: connect an account, review and submit payments | viem |
|
|
14
|
+
| `@me.jango/signa/core` | The connect step, headless: sign-in options, popup or frame, callback page. Also re-exports the wallet client | viem |
|
|
15
|
+
| `@me.jango/signa/wagmi` | A read-only wagmi connector for the passkey account | wagmi |
|
|
16
|
+
| `@me.jango/signa/react` | The sign-in modal | React |
|
|
17
|
+
|
|
18
|
+
React and wagmi are optional peers: an app that imports only the root entry or `/core` never loads them.
|
|
19
|
+
These subpaths replace `@bananapus/nana-sdk-connect`; the export names are the same, so moving over only
|
|
20
|
+
changes the import specifier.
|
|
21
|
+
|
|
22
|
+
## Wallet client
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { createCenterWalletClient } from '@me.jango/signa';
|
|
26
|
+
const wallet = createCenterWalletClient({
|
|
27
|
+
issuer: 'https://signa.center', audience: 'https://api.signa.center',
|
|
28
|
+
callbackUri: window.location.origin + '/center/callback',
|
|
29
|
+
});
|
|
30
|
+
const prepared = await wallet.prepareConnection();
|
|
31
|
+
prepared.launch(); // full-page redirect to the account
|
|
32
|
+
// prepared.launch({ target: name }) submits into a window or iframe the app opened under that
|
|
33
|
+
// name; Signa serves the sign-in inside an iframe only for apps its operator admits to frame.
|
|
34
|
+
// On /center/callback:
|
|
35
|
+
const connection = await wallet.completeConnection(window.location.href);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your app origin and exact callback URI must be allowlisted by Signa. Allow the issuer in your
|
|
39
|
+
`form-action` CSP directive and send `Referrer-Policy: strict-origin`. `restoreConnection()` reads the
|
|
40
|
+
saved connection, `disconnect()` clears it, and `payments()` prepares, completes and submits reviewed
|
|
41
|
+
payments. Protocol details: https://api.signa.center/api/docs/client
|
|
42
|
+
`refreshPayment({ waitSeconds })` holds a submitted payment's read on Signa (up to 20 s) until the
|
|
43
|
+
operation moves, so an app can wait on it instead of polling. A record whose app grant is gone and whose
|
|
44
|
+
signed validity has passed can be cleared; it is archived as unknown and Signa keeps the outcome.
|
|
45
|
+
When Signa refuses to send an approved payment, `submitPayment()` throws the refusal (with
|
|
46
|
+
`payFailure` when the calls say why) after asking Signa to retire the approval. Read
|
|
47
|
+
`pendingPayment()` next: `refused` means Signa retired it and never sends it, so `clearPayment()` and a
|
|
48
|
+
fresh `preparePayment()` can follow at once; the fresh payment takes the refused one's nonce, so at most
|
|
49
|
+
one of them executes. `pending` or `confirming` means a send won before the retirement: follow it.
|
|
50
|
+
`unknown` means the retirement or the answer was lost: `retirePayment()` retires an approved payment
|
|
51
|
+
Signa has not sent, or returns the submission that already claimed it.
|
|
52
|
+
A reverted payment's status and a refused submission's `RestClientError` carry `payFailure`: the SDK's own
|
|
53
|
+
`PayFailure`, as `decodePayFailure` reads the same revert (bigint `received` and `minimum`, plus `zeroFill`
|
|
54
|
+
and `adjustable` for a shortfall), so it goes straight to the SDK's `payRetrySlippageBps` and to the same
|
|
55
|
+
display as an external-wallet payment. `observation.payFailure` keeps the reason as Signa served it. Because the
|
|
56
|
+
amounts are bigints, `JSON.stringify` of a status or error needs a replacer such as
|
|
57
|
+
`(_, value) => typeof value === 'bigint' ? value.toString() : value`. Without `adjustable`, no setting the
|
|
58
|
+
payer controls would help, so never relax a minimum then.
|
|
59
|
+
|
|
60
|
+
## Connect
|
|
61
|
+
|
|
62
|
+
- `/core`: `createConnectController(options)` (headless, no React or wagmi) takes the passkey option and
|
|
63
|
+
any external-wallet options (each a `ConnectOption`: `id`, `name`, optional `icon`, and `connect`, which
|
|
64
|
+
may publish a pairing URI through `handoff` for a QR code). `passkeyOption({ wallet, connected })` is the
|
|
65
|
+
Signa account. For the app's `/center/callback` page, call `deliverCenterCallback(url, { window })`, then
|
|
66
|
+
`completeCenterCallback(wallet, url)` when it returns false. The passkey account opens Signa in a popup
|
|
67
|
+
and the page stays; the callback page hands its URL back to the page, which finishes the exchange and runs
|
|
68
|
+
`connected`. A blocked popup falls back to a full-page redirect, where the callback page completes the
|
|
69
|
+
exchange itself. With `passkeyOption({ frame: true })` Signa opens in a frame the modal shows instead;
|
|
70
|
+
the callback page hands its URL up to the framing page through the same `deliverCenterCallback`. Signa
|
|
71
|
+
serves the sign-in framed only for apps its operator admits to frame (`WALLET_FRAMEABLE_APP_ORIGINS`),
|
|
72
|
+
and the page inside offers "Open as a page" when it cannot continue there. The frame delegates passkey
|
|
73
|
+
prompts and creation to Signa's origin by name, so a new user can sign up inside it.
|
|
74
|
+
- `/wagmi`: `centerAccountConnector({ wallet, read })`, a read-only Base connector for the passkey account.
|
|
75
|
+
It answers reads through `read` and refuses to sign: payments go through Signa's review.
|
|
76
|
+
- `/react`: `<JBConnectModal open controller onClose />` shows the passkey account as the primary button
|
|
77
|
+
(named after the platform prompt: Touch ID, Face ID, Windows Hello, unless `passkeyLabel` overrides it)
|
|
78
|
+
and external wallets as tiles. Theme it with the `--jb-connect-*` custom properties (`--jb-connect-pad`
|
|
79
|
+
is the dialog's inset, which the Signa frame bleeds through). The modal answers the frame's size reports
|
|
80
|
+
with its resolved theme, so the Signa page inside takes the app's colours, font and corners.
|
|
81
|
+
|
|
82
|
+
The passkey account signs nothing in the app. Payments go through Signa's review flow.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** One way in. The passkey account and every external wallet share this shape, so the modal and
|
|
2
|
+
* the apps never special-case Signa. `handoff` publishes a pairing URI (WalletConnect, a mobile
|
|
3
|
+
* deep link) for the app to render as a QR code or a link. */
|
|
4
|
+
type ConnectOption = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** A data: image URL. Apps render their own mark when absent. */
|
|
8
|
+
icon?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
connect(context: {
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
handoff(uri: string): void;
|
|
13
|
+
/** Asks the app to show a frame under this name for the option to continue in (the passkey
|
|
14
|
+
* sign-in at Signa, when Signa admits the app to frame it). `origin` is the page that will
|
|
15
|
+
* load in it, named in the frame's `allow` so the browser delegates passkeys to it: a frame
|
|
16
|
+
* without a `src` (Signa's launch form posts into it by name) delegates nothing otherwise. */
|
|
17
|
+
frame(name: string, origin: string): void;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
type ConnectState = {
|
|
21
|
+
/** The option being connected, or null when idle. A passkey connection leaves the page, so it
|
|
22
|
+
* stays pending until the browser returns. */
|
|
23
|
+
pending: string | null;
|
|
24
|
+
error: string | null;
|
|
25
|
+
handoffUri: string | null;
|
|
26
|
+
/** The name of the frame the pending option continues in, once it asks for one. */
|
|
27
|
+
frameName: string | null;
|
|
28
|
+
/** The origin the frame delegates passkeys to. */
|
|
29
|
+
frameOrigin: string | null;
|
|
30
|
+
};
|
|
31
|
+
type ConnectController = {
|
|
32
|
+
readonly options: readonly ConnectOption[];
|
|
33
|
+
getState(): ConnectState;
|
|
34
|
+
subscribe(listener: () => void): () => void;
|
|
35
|
+
/** Starts an option; rejections become `state.error`, never a thrown promise. */
|
|
36
|
+
choose(id: string): Promise<void>;
|
|
37
|
+
/** Aborts the in-flight option and clears the state. */
|
|
38
|
+
cancel(): void;
|
|
39
|
+
};
|
|
40
|
+
declare function createConnectController(options: readonly ConnectOption[]): ConnectController;
|
|
41
|
+
|
|
42
|
+
export { createConnectController as c };
|
|
43
|
+
export type { ConnectOption as C, ConnectController as a, ConnectState as b };
|
package/core/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { C as ConnectOption } from './controller.js';
|
|
2
|
+
export { a as ConnectController, b as ConnectState, c as createConnectController } from './controller.js';
|
|
3
|
+
export * from '../index.js';
|
|
4
|
+
|
|
5
|
+
/** The wallet client surface the passkey option needs (`createCenterWalletClient` satisfies it). */
|
|
6
|
+
type PasskeyWallet = {
|
|
7
|
+
prepareConnection(): Promise<{
|
|
8
|
+
/** Where the launch lands: its origin is what a frame delegates passkeys to. */
|
|
9
|
+
authorizationUrl: string;
|
|
10
|
+
launch(options?: {
|
|
11
|
+
target?: string;
|
|
12
|
+
}): void;
|
|
13
|
+
}>;
|
|
14
|
+
completeConnection(url: string): Promise<unknown>;
|
|
15
|
+
retryConnection(): Promise<unknown>;
|
|
16
|
+
/** The current connection, when one is already held in this tab. */
|
|
17
|
+
restoreConnection?(): unknown;
|
|
18
|
+
disconnect(): void;
|
|
19
|
+
payments(): {
|
|
20
|
+
pendingPayment(): {
|
|
21
|
+
status: string;
|
|
22
|
+
} | null;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** The built-in way in: a passkey account at Signa. By default Signa opens in a popup
|
|
26
|
+
* and the page stays; the callback comes back by message and the exchange finishes here, then
|
|
27
|
+
* `connected` runs. With `frame: true` Signa opens in a frame the app shows instead (the modal
|
|
28
|
+
* renders it under the name the option asks for); Signa serves the sign-in framed only for apps
|
|
29
|
+
* its operator admits, and the page inside offers "Open as a page" when it cannot continue there.
|
|
30
|
+
* When the popup is blocked, or with `popup: false`, connecting is a full-page redirect, so
|
|
31
|
+
* `beforeLaunch` is where the app saves what it needs to come back to. */
|
|
32
|
+
declare function passkeyOption(input: {
|
|
33
|
+
wallet(): Promise<PasskeyWallet> | PasskeyWallet;
|
|
34
|
+
beforeLaunch?(): void;
|
|
35
|
+
popup?: boolean;
|
|
36
|
+
frame?: boolean;
|
|
37
|
+
/** After a popup sign-in completes in this page: connect the app's wallet layer. */
|
|
38
|
+
connected?(connection: unknown): Promise<void> | void;
|
|
39
|
+
/** Defaults to the global window. */
|
|
40
|
+
window?: Window;
|
|
41
|
+
}): ConnectOption;
|
|
42
|
+
|
|
43
|
+
/** For the app's callback page. Hands the callback URL to the same-origin page that opened this
|
|
44
|
+
* window (a popup sign-in) or frames it (a framed sign-in or payment review), and closes a popup
|
|
45
|
+
* once that page acknowledges. The page may be slow to answer (a backgrounded
|
|
46
|
+
* tab on a phone), so the offer repeats until it is acknowledged or the opener closes. The cap
|
|
47
|
+
* (default fifteen minutes, the life of a Signa request) covers an opener that navigated away;
|
|
48
|
+
* after it the callback page completes the exchange itself, as it does after a full-page
|
|
49
|
+
* redirect, while still closing on a late acknowledgement. False when there is no same-origin
|
|
50
|
+
* opener. */
|
|
51
|
+
declare function deliverCenterCallback(url: string, options?: {
|
|
52
|
+
window?: Window | undefined;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
retryMs?: number;
|
|
55
|
+
}): Promise<boolean>;
|
|
56
|
+
|
|
57
|
+
/** The wallet client surface the callback page needs. */
|
|
58
|
+
type CenterCallbackWallet = {
|
|
59
|
+
completeConnection(url: string): Promise<unknown>;
|
|
60
|
+
retryConnection(): Promise<unknown>;
|
|
61
|
+
payments(): {
|
|
62
|
+
completePayment(url: string): Promise<unknown>;
|
|
63
|
+
refreshPayment(): Promise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
type CenterCallbackResult = {
|
|
67
|
+
kind: "connection";
|
|
68
|
+
connection: unknown;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "payment";
|
|
71
|
+
status: unknown;
|
|
72
|
+
};
|
|
73
|
+
/** Finishes whatever Signa sent the browser back for: a connection (`code`) or a payment
|
|
74
|
+
* review (`review`). Without parameters it retries the pending exchange, so a reload of the
|
|
75
|
+
* callback page after a failed exchange resumes instead of starting over. */
|
|
76
|
+
declare function completeCenterCallback(wallet: CenterCallbackWallet, url: string): Promise<CenterCallbackResult>;
|
|
77
|
+
|
|
78
|
+
export { ConnectOption, completeCenterCallback, deliverCenterCallback, passkeyOption };
|
|
79
|
+
export type { CenterCallbackResult, CenterCallbackWallet, PasskeyWallet };
|
package/core/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// wallet-client/src/core/controller.ts
|
|
4
|
+
var idle = {
|
|
5
|
+
pending: null,
|
|
6
|
+
error: null,
|
|
7
|
+
handoffUri: null,
|
|
8
|
+
frameName: null,
|
|
9
|
+
frameOrigin: null
|
|
10
|
+
};
|
|
11
|
+
function createConnectController(options) {
|
|
12
|
+
let state = idle;
|
|
13
|
+
let current = null;
|
|
14
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
const set = (next) => {
|
|
16
|
+
state = { ...state, ...next };
|
|
17
|
+
for (const listener of listeners) listener();
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
options,
|
|
21
|
+
getState: () => state,
|
|
22
|
+
subscribe(listener) {
|
|
23
|
+
listeners.add(listener);
|
|
24
|
+
return () => listeners.delete(listener);
|
|
25
|
+
},
|
|
26
|
+
async choose(id) {
|
|
27
|
+
const option = options.find((candidate) => candidate.id === id);
|
|
28
|
+
if (!option || option.disabled || current) return;
|
|
29
|
+
const controller = current = new AbortController();
|
|
30
|
+
set({
|
|
31
|
+
pending: id,
|
|
32
|
+
error: null,
|
|
33
|
+
handoffUri: null,
|
|
34
|
+
frameName: null,
|
|
35
|
+
frameOrigin: null
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await option.connect({
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
handoff: (uri) => {
|
|
41
|
+
if (current === controller) set({ handoffUri: uri });
|
|
42
|
+
},
|
|
43
|
+
frame: (name, origin) => {
|
|
44
|
+
if (current === controller)
|
|
45
|
+
set({ frameName: name, frameOrigin: origin });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (current === controller)
|
|
50
|
+
set({
|
|
51
|
+
pending: null,
|
|
52
|
+
handoffUri: null,
|
|
53
|
+
frameName: null,
|
|
54
|
+
frameOrigin: null,
|
|
55
|
+
error: messageOf(error)
|
|
56
|
+
});
|
|
57
|
+
} finally {
|
|
58
|
+
if (current === controller) current = null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
cancel() {
|
|
62
|
+
current?.abort();
|
|
63
|
+
current = null;
|
|
64
|
+
set(idle);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function messageOf(error) {
|
|
69
|
+
const value = error;
|
|
70
|
+
if (value?.code === 4001 || value?.name === "AbortError") return null;
|
|
71
|
+
return value?.shortMessage || error instanceof Error && error.message || "The connection did not complete.";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// wallet-client/src/core/popup.ts
|
|
75
|
+
var popupName = "juicebox-center";
|
|
76
|
+
var frameName = "juicebox-center-frame";
|
|
77
|
+
var callbackType = "juicebox-center:callback";
|
|
78
|
+
var receivedType = "juicebox-center:received";
|
|
79
|
+
function openCenterPopup(win) {
|
|
80
|
+
if (!win) return null;
|
|
81
|
+
const width = 460, height = 720;
|
|
82
|
+
const left = Math.max(
|
|
83
|
+
0,
|
|
84
|
+
Math.round((win.outerWidth ?? width) / 2 - width / 2 + (win.screenX ?? 0))
|
|
85
|
+
);
|
|
86
|
+
const top = Math.max(
|
|
87
|
+
0,
|
|
88
|
+
Math.round(
|
|
89
|
+
(win.outerHeight ?? height) / 2 - height / 2 + (win.screenY ?? 0)
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
try {
|
|
93
|
+
return win.open(
|
|
94
|
+
"",
|
|
95
|
+
popupName,
|
|
96
|
+
`popup,width=${width},height=${height},left=${left},top=${top}`
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var abort = () => new DOMException("The sign-in window was closed.", "AbortError");
|
|
103
|
+
function awaitPopupCallback(win, popup, signal) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const origin = win.location.origin;
|
|
106
|
+
const done = () => {
|
|
107
|
+
win.removeEventListener("message", onMessage);
|
|
108
|
+
signal.removeEventListener("abort", onAbort);
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
};
|
|
111
|
+
const onMessage = (event) => {
|
|
112
|
+
const data = event.data;
|
|
113
|
+
if (event.source !== popup || event.origin !== origin || data?.type !== callbackType || typeof data.url !== "string")
|
|
114
|
+
return;
|
|
115
|
+
done();
|
|
116
|
+
popup.postMessage({ type: receivedType }, origin);
|
|
117
|
+
resolve(data.url);
|
|
118
|
+
};
|
|
119
|
+
const onAbort = () => {
|
|
120
|
+
done();
|
|
121
|
+
popup.close();
|
|
122
|
+
reject(abort());
|
|
123
|
+
};
|
|
124
|
+
const timer = setInterval(() => {
|
|
125
|
+
if (!popup.closed) return;
|
|
126
|
+
done();
|
|
127
|
+
reject(abort());
|
|
128
|
+
}, 250);
|
|
129
|
+
win.addEventListener("message", onMessage);
|
|
130
|
+
signal.addEventListener("abort", onAbort);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function awaitFrameCallback(win, frame, signal) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const origin = win.location.origin;
|
|
136
|
+
const done = () => {
|
|
137
|
+
win.removeEventListener("message", onMessage);
|
|
138
|
+
signal.removeEventListener("abort", onAbort);
|
|
139
|
+
};
|
|
140
|
+
const onMessage = (event) => {
|
|
141
|
+
const data = event.data;
|
|
142
|
+
if (!frame.contentWindow || event.source !== frame.contentWindow || event.origin !== origin || data?.type !== callbackType || typeof data.url !== "string")
|
|
143
|
+
return;
|
|
144
|
+
done();
|
|
145
|
+
frame.contentWindow.postMessage({ type: receivedType }, origin);
|
|
146
|
+
resolve(data.url);
|
|
147
|
+
};
|
|
148
|
+
const onAbort = () => {
|
|
149
|
+
done();
|
|
150
|
+
reject(new DOMException("The sign-in was closed.", "AbortError"));
|
|
151
|
+
};
|
|
152
|
+
if (signal.aborted) return onAbort();
|
|
153
|
+
win.addEventListener("message", onMessage);
|
|
154
|
+
signal.addEventListener("abort", onAbort);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function deliverCenterCallback(url, options = {}) {
|
|
158
|
+
const win = options.window;
|
|
159
|
+
if (!win) return Promise.resolve(false);
|
|
160
|
+
const origin = win.location.origin;
|
|
161
|
+
if (win.parent && win.parent !== win) {
|
|
162
|
+
try {
|
|
163
|
+
if (win.parent.location.origin === origin) {
|
|
164
|
+
win.parent.postMessage({ type: callbackType, url }, origin);
|
|
165
|
+
return Promise.resolve(true);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const opener = win.opener;
|
|
171
|
+
if (!opener) return Promise.resolve(false);
|
|
172
|
+
try {
|
|
173
|
+
if (opener.closed || opener.location.origin !== origin)
|
|
174
|
+
return Promise.resolve(false);
|
|
175
|
+
} catch {
|
|
176
|
+
return Promise.resolve(false);
|
|
177
|
+
}
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
let settled = false;
|
|
180
|
+
const finish = (delivered) => {
|
|
181
|
+
clearInterval(again);
|
|
182
|
+
clearTimeout(cap);
|
|
183
|
+
if (delivered) {
|
|
184
|
+
win.removeEventListener("message", onMessage);
|
|
185
|
+
win.close();
|
|
186
|
+
}
|
|
187
|
+
if (!settled) {
|
|
188
|
+
settled = true;
|
|
189
|
+
resolve(delivered);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const onMessage = (event) => {
|
|
193
|
+
const data = event.data;
|
|
194
|
+
if (event.source !== opener || event.origin !== origin || data?.type !== receivedType)
|
|
195
|
+
return;
|
|
196
|
+
finish(true);
|
|
197
|
+
};
|
|
198
|
+
const offer = () => {
|
|
199
|
+
if (opener.closed) return finish(false);
|
|
200
|
+
opener.postMessage({ type: callbackType, url }, origin);
|
|
201
|
+
};
|
|
202
|
+
const again = setInterval(offer, options.retryMs ?? 1500);
|
|
203
|
+
const cap = setTimeout(() => finish(false), options.timeoutMs ?? 9e5);
|
|
204
|
+
win.addEventListener("message", onMessage);
|
|
205
|
+
offer();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/rest/client/paymentStates.ts
|
|
210
|
+
var settledPaymentStates = ["paid", "reverted", "cancelled", "expired", "refused"];
|
|
211
|
+
var isSettledPayment = (status) => settledPaymentStates.includes(status);
|
|
212
|
+
|
|
213
|
+
// wallet-client/src/core/passkey.ts
|
|
214
|
+
function passkeyOption(input) {
|
|
215
|
+
return {
|
|
216
|
+
id: "juicebox-center",
|
|
217
|
+
name: "Signa account",
|
|
218
|
+
async connect({ signal, frame }) {
|
|
219
|
+
signal.throwIfAborted();
|
|
220
|
+
const connected = async (connection) => {
|
|
221
|
+
signal.throwIfAborted();
|
|
222
|
+
await input.connected?.(connection);
|
|
223
|
+
};
|
|
224
|
+
const win = input.window ?? globalThis.window;
|
|
225
|
+
const popup = input.frame || input.popup === false ? null : openCenterPopup(win);
|
|
226
|
+
try {
|
|
227
|
+
const wallet = await input.wallet();
|
|
228
|
+
const payment = wallet.payments().pendingPayment();
|
|
229
|
+
if (payment && !isSettledPayment(payment.status))
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Finish your pending Signa payment before signing in again."
|
|
232
|
+
);
|
|
233
|
+
input.beforeLaunch?.();
|
|
234
|
+
let prepared = await wallet.prepareConnection().catch(async (error) => {
|
|
235
|
+
const code = error?.code;
|
|
236
|
+
if (code === "WALLET_HANDOFF_PENDING" || code === "WALLET_ALREADY_CONNECTED" && wallet.restoreConnection?.())
|
|
237
|
+
return null;
|
|
238
|
+
if (code === "WALLET_HANDOFF_CHANGED")
|
|
239
|
+
return wallet.prepareConnection();
|
|
240
|
+
if (code !== "WALLET_HANDOFF_EXPIRED" && code !== "WALLET_ALREADY_CONNECTED" && code !== "WALLET_STORAGE_INVALID")
|
|
241
|
+
throw error;
|
|
242
|
+
wallet.disconnect();
|
|
243
|
+
return wallet.prepareConnection();
|
|
244
|
+
});
|
|
245
|
+
if (!prepared) {
|
|
246
|
+
const restored = wallet.restoreConnection?.();
|
|
247
|
+
if (restored) {
|
|
248
|
+
await connected(restored);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let connection2;
|
|
252
|
+
try {
|
|
253
|
+
connection2 = await wallet.retryConnection();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const code = error?.code;
|
|
256
|
+
const raced = code === "WALLET_HANDOFF_CHANGED" ? wallet.restoreConnection?.() : null;
|
|
257
|
+
if (raced) {
|
|
258
|
+
await connected(raced);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (code !== "WALLET_REQUEST_REJECTED") throw error;
|
|
262
|
+
wallet.disconnect();
|
|
263
|
+
prepared = await wallet.prepareConnection();
|
|
264
|
+
}
|
|
265
|
+
if (!prepared) {
|
|
266
|
+
await connected(connection2);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
signal.throwIfAborted();
|
|
271
|
+
if (input.frame) {
|
|
272
|
+
frame(frameName, new URL(prepared.authorizationUrl).origin);
|
|
273
|
+
const element = await frameElement(win, signal);
|
|
274
|
+
prepared.launch({ target: frameName });
|
|
275
|
+
const url2 = await awaitFrameCallback(win, element, signal);
|
|
276
|
+
const connection2 = await wallet.completeConnection(url2);
|
|
277
|
+
await connected(connection2);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!popup) {
|
|
281
|
+
prepared.launch();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (popup.closed)
|
|
285
|
+
throw new DOMException(
|
|
286
|
+
"The sign-in window was closed.",
|
|
287
|
+
"AbortError"
|
|
288
|
+
);
|
|
289
|
+
prepared.launch({ target: popupName });
|
|
290
|
+
const url = await awaitPopupCallback(win, popup, signal);
|
|
291
|
+
const connection = await wallet.completeConnection(url);
|
|
292
|
+
await connected(connection);
|
|
293
|
+
} finally {
|
|
294
|
+
popup?.close();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function frameElement(win, signal) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const started = Date.now();
|
|
302
|
+
const look = () => {
|
|
303
|
+
if (signal.aborted)
|
|
304
|
+
return reject(
|
|
305
|
+
new DOMException("The sign-in was closed.", "AbortError")
|
|
306
|
+
);
|
|
307
|
+
const element = win.document?.querySelector(
|
|
308
|
+
`iframe[name="${frameName}"]`
|
|
309
|
+
);
|
|
310
|
+
if (element) return resolve(element);
|
|
311
|
+
if (Date.now() - started > 5e3)
|
|
312
|
+
return reject(new Error("The sign-in frame did not appear."));
|
|
313
|
+
setTimeout(look, 20);
|
|
314
|
+
};
|
|
315
|
+
look();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// wallet-client/src/core/callback.ts
|
|
320
|
+
async function completeCenterCallback(wallet, url) {
|
|
321
|
+
const parameters = new URL(url).searchParams;
|
|
322
|
+
if (parameters.has("review"))
|
|
323
|
+
return {
|
|
324
|
+
kind: "payment",
|
|
325
|
+
status: await wallet.payments().completePayment(url)
|
|
326
|
+
};
|
|
327
|
+
if (parameters.has("code"))
|
|
328
|
+
return {
|
|
329
|
+
kind: "connection",
|
|
330
|
+
connection: await wallet.completeConnection(url)
|
|
331
|
+
};
|
|
332
|
+
if (parameters.has("state"))
|
|
333
|
+
return {
|
|
334
|
+
kind: "payment",
|
|
335
|
+
status: await wallet.payments().refreshPayment()
|
|
336
|
+
};
|
|
337
|
+
return { kind: "connection", connection: await wallet.retryConnection() };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// wallet-client/src/core/index.ts
|
|
341
|
+
export * from "../index.js";
|
|
342
|
+
export {
|
|
343
|
+
completeCenterCallback,
|
|
344
|
+
createConnectController,
|
|
345
|
+
deliverCenterCallback,
|
|
346
|
+
passkeyOption
|
|
347
|
+
};
|