@dloizides/ui-access-admin 1.0.1
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/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/index.d.mts +544 -0
- package/dist/index.d.ts +544 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +558 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +121 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, Linking } from 'react-native';
|
|
3
|
+
import { Button } from '@dloizides/ui-buttons';
|
|
4
|
+
import { useUi } from '@dloizides/ui-feedback';
|
|
5
|
+
import { copyText } from '@dloizides/ui-layout';
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
+
import { SelectControl } from '@dloizides/ui-forms';
|
|
8
|
+
|
|
9
|
+
// src/AccessLinkPanel/AccessLinkPanel.tsx
|
|
10
|
+
|
|
11
|
+
// src/AccessResultTone.ts
|
|
12
|
+
var AccessResultTone = /* @__PURE__ */ ((AccessResultTone2) => {
|
|
13
|
+
AccessResultTone2["Success"] = "success";
|
|
14
|
+
AccessResultTone2["Error"] = "error";
|
|
15
|
+
return AccessResultTone2;
|
|
16
|
+
})(AccessResultTone || {});
|
|
17
|
+
|
|
18
|
+
// src/constants.ts
|
|
19
|
+
var ACCESS_ADMIN_I18N = {
|
|
20
|
+
// ── AccessLinkPanel: framing ────────────────────────────────────────────────
|
|
21
|
+
/** Panel heading. `{{p1}}` = subject display name. */
|
|
22
|
+
panelHeading: "accessAdmin.link.panelHeading",
|
|
23
|
+
/** Supporting line explaining what the link grants. */
|
|
24
|
+
panelNote: "accessAdmin.link.panelNote",
|
|
25
|
+
/** The security warning. Shown always, not only after a mint — see AccessLinkPanel. */
|
|
26
|
+
capabilityWarning: "accessAdmin.link.capabilityWarning",
|
|
27
|
+
/** "A link created now stops working on {{p1}}." `{{p1}}` = yyyy-mm-dd. */
|
|
28
|
+
expiryPreview: "accessAdmin.link.expiryPreview",
|
|
29
|
+
// ── AccessLinkPanel: minting ────────────────────────────────────────────────
|
|
30
|
+
mint: "accessAdmin.link.mint",
|
|
31
|
+
mintHint: "accessAdmin.link.mintHint",
|
|
32
|
+
/** Heading over the revealed URL. `{{p1}}` = subject display name. */
|
|
33
|
+
mintedTitle: "accessAdmin.link.mintedTitle",
|
|
34
|
+
/** "Shown once — copy it now." The honesty that makes the one-shot reveal safe. */
|
|
35
|
+
mintedHint: "accessAdmin.link.mintedHint",
|
|
36
|
+
/** Reported when the mint landed AND the clipboard write succeeded. `{{p1}}` = name. */
|
|
37
|
+
copySuccess: "accessAdmin.link.copySuccess",
|
|
38
|
+
/** Reported when the mint landed but the clipboard REFUSED. `{{p1}}` = name. */
|
|
39
|
+
mintNotCopied: "accessAdmin.link.mintNotCopied",
|
|
40
|
+
/** Reported when a re-copy of an already-revealed URL failed. `{{p1}}` = name. */
|
|
41
|
+
copyFailed: "accessAdmin.link.copyFailed",
|
|
42
|
+
// ── AccessLinkPanel: acting on a revealed URL ───────────────────────────────
|
|
43
|
+
recopy: "accessAdmin.link.recopy",
|
|
44
|
+
recopyHint: "accessAdmin.link.recopyHint",
|
|
45
|
+
copied: "accessAdmin.link.copied",
|
|
46
|
+
dismiss: "accessAdmin.link.dismiss",
|
|
47
|
+
dismissHint: "accessAdmin.link.dismissHint",
|
|
48
|
+
email: "accessAdmin.link.email",
|
|
49
|
+
emailHint: "accessAdmin.link.emailHint",
|
|
50
|
+
/** "This opens YOUR mail app — we do not send it for you." */
|
|
51
|
+
emailNote: "accessAdmin.link.emailNote",
|
|
52
|
+
/** Mail subject. `{{p1}}` = subject display name. */
|
|
53
|
+
emailSubject: "accessAdmin.link.emailSubject",
|
|
54
|
+
/** Mail body. `{{p1}}` = name, `{{p2}}` = url, `{{p3}}` = expiry date. */
|
|
55
|
+
emailBody: "accessAdmin.link.emailBody",
|
|
56
|
+
// ── AccessLinkPanel: the live list ──────────────────────────────────────────
|
|
57
|
+
/** "Live links ({{p1}})". `{{p1}}` = count. */
|
|
58
|
+
liveHeading: "accessAdmin.link.liveHeading",
|
|
59
|
+
/** "Created {{p1}} · stops working {{p2}}". */
|
|
60
|
+
liveMeta: "accessAdmin.link.liveMeta",
|
|
61
|
+
/** Shown in `liveMeta`'s `{{p2}}` slot when a link has no expiry at all. */
|
|
62
|
+
expiresNever: "accessAdmin.link.expiresNever",
|
|
63
|
+
noLive: "accessAdmin.link.noLive",
|
|
64
|
+
/** Shown when the link list FAILED to load — never conflated with "there are none". */
|
|
65
|
+
liveLoadError: "accessAdmin.link.liveLoadError",
|
|
66
|
+
close: "accessAdmin.link.close",
|
|
67
|
+
closeHint: "accessAdmin.link.closeHint",
|
|
68
|
+
// ── AccountLinkPanel ────────────────────────────────────────────────────────
|
|
69
|
+
/** Panel heading. `{{p1}}` = subject display name. */
|
|
70
|
+
accountPanelHeading: "accessAdmin.account.panelHeading",
|
|
71
|
+
accountPanelNote: "accessAdmin.account.panelNote",
|
|
72
|
+
accountPickerLabel: "accessAdmin.account.pickerLabel",
|
|
73
|
+
accountPickerHint: "accessAdmin.account.pickerHint",
|
|
74
|
+
accountPickerPlaceholder: "accessAdmin.account.pickerPlaceholder",
|
|
75
|
+
accountOptionHint: "accessAdmin.account.optionHint",
|
|
76
|
+
/** Label for a directory entry carrying neither a display name nor an email. */
|
|
77
|
+
accountUnnamed: "accessAdmin.account.unnamed",
|
|
78
|
+
/** Shown when the directory is empty (or every entry is taken by another subject). */
|
|
79
|
+
accountNone: "accessAdmin.account.none",
|
|
80
|
+
accountLink: "accessAdmin.account.link",
|
|
81
|
+
accountLinkHint: "accessAdmin.account.linkHint",
|
|
82
|
+
/** `{{p1}}` = subject display name. */
|
|
83
|
+
accountLinkSuccess: "accessAdmin.account.linkSuccess",
|
|
84
|
+
accountUnlink: "accessAdmin.account.unlink",
|
|
85
|
+
accountUnlinkHint: "accessAdmin.account.unlinkHint",
|
|
86
|
+
/** `{{p1}}` = subject display name. */
|
|
87
|
+
accountUnlinkSuccess: "accessAdmin.account.unlinkSuccess",
|
|
88
|
+
accountClose: "accessAdmin.account.close",
|
|
89
|
+
accountCloseHint: "accessAdmin.account.closeHint"
|
|
90
|
+
};
|
|
91
|
+
var ACCESS_ADMIN_TEST_IDS = {
|
|
92
|
+
linkPanel: "access-link-panel",
|
|
93
|
+
accountPanel: "account-link-panel"
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/expiry/expiryPolicy.ts
|
|
97
|
+
var MS_PER_DAY = 864e5;
|
|
98
|
+
var ISO_DATE_LENGTH = 10;
|
|
99
|
+
function daysUntilAnchor(anchorDate, now) {
|
|
100
|
+
if (typeof anchorDate !== "string" || anchorDate.trim() === "") return null;
|
|
101
|
+
const anchor = Date.parse(anchorDate);
|
|
102
|
+
if (Number.isNaN(anchor)) return null;
|
|
103
|
+
return Math.floor(anchor / MS_PER_DAY) - Math.floor(now.getTime() / MS_PER_DAY);
|
|
104
|
+
}
|
|
105
|
+
function capabilityExpiryDays(policy, now) {
|
|
106
|
+
const untilAnchor = daysUntilAnchor(policy.anchorDate, now);
|
|
107
|
+
const raw = untilAnchor === null ? policy.tailDays : untilAnchor + policy.tailDays;
|
|
108
|
+
if (raw < policy.minDays) return policy.minDays;
|
|
109
|
+
if (raw > policy.maxDays) return policy.maxDays;
|
|
110
|
+
return raw;
|
|
111
|
+
}
|
|
112
|
+
function capabilityExpiryDate(policy, now) {
|
|
113
|
+
const days = capabilityExpiryDays(policy, now);
|
|
114
|
+
return new Date(now.getTime() + days * MS_PER_DAY).toISOString().slice(0, ISO_DATE_LENGTH);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/links/capabilityLinkHelpers.ts
|
|
118
|
+
var ISO_DATE_LENGTH2 = 10;
|
|
119
|
+
function linksForSubject(links, subjectId) {
|
|
120
|
+
return links.filter((link) => link.subjectId === subjectId).sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")));
|
|
121
|
+
}
|
|
122
|
+
function activeLinksForSubject(links, subjectId) {
|
|
123
|
+
return linksForSubject(links, subjectId).filter((link) => link.isActive);
|
|
124
|
+
}
|
|
125
|
+
function formatLinkDate(value, fallback) {
|
|
126
|
+
if (typeof value !== "string" || value.trim() === "") return fallback;
|
|
127
|
+
return value.slice(0, ISO_DATE_LENGTH2);
|
|
128
|
+
}
|
|
129
|
+
function buildCapabilityLinkMailto(subject, body, url) {
|
|
130
|
+
if (url.trim() === "") return null;
|
|
131
|
+
return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
132
|
+
}
|
|
133
|
+
function useCapabilityLinkMint(options) {
|
|
134
|
+
const { subject, policy, mint, describeError, onResult, t, now, clipboardWriter } = options;
|
|
135
|
+
const [minted, setMinted] = useState(null);
|
|
136
|
+
const [copied, setCopied] = useState(false);
|
|
137
|
+
const [isPending, setIsPending] = useState(false);
|
|
138
|
+
const mountedRef = useRef(true);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
mountedRef.current = true;
|
|
141
|
+
return () => {
|
|
142
|
+
mountedRef.current = false;
|
|
143
|
+
};
|
|
144
|
+
}, []);
|
|
145
|
+
const copyUrl = useCallback(
|
|
146
|
+
async (url) => {
|
|
147
|
+
const ok = await copyText(url, clipboardWriter);
|
|
148
|
+
if (mountedRef.current) setCopied(ok);
|
|
149
|
+
return ok;
|
|
150
|
+
},
|
|
151
|
+
[clipboardWriter]
|
|
152
|
+
);
|
|
153
|
+
const runMint = useCallback(() => {
|
|
154
|
+
if (isPending) return;
|
|
155
|
+
setIsPending(true);
|
|
156
|
+
void (async () => {
|
|
157
|
+
try {
|
|
158
|
+
const expiresInDays = capabilityExpiryDays(policy, now === void 0 ? /* @__PURE__ */ new Date() : now());
|
|
159
|
+
const link = await mint({ subject, expiresInDays });
|
|
160
|
+
if (!mountedRef.current) return;
|
|
161
|
+
setMinted(link);
|
|
162
|
+
const ok = await copyUrl(link.url ?? "");
|
|
163
|
+
if (!mountedRef.current) return;
|
|
164
|
+
onResult(
|
|
165
|
+
t(
|
|
166
|
+
ok ? ACCESS_ADMIN_I18N.copySuccess : ACCESS_ADMIN_I18N.mintNotCopied,
|
|
167
|
+
subject.displayName
|
|
168
|
+
),
|
|
169
|
+
ok ? "success" /* Success */ : "error" /* Error */
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!mountedRef.current) return;
|
|
173
|
+
onResult(describeError(error), "error" /* Error */);
|
|
174
|
+
} finally {
|
|
175
|
+
if (mountedRef.current) setIsPending(false);
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
}, [isPending, policy, now, mint, subject, copyUrl, onResult, t, describeError]);
|
|
179
|
+
const recopy = useCallback(() => {
|
|
180
|
+
const url = minted?.url ?? "";
|
|
181
|
+
if (url === "") return;
|
|
182
|
+
void (async () => {
|
|
183
|
+
const ok = await copyUrl(url);
|
|
184
|
+
if (!mountedRef.current) return;
|
|
185
|
+
onResult(
|
|
186
|
+
t(ok ? ACCESS_ADMIN_I18N.copySuccess : ACCESS_ADMIN_I18N.copyFailed, subject.displayName),
|
|
187
|
+
ok ? "success" /* Success */ : "error" /* Error */
|
|
188
|
+
);
|
|
189
|
+
})();
|
|
190
|
+
}, [minted, copyUrl, onResult, t, subject.displayName]);
|
|
191
|
+
const dismiss = useCallback(() => {
|
|
192
|
+
setMinted(null);
|
|
193
|
+
setCopied(false);
|
|
194
|
+
}, []);
|
|
195
|
+
return { minted, isPending, copied, mint: runMint, recopy, dismiss };
|
|
196
|
+
}
|
|
197
|
+
function useAccessAdminPalette() {
|
|
198
|
+
const { theme } = useUi();
|
|
199
|
+
const { colors, palette, semantic } = theme;
|
|
200
|
+
return useMemo(() => {
|
|
201
|
+
const success = semantic.success ?? semantic.error;
|
|
202
|
+
return {
|
|
203
|
+
panelSurface: colors.surface,
|
|
204
|
+
border: colors.border,
|
|
205
|
+
textPrimary: colors.text,
|
|
206
|
+
textSecondary: colors.textSecondary,
|
|
207
|
+
danger: semantic.error["500"],
|
|
208
|
+
// A tinted step when the host's scale has one, else the elevated surface —
|
|
209
|
+
// never a hardcoded pastel, which would be wrong in dark mode.
|
|
210
|
+
revealSurface: success["50"] ?? success["100"] ?? colors.surfaceElevated,
|
|
211
|
+
revealBorder: success["500"],
|
|
212
|
+
revealHeading: palette.primary["700"] ?? palette.primary["500"]
|
|
213
|
+
};
|
|
214
|
+
}, [colors, palette, semantic]);
|
|
215
|
+
}
|
|
216
|
+
var SPACE_XS = 4;
|
|
217
|
+
var SPACE_SM = 8;
|
|
218
|
+
var SPACE_MD = 12;
|
|
219
|
+
var SPACE_LG = 16;
|
|
220
|
+
var FONT_SM = 12;
|
|
221
|
+
var FONT_MD = 13;
|
|
222
|
+
var AccessLinkPanel = ({
|
|
223
|
+
subject,
|
|
224
|
+
policy,
|
|
225
|
+
links,
|
|
226
|
+
linksFailed = false,
|
|
227
|
+
mint,
|
|
228
|
+
describeError,
|
|
229
|
+
onClose,
|
|
230
|
+
onResult,
|
|
231
|
+
renderLinkActions,
|
|
232
|
+
now,
|
|
233
|
+
clipboardWriter,
|
|
234
|
+
testIDPrefix = ACCESS_ADMIN_TEST_IDS.linkPanel
|
|
235
|
+
}) => {
|
|
236
|
+
const { t } = useUi();
|
|
237
|
+
const palette = useAccessAdminPalette();
|
|
238
|
+
const styles = useMemo(() => buildStyles(palette), [palette]);
|
|
239
|
+
const state = useCapabilityLinkMint({
|
|
240
|
+
subject,
|
|
241
|
+
policy,
|
|
242
|
+
mint,
|
|
243
|
+
describeError,
|
|
244
|
+
onResult,
|
|
245
|
+
t,
|
|
246
|
+
now,
|
|
247
|
+
clipboardWriter
|
|
248
|
+
});
|
|
249
|
+
const expiryDate = useMemo(
|
|
250
|
+
() => capabilityExpiryDate(policy, now === void 0 ? /* @__PURE__ */ new Date() : now()),
|
|
251
|
+
[policy, now]
|
|
252
|
+
);
|
|
253
|
+
const liveLinks = useMemo(() => activeLinksForSubject(links, subject.id), [links, subject.id]);
|
|
254
|
+
const mintedUrl = state.minted?.url ?? "";
|
|
255
|
+
const handleEmail = () => {
|
|
256
|
+
const mailto = buildCapabilityLinkMailto(
|
|
257
|
+
t(ACCESS_ADMIN_I18N.emailSubject, subject.displayName),
|
|
258
|
+
t(ACCESS_ADMIN_I18N.emailBody, subject.displayName, mintedUrl, expiryDate),
|
|
259
|
+
mintedUrl
|
|
260
|
+
);
|
|
261
|
+
if (mailto === null) return;
|
|
262
|
+
void Linking.openURL(mailto).catch(() => {
|
|
263
|
+
onResult(t(ACCESS_ADMIN_I18N.copyFailed, subject.displayName), "error" /* Error */);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.panel, testID: testIDPrefix, children: [
|
|
267
|
+
/* @__PURE__ */ jsx(Text, { style: styles.heading, children: t(ACCESS_ADMIN_I18N.panelHeading, subject.displayName) }),
|
|
268
|
+
/* @__PURE__ */ jsx(Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.panelNote) }),
|
|
269
|
+
/* @__PURE__ */ jsx(Text, { style: styles.warning, testID: `${testIDPrefix}-capability-warning`, children: t(ACCESS_ADMIN_I18N.capabilityWarning) }),
|
|
270
|
+
/* @__PURE__ */ jsx(Text, { style: styles.note, testID: `${testIDPrefix}-expiry-preview`, children: t(ACCESS_ADMIN_I18N.expiryPreview, expiryDate) }),
|
|
271
|
+
state.minted !== null ? /* @__PURE__ */ jsxs(View, { style: styles.revealBox, testID: `${testIDPrefix}-minted`, children: [
|
|
272
|
+
/* @__PURE__ */ jsx(Text, { style: styles.revealTitle, children: t(ACCESS_ADMIN_I18N.mintedTitle, subject.displayName) }),
|
|
273
|
+
/* @__PURE__ */ jsx(Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.mintedHint) }),
|
|
274
|
+
/* @__PURE__ */ jsx(Text, { selectable: true, style: styles.url, testID: `${testIDPrefix}-minted-url`, children: mintedUrl }),
|
|
275
|
+
/* @__PURE__ */ jsxs(View, { style: styles.actions, children: [
|
|
276
|
+
/* @__PURE__ */ jsx(
|
|
277
|
+
Button,
|
|
278
|
+
{
|
|
279
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.recopyHint),
|
|
280
|
+
accessibilityLabel: state.copied ? t(ACCESS_ADMIN_I18N.copied) : t(ACCESS_ADMIN_I18N.recopy),
|
|
281
|
+
label: state.copied ? t(ACCESS_ADMIN_I18N.copied) : t(ACCESS_ADMIN_I18N.recopy),
|
|
282
|
+
onPress: state.recopy,
|
|
283
|
+
testID: `${testIDPrefix}-recopy`
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
/* @__PURE__ */ jsx(
|
|
287
|
+
Button,
|
|
288
|
+
{
|
|
289
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.emailHint),
|
|
290
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.email),
|
|
291
|
+
label: t(ACCESS_ADMIN_I18N.email),
|
|
292
|
+
onPress: handleEmail,
|
|
293
|
+
testID: `${testIDPrefix}-email`,
|
|
294
|
+
variant: "secondary"
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
/* @__PURE__ */ jsx(
|
|
298
|
+
Button,
|
|
299
|
+
{
|
|
300
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.dismissHint),
|
|
301
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.dismiss),
|
|
302
|
+
label: t(ACCESS_ADMIN_I18N.dismiss),
|
|
303
|
+
onPress: state.dismiss,
|
|
304
|
+
testID: `${testIDPrefix}-dismiss`,
|
|
305
|
+
variant: "secondary"
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
] }),
|
|
309
|
+
/* @__PURE__ */ jsx(Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.emailNote) })
|
|
310
|
+
] }) : null,
|
|
311
|
+
linksFailed ? /* @__PURE__ */ jsx(Text, { style: styles.warning, testID: `${testIDPrefix}-load-error`, children: t(ACCESS_ADMIN_I18N.liveLoadError) }) : null,
|
|
312
|
+
!linksFailed && liveLinks.length > 0 ? /* @__PURE__ */ jsxs(View, { style: styles.liveBox, children: [
|
|
313
|
+
/* @__PURE__ */ jsx(Text, { style: styles.subheading, children: t(ACCESS_ADMIN_I18N.liveHeading, String(liveLinks.length)) }),
|
|
314
|
+
liveLinks.map((link) => /* @__PURE__ */ jsxs(View, { style: styles.liveRow, children: [
|
|
315
|
+
/* @__PURE__ */ jsx(Text, { style: styles.liveMeta, children: t(
|
|
316
|
+
ACCESS_ADMIN_I18N.liveMeta,
|
|
317
|
+
formatLinkDate(link.createdAt, ""),
|
|
318
|
+
formatLinkDate(link.expiresAt, t(ACCESS_ADMIN_I18N.expiresNever))
|
|
319
|
+
) }),
|
|
320
|
+
renderLinkActions === void 0 ? null : renderLinkActions(link)
|
|
321
|
+
] }, link.id))
|
|
322
|
+
] }) : null,
|
|
323
|
+
!linksFailed && liveLinks.length === 0 ? /* @__PURE__ */ jsx(Text, { style: styles.note, testID: `${testIDPrefix}-no-live`, children: t(ACCESS_ADMIN_I18N.noLive) }) : null,
|
|
324
|
+
/* @__PURE__ */ jsxs(View, { style: styles.actions, children: [
|
|
325
|
+
/* @__PURE__ */ jsx(
|
|
326
|
+
Button,
|
|
327
|
+
{
|
|
328
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.mintHint),
|
|
329
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.mint),
|
|
330
|
+
disabled: state.isPending,
|
|
331
|
+
label: t(ACCESS_ADMIN_I18N.mint),
|
|
332
|
+
loading: state.isPending,
|
|
333
|
+
onPress: state.mint,
|
|
334
|
+
testID: `${testIDPrefix}-mint`
|
|
335
|
+
}
|
|
336
|
+
),
|
|
337
|
+
/* @__PURE__ */ jsx(
|
|
338
|
+
Button,
|
|
339
|
+
{
|
|
340
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.closeHint),
|
|
341
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.close),
|
|
342
|
+
disabled: state.isPending,
|
|
343
|
+
label: t(ACCESS_ADMIN_I18N.close),
|
|
344
|
+
onPress: onClose,
|
|
345
|
+
testID: `${testIDPrefix}-close`,
|
|
346
|
+
variant: "secondary"
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
] })
|
|
350
|
+
] });
|
|
351
|
+
};
|
|
352
|
+
var buildStyles = (c) => StyleSheet.create({
|
|
353
|
+
actions: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_SM },
|
|
354
|
+
heading: { color: c.textPrimary, fontSize: FONT_MD, fontWeight: "700" },
|
|
355
|
+
liveBox: { gap: SPACE_SM, marginTop: SPACE_XS },
|
|
356
|
+
liveMeta: { color: c.textSecondary, fontSize: FONT_SM },
|
|
357
|
+
liveRow: {
|
|
358
|
+
borderTopColor: c.border,
|
|
359
|
+
borderTopWidth: 1,
|
|
360
|
+
gap: SPACE_XS,
|
|
361
|
+
paddingTop: SPACE_SM
|
|
362
|
+
},
|
|
363
|
+
note: { color: c.textSecondary, fontSize: FONT_SM },
|
|
364
|
+
panel: {
|
|
365
|
+
backgroundColor: c.panelSurface,
|
|
366
|
+
borderColor: c.border,
|
|
367
|
+
borderRadius: SPACE_SM,
|
|
368
|
+
borderWidth: 1,
|
|
369
|
+
gap: SPACE_XS,
|
|
370
|
+
marginBottom: SPACE_LG,
|
|
371
|
+
padding: SPACE_MD
|
|
372
|
+
},
|
|
373
|
+
revealBox: {
|
|
374
|
+
backgroundColor: c.revealSurface,
|
|
375
|
+
borderColor: c.revealBorder,
|
|
376
|
+
borderRadius: SPACE_SM,
|
|
377
|
+
borderWidth: 1,
|
|
378
|
+
gap: SPACE_XS,
|
|
379
|
+
marginVertical: SPACE_SM,
|
|
380
|
+
padding: SPACE_MD
|
|
381
|
+
},
|
|
382
|
+
revealTitle: { color: c.revealHeading, fontSize: FONT_MD, fontWeight: "800" },
|
|
383
|
+
subheading: { color: c.textPrimary, fontSize: FONT_SM, fontWeight: "700" },
|
|
384
|
+
url: { color: c.textPrimary, fontSize: FONT_MD, fontWeight: "600" },
|
|
385
|
+
warning: { color: c.danger, fontSize: FONT_SM, fontWeight: "600" }
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/accounts/accountOptions.ts
|
|
389
|
+
function isFilled(value) {
|
|
390
|
+
return typeof value === "string" && value.trim() !== "";
|
|
391
|
+
}
|
|
392
|
+
function accountDisplayLabel(account, unnamedLabel) {
|
|
393
|
+
if (isFilled(account.displayName)) return account.displayName.trim();
|
|
394
|
+
if (isFilled(account.email)) return account.email.trim();
|
|
395
|
+
return unnamedLabel;
|
|
396
|
+
}
|
|
397
|
+
function buildAccountOptions(accounts, bindings, subject, unnamedLabel) {
|
|
398
|
+
const takenByOthers = /* @__PURE__ */ new Set();
|
|
399
|
+
bindings.forEach((accountId, subjectId) => {
|
|
400
|
+
if (subjectId !== subject.id && isFilled(accountId)) takenByOthers.add(accountId);
|
|
401
|
+
});
|
|
402
|
+
return accounts.filter((account) => !takenByOthers.has(account.userId)).map((account) => ({
|
|
403
|
+
value: account.userId,
|
|
404
|
+
label: accountDisplayLabel(account, unnamedLabel)
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
function useSubjectAccountLink(options) {
|
|
408
|
+
const { subject, boundAccountId, linkAccount, describeError, onResult, t } = options;
|
|
409
|
+
const [selectedAccountId, setSelectedAccountId] = useState(boundAccountId);
|
|
410
|
+
const [isPending, setIsPending] = useState(false);
|
|
411
|
+
const mountedRef = useRef(true);
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
mountedRef.current = true;
|
|
414
|
+
return () => {
|
|
415
|
+
mountedRef.current = false;
|
|
416
|
+
};
|
|
417
|
+
}, []);
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
setSelectedAccountId(boundAccountId);
|
|
420
|
+
}, [subject.id, boundAccountId]);
|
|
421
|
+
const canLink = !isPending && selectedAccountId !== null && selectedAccountId !== boundAccountId;
|
|
422
|
+
const canUnlink = !isPending && boundAccountId !== null;
|
|
423
|
+
const submit = useCallback(
|
|
424
|
+
(accountId, successKey) => {
|
|
425
|
+
setIsPending(true);
|
|
426
|
+
void (async () => {
|
|
427
|
+
try {
|
|
428
|
+
await linkAccount(subject, accountId);
|
|
429
|
+
if (!mountedRef.current) return;
|
|
430
|
+
onResult(t(successKey, subject.displayName), "success" /* Success */);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (!mountedRef.current) return;
|
|
433
|
+
onResult(describeError(error), "error" /* Error */);
|
|
434
|
+
} finally {
|
|
435
|
+
if (mountedRef.current) setIsPending(false);
|
|
436
|
+
}
|
|
437
|
+
})();
|
|
438
|
+
},
|
|
439
|
+
[linkAccount, subject, onResult, t, describeError]
|
|
440
|
+
);
|
|
441
|
+
const link = useCallback(() => {
|
|
442
|
+
if (!canLink || selectedAccountId === null) return;
|
|
443
|
+
submit(selectedAccountId, ACCESS_ADMIN_I18N.accountLinkSuccess);
|
|
444
|
+
}, [canLink, selectedAccountId, submit]);
|
|
445
|
+
const unlink = useCallback(() => {
|
|
446
|
+
if (!canUnlink) return;
|
|
447
|
+
submit(null, ACCESS_ADMIN_I18N.accountUnlinkSuccess);
|
|
448
|
+
}, [canUnlink, submit]);
|
|
449
|
+
return { selectedAccountId, select: setSelectedAccountId, isPending, canLink, canUnlink, link, unlink };
|
|
450
|
+
}
|
|
451
|
+
var SPACE_XS2 = 4;
|
|
452
|
+
var SPACE_SM2 = 8;
|
|
453
|
+
var SPACE_MD2 = 12;
|
|
454
|
+
var SPACE_LG2 = 16;
|
|
455
|
+
var FONT_SM2 = 12;
|
|
456
|
+
var FONT_MD2 = 13;
|
|
457
|
+
var AccountLinkPanel = ({
|
|
458
|
+
subject,
|
|
459
|
+
boundAccountId,
|
|
460
|
+
accounts,
|
|
461
|
+
bindings,
|
|
462
|
+
linkAccount,
|
|
463
|
+
describeError,
|
|
464
|
+
onClose,
|
|
465
|
+
onResult,
|
|
466
|
+
testIDPrefix = ACCESS_ADMIN_TEST_IDS.accountPanel
|
|
467
|
+
}) => {
|
|
468
|
+
const { t } = useUi();
|
|
469
|
+
const palette = useAccessAdminPalette();
|
|
470
|
+
const styles = useMemo(() => buildStyles2(palette), [palette]);
|
|
471
|
+
const state = useSubjectAccountLink({
|
|
472
|
+
subject,
|
|
473
|
+
boundAccountId,
|
|
474
|
+
linkAccount,
|
|
475
|
+
describeError,
|
|
476
|
+
onResult,
|
|
477
|
+
t
|
|
478
|
+
});
|
|
479
|
+
const unnamedLabel = t(ACCESS_ADMIN_I18N.accountUnnamed);
|
|
480
|
+
const options = useMemo(
|
|
481
|
+
() => buildAccountOptions(accounts, bindings, subject, unnamedLabel),
|
|
482
|
+
[accounts, bindings, subject, unnamedLabel]
|
|
483
|
+
);
|
|
484
|
+
const selectedLabel = options.find((option) => option.value === state.selectedAccountId)?.label ?? "";
|
|
485
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.panel, testID: testIDPrefix, children: [
|
|
486
|
+
/* @__PURE__ */ jsx(Text, { style: styles.heading, children: t(ACCESS_ADMIN_I18N.accountPanelHeading, subject.displayName) }),
|
|
487
|
+
/* @__PURE__ */ jsx(Text, { style: styles.note, children: t(ACCESS_ADMIN_I18N.accountPanelNote) }),
|
|
488
|
+
options.length > 0 ? /* @__PURE__ */ jsx(
|
|
489
|
+
SelectControl,
|
|
490
|
+
{
|
|
491
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.accountPickerHint),
|
|
492
|
+
accessibilityLabel: `${t(ACCESS_ADMIN_I18N.accountPickerLabel)}: ${selectedLabel === "" ? t(ACCESS_ADMIN_I18N.accountPickerPlaceholder) : selectedLabel}`,
|
|
493
|
+
onChange: state.select,
|
|
494
|
+
optionHint: t(ACCESS_ADMIN_I18N.accountOptionHint),
|
|
495
|
+
options,
|
|
496
|
+
placeholder: t(ACCESS_ADMIN_I18N.accountPickerPlaceholder),
|
|
497
|
+
testID: `${testIDPrefix}-picker`,
|
|
498
|
+
value: state.selectedAccountId ?? ""
|
|
499
|
+
}
|
|
500
|
+
) : /* @__PURE__ */ jsx(Text, { style: styles.note, testID: `${testIDPrefix}-no-accounts`, children: t(ACCESS_ADMIN_I18N.accountNone) }),
|
|
501
|
+
/* @__PURE__ */ jsxs(View, { style: styles.actions, children: [
|
|
502
|
+
/* @__PURE__ */ jsx(
|
|
503
|
+
Button,
|
|
504
|
+
{
|
|
505
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.accountLinkHint),
|
|
506
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.accountLink),
|
|
507
|
+
disabled: !state.canLink,
|
|
508
|
+
label: t(ACCESS_ADMIN_I18N.accountLink),
|
|
509
|
+
loading: state.isPending,
|
|
510
|
+
onPress: state.link,
|
|
511
|
+
testID: `${testIDPrefix}-link`
|
|
512
|
+
}
|
|
513
|
+
),
|
|
514
|
+
state.canUnlink ? /* @__PURE__ */ jsx(
|
|
515
|
+
Button,
|
|
516
|
+
{
|
|
517
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.accountUnlinkHint),
|
|
518
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.accountUnlink),
|
|
519
|
+
disabled: state.isPending,
|
|
520
|
+
label: t(ACCESS_ADMIN_I18N.accountUnlink),
|
|
521
|
+
onPress: state.unlink,
|
|
522
|
+
testID: `${testIDPrefix}-unlink`,
|
|
523
|
+
variant: "danger"
|
|
524
|
+
}
|
|
525
|
+
) : null,
|
|
526
|
+
/* @__PURE__ */ jsx(
|
|
527
|
+
Button,
|
|
528
|
+
{
|
|
529
|
+
accessibilityHint: t(ACCESS_ADMIN_I18N.accountCloseHint),
|
|
530
|
+
accessibilityLabel: t(ACCESS_ADMIN_I18N.accountClose),
|
|
531
|
+
disabled: state.isPending,
|
|
532
|
+
label: t(ACCESS_ADMIN_I18N.accountClose),
|
|
533
|
+
onPress: onClose,
|
|
534
|
+
testID: `${testIDPrefix}-close`,
|
|
535
|
+
variant: "secondary"
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
] })
|
|
539
|
+
] });
|
|
540
|
+
};
|
|
541
|
+
var buildStyles2 = (c) => StyleSheet.create({
|
|
542
|
+
actions: { flexDirection: "row", flexWrap: "wrap", gap: SPACE_SM2 },
|
|
543
|
+
heading: { color: c.textPrimary, fontSize: FONT_MD2, fontWeight: "700" },
|
|
544
|
+
note: { color: c.textSecondary, fontSize: FONT_SM2 },
|
|
545
|
+
panel: {
|
|
546
|
+
backgroundColor: c.panelSurface,
|
|
547
|
+
borderColor: c.border,
|
|
548
|
+
borderRadius: SPACE_SM2,
|
|
549
|
+
borderWidth: 1,
|
|
550
|
+
gap: SPACE_XS2,
|
|
551
|
+
marginBottom: SPACE_LG2,
|
|
552
|
+
padding: SPACE_MD2
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
export { ACCESS_ADMIN_I18N, ACCESS_ADMIN_TEST_IDS, AccessLinkPanel, AccessResultTone, AccountLinkPanel, accountDisplayLabel, activeLinksForSubject, buildAccountOptions, buildCapabilityLinkMailto, capabilityExpiryDate, capabilityExpiryDays, daysUntilAnchor, formatLinkDate, linksForSubject, useAccessAdminPalette, useCapabilityLinkMint, useSubjectAccountLink };
|
|
557
|
+
//# sourceMappingURL=index.mjs.map
|
|
558
|
+
//# sourceMappingURL=index.mjs.map
|