@parity/product-sdk-host 0.11.0 → 0.12.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 +528 -534
- package/dist/index.js +853 -285
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/accounts.ts +544 -0
- package/src/chain-spec.ts +126 -84
- package/src/chain-transaction.ts +107 -78
- package/src/chat.ts +81 -85
- package/src/container.ts +211 -246
- package/src/entropy.ts +63 -25
- package/src/errors.ts +198 -0
- package/src/features.ts +66 -55
- package/src/index.ts +33 -22
- package/src/navigation.ts +50 -49
- package/src/notifications.ts +59 -69
- package/src/papi-provider.ts +673 -0
- package/src/payments.ts +77 -61
- package/src/permissions.ts +107 -105
- package/src/result.ts +56 -0
- package/src/theme.ts +35 -63
- package/src/transport.ts +71 -0
- package/src/truapi.ts +166 -409
- package/src/types.ts +69 -61
package/src/payments.ts
CHANGED
|
@@ -1,61 +1,91 @@
|
|
|
1
1
|
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
/**
|
|
4
|
-
* Wrapper for the host's payment manager (RFC-0006)
|
|
4
|
+
* Wrapper for the host's payment manager (RFC-0006), backed by
|
|
5
|
+
* `truApi.payment.*`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* {@link getHostLocalStorage}, and {@link getAccountsProvider}.
|
|
11
|
-
*
|
|
12
|
-
* Returns the shared `paymentManager` singleton from
|
|
13
|
-
* `@novasamatech/host-api-wrapper` (not a fresh `createPaymentManager()`
|
|
14
|
-
* instance) so callers share one wrapper + hostApi closure across the app.
|
|
15
|
-
*
|
|
16
|
-
* Distinct from the CoinPayment / merchant-payments surface tracked under
|
|
17
|
-
* `@parity/product-sdk-merchant-payments` (RFC-0017). RFC-0006 is the
|
|
18
|
-
* user-initiated balance / top-up / payment-request flow; RFC-0017 is the
|
|
19
|
-
* merchant-initiated checkout flow.
|
|
7
|
+
* Exposes balance subscription, top-up, payment requests, and payment-status
|
|
8
|
+
* subscription. Distinct from the CoinPayment / merchant-payments surface
|
|
9
|
+
* (RFC-0017): RFC-0006 is the user-initiated balance / top-up / payment-request
|
|
10
|
+
* flow.
|
|
20
11
|
*
|
|
21
12
|
* @module
|
|
22
13
|
*/
|
|
23
14
|
|
|
24
|
-
import { createLogger } from "@parity/product-sdk-logger";
|
|
25
|
-
|
|
26
15
|
import type {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Available balance for the user's payment account. Re-exported from `@novasamatech/host-api-wrapper`. */
|
|
36
|
-
export type PaymentBalance = NovasamaPaymentBalance;
|
|
16
|
+
Balance,
|
|
17
|
+
PaymentPurseId,
|
|
18
|
+
HexString,
|
|
19
|
+
HostPaymentBalanceSubscribeItem,
|
|
20
|
+
HostPaymentStatusSubscribeItem,
|
|
21
|
+
PaymentTopUpSource,
|
|
22
|
+
TrUApiClient,
|
|
23
|
+
} from "@parity/truapi";
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Source for {@link PaymentManager.topUp}. Re-exported from `@novasamatech/host-api-wrapper`. */
|
|
42
|
-
export type TopUpSource = NovasamaTopUpSource;
|
|
25
|
+
import { getClient, subscribeWithInterrupt } from "./transport.js";
|
|
26
|
+
import { unwrapHostResult } from "./truapi.js";
|
|
27
|
+
import type { HostSubscription } from "./types.js";
|
|
43
28
|
|
|
44
29
|
/**
|
|
45
30
|
* Payment manager handle. Exposes balance subscription, top-up, payment
|
|
46
|
-
* requests, and payment
|
|
31
|
+
* requests, and payment-status subscription.
|
|
47
32
|
*
|
|
48
|
-
*
|
|
33
|
+
* The balance / status / top-up-source shapes are `@parity/truapi`'s
|
|
34
|
+
* `HostPaymentBalanceSubscribeItem`, `HostPaymentStatusSubscribeItem`, and
|
|
35
|
+
* `PaymentTopUpSource` — used directly rather than re-aliased.
|
|
49
36
|
*/
|
|
50
|
-
export
|
|
37
|
+
export interface PaymentManager {
|
|
38
|
+
subscribeBalance(
|
|
39
|
+
callback: (balance: HostPaymentBalanceSubscribeItem) => void,
|
|
40
|
+
purse?: PaymentPurseId,
|
|
41
|
+
): HostSubscription;
|
|
42
|
+
topUp(amount: Balance, source: PaymentTopUpSource, into?: PaymentPurseId): Promise<void>;
|
|
43
|
+
requestPayment(
|
|
44
|
+
amount: Balance,
|
|
45
|
+
destination: HexString,
|
|
46
|
+
from?: PaymentPurseId,
|
|
47
|
+
): Promise<{ id: string }>;
|
|
48
|
+
subscribePaymentStatus(
|
|
49
|
+
paymentId: string,
|
|
50
|
+
callback: (status: HostPaymentStatusSubscribeItem) => void,
|
|
51
|
+
): HostSubscription;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Build a {@link PaymentManager} over a TruAPI client's `payment` domain. */
|
|
55
|
+
function adaptPaymentManager(client: TrUApiClient): PaymentManager {
|
|
56
|
+
const payment = client.payment;
|
|
57
|
+
return {
|
|
58
|
+
subscribeBalance(callback, purse) {
|
|
59
|
+
return subscribeWithInterrupt(
|
|
60
|
+
payment.balanceSubscribe({ request: { purse } }),
|
|
61
|
+
callback,
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
topUp(amount, source, into) {
|
|
65
|
+
return unwrapHostResult(
|
|
66
|
+
payment.topUp({ into, amount, source }),
|
|
67
|
+
"payment topUp failed",
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
async requestPayment(amount, destination, from) {
|
|
71
|
+
const response = await unwrapHostResult(
|
|
72
|
+
payment.request({ from, amount, destination }),
|
|
73
|
+
"payment requestPayment failed",
|
|
74
|
+
);
|
|
75
|
+
return { id: response.id };
|
|
76
|
+
},
|
|
77
|
+
subscribePaymentStatus(paymentId, callback) {
|
|
78
|
+
return subscribeWithInterrupt(
|
|
79
|
+
payment.statusSubscribe({ request: { paymentId } }),
|
|
80
|
+
callback,
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
51
85
|
|
|
52
86
|
/**
|
|
53
|
-
* Get the host payment manager.
|
|
54
|
-
*
|
|
55
|
-
* Returns the shared `paymentManager` singleton from
|
|
56
|
-
* `@novasamatech/host-api-wrapper`, or `null` if the package is unavailable
|
|
57
|
-
* (running outside a host container or the optional peer dep isn't
|
|
58
|
-
* installed).
|
|
87
|
+
* Get the host payment manager, backed by `truApi.payment.*`. Returns `null`
|
|
88
|
+
* when running outside a host container.
|
|
59
89
|
*
|
|
60
90
|
* @returns The payment manager, or `null` if unavailable.
|
|
61
91
|
*
|
|
@@ -66,35 +96,21 @@ export type PaymentManager = typeof paymentManager;
|
|
|
66
96
|
* const payments = await getPaymentManager();
|
|
67
97
|
* if (payments) {
|
|
68
98
|
* const sub = payments.subscribeBalance((b) => { ... });
|
|
69
|
-
* await payments.topUp(1_000_000n, {
|
|
70
|
-
* const
|
|
71
|
-
* const { id } = await payments.requestPayment(500n, destination);
|
|
99
|
+
* await payments.topUp(1_000_000n, { tag: "ProductAccount", value: { derivationIndex: 0 } });
|
|
100
|
+
* const { id } = await payments.requestPayment(500n, "0x…");
|
|
72
101
|
* sub.unsubscribe();
|
|
73
102
|
* }
|
|
74
103
|
* ```
|
|
75
104
|
*/
|
|
76
105
|
export async function getPaymentManager(): Promise<PaymentManager | null> {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return sdk.paymentManager;
|
|
80
|
-
} catch (err) {
|
|
81
|
-
log.debug("getPaymentManager unavailable", err);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
106
|
+
const client = await getClient();
|
|
107
|
+
return client ? adaptPaymentManager(client) : null;
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
if (import.meta.vitest) {
|
|
87
111
|
const { test, expect } = import.meta.vitest;
|
|
88
112
|
|
|
89
|
-
test("getPaymentManager returns
|
|
90
|
-
|
|
91
|
-
if (payments === null) {
|
|
92
|
-
// Acceptable: SDK couldn't load (e.g. peer dep missing in some envs).
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
expect(typeof payments.subscribeBalance).toBe("function");
|
|
96
|
-
expect(typeof payments.topUp).toBe("function");
|
|
97
|
-
expect(typeof payments.requestPayment).toBe("function");
|
|
98
|
-
expect(typeof payments.subscribePaymentStatus).toBe("function");
|
|
113
|
+
test("getPaymentManager returns null outside a container", async () => {
|
|
114
|
+
expect(await getPaymentManager()).toBeNull();
|
|
99
115
|
});
|
|
100
116
|
}
|
package/src/permissions.ts
CHANGED
|
@@ -3,71 +3,68 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Higher-level wrappers for the host's single-permission flows.
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* {@link
|
|
11
|
-
* shape of {@link requestResourceAllocation} (throws on error, returns
|
|
12
|
-
* the unwrapped payload on success).
|
|
6
|
+
* `truApi.permissions.requestRemotePermission` / `requestDevicePermission`
|
|
7
|
+
* return a neverthrow `ResultAsync` of a `{ granted }` response.
|
|
8
|
+
* {@link requestPermission} and {@link requestDevicePermission} collapse that
|
|
9
|
+
* to one-liners returning a `Result<boolean, HostError>` — the granted flag on
|
|
10
|
+
* success, a typed {@link HostError} on the `err` channel.
|
|
13
11
|
*
|
|
14
12
|
* @module
|
|
15
13
|
*/
|
|
16
14
|
|
|
15
|
+
import type { HostDevicePermissionRequest } from "@parity/truapi";
|
|
17
16
|
import { createLogger } from "@parity/product-sdk-logger";
|
|
18
17
|
|
|
19
|
-
import type
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
import { enumValue, formatHostError, getTruApi, type RemotePermission } from "./truapi.js";
|
|
18
|
+
import { type HostError, HostUnavailableError } from "./errors.js";
|
|
19
|
+
import { type Result, err } from "./result.js";
|
|
20
|
+
import { getTruApi, mapHostResult, type RemotePermission } from "./truapi.js";
|
|
23
21
|
|
|
24
22
|
const log = createLogger("host:permissions");
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
25
|
* Device permission the dapp can ask the host to grant via
|
|
28
|
-
* {@link requestDevicePermission}.
|
|
29
|
-
*
|
|
30
|
-
* Derived from the upstream codec so variant renames surface as compile
|
|
31
|
-
* errors, not runtime failures.
|
|
26
|
+
* {@link requestDevicePermission}. A string union (`"Camera"`, `"Microphone"`,
|
|
27
|
+
* …) re-exported from `@parity/truapi`.
|
|
32
28
|
*/
|
|
33
|
-
export type DevicePermissionKind =
|
|
29
|
+
export type DevicePermissionKind = HostDevicePermissionRequest;
|
|
34
30
|
|
|
35
31
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* Legacy alias of {@link RemotePermission}, kept for back-compat with code that
|
|
33
|
+
* used the older name. Use either freely.
|
|
38
34
|
*/
|
|
39
35
|
export type RemotePermissionItem = RemotePermission;
|
|
40
36
|
|
|
41
37
|
/**
|
|
42
38
|
* Request a single remote permission from the host.
|
|
43
39
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
40
|
+
* Calls `truApi.permissions.requestRemotePermission` and returns the host's
|
|
41
|
+
* boolean granted/denied outcome.
|
|
46
42
|
*
|
|
47
43
|
* @param permission - The remote permission to request.
|
|
48
|
-
* @returns `true` if the host granted the permission, `false` if denied
|
|
49
|
-
*
|
|
44
|
+
* @returns `ok(true)` if the host granted the permission, `ok(false)` if denied,
|
|
45
|
+
* or `err(HostUnavailableError | HostCallFailedError)`.
|
|
50
46
|
*
|
|
51
47
|
* @example
|
|
52
48
|
* ```ts
|
|
53
|
-
* const
|
|
54
|
-
* if (!
|
|
49
|
+
* const r = await requestPermission({ tag: "ChainSubmit", value: undefined });
|
|
50
|
+
* if (!r.ok || !r.value) {
|
|
55
51
|
* tellUserToReconnect();
|
|
56
52
|
* }
|
|
57
53
|
* ```
|
|
58
54
|
*/
|
|
59
|
-
export async function requestPermission(
|
|
55
|
+
export async function requestPermission(
|
|
56
|
+
permission: RemotePermission,
|
|
57
|
+
): Promise<Result<boolean, HostError>> {
|
|
60
58
|
const truApi = await getTruApi();
|
|
61
59
|
if (!truApi) {
|
|
62
|
-
|
|
60
|
+
return err(new HostUnavailableError("requestPermission: TruAPI unavailable"));
|
|
63
61
|
}
|
|
64
62
|
log.debug("requestPermission", { tag: permission.tag });
|
|
65
63
|
|
|
66
|
-
return
|
|
67
|
-
(
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
},
|
|
64
|
+
return mapHostResult(
|
|
65
|
+
truApi.permissions.requestRemotePermission({ permission }),
|
|
66
|
+
(response) => response.granted,
|
|
67
|
+
"requestPermission failed",
|
|
71
68
|
);
|
|
72
69
|
}
|
|
73
70
|
|
|
@@ -75,56 +72,57 @@ export async function requestPermission(permission: RemotePermission): Promise<b
|
|
|
75
72
|
* Request a single device permission (camera, microphone, etc.) from the
|
|
76
73
|
* host.
|
|
77
74
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
75
|
+
* Calls `truApi.permissions.requestDevicePermission` and returns the host's
|
|
76
|
+
* boolean granted/denied outcome.
|
|
80
77
|
*
|
|
81
78
|
* @param permission - The device permission to request.
|
|
82
|
-
* @returns `true` if the host granted the permission, `false` if denied
|
|
83
|
-
*
|
|
79
|
+
* @returns `ok(true)` if the host granted the permission, `ok(false)` if denied,
|
|
80
|
+
* or `err(HostUnavailableError | HostCallFailedError)`.
|
|
84
81
|
*
|
|
85
82
|
* @example
|
|
86
83
|
* ```ts
|
|
87
|
-
* const
|
|
88
|
-
* if (!
|
|
84
|
+
* const r = await requestDevicePermission("Camera");
|
|
85
|
+
* if (!r.ok || !r.value) {
|
|
89
86
|
* showCameraDeniedMessage();
|
|
90
87
|
* }
|
|
91
88
|
* ```
|
|
92
89
|
*/
|
|
93
|
-
export async function requestDevicePermission(
|
|
90
|
+
export async function requestDevicePermission(
|
|
91
|
+
permission: DevicePermissionKind,
|
|
92
|
+
): Promise<Result<boolean, HostError>> {
|
|
94
93
|
const truApi = await getTruApi();
|
|
95
94
|
if (!truApi) {
|
|
96
|
-
|
|
95
|
+
return err(new HostUnavailableError("requestDevicePermission: TruAPI unavailable"));
|
|
97
96
|
}
|
|
98
97
|
log.debug("requestDevicePermission", { permission });
|
|
99
98
|
|
|
100
|
-
return
|
|
101
|
-
(
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
cause: err,
|
|
105
|
-
});
|
|
106
|
-
},
|
|
99
|
+
return mapHostResult(
|
|
100
|
+
truApi.permissions.requestDevicePermission(permission),
|
|
101
|
+
(response) => response.granted,
|
|
102
|
+
"requestDevicePermission failed",
|
|
107
103
|
);
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
if (import.meta.vitest) {
|
|
111
107
|
const { test, expect, describe, vi } = import.meta.vitest;
|
|
112
108
|
|
|
109
|
+
function okAsync<T>(value: T) {
|
|
110
|
+
return { match: async (onOk: (v: T) => unknown) => onOk(value) };
|
|
111
|
+
}
|
|
112
|
+
function errAsync<E>(error: E) {
|
|
113
|
+
return {
|
|
114
|
+
match: async (_onOk: (v: unknown) => unknown, onErr: (e: E) => unknown) => onErr(error),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
113
118
|
async function withMockedTruApi<T>(
|
|
114
|
-
|
|
115
|
-
permission?: (req: unknown) => unknown;
|
|
116
|
-
devicePermission?: (req: unknown) => unknown;
|
|
117
|
-
} | null,
|
|
119
|
+
client: unknown,
|
|
118
120
|
fn: (mod: typeof import("./permissions.js")) => Promise<T>,
|
|
119
121
|
): Promise<T> {
|
|
120
122
|
vi.resetModules();
|
|
121
123
|
vi.doMock("./truapi.js", async (importOriginal) => {
|
|
122
124
|
const original = await importOriginal<typeof import("./truapi.js")>();
|
|
123
|
-
return {
|
|
124
|
-
...original,
|
|
125
|
-
getTruApi: async () => bridge,
|
|
126
|
-
enumValue: (version: string, value: unknown) => ({ tag: version, value }),
|
|
127
|
-
};
|
|
125
|
+
return { ...original, getTruApi: async () => client };
|
|
128
126
|
});
|
|
129
127
|
try {
|
|
130
128
|
const mod = await import("./permissions.js");
|
|
@@ -136,97 +134,101 @@ if (import.meta.vitest) {
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
describe("requestPermission", () => {
|
|
139
|
-
test("
|
|
137
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
140
138
|
await withMockedTruApi(null, async (mod) => {
|
|
141
|
-
await
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
const result = await mod.requestPermission({
|
|
140
|
+
tag: "ChainSubmit",
|
|
141
|
+
value: undefined,
|
|
142
|
+
});
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
146
|
+
}
|
|
144
147
|
});
|
|
145
148
|
});
|
|
146
149
|
|
|
147
|
-
test("
|
|
150
|
+
test("returns ok with the granted flag", async () => {
|
|
148
151
|
await withMockedTruApi(
|
|
149
152
|
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}),
|
|
153
|
+
permissions: {
|
|
154
|
+
requestRemotePermission: vi.fn(() => okAsync({ granted: true })),
|
|
155
|
+
},
|
|
154
156
|
},
|
|
155
157
|
async (mod) => {
|
|
156
|
-
const
|
|
158
|
+
const result = await mod.requestPermission({
|
|
157
159
|
tag: "ChainSubmit",
|
|
158
160
|
value: undefined,
|
|
159
161
|
});
|
|
160
|
-
expect(
|
|
162
|
+
expect(result).toEqual({ ok: true, value: true });
|
|
161
163
|
},
|
|
162
164
|
);
|
|
163
165
|
});
|
|
164
166
|
|
|
165
|
-
test("wraps host errors with a diagnostic message", async () => {
|
|
167
|
+
test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
|
|
166
168
|
await withMockedTruApi(
|
|
167
169
|
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
onErr: (e: unknown) => unknown,
|
|
172
|
-
) =>
|
|
173
|
-
onErr({
|
|
174
|
-
tag: "v1",
|
|
175
|
-
value: { name: "GenericError", message: "boom" },
|
|
176
|
-
}),
|
|
177
|
-
}),
|
|
170
|
+
permissions: {
|
|
171
|
+
requestRemotePermission: vi.fn(() => errAsync({ reason: "boom" })),
|
|
172
|
+
},
|
|
178
173
|
},
|
|
179
174
|
async (mod) => {
|
|
180
|
-
await
|
|
181
|
-
|
|
182
|
-
|
|
175
|
+
const result = await mod.requestPermission({
|
|
176
|
+
tag: "ChainSubmit",
|
|
177
|
+
value: undefined,
|
|
178
|
+
});
|
|
179
|
+
expect(result.ok).toBe(false);
|
|
180
|
+
if (!result.ok) {
|
|
181
|
+
expect(result.error.name).toBe("HostCallFailedError");
|
|
182
|
+
expect(result.error.message).toMatch(/requestPermission failed: boom/);
|
|
183
|
+
}
|
|
183
184
|
},
|
|
184
185
|
);
|
|
185
186
|
});
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
describe("requestDevicePermission", () => {
|
|
189
|
-
test("
|
|
190
|
+
test("returns err(HostUnavailableError) when TruAPI is unavailable", async () => {
|
|
190
191
|
await withMockedTruApi(null, async (mod) => {
|
|
191
|
-
await
|
|
192
|
-
|
|
193
|
-
)
|
|
192
|
+
const result = await mod.requestDevicePermission("Camera");
|
|
193
|
+
expect(result.ok).toBe(false);
|
|
194
|
+
if (!result.ok) {
|
|
195
|
+
expect(result.error.name).toBe("HostUnavailableError");
|
|
196
|
+
}
|
|
194
197
|
});
|
|
195
198
|
});
|
|
196
199
|
|
|
197
|
-
test("
|
|
200
|
+
test("returns ok with the granted flag", async () => {
|
|
198
201
|
await withMockedTruApi(
|
|
199
202
|
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}),
|
|
203
|
+
permissions: {
|
|
204
|
+
requestDevicePermission: vi.fn(() => okAsync({ granted: true })),
|
|
205
|
+
},
|
|
204
206
|
},
|
|
205
207
|
async (mod) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
expect(await mod.requestDevicePermission("Camera")).toEqual({
|
|
209
|
+
ok: true,
|
|
210
|
+
value: true,
|
|
211
|
+
});
|
|
208
212
|
},
|
|
209
213
|
);
|
|
210
214
|
});
|
|
211
215
|
|
|
212
|
-
test("wraps host errors with a diagnostic message", async () => {
|
|
216
|
+
test("wraps host errors in err(HostCallFailedError) with a diagnostic message", async () => {
|
|
213
217
|
await withMockedTruApi(
|
|
214
218
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
onErr: (e: unknown) => unknown,
|
|
219
|
-
) =>
|
|
220
|
-
onErr({
|
|
221
|
-
tag: "v1",
|
|
222
|
-
value: { name: "GenericError", message: "boom" },
|
|
223
|
-
}),
|
|
224
|
-
}),
|
|
219
|
+
permissions: {
|
|
220
|
+
requestDevicePermission: vi.fn(() => errAsync({ reason: "boom" })),
|
|
221
|
+
},
|
|
225
222
|
},
|
|
226
223
|
async (mod) => {
|
|
227
|
-
await
|
|
228
|
-
|
|
229
|
-
)
|
|
224
|
+
const result = await mod.requestDevicePermission("Camera");
|
|
225
|
+
expect(result.ok).toBe(false);
|
|
226
|
+
if (!result.ok) {
|
|
227
|
+
expect(result.error.name).toBe("HostCallFailedError");
|
|
228
|
+
expect(result.error.message).toMatch(
|
|
229
|
+
/requestDevicePermission failed: boom/,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
230
232
|
},
|
|
231
233
|
);
|
|
232
234
|
});
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* A lightweight tagged `Result` type for the host public API.
|
|
5
|
+
*
|
|
6
|
+
* Host functions return `Promise<Result<T, HostError>>` rather than throwing, so
|
|
7
|
+
* consumers get typed errors on the `err` channel instead of opaque thrown
|
|
8
|
+
* `Error`s. The shape is intentionally identical to the one
|
|
9
|
+
* `@parity/product-sdk-signer` exposes (`{ ok: true; value } | { ok: false; error }`),
|
|
10
|
+
* so the two layers compose with no adapter — host's `Result` flows straight into
|
|
11
|
+
* the signer's pattern matching.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: host owns its own copy because the dependency edge runs `signer → host`,
|
|
14
|
+
* so host cannot import the signer's definition. If a third package ever needs
|
|
15
|
+
* this shape, extract it into a shared `@parity/product-sdk-result` package and
|
|
16
|
+
* have both depend on that instead of duplicating.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A value that is either a success (`ok`) carrying `T`, or a failure (`err`) carrying `E`. */
|
|
22
|
+
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
|
23
|
+
|
|
24
|
+
/** Create a successful {@link Result}. */
|
|
25
|
+
export function ok<T>(value: T): Result<T, never> {
|
|
26
|
+
return { ok: true, value };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Create a failed {@link Result}. */
|
|
30
|
+
export function err<E>(error: E): Result<never, E> {
|
|
31
|
+
return { ok: false, error };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (import.meta.vitest) {
|
|
35
|
+
const { test, expect, describe } = import.meta.vitest;
|
|
36
|
+
|
|
37
|
+
describe("ok", () => {
|
|
38
|
+
test("produces an ok result with value", () => {
|
|
39
|
+
const result = ok(42);
|
|
40
|
+
expect(result.ok).toBe(true);
|
|
41
|
+
expect(result).toEqual({ ok: true, value: 42 });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("works with null value", () => {
|
|
45
|
+
expect(ok(null)).toEqual({ ok: true, value: null });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("err", () => {
|
|
50
|
+
test("produces an error result", () => {
|
|
51
|
+
const result = err("boom");
|
|
52
|
+
expect(result.ok).toBe(false);
|
|
53
|
+
expect(result).toEqual({ ok: false, error: "boom" });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|