@parity/product-sdk-host 0.3.0 → 0.5.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/dist/index.d.ts +462 -203
- package/dist/index.js +163 -19
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/chains.ts +2 -8
- package/src/chat.ts +124 -0
- package/src/container.ts +45 -9
- package/src/entropy.ts +65 -0
- package/src/index.ts +41 -1
- package/src/payments.ts +98 -0
- package/src/permissions.ts +232 -0
- package/src/theme.ts +82 -0
- package/src/truapi.ts +225 -126
- package/src/types.ts +62 -71
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Higher-level wrappers for the host's single-permission flows.
|
|
3
|
+
*
|
|
4
|
+
* `hostApi.permission` / `hostApi.devicePermission` take a versioned
|
|
5
|
+
* envelope (`enumValue("v1", ...)`) and return a neverthrow `ResultAsync`
|
|
6
|
+
* of an unwrapped versioned response. Consumers rebuild that wrap/unwrap
|
|
7
|
+
* dance every time. {@link requestPermission} and
|
|
8
|
+
* {@link requestDevicePermission} collapse it to one-liners that match the
|
|
9
|
+
* shape of {@link requestResourceAllocation} (throws on error, returns
|
|
10
|
+
* the unwrapped payload on success).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
16
|
+
|
|
17
|
+
import type { CodecType } from "@novasamatech/host-api";
|
|
18
|
+
import type { DevicePermission as DevicePermissionCodec } from "@novasamatech/host-api";
|
|
19
|
+
|
|
20
|
+
import { enumValue, formatHostError, getTruApi, type RemotePermission } from "./truapi.js";
|
|
21
|
+
|
|
22
|
+
const log = createLogger("host:permissions");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Device permission the dapp can ask the host to grant via
|
|
26
|
+
* {@link requestDevicePermission}.
|
|
27
|
+
*
|
|
28
|
+
* Derived from the upstream codec so variant renames surface as compile
|
|
29
|
+
* errors, not runtime failures.
|
|
30
|
+
*/
|
|
31
|
+
export type DevicePermissionKind = CodecType<typeof DevicePermissionCodec>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Alias of {@link RemotePermission} matching the upstream
|
|
35
|
+
* `@novasamatech/host-api-wrapper` name. Use either freely.
|
|
36
|
+
*/
|
|
37
|
+
export type RemotePermissionItem = RemotePermission;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Request a single remote permission from the host.
|
|
41
|
+
*
|
|
42
|
+
* Builds the `v1` envelope, calls `hostApi.permission`, unwraps the response,
|
|
43
|
+
* and returns the host's boolean granted/denied outcome.
|
|
44
|
+
*
|
|
45
|
+
* @param permission - The remote permission to request.
|
|
46
|
+
* @returns `true` if the host granted the permission, `false` if denied.
|
|
47
|
+
* @throws If the host is unavailable or the request fails.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const granted = await requestPermission({ tag: "ChainSubmit", value: undefined });
|
|
52
|
+
* if (!granted) {
|
|
53
|
+
* tellUserToReconnect();
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export async function requestPermission(permission: RemotePermission): Promise<boolean> {
|
|
58
|
+
const truApi = await getTruApi();
|
|
59
|
+
if (!truApi) {
|
|
60
|
+
throw new Error("requestPermission: TruAPI unavailable");
|
|
61
|
+
}
|
|
62
|
+
log.debug("requestPermission", { tag: permission.tag });
|
|
63
|
+
|
|
64
|
+
return await truApi.permission(enumValue("v1", permission)).match(
|
|
65
|
+
(envelope: { tag: "v1"; value: boolean }) => envelope.value,
|
|
66
|
+
(err: unknown) => {
|
|
67
|
+
throw new Error(`requestPermission failed: ${formatHostError(err)}`, { cause: err });
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request a single device permission (camera, microphone, etc.) from the
|
|
74
|
+
* host.
|
|
75
|
+
*
|
|
76
|
+
* Builds the `v1` envelope, calls `hostApi.devicePermission`, unwraps the
|
|
77
|
+
* response, and returns the host's boolean granted/denied outcome.
|
|
78
|
+
*
|
|
79
|
+
* @param permission - The device permission to request.
|
|
80
|
+
* @returns `true` if the host granted the permission, `false` if denied.
|
|
81
|
+
* @throws If the host is unavailable or the request fails.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const granted = await requestDevicePermission("Camera");
|
|
86
|
+
* if (!granted) {
|
|
87
|
+
* showCameraDeniedMessage();
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export async function requestDevicePermission(permission: DevicePermissionKind): Promise<boolean> {
|
|
92
|
+
const truApi = await getTruApi();
|
|
93
|
+
if (!truApi) {
|
|
94
|
+
throw new Error("requestDevicePermission: TruAPI unavailable");
|
|
95
|
+
}
|
|
96
|
+
log.debug("requestDevicePermission", { permission });
|
|
97
|
+
|
|
98
|
+
return await truApi.devicePermission(enumValue("v1", permission)).match(
|
|
99
|
+
(envelope: { tag: "v1"; value: boolean }) => envelope.value,
|
|
100
|
+
(err: unknown) => {
|
|
101
|
+
throw new Error(`requestDevicePermission failed: ${formatHostError(err)}`, {
|
|
102
|
+
cause: err,
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (import.meta.vitest) {
|
|
109
|
+
const { test, expect, describe, vi } = import.meta.vitest;
|
|
110
|
+
|
|
111
|
+
async function withMockedTruApi<T>(
|
|
112
|
+
bridge: {
|
|
113
|
+
permission?: (req: unknown) => unknown;
|
|
114
|
+
devicePermission?: (req: unknown) => unknown;
|
|
115
|
+
} | null,
|
|
116
|
+
fn: (mod: typeof import("./permissions.js")) => Promise<T>,
|
|
117
|
+
): Promise<T> {
|
|
118
|
+
vi.resetModules();
|
|
119
|
+
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
120
|
+
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
121
|
+
return {
|
|
122
|
+
...original,
|
|
123
|
+
getTruApi: async () => bridge,
|
|
124
|
+
enumValue: (version: string, value: unknown) => ({ tag: version, value }),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
const mod = await import("./permissions.js");
|
|
129
|
+
return await fn(mod);
|
|
130
|
+
} finally {
|
|
131
|
+
vi.doUnmock("./truapi.js");
|
|
132
|
+
vi.resetModules();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe("requestPermission", () => {
|
|
137
|
+
test("throws when TruAPI is unavailable", async () => {
|
|
138
|
+
await withMockedTruApi(null, async (mod) => {
|
|
139
|
+
await expect(
|
|
140
|
+
mod.requestPermission({ tag: "ChainSubmit", value: undefined }),
|
|
141
|
+
).rejects.toThrow(/TruAPI unavailable/);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("unwraps the v1 boolean outcome", async () => {
|
|
146
|
+
await withMockedTruApi(
|
|
147
|
+
{
|
|
148
|
+
permission: vi.fn().mockReturnValue({
|
|
149
|
+
match: async (onOk: (v: unknown) => unknown) =>
|
|
150
|
+
onOk({ tag: "v1", value: true }),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
async (mod) => {
|
|
154
|
+
const granted = await mod.requestPermission({
|
|
155
|
+
tag: "ChainSubmit",
|
|
156
|
+
value: undefined,
|
|
157
|
+
});
|
|
158
|
+
expect(granted).toBe(true);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("wraps host errors with a diagnostic message", async () => {
|
|
164
|
+
await withMockedTruApi(
|
|
165
|
+
{
|
|
166
|
+
permission: vi.fn().mockReturnValue({
|
|
167
|
+
match: async (
|
|
168
|
+
_onOk: (v: unknown) => unknown,
|
|
169
|
+
onErr: (e: unknown) => unknown,
|
|
170
|
+
) =>
|
|
171
|
+
onErr({
|
|
172
|
+
tag: "v1",
|
|
173
|
+
value: { name: "GenericError", message: "boom" },
|
|
174
|
+
}),
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
async (mod) => {
|
|
178
|
+
await expect(
|
|
179
|
+
mod.requestPermission({ tag: "ChainSubmit", value: undefined }),
|
|
180
|
+
).rejects.toThrow(/requestPermission failed: GenericError: boom/);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("requestDevicePermission", () => {
|
|
187
|
+
test("throws when TruAPI is unavailable", async () => {
|
|
188
|
+
await withMockedTruApi(null, async (mod) => {
|
|
189
|
+
await expect(mod.requestDevicePermission("Camera")).rejects.toThrow(
|
|
190
|
+
/TruAPI unavailable/,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("unwraps the v1 boolean outcome", async () => {
|
|
196
|
+
await withMockedTruApi(
|
|
197
|
+
{
|
|
198
|
+
devicePermission: vi.fn().mockReturnValue({
|
|
199
|
+
match: async (onOk: (v: unknown) => unknown) =>
|
|
200
|
+
onOk({ tag: "v1", value: true }),
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
async (mod) => {
|
|
204
|
+
const granted = await mod.requestDevicePermission("Camera");
|
|
205
|
+
expect(granted).toBe(true);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("wraps host errors with a diagnostic message", async () => {
|
|
211
|
+
await withMockedTruApi(
|
|
212
|
+
{
|
|
213
|
+
devicePermission: vi.fn().mockReturnValue({
|
|
214
|
+
match: async (
|
|
215
|
+
_onOk: (v: unknown) => unknown,
|
|
216
|
+
onErr: (e: unknown) => unknown,
|
|
217
|
+
) =>
|
|
218
|
+
onErr({
|
|
219
|
+
tag: "v1",
|
|
220
|
+
value: { name: "GenericError", message: "boom" },
|
|
221
|
+
}),
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
async (mod) => {
|
|
225
|
+
await expect(mod.requestDevicePermission("Camera")).rejects.toThrow(
|
|
226
|
+
/requestDevicePermission failed: GenericError: boom/,
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Higher-level wrapper for the host's theme subscription.
|
|
3
|
+
*
|
|
4
|
+
* `hostApi.themeSubscribe` is reachable via {@link getTruApi}, but consumers
|
|
5
|
+
* have to wire the subscription envelope themselves. `getThemeProvider`
|
|
6
|
+
* returns the `@novasamatech/host-api-wrapper` theme provider object directly,
|
|
7
|
+
* giving callers a `subscribeTheme(cb)` method that resolves to a typed
|
|
8
|
+
* `ThemeMode` ("Light" | "Dark") and yields a `Subscription<void>` handle.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createLogger } from "@parity/product-sdk-logger";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
createThemeProvider,
|
|
17
|
+
ThemeMode as NovasamaThemeMode,
|
|
18
|
+
} from "@novasamatech/host-api-wrapper";
|
|
19
|
+
|
|
20
|
+
const log = createLogger("host:theme");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Host theme provider handle. Exposes `subscribeTheme(callback)` which
|
|
24
|
+
* receives a typed `ThemeMode` on every change and returns a
|
|
25
|
+
* `Subscription<void>` (`unsubscribe` + `onInterrupt`).
|
|
26
|
+
*
|
|
27
|
+
* Type identical to `createThemeProvider()` from
|
|
28
|
+
* `@novasamatech/host-api-wrapper`.
|
|
29
|
+
*/
|
|
30
|
+
export type ThemeProvider = ReturnType<typeof createThemeProvider>;
|
|
31
|
+
|
|
32
|
+
/** Host theme mode value. Re-exported from `@novasamatech/host-api-wrapper`. */
|
|
33
|
+
export type ThemeMode = NovasamaThemeMode;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the host theme provider.
|
|
37
|
+
*
|
|
38
|
+
* Returns the theme-subscription handle exported by
|
|
39
|
+
* `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
|
|
40
|
+
* (running outside a host container or the optional peer dep isn't
|
|
41
|
+
* installed).
|
|
42
|
+
*
|
|
43
|
+
* Implementation note: upstream `@novasamatech/host-api-wrapper` exports only
|
|
44
|
+
* the `createThemeProvider` factory and no `themeProvider` singleton, so
|
|
45
|
+
* this getter constructs a fresh instance on each call (unlike
|
|
46
|
+
* {@link getPreimageManager} or {@link getHostLocalStorage}, which return
|
|
47
|
+
* upstream singletons). The constructed provider is cheap to allocate; it
|
|
48
|
+
* only opens a subscription when `subscribeTheme` is called.
|
|
49
|
+
*
|
|
50
|
+
* @returns The theme provider, or `null` if unavailable.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { getThemeProvider } from "@parity/product-sdk-host";
|
|
55
|
+
*
|
|
56
|
+
* const provider = await getThemeProvider();
|
|
57
|
+
* if (provider) {
|
|
58
|
+
* const sub = provider.subscribeTheme((mode) => {
|
|
59
|
+
* document.documentElement.dataset.theme = mode.toLowerCase();
|
|
60
|
+
* });
|
|
61
|
+
* // sub.unsubscribe() to stop listening
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export async function getThemeProvider(): Promise<ThemeProvider | null> {
|
|
66
|
+
try {
|
|
67
|
+
const sdk = await import("@novasamatech/host-api-wrapper");
|
|
68
|
+
return sdk.createThemeProvider();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.debug("getThemeProvider unavailable", err);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (import.meta.vitest) {
|
|
76
|
+
const { test, expect } = import.meta.vitest;
|
|
77
|
+
|
|
78
|
+
test("getThemeProvider returns provider when SDK is available", async () => {
|
|
79
|
+
const provider = await getThemeProvider();
|
|
80
|
+
expect(provider === null || typeof provider === "object").toBe(true);
|
|
81
|
+
});
|
|
82
|
+
}
|