@appattest/react-native 0.1.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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @appattest/react-native — public API.
3
+ *
4
+ * Surface mirrors the Swift SDK's bridge translation:
5
+ * - AppAttest.start() — fire-and-forget setup (zero-arg)
6
+ * - AppAttest.getSecret(name) — Promise<string | null>
7
+ * - AppAttest.getAllSecrets() — Promise<Record<string, string>>
8
+ * - AppAttest.getState() — Promise<AppAttestState>
9
+ * - AppAttest.waitForReady() — Promise<void>
10
+ * - AppAttest.retry() — Promise<void>
11
+ * - AppAttest.addStateListener(fn) — (state) => void; returns unsubscribe
12
+ * - AppAttest.setDebugMode
13
+ *
14
+ * React hooks (idiomatic for RN consumers):
15
+ * - useSecret(name) — string | null, re-renders on rotation
16
+ * - useAllSecrets() — Record<string, string>, re-renders on rotation
17
+ * - useAppAttestState() — AppAttestState, re-renders on transition
18
+ *
19
+ * There is no sandbox-mode modal logic. Developers handle their
20
+ * own UX for `.subscriptionRequired` / `.creditsRequired` / `.unavailable`.
21
+ */
22
+ /**
23
+ * Lifecycle state. Mirrors `AppAttestClient.State` on the native side.
24
+ */
25
+ export type AppAttestStateName = 'initializing' | 'attesting' | 'syncing' | 'ready' | 'subscription_required' | 'credits_required' | 'unavailable';
26
+ export interface AppAttestState {
27
+ name: AppAttestStateName;
28
+ error?: AppAttestError;
29
+ }
30
+ /**
31
+ * Typed error thrown by every method. `code` matches the Swift SDK's
32
+ * `AppAttestError.code` one-for-one.
33
+ */
34
+ export declare class AppAttestError extends Error {
35
+ readonly code: string;
36
+ /** For `subscription_required`: URL to (re)start the project subscription. */
37
+ readonly subscribeUrl?: string;
38
+ /** For `credits_required`: URL to top up the project balance. */
39
+ readonly topupUrl?: string;
40
+ readonly nativeError?: unknown;
41
+ constructor(code: string, message: string, extras?: {
42
+ subscribeUrl?: string;
43
+ topupUrl?: string;
44
+ nativeError?: unknown;
45
+ });
46
+ /**
47
+ * Single accessor for the dashboard URL regardless of code. Returns
48
+ * `subscribeUrl` for `subscription_required`, `topupUrl` for
49
+ * `credits_required`, else `undefined`.
50
+ */
51
+ get actionUrl(): string | undefined;
52
+ }
53
+ /** Stable string codes. Match Swift `AppAttestError.code` one-for-one. */
54
+ export declare const ErrorCode: {
55
+ readonly SubscriptionRequired: "subscription_required";
56
+ readonly CreditsRequired: "credits_required";
57
+ readonly AttestationRejected: "attestation_rejected";
58
+ readonly ServiceUnavailable: "service_unavailable";
59
+ readonly Network: "network";
60
+ readonly DebugModeReleaseBlocked: "debug_mode_release_blocked";
61
+ readonly InvalidArgument: "invalid_argument";
62
+ };
63
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
64
+ export type DebugMode = 'production' | 'local';
65
+ export declare const AppAttest: {
66
+ /**
67
+ * Synchronous, idempotent setup. Zero-argument. Apple's AAGUID
68
+ * determines the bucket (sandbox vs production) server-side; the SDK
69
+ * is bucket-blind.
70
+ */
71
+ start(): Promise<void>;
72
+ /** Awaits a terminal state. Resolves on `ready`; rejects with
73
+ * AppAttestError on subscriptionRequired / creditsRequired / unavailable. */
74
+ waitForReady(): Promise<void>;
75
+ /** Re-runs the background sync. */
76
+ retry(): Promise<void>;
77
+ /** Wipes stored credentials and secrets. */
78
+ reset(): Promise<void>;
79
+ /**
80
+ * Invalidate the cached secrets bundle and immediately sync. Keeps
81
+ * attestation credentials. Forces a 200 on the next sync, which
82
+ * consumes one credit on the production bucket.
83
+ *
84
+ * Use this for host-app "force refresh" / "sync now" UX. For wiping
85
+ * everything (including attestation), see ``reset()``.
86
+ */
87
+ invalidateBundle(): Promise<void>;
88
+ /** Returns the secret for `name`, or `null`. */
89
+ getSecret(name: string): Promise<string | null>;
90
+ /** Snapshot of every synced secret. */
91
+ getAllSecrets(): Promise<Record<string, string>>;
92
+ /** Current state snapshot. */
93
+ getState(): Promise<AppAttestState>;
94
+ /** Subscribe to state transitions. Returns an unsubscribe fn. */
95
+ addStateListener(listener: (state: AppAttestState) => void): () => void;
96
+ /** Set runtime mode. `'production'` (or `null`) is default. */
97
+ setDebugMode(mode: DebugMode | null, stubs?: Record<string, string>): Promise<void>;
98
+ };
99
+ /**
100
+ * React hook returning the current value of `secrets[name]`. Re-renders
101
+ * the host component whenever the underlying value changes (e.g. after
102
+ * a foreground rotation pulled new data).
103
+ */
104
+ export declare function useSecret(name: string): string | null;
105
+ /**
106
+ * React hook returning every synced secret. Re-renders on rotation.
107
+ */
108
+ export declare function useAllSecrets(): Record<string, string>;
109
+ /**
110
+ * React hook returning the current `AppAttestState`. Re-renders on
111
+ * every transition.
112
+ */
113
+ export declare function useAppAttestState(): AppAttestState;
114
+ export default AppAttest;
115
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,WAAW,GACX,SAAS,GACT,OAAO,GACP,uBAAuB,GACvB,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;gBAG7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB;IAUH;;;;OAIG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;CACF;AAED,0EAA0E;AAC1E,eAAO,MAAM,SAAS;;;;;;;;CAQZ,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAEnE,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC;AAwC/C,eAAO,MAAM,SAAS;IACpB;;;;OAIG;aACM,OAAO,CAAC,IAAI,CAAC;IAItB;kFAC8E;oBAC9D,OAAO,CAAC,IAAI,CAAC;IAI7B,mCAAmC;aAC1B,OAAO,CAAC,IAAI,CAAC;IAItB,4CAA4C;aACnC,OAAO,CAAC,IAAI,CAAC;IAItB;;;;;;;OAOG;wBACiB,OAAO,CAAC,IAAI,CAAC;IAIjC,gDAAgD;oBAChC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI/C,uCAAuC;qBACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAIhD,8BAA8B;gBACZ,OAAO,CAAC,cAAc,CAAC;IAKzC,iEAAiE;+BACtC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvE,+DAA+D;uBAC5C,SAAS,GAAG,IAAI,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAMpF,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBrD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAclD;AAED,eAAe,SAAS,CAAC"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@appattest/react-native",
3
+ "version": "0.1.0",
4
+ "description": "React Native bridge for AppAttest — App-Attest-gated secret delivery for iOS.",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "ios",
14
+ "!ios/build",
15
+ "*.podspec",
16
+ "!**/.*"
17
+ ],
18
+ "scripts": {
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "jest",
21
+ "prepare": "bob build"
22
+ },
23
+ "jest": {
24
+ "preset": "ts-jest",
25
+ "testEnvironment": "node",
26
+ "testMatch": [
27
+ "<rootDir>/__tests__/**/*.test.ts"
28
+ ]
29
+ },
30
+ "keywords": [
31
+ "react-native",
32
+ "ios",
33
+ "app-attest",
34
+ "secrets",
35
+ "keychain"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/AppAttest/appAttest-sdk.git",
40
+ "directory": "bridges/react-native"
41
+ },
42
+ "author": "Bault LLC",
43
+ "license": "MIT",
44
+ "bugs": {
45
+ "url": "https://github.com/AppAttest/appAttest-sdk/issues"
46
+ },
47
+ "homepage": "https://github.com/AppAttest/appAttest-sdk/tree/main/bridges/react-native#readme",
48
+ "peerDependencies": {
49
+ "react": "*",
50
+ "react-native": ">=0.71.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/jest": "^29.5.0",
54
+ "@types/react": "^18.0.0",
55
+ "jest": "^29.7.0",
56
+ "react-native": "^0.77.0",
57
+ "react-native-builder-bob": "^0.42.1",
58
+ "ts-jest": "^29.1.0",
59
+ "typescript": "^5.4.0"
60
+ },
61
+ "codegenConfig": {
62
+ "name": "RNAppAttestSpec",
63
+ "type": "modules",
64
+ "jsSrcsDir": "src",
65
+ "android": {}
66
+ },
67
+ "react-native-builder-bob": {
68
+ "source": "src",
69
+ "output": "lib",
70
+ "targets": [
71
+ ["commonjs"],
72
+ ["module"],
73
+ ["typescript", { "project": "tsconfig.json" }]
74
+ ]
75
+ }
76
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * TurboModule spec for the AppAttest iOS native module.
3
+ *
4
+ * Codegen reads this file. The shape is restricted to what TurboModule
5
+ * codegen can serialize (primitives, Object, arrays of primitives,
6
+ * Promises, null).
7
+ *
8
+ * Errors: rejection objects carry `{ code, message,
9
+ * subscribeUrl?, topupUrl? }`. The TS wrapper in `index.ts` translates
10
+ * these into `AppAttestError`.
11
+ */
12
+ import type { TurboModule } from 'react-native';
13
+ import { TurboModuleRegistry } from 'react-native';
14
+
15
+ export interface Spec extends TurboModule {
16
+ // Lifecycle
17
+
18
+ /** Synchronous, idempotent setup. Zero-argument (bucket is
19
+ * AAGUID-derived server-side). */
20
+ start(): Promise<void>;
21
+
22
+ /** Awaits the next terminal state. Resolves on `ready`; rejects on
23
+ * subscriptionRequired / creditsRequired / unavailable. */
24
+ waitForReady(): Promise<void>;
25
+
26
+ /** Re-runs the background sync. */
27
+ retry(): Promise<void>;
28
+
29
+ /** Wipes stored credentials and secrets. */
30
+ reset(): Promise<void>;
31
+
32
+ /** Invalidate the cached secrets bundle and immediately sync. Keeps
33
+ * attestation; forces a 200 (1 credit on production). */
34
+ invalidateBundle(): Promise<void>;
35
+
36
+ // Reads
37
+
38
+ /** Synchronous-feeling secret lookup. Returns `null` if not yet
39
+ * synced or absent. */
40
+ getSecret(name: string): Promise<string | null>;
41
+
42
+ /** Snapshot of every synced secret as `{ [name]: value }`. */
43
+ getAllSecrets(): Promise<{ [key: string]: string }>;
44
+
45
+ /** Current state as `{ name, error? }`. */
46
+ getState(): Promise<{
47
+ name: string;
48
+ error?: {
49
+ code: string;
50
+ message: string;
51
+ subscribeUrl?: string;
52
+ topupUrl?: string;
53
+ };
54
+ }>;
55
+
56
+ // Configuration
57
+
58
+ /** `null` / `'production'` for production; `'local'` for the DEBUG-only
59
+ * stub mode. `'local'` requires `stubs`. */
60
+ setDebugMode(name: string | null, stubs: { [key: string]: string } | null): Promise<void>;
61
+
62
+ // setApiBaseUrl removed — base URL is hardcoded in the Swift SDK.
63
+
64
+ // Event emitter plumbing
65
+
66
+ addListener(eventName: string): void;
67
+ removeListeners(count: number): void;
68
+ }
69
+
70
+ export default TurboModuleRegistry.getEnforcing<Spec>('RNAppAttest');
package/src/index.ts ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @appattest/react-native — public API.
3
+ *
4
+ * Surface mirrors the Swift SDK's bridge translation:
5
+ * - AppAttest.start() — fire-and-forget setup (zero-arg)
6
+ * - AppAttest.getSecret(name) — Promise<string | null>
7
+ * - AppAttest.getAllSecrets() — Promise<Record<string, string>>
8
+ * - AppAttest.getState() — Promise<AppAttestState>
9
+ * - AppAttest.waitForReady() — Promise<void>
10
+ * - AppAttest.retry() — Promise<void>
11
+ * - AppAttest.addStateListener(fn) — (state) => void; returns unsubscribe
12
+ * - AppAttest.setDebugMode
13
+ *
14
+ * React hooks (idiomatic for RN consumers):
15
+ * - useSecret(name) — string | null, re-renders on rotation
16
+ * - useAllSecrets() — Record<string, string>, re-renders on rotation
17
+ * - useAppAttestState() — AppAttestState, re-renders on transition
18
+ *
19
+ * There is no sandbox-mode modal logic. Developers handle their
20
+ * own UX for `.subscriptionRequired` / `.creditsRequired` / `.unavailable`.
21
+ */
22
+
23
+ import { useEffect, useState } from 'react';
24
+ import { NativeEventEmitter, NativeModules } from 'react-native';
25
+ import NativeAppAttest from './NativeAppAttest';
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ const eventEmitter = new NativeEventEmitter(NativeModules.RNAppAttest as any);
29
+
30
+ /**
31
+ * Lifecycle state. Mirrors `AppAttestClient.State` on the native side.
32
+ */
33
+ export type AppAttestStateName =
34
+ | 'initializing'
35
+ | 'attesting'
36
+ | 'syncing'
37
+ | 'ready'
38
+ | 'subscription_required'
39
+ | 'credits_required'
40
+ | 'unavailable';
41
+
42
+ export interface AppAttestState {
43
+ name: AppAttestStateName;
44
+ error?: AppAttestError;
45
+ }
46
+
47
+ /**
48
+ * Typed error thrown by every method. `code` matches the Swift SDK's
49
+ * `AppAttestError.code` one-for-one.
50
+ */
51
+ export class AppAttestError extends Error {
52
+ readonly code: string;
53
+ /** For `subscription_required`: URL to (re)start the project subscription. */
54
+ readonly subscribeUrl?: string;
55
+ /** For `credits_required`: URL to top up the project balance. */
56
+ readonly topupUrl?: string;
57
+ readonly nativeError?: unknown;
58
+
59
+ constructor(
60
+ code: string,
61
+ message: string,
62
+ extras?: {
63
+ subscribeUrl?: string;
64
+ topupUrl?: string;
65
+ nativeError?: unknown;
66
+ },
67
+ ) {
68
+ super(message);
69
+ this.name = 'AppAttestError';
70
+ this.code = code;
71
+ this.subscribeUrl = extras?.subscribeUrl;
72
+ this.topupUrl = extras?.topupUrl;
73
+ this.nativeError = extras?.nativeError;
74
+ }
75
+
76
+ /**
77
+ * Single accessor for the dashboard URL regardless of code. Returns
78
+ * `subscribeUrl` for `subscription_required`, `topupUrl` for
79
+ * `credits_required`, else `undefined`.
80
+ */
81
+ get actionUrl(): string | undefined {
82
+ return this.subscribeUrl ?? this.topupUrl;
83
+ }
84
+ }
85
+
86
+ /** Stable string codes. Match Swift `AppAttestError.code` one-for-one. */
87
+ export const ErrorCode = {
88
+ SubscriptionRequired: 'subscription_required',
89
+ CreditsRequired: 'credits_required',
90
+ AttestationRejected: 'attestation_rejected',
91
+ ServiceUnavailable: 'service_unavailable',
92
+ Network: 'network',
93
+ DebugModeReleaseBlocked: 'debug_mode_release_blocked',
94
+ InvalidArgument: 'invalid_argument',
95
+ } as const;
96
+
97
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
98
+
99
+ export type DebugMode = 'production' | 'local';
100
+
101
+ interface ErrorEnvelope {
102
+ code?: string;
103
+ message?: string;
104
+ userInfo?: {
105
+ subscribeUrl?: string;
106
+ topupUrl?: string;
107
+ };
108
+ // RN flattens NSError userInfo onto the rejection too; cover both shapes.
109
+ subscribeUrl?: string;
110
+ topupUrl?: string;
111
+ }
112
+
113
+ function buildError(e: ErrorEnvelope, native: unknown): AppAttestError {
114
+ return new AppAttestError(e.code ?? 'internal_error', e.message ?? String(native), {
115
+ subscribeUrl: e.userInfo?.subscribeUrl ?? e.subscribeUrl,
116
+ topupUrl: e.userInfo?.topupUrl ?? e.topupUrl,
117
+ nativeError: native,
118
+ });
119
+ }
120
+
121
+ async function wrap<T>(p: Promise<T>): Promise<T> {
122
+ try {
123
+ return await p;
124
+ } catch (err) {
125
+ throw buildError(err as ErrorEnvelope, err);
126
+ }
127
+ }
128
+
129
+ interface NativeStateRaw {
130
+ name: string;
131
+ error?: ErrorEnvelope;
132
+ }
133
+
134
+ function decodeState(raw: NativeStateRaw): AppAttestState {
135
+ const error = raw.error ? buildError(raw.error, raw.error) : undefined;
136
+ return { name: raw.name as AppAttestStateName, error };
137
+ }
138
+
139
+ export const AppAttest = {
140
+ /**
141
+ * Synchronous, idempotent setup. Zero-argument. Apple's AAGUID
142
+ * determines the bucket (sandbox vs production) server-side; the SDK
143
+ * is bucket-blind.
144
+ */
145
+ start(): Promise<void> {
146
+ return wrap(NativeAppAttest.start());
147
+ },
148
+
149
+ /** Awaits a terminal state. Resolves on `ready`; rejects with
150
+ * AppAttestError on subscriptionRequired / creditsRequired / unavailable. */
151
+ waitForReady(): Promise<void> {
152
+ return wrap(NativeAppAttest.waitForReady());
153
+ },
154
+
155
+ /** Re-runs the background sync. */
156
+ retry(): Promise<void> {
157
+ return wrap(NativeAppAttest.retry());
158
+ },
159
+
160
+ /** Wipes stored credentials and secrets. */
161
+ reset(): Promise<void> {
162
+ return wrap(NativeAppAttest.reset());
163
+ },
164
+
165
+ /**
166
+ * Invalidate the cached secrets bundle and immediately sync. Keeps
167
+ * attestation credentials. Forces a 200 on the next sync, which
168
+ * consumes one credit on the production bucket.
169
+ *
170
+ * Use this for host-app "force refresh" / "sync now" UX. For wiping
171
+ * everything (including attestation), see ``reset()``.
172
+ */
173
+ invalidateBundle(): Promise<void> {
174
+ return wrap(NativeAppAttest.invalidateBundle());
175
+ },
176
+
177
+ /** Returns the secret for `name`, or `null`. */
178
+ getSecret(name: string): Promise<string | null> {
179
+ return wrap(NativeAppAttest.getSecret(name));
180
+ },
181
+
182
+ /** Snapshot of every synced secret. */
183
+ getAllSecrets(): Promise<Record<string, string>> {
184
+ return wrap(NativeAppAttest.getAllSecrets());
185
+ },
186
+
187
+ /** Current state snapshot. */
188
+ async getState(): Promise<AppAttestState> {
189
+ const raw = await wrap(NativeAppAttest.getState());
190
+ return decodeState(raw as NativeStateRaw);
191
+ },
192
+
193
+ /** Subscribe to state transitions. Returns an unsubscribe fn. */
194
+ addStateListener(listener: (state: AppAttestState) => void): () => void {
195
+ const subscription = eventEmitter.addListener('stateChanged', (raw) => {
196
+ listener(decodeState(raw as NativeStateRaw));
197
+ });
198
+ return () => subscription.remove();
199
+ },
200
+
201
+ /** Set runtime mode. `'production'` (or `null`) is default. */
202
+ setDebugMode(mode: DebugMode | null, stubs?: Record<string, string>): Promise<void> {
203
+ return wrap(NativeAppAttest.setDebugMode(mode, stubs ?? null));
204
+ },
205
+
206
+ // setApiBaseUrl is not exposed — the base URL is hardcoded in the Swift
207
+ // SDK. There is no runtime override path from JavaScript.
208
+ };
209
+
210
+ // MARK: - React hooks
211
+
212
+ /**
213
+ * React hook returning the current value of `secrets[name]`. Re-renders
214
+ * the host component whenever the underlying value changes (e.g. after
215
+ * a foreground rotation pulled new data).
216
+ */
217
+ export function useSecret(name: string): string | null {
218
+ const [value, setValue] = useState<string | null>(null);
219
+ useEffect(() => {
220
+ let active = true;
221
+ AppAttest.getSecret(name).then((v) => { if (active) setValue(v); }).catch(() => {});
222
+ const unsubscribe = AppAttest.addStateListener(async (s) => {
223
+ if (s.name === 'ready') {
224
+ try {
225
+ const v = await AppAttest.getSecret(name);
226
+ if (active) setValue(v);
227
+ } catch { /* ignore */ }
228
+ }
229
+ });
230
+ return () => {
231
+ active = false;
232
+ unsubscribe();
233
+ };
234
+ }, [name]);
235
+ return value;
236
+ }
237
+
238
+ /**
239
+ * React hook returning every synced secret. Re-renders on rotation.
240
+ */
241
+ export function useAllSecrets(): Record<string, string> {
242
+ const [all, setAll] = useState<Record<string, string>>({});
243
+ useEffect(() => {
244
+ let active = true;
245
+ AppAttest.getAllSecrets().then((v) => { if (active) setAll(v); }).catch(() => {});
246
+ const unsubscribe = AppAttest.addStateListener(async (s) => {
247
+ if (s.name === 'ready') {
248
+ try {
249
+ const v = await AppAttest.getAllSecrets();
250
+ if (active) setAll(v);
251
+ } catch { /* ignore */ }
252
+ }
253
+ });
254
+ return () => {
255
+ active = false;
256
+ unsubscribe();
257
+ };
258
+ }, []);
259
+ return all;
260
+ }
261
+
262
+ /**
263
+ * React hook returning the current `AppAttestState`. Re-renders on
264
+ * every transition.
265
+ */
266
+ export function useAppAttestState(): AppAttestState {
267
+ const [state, setState] = useState<AppAttestState>({ name: 'initializing' });
268
+ useEffect(() => {
269
+ let active = true;
270
+ AppAttest.getState().then((s) => { if (active) setState(s); }).catch(() => {});
271
+ const unsubscribe = AppAttest.addStateListener((s) => {
272
+ if (active) setState(s);
273
+ });
274
+ return () => {
275
+ active = false;
276
+ unsubscribe();
277
+ };
278
+ }, []);
279
+ return state;
280
+ }
281
+
282
+ export default AppAttest;