@me.jango/signa 0.3.8 → 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 +61 -10
- package/core/controller.d.ts +43 -0
- package/core/index.d.ts +79 -0
- package/core/index.js +347 -0
- package/index.d.ts +202 -148
- package/index.js +33 -20
- package/package.json +12 -5
- 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/react/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// wallet-client/src/react/JBConnectModal.tsx
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useSyncExternalStore
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// wallet-client/src/react/passkeyLabel.ts
|
|
12
|
+
function passkeyLabel(userAgent) {
|
|
13
|
+
if (/iPhone/.test(userAgent)) return "Continue with Face ID";
|
|
14
|
+
if (/Macintosh|iPad/.test(userAgent)) return "Continue with Touch ID";
|
|
15
|
+
if (/Windows/.test(userAgent)) return "Continue with Windows Hello";
|
|
16
|
+
return "Continue with a passkey";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// wallet-client/src/react/styles.ts
|
|
20
|
+
var connectModalCss = `
|
|
21
|
+
.jb-connect{--jb-connect-bg:#fff;--jb-connect-fg:#1a1a1a;--jb-connect-muted:#6b6b6b;--jb-connect-line:#dcdcdc;--jb-connect-accent:#1a1a1a;--jb-connect-accent-fg:#fff;--jb-connect-radius:16px;--jb-connect-tile:44px;--jb-connect-font:inherit;--jb-connect-pad:32px;--jb-connect-backdrop:rgba(0,0,0,.45);--jb-connect-width:440px;
|
|
22
|
+
box-sizing:border-box;width:min(var(--jb-connect-width),calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:var(--jb-connect-pad);margin:auto;border:1px solid var(--jb-connect-line);border-radius:var(--jb-connect-radius);background:var(--jb-connect-bg);color:var(--jb-connect-fg);font:16px/1.4 var(--jb-connect-font);overscroll-behavior:contain}
|
|
23
|
+
.jb-connect::backdrop{background:var(--jb-connect-backdrop)}
|
|
24
|
+
.jb-connect *{box-sizing:border-box}
|
|
25
|
+
.jb-connect h2{margin:0 0 20px;font-size:28px;font-weight:600;line-height:1.1}
|
|
26
|
+
.jb-connect button{font:inherit;color:inherit;cursor:pointer}
|
|
27
|
+
.jb-connect button:disabled{cursor:default;opacity:.55}
|
|
28
|
+
.jb-connect .jb-connect-primary{display:block;width:100%;min-height:50px;padding:14px 20px;border:0;border-radius:calc(var(--jb-connect-radius) / 2);background:var(--jb-connect-accent);color:var(--jb-connect-accent-fg);font-weight:600}
|
|
29
|
+
.jb-connect .jb-connect-powered{margin:8px 0 0;font-size:13px;color:var(--jb-connect-muted);text-align:center}
|
|
30
|
+
.jb-connect .jb-connect-frame{display:block;width:calc(100% + 2 * var(--jb-connect-pad));height:14rem;margin:0 calc(-1 * var(--jb-connect-pad));border:1px solid var(--jb-connect-line);border-width:1px 0;background:var(--jb-connect-bg);transition:height .2s ease}
|
|
31
|
+
@media (prefers-reduced-motion:reduce){.jb-connect .jb-connect-frame{transition:none}}
|
|
32
|
+
.jb-connect .jb-connect-divider{margin:24px 0 10px;font-size:13px;color:var(--jb-connect-muted)}
|
|
33
|
+
.jb-connect .jb-connect-tiles{display:flex;gap:10px;min-height:var(--jb-connect-tile);padding:4px;margin:-4px;overflow-x:auto}
|
|
34
|
+
.jb-connect .jb-connect-tile{display:grid;place-items:center;flex:0 0 var(--jb-connect-tile);width:var(--jb-connect-tile);height:var(--jb-connect-tile);padding:8px;border:1px solid var(--jb-connect-line);border-radius:9px;background:var(--jb-connect-bg)}
|
|
35
|
+
.jb-connect .jb-connect-tile img,.jb-connect .jb-connect-tile svg{width:22px;height:22px;object-fit:contain}
|
|
36
|
+
.jb-connect .jb-connect-tile span{font-size:12px;font-weight:600;line-height:1}
|
|
37
|
+
.jb-connect .jb-connect-status,.jb-connect .jb-connect-error{margin:16px 0 0;font-size:13px}
|
|
38
|
+
.jb-connect .jb-connect-status{color:var(--jb-connect-muted)}
|
|
39
|
+
.jb-connect .jb-connect-error{padding:12px 15px;border-left:3px solid #c8755e;background:#fae9e4;color:#873b2e;overflow-wrap:anywhere}
|
|
40
|
+
.jb-connect .jb-connect-handoff{margin-top:16px}
|
|
41
|
+
.jb-connect .jb-connect-footer{display:flex;align-items:baseline;justify-content:flex-end;gap:16px;margin-top:20px}
|
|
42
|
+
.jb-connect .jb-connect-text{min-height:auto;padding:4px 0;margin:-4px 0;border:0;background:none;font-size:14px;text-decoration:underline;text-underline-offset:3px}
|
|
43
|
+
@media (max-width:540px){.jb-connect{--jb-connect-pad:20px;padding:24px var(--jb-connect-pad)}.jb-connect h2{font-size:24px}}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// wallet-client/src/react/JBConnectModal.tsx
|
|
47
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
48
|
+
function JBConnectModal(props) {
|
|
49
|
+
const { controller, open, onClose, passkeyId = "juicebox-center" } = props;
|
|
50
|
+
const state = useSyncExternalStore(
|
|
51
|
+
controller.subscribe,
|
|
52
|
+
controller.getState,
|
|
53
|
+
controller.getState
|
|
54
|
+
);
|
|
55
|
+
const dialog = useRef(null);
|
|
56
|
+
const [platformLabel, setPlatformLabel] = useState("Continue with a passkey");
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setPlatformLabel(passkeyLabel(navigator.userAgent));
|
|
59
|
+
}, []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const element = dialog.current;
|
|
62
|
+
if (open && !element.open) {
|
|
63
|
+
element.showModal();
|
|
64
|
+
element.querySelector("h2")?.focus();
|
|
65
|
+
} else if (!open && element.open) {
|
|
66
|
+
element.close();
|
|
67
|
+
controller.cancel();
|
|
68
|
+
}
|
|
69
|
+
}, [open, controller]);
|
|
70
|
+
const cancel = () => {
|
|
71
|
+
controller.cancel();
|
|
72
|
+
onClose();
|
|
73
|
+
};
|
|
74
|
+
const frame = useRef(null);
|
|
75
|
+
const [frameHeight, setFrameHeight] = useState();
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!state.frameName) return;
|
|
78
|
+
setFrameHeight(void 0);
|
|
79
|
+
const sized = (event) => {
|
|
80
|
+
const data = event.data;
|
|
81
|
+
if (!frame.current?.contentWindow || event.source !== frame.current.contentWindow || data?.type !== "juicebox-center:size" || typeof data.height !== "number" || !Number.isFinite(data.height))
|
|
82
|
+
return;
|
|
83
|
+
setFrameHeight(Math.min(1200, Math.max(160, Math.ceil(data.height))) + 2);
|
|
84
|
+
if (dialog.current)
|
|
85
|
+
frame.current.contentWindow.postMessage(
|
|
86
|
+
{ type: "juicebox-center:theme", theme: themeOf(dialog.current) },
|
|
87
|
+
// Design tokens only; the page inside checks its framer, this dialog does not learn the issuer.
|
|
88
|
+
"*"
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener("message", sized);
|
|
92
|
+
return () => window.removeEventListener("message", sized);
|
|
93
|
+
}, [state.frameName]);
|
|
94
|
+
const passkey = controller.options.find((option) => option.id === passkeyId);
|
|
95
|
+
const wallets = controller.options.filter(
|
|
96
|
+
(option) => option.id !== passkeyId
|
|
97
|
+
);
|
|
98
|
+
const pending = state.pending ? controller.options.find((option) => option.id === state.pending) : void 0;
|
|
99
|
+
return /* @__PURE__ */ jsxs(
|
|
100
|
+
"dialog",
|
|
101
|
+
{
|
|
102
|
+
ref: dialog,
|
|
103
|
+
className: "jb-connect" + (props.className ? " " + props.className : ""),
|
|
104
|
+
"aria-label": typeof props.title === "string" ? props.title : "Sign in",
|
|
105
|
+
onCancel: (event) => {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
cancel();
|
|
108
|
+
},
|
|
109
|
+
onMouseDown: (event) => {
|
|
110
|
+
if (event.target !== event.currentTarget) return;
|
|
111
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
112
|
+
if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom)
|
|
113
|
+
cancel();
|
|
114
|
+
},
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsx("style", { children: connectModalCss }),
|
|
117
|
+
/* @__PURE__ */ jsx("h2", { tabIndex: -1, children: props.title ?? "Sign in" }),
|
|
118
|
+
pending ? /* @__PURE__ */ jsx("div", { role: "status", "aria-label": "Connection", children: state.frameName ? /* @__PURE__ */ jsx(
|
|
119
|
+
"iframe",
|
|
120
|
+
{
|
|
121
|
+
ref: frame,
|
|
122
|
+
name: state.frameName,
|
|
123
|
+
className: "jb-connect-frame",
|
|
124
|
+
title: "Signa",
|
|
125
|
+
allow: `publickey-credentials-get ${state.frameOrigin}; publickey-credentials-create ${state.frameOrigin}`,
|
|
126
|
+
referrerPolicy: "no-referrer",
|
|
127
|
+
style: frameHeight ? { height: frameHeight } : void 0
|
|
128
|
+
}
|
|
129
|
+
) : state.handoffUri && props.renderHandoff ? /* @__PURE__ */ jsx("div", { className: "jb-connect-handoff", children: props.renderHandoff(state.handoffUri, pending) }) : /* @__PURE__ */ jsx("p", { className: "jb-connect-status", children: pending === passkey ? "Continuing at Signa\u2026" : `Opening ${pending.name}\u2026` }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
130
|
+
passkey && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
131
|
+
/* @__PURE__ */ jsx(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
className: "jb-connect-primary",
|
|
136
|
+
disabled: passkey.disabled,
|
|
137
|
+
onClick: () => void controller.choose(passkey.id),
|
|
138
|
+
children: props.passkeyLabel ?? platformLabel
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx("p", { className: "jb-connect-powered", children: "Powered by Signa" })
|
|
142
|
+
] }),
|
|
143
|
+
wallets.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx("p", { className: "jb-connect-divider", children: passkey ? "or connect a wallet" : "Connect a wallet" }),
|
|
145
|
+
/* @__PURE__ */ jsx("div", { className: "jb-connect-tiles", children: wallets.map((option) => /* @__PURE__ */ jsx(
|
|
146
|
+
"button",
|
|
147
|
+
{
|
|
148
|
+
type: "button",
|
|
149
|
+
className: "jb-connect-tile",
|
|
150
|
+
title: option.name,
|
|
151
|
+
"aria-label": option.name,
|
|
152
|
+
disabled: option.disabled,
|
|
153
|
+
onClick: () => void controller.choose(option.id),
|
|
154
|
+
children: option.icon ? /* @__PURE__ */ jsx("img", { src: option.icon, alt: "" }) : props.renderIcon?.(option) ?? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: option.name.slice(0, 1) })
|
|
155
|
+
},
|
|
156
|
+
option.id
|
|
157
|
+
)) })
|
|
158
|
+
] }),
|
|
159
|
+
props.children
|
|
160
|
+
] }),
|
|
161
|
+
state.error && /* @__PURE__ */ jsx("p", { role: "alert", className: "jb-connect-error", children: state.error }),
|
|
162
|
+
/* @__PURE__ */ jsxs("div", { className: "jb-connect-footer", children: [
|
|
163
|
+
props.footer,
|
|
164
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "jb-connect-text", onClick: cancel, children: "Cancel" })
|
|
165
|
+
] })
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
function themeOf(dialog) {
|
|
171
|
+
const style = getComputedStyle(dialog);
|
|
172
|
+
const token = (name) => style.getPropertyValue(name).trim();
|
|
173
|
+
const theme = {};
|
|
174
|
+
for (const [key, name] of [
|
|
175
|
+
["background", "--jb-connect-bg"],
|
|
176
|
+
["foreground", "--jb-connect-fg"],
|
|
177
|
+
["muted", "--jb-connect-muted"],
|
|
178
|
+
["line", "--jb-connect-line"],
|
|
179
|
+
["accent", "--jb-connect-accent"],
|
|
180
|
+
["accentForeground", "--jb-connect-accent-fg"],
|
|
181
|
+
["radius", "--jb-connect-radius"],
|
|
182
|
+
["inset", "--jb-connect-pad"]
|
|
183
|
+
]) {
|
|
184
|
+
const value = token(name);
|
|
185
|
+
if (value) theme[key] = value;
|
|
186
|
+
}
|
|
187
|
+
const font = style.fontFamily;
|
|
188
|
+
if (font) theme.font = font;
|
|
189
|
+
return theme;
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
JBConnectModal,
|
|
193
|
+
connectModalCss,
|
|
194
|
+
passkeyLabel
|
|
195
|
+
};
|
package/wagmi/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CreateConnectorFn } from 'wagmi';
|
|
2
|
+
import { Address } from 'viem';
|
|
3
|
+
|
|
4
|
+
/** The slice of a Signa wallet client the connector needs. Signing never crosses this seam. */
|
|
5
|
+
type CenterAccountWallet = {
|
|
6
|
+
restoreConnection(): {
|
|
7
|
+
address: Address;
|
|
8
|
+
chainId: 8453;
|
|
9
|
+
accountId: string;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
} | null;
|
|
12
|
+
disconnect(): void;
|
|
13
|
+
};
|
|
14
|
+
type CenterAccountConnectorOptions = {
|
|
15
|
+
wallet(): Promise<CenterAccountWallet>;
|
|
16
|
+
read(input: {
|
|
17
|
+
method: string;
|
|
18
|
+
params?: unknown[];
|
|
19
|
+
}): Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
/** A read-only wagmi connector for a Signa passkey account on Base. The connection's
|
|
22
|
+
* request key cannot sign or send arbitrary operations; payments go through Signa's review flow. */
|
|
23
|
+
declare function centerAccountConnector(options: CenterAccountConnectorOptions): CreateConnectorFn;
|
|
24
|
+
|
|
25
|
+
export { centerAccountConnector };
|
|
26
|
+
export type { CenterAccountConnectorOptions, CenterAccountWallet };
|
package/wagmi/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// wallet-client/src/wagmi/connector.ts
|
|
4
|
+
import { createConnector } from "wagmi";
|
|
5
|
+
import { base } from "wagmi/chains";
|
|
6
|
+
var reads = /* @__PURE__ */ new Set([
|
|
7
|
+
"eth_call",
|
|
8
|
+
"eth_getBalance",
|
|
9
|
+
"eth_getCode",
|
|
10
|
+
"eth_getStorageAt",
|
|
11
|
+
"eth_getTransactionCount",
|
|
12
|
+
"eth_getTransactionReceipt",
|
|
13
|
+
"eth_getTransactionByHash",
|
|
14
|
+
"eth_getBlockByNumber",
|
|
15
|
+
"eth_getBlockByHash",
|
|
16
|
+
"eth_blockNumber",
|
|
17
|
+
"eth_getLogs"
|
|
18
|
+
]);
|
|
19
|
+
var unsupported = (chain = false) => Object.assign(
|
|
20
|
+
new Error(
|
|
21
|
+
chain ? "This Signa account uses Base. Connect an external wallet for other networks." : "This action needs its own review at Signa. Use a payment review, or connect an external wallet."
|
|
22
|
+
),
|
|
23
|
+
{ code: chain ? 4902 : 4200 }
|
|
24
|
+
);
|
|
25
|
+
function centerAccountConnector(options) {
|
|
26
|
+
return createConnector((config) => {
|
|
27
|
+
const connection = async () => {
|
|
28
|
+
const value = (await options.wallet()).restoreConnection();
|
|
29
|
+
if (!value || value.chainId !== 8453 || value.accountId !== "eip155:8453:" + value.address.toLowerCase() || value.expiresAt <= Math.floor(Date.now() / 1e3))
|
|
30
|
+
return null;
|
|
31
|
+
return value;
|
|
32
|
+
};
|
|
33
|
+
const provider = {
|
|
34
|
+
async request(input) {
|
|
35
|
+
const current = await connection();
|
|
36
|
+
if (input.method === "eth_chainId") return "0x2105";
|
|
37
|
+
if (input.method === "eth_accounts" || input.method === "eth_requestAccounts")
|
|
38
|
+
return current ? [current.address] : [];
|
|
39
|
+
if (input.method === "wallet_switchEthereumChain") {
|
|
40
|
+
const requested = input.params?.[0];
|
|
41
|
+
if (requested?.chainId !== "0x2105") throw unsupported(true);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (reads.has(input.method)) return options.read(input);
|
|
45
|
+
throw unsupported();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
id: "juicebox-center",
|
|
50
|
+
name: "Signa account",
|
|
51
|
+
type: "juicebox-center",
|
|
52
|
+
async connect(parameters) {
|
|
53
|
+
if (parameters?.chainId !== void 0 && parameters.chainId !== 8453)
|
|
54
|
+
throw unsupported(true);
|
|
55
|
+
const current = await connection();
|
|
56
|
+
if (!current)
|
|
57
|
+
throw new Error("Sign in with your Signa account first.");
|
|
58
|
+
const accounts = parameters?.withCapabilities ? [{ address: current.address, capabilities: {} }] : [current.address];
|
|
59
|
+
return { accounts, chainId: 8453 };
|
|
60
|
+
},
|
|
61
|
+
async disconnect() {
|
|
62
|
+
(await options.wallet()).disconnect();
|
|
63
|
+
},
|
|
64
|
+
async getAccounts() {
|
|
65
|
+
const current = await connection();
|
|
66
|
+
return current ? [current.address] : [];
|
|
67
|
+
},
|
|
68
|
+
async getChainId() {
|
|
69
|
+
return 8453;
|
|
70
|
+
},
|
|
71
|
+
async getProvider() {
|
|
72
|
+
return provider;
|
|
73
|
+
},
|
|
74
|
+
async isAuthorized() {
|
|
75
|
+
return !!await connection();
|
|
76
|
+
},
|
|
77
|
+
async switchChain({ chainId }) {
|
|
78
|
+
if (chainId !== 8453) throw unsupported(true);
|
|
79
|
+
config.emitter.emit("change", { chainId });
|
|
80
|
+
return base;
|
|
81
|
+
},
|
|
82
|
+
onAccountsChanged(accounts) {
|
|
83
|
+
if (!accounts.length) config.emitter.emit("disconnect");
|
|
84
|
+
else config.emitter.emit("change", { accounts });
|
|
85
|
+
},
|
|
86
|
+
onChainChanged(chainId) {
|
|
87
|
+
if (Number(chainId) !== 8453) config.emitter.emit("disconnect");
|
|
88
|
+
},
|
|
89
|
+
onDisconnect() {
|
|
90
|
+
config.emitter.emit("disconnect");
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
centerAccountConnector
|
|
97
|
+
};
|