@limrun/ui 0.14.0-rc.5 → 0.14.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.
Files changed (115) hide show
  1. package/README.md +9 -26
  2. package/dist/components/remote-control.d.ts +36 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1189 -1089
  6. package/package.json +10 -51
  7. package/src/components/remote-control.tsx +267 -0
  8. package/src/index.ts +1 -1
  9. package/vite.config.ts +0 -9
  10. package/dist/app-store-relay/index.cjs +0 -9
  11. package/dist/app-store-relay/index.d.ts +0 -204
  12. package/dist/app-store-relay/index.js +0 -677
  13. package/dist/app-store-relay/react.cjs +0 -1
  14. package/dist/app-store-relay/react.d.ts +0 -15
  15. package/dist/app-store-relay/react.js +0 -69
  16. package/dist/apple-credentials/index.cjs +0 -1
  17. package/dist/apple-credentials/index.d.ts +0 -110
  18. package/dist/apple-credentials/index.js +0 -291
  19. package/dist/browser-storage-BnO-Ma-e.mjs +0 -219
  20. package/dist/browser-storage-DLxIqZk5.js +0 -1
  21. package/dist/client-C9z_sgOf.js +0 -1
  22. package/dist/client-CFDB4mX5.mjs +0 -191
  23. package/dist/core/device-install/apple/client.d.ts +0 -18
  24. package/dist/core/device-install/apple/crypto.d.ts +0 -20
  25. package/dist/core/device-install/apple/gsa-srp.d.ts +0 -26
  26. package/dist/core/device-install/apple/index.d.ts +0 -5
  27. package/dist/core/device-install/apple/provisioning.d.ts +0 -237
  28. package/dist/core/device-install/apple/relay.d.ts +0 -53
  29. package/dist/core/device-install/index.d.ts +0 -4
  30. package/dist/core/device-install/operations/index.d.ts +0 -6
  31. package/dist/core/device-install/operations/limbuild-client.d.ts +0 -43
  32. package/dist/core/device-install/operations/operations.d.ts +0 -47
  33. package/dist/core/device-install/operations/relay-client.d.ts +0 -26
  34. package/dist/core/device-install/operations/relay-protocol.d.ts +0 -27
  35. package/dist/core/device-install/operations/usbmux.d.ts +0 -32
  36. package/dist/core/device-install/operations/webusb.d.ts +0 -21
  37. package/dist/core/device-install/storage/browser-storage.d.ts +0 -56
  38. package/dist/core/device-install/storage/index.d.ts +0 -2
  39. package/dist/core/device-install/storage/secret-store.d.ts +0 -124
  40. package/dist/core/device-install/types.d.ts +0 -57
  41. package/dist/core/errors.d.ts +0 -1
  42. package/dist/core/play-publish/google.d.ts +0 -13
  43. package/dist/core/play-publish/publish.d.ts +0 -34
  44. package/dist/device-build/index.cjs +0 -1
  45. package/dist/device-build/index.d.ts +0 -4
  46. package/dist/device-build/index.js +0 -85
  47. package/dist/device-build/react.cjs +0 -1
  48. package/dist/device-build/react.d.ts +0 -21
  49. package/dist/device-build/react.js +0 -68
  50. package/dist/device-build/signing.d.ts +0 -20
  51. package/dist/device-install/demo/demo.d.ts +0 -0
  52. package/dist/device-install/index.cjs +0 -1
  53. package/dist/device-install/index.d.ts +0 -18
  54. package/dist/device-install/index.js +0 -594
  55. package/dist/device-install/react.cjs +0 -1
  56. package/dist/device-install/react.d.ts +0 -24
  57. package/dist/device-install/react.js +0 -105
  58. package/dist/errors-BSkqS7U-.js +0 -1
  59. package/dist/errors-gy8yQJos.mjs +0 -6
  60. package/dist/gsa-srp-Bme3JLEi.mjs +0 -266
  61. package/dist/gsa-srp-CCIfUz7Y.js +0 -1
  62. package/dist/index-ChhtuQ7e.js +0 -22
  63. package/dist/index-YDu8K9a4.mjs +0 -11279
  64. package/dist/limbuild-client-BagK_FWE.mjs +0 -83
  65. package/dist/limbuild-client-CVDwq54z.js +0 -1
  66. package/dist/play-publish/index.cjs +0 -1
  67. package/dist/play-publish/index.d.ts +0 -2
  68. package/dist/play-publish/index.js +0 -109
  69. package/dist/play-publish/react.cjs +0 -1
  70. package/dist/play-publish/react.d.ts +0 -31
  71. package/dist/play-publish/react.js +0 -91
  72. package/dist/provisioning-CDPvxd1h.mjs +0 -270
  73. package/dist/provisioning-CbC4h--7.js +0 -1
  74. package/src/app-store-relay/index.test.ts +0 -60
  75. package/src/app-store-relay/index.ts +0 -846
  76. package/src/app-store-relay/react.ts +0 -131
  77. package/src/apple-credentials/index.test.ts +0 -180
  78. package/src/apple-credentials/index.ts +0 -388
  79. package/src/core/device-install/apple/client.ts +0 -150
  80. package/src/core/device-install/apple/crypto.ts +0 -205
  81. package/src/core/device-install/apple/gsa-srp.ts +0 -123
  82. package/src/core/device-install/apple/index.ts +0 -5
  83. package/src/core/device-install/apple/provisioning.test.ts +0 -114
  84. package/src/core/device-install/apple/provisioning.ts +0 -419
  85. package/src/core/device-install/apple/relay.ts +0 -202
  86. package/src/core/device-install/index.ts +0 -4
  87. package/src/core/device-install/operations/index.ts +0 -6
  88. package/src/core/device-install/operations/limbuild-client.ts +0 -144
  89. package/src/core/device-install/operations/operations.ts +0 -239
  90. package/src/core/device-install/operations/relay-client.ts +0 -286
  91. package/src/core/device-install/operations/relay-protocol.ts +0 -71
  92. package/src/core/device-install/operations/usbmux.ts +0 -270
  93. package/src/core/device-install/operations/webusb-dom.d.ts +0 -54
  94. package/src/core/device-install/operations/webusb.ts +0 -110
  95. package/src/core/device-install/storage/browser-storage.test.ts +0 -65
  96. package/src/core/device-install/storage/browser-storage.ts +0 -325
  97. package/src/core/device-install/storage/index.ts +0 -2
  98. package/src/core/device-install/storage/secret-store.ts +0 -332
  99. package/src/core/device-install/types.ts +0 -69
  100. package/src/core/errors.ts +0 -3
  101. package/src/core/play-publish/google.ts +0 -113
  102. package/src/core/play-publish/publish.test.ts +0 -68
  103. package/src/core/play-publish/publish.ts +0 -89
  104. package/src/device-build/index.ts +0 -42
  105. package/src/device-build/react.ts +0 -130
  106. package/src/device-build/signing.ts +0 -94
  107. package/src/device-install/README.md +0 -423
  108. package/src/device-install/demo/demo.css +0 -493
  109. package/src/device-install/demo/demo.tsx +0 -1061
  110. package/src/device-install/demo/index.html +0 -12
  111. package/src/device-install/index.ts +0 -50
  112. package/src/device-install/react.ts +0 -195
  113. package/src/play-publish/README.md +0 -66
  114. package/src/play-publish/index.ts +0 -12
  115. package/src/play-publish/react.ts +0 -208
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Real Device Install Demo - @limrun/ui</title>
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="./demo.tsx"></script>
11
- </body>
12
- </html>
@@ -1,50 +0,0 @@
1
- import {
2
- requestUSBAccess as requestCoreUSBAccess,
3
- startInstallRelay,
4
- startPairingRelay,
5
- type RequestUSBAccessOptions as CoreRequestUSBAccessOptions,
6
- type StartInstallRelayOptions,
7
- type StartPairingRelayOptions,
8
- } from '../core/device-install/operations';
9
- import type { DeviceInstallLog } from '../core/device-install/types';
10
-
11
- export {
12
- closeDeviceRelayTarget,
13
- deviceRelayWebSocketUrl,
14
- startInstallRelay as startDeviceInstallRelay,
15
- startPairingRelay as startDevicePairingRelay,
16
- type DeviceRelayTarget,
17
- type InstallSource,
18
- type StartInstallRelayOptions,
19
- type StartPairingRelayOptions,
20
- } from '../core/device-install/operations';
21
- export { getPairRecord, normalizeUDID, putPairRecord } from '../core/device-install/storage';
22
- export type {
23
- DeviceHello,
24
- DeviceInstallLog,
25
- PairRecordPayload,
26
- StoredPairRecord,
27
- } from '../core/device-install/types';
28
- export { RelayClient } from '../core/device-install/operations';
29
-
30
- export type RequestUSBAccessOptions = Partial<CoreRequestUSBAccessOptions>;
31
-
32
- export async function requestUSBAccess(options: RequestUSBAccessOptions = {}) {
33
- return requestCoreUSBAccess({ log: options.log ?? noopLog });
34
- }
35
-
36
- export async function pairDevice(
37
- options: Omit<StartPairingRelayOptions, 'log'> & { log?: DeviceInstallLog },
38
- ) {
39
- return startPairingRelay({ ...options, log: options.log ?? noopLog });
40
- }
41
-
42
- export async function startDeviceInstall(
43
- options: Omit<StartInstallRelayOptions, 'log'> & { log?: DeviceInstallLog },
44
- ) {
45
- return startInstallRelay({ ...options, log: options.log ?? noopLog });
46
- }
47
-
48
- function noopLog() {
49
- // Intentionally empty. Consumers can pass a logger for progress messages.
50
- }
@@ -1,195 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import {
3
- closeDeviceRelayTarget,
4
- getPairRecord,
5
- pairDevice,
6
- putPairRecord,
7
- requestUSBAccess,
8
- startDeviceInstall,
9
- type DeviceInstallLog,
10
- type DeviceRelayTarget,
11
- type InstallSource,
12
- type RelayClient,
13
- type StoredPairRecord,
14
- } from './index';
15
- import { errorMessage } from '../core/errors';
16
-
17
- export type DeviceInstallRelayBusyAction = 'usb' | 'pair' | 'install';
18
-
19
- export type UseDeviceInstallRelayOptions = {
20
- apiUrl?: string;
21
- token?: string;
22
- organizationId?: string;
23
- log?: DeviceInstallLog;
24
- };
25
-
26
- export type UseDeviceInstallRelayResult = {
27
- device?: DeviceRelayTarget;
28
- pairRecord?: StoredPairRecord;
29
- busyAction?: DeviceInstallRelayBusyAction;
30
- error?: string;
31
- pairConfirmationRequired: boolean;
32
- hasPairRecord: boolean;
33
- canPair: boolean;
34
- canInstall: boolean;
35
- requestUSBAccess: () => Promise<DeviceRelayTarget | undefined>;
36
- pairBrowser: () => Promise<StoredPairRecord | undefined>;
37
- startInstallation: (installSource: InstallSource) => Promise<RelayClient | undefined>;
38
- stopRelay: () => void;
39
- clearError: () => void;
40
- };
41
-
42
- export function useDeviceInstallRelay({
43
- apiUrl,
44
- token,
45
- organizationId,
46
- log = noopLog,
47
- }: UseDeviceInstallRelayOptions): UseDeviceInstallRelayResult {
48
- const [device, setDevice] = useState<DeviceRelayTarget | undefined>();
49
- const [pairRecord, setPairRecord] = useState<StoredPairRecord | undefined>();
50
- const [busyAction, setBusyAction] = useState<DeviceInstallRelayBusyAction | undefined>();
51
- const [error, setError] = useState<string | undefined>();
52
- const [pairConfirmationRequired, setPairConfirmationRequired] = useState(false);
53
- const relayRef = useRef<RelayClient | undefined>(undefined);
54
- const deviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
55
- // Keep the logger in a ref so callbacks below don't change identity when the
56
- // consumer passes an unmemoized `log`. Otherwise every render would recreate
57
- // cleanupDeviceAccess, re-run the unmount effect, and close the USB device
58
- // mid-claim (surfacing as "Unable to claim interface" / "operation in progress").
59
- const logRef = useRef(log);
60
- logRef.current = log;
61
-
62
- const cleanupDeviceAccess = useCallback(async () => {
63
- relayRef.current?.close();
64
- relayRef.current = undefined;
65
- await closeDeviceRelayTarget(deviceRef.current, logRef.current);
66
- }, []);
67
-
68
- useEffect(() => {
69
- deviceRef.current = device;
70
- }, [device]);
71
-
72
- useEffect(() => {
73
- return () => {
74
- void cleanupDeviceAccess();
75
- };
76
- }, [cleanupDeviceAccess]);
77
-
78
- const selectUSBDevice = useCallback(async () => {
79
- setBusyAction('usb');
80
- setError(undefined);
81
- setPairConfirmationRequired(false);
82
- let target: DeviceRelayTarget | undefined;
83
- try {
84
- await cleanupDeviceAccess();
85
- target = await requestUSBAccess({ log: logRef.current });
86
- const storedPairRecord = await getPairRecord(target.hello.serialNumber);
87
- setDevice(target);
88
- setPairRecord(storedPairRecord);
89
- logRef.current(
90
- storedPairRecord ? 'Pair record found' : 'No pair record found',
91
- target.hello.serialNumber,
92
- );
93
- return target;
94
- } catch (caught) {
95
- await closeDeviceRelayTarget(target, logRef.current);
96
- setDevice(undefined);
97
- setPairRecord(undefined);
98
- setError(errorMessage(caught));
99
- return undefined;
100
- } finally {
101
- setBusyAction(undefined);
102
- }
103
- }, [cleanupDeviceAccess]);
104
-
105
- const pairBrowser = useCallback(async () => {
106
- if (!apiUrl || !device) {
107
- throw new Error('Select a USB device before pairing.');
108
- }
109
- setBusyAction('pair');
110
- setError(undefined);
111
- setPairConfirmationRequired(false);
112
- try {
113
- await cleanupDeviceAccess();
114
- const result = await pairDevice({
115
- registryApiUrl: apiUrl,
116
- token,
117
- organizationId,
118
- log: logRef.current,
119
- target: device,
120
- });
121
- const stored = await putPairRecord(result.pairRecord, {
122
- productName: device.hello.productName,
123
- });
124
- result.relay.close();
125
- await closeDeviceRelayTarget(device, logRef.current);
126
- setPairRecord(stored);
127
- setPairConfirmationRequired(false);
128
- logRef.current('Device paired', 'The pair record was stored locally in this browser.');
129
- return stored;
130
- } catch (caught) {
131
- await closeDeviceRelayTarget(device, logRef.current);
132
- setPairConfirmationRequired(true);
133
- setError(errorMessage(caught));
134
- return undefined;
135
- } finally {
136
- setBusyAction(undefined);
137
- }
138
- }, [apiUrl, cleanupDeviceAccess, device, organizationId, token]);
139
-
140
- const startInstallation = useCallback(
141
- async (installSource: InstallSource) => {
142
- if (!apiUrl || !device || !pairRecord) {
143
- throw new Error('Select and pair a USB device before starting installation.');
144
- }
145
- setBusyAction('install');
146
- setError(undefined);
147
- try {
148
- await cleanupDeviceAccess();
149
- relayRef.current = await startDeviceInstall({
150
- registryApiUrl: apiUrl,
151
- token,
152
- organizationId,
153
- log: logRef.current,
154
- target: device,
155
- pairRecord,
156
- installSource,
157
- });
158
- logRef.current('Device install started', 'Installation will continue through the connected iPhone.');
159
- return relayRef.current;
160
- } catch (caught) {
161
- await closeDeviceRelayTarget(device, logRef.current);
162
- setError(errorMessage(caught));
163
- return undefined;
164
- } finally {
165
- setBusyAction(undefined);
166
- }
167
- },
168
- [apiUrl, cleanupDeviceAccess, device, organizationId, pairRecord, token],
169
- );
170
-
171
- const stopRelay = useCallback(() => {
172
- void cleanupDeviceAccess();
173
- logRef.current('Device relay stopped');
174
- }, [cleanupDeviceAccess]);
175
-
176
- return {
177
- device,
178
- pairRecord,
179
- busyAction,
180
- error,
181
- pairConfirmationRequired,
182
- hasPairRecord: !!pairRecord,
183
- canPair: !!apiUrl && !busyAction && !!device,
184
- canInstall: !!apiUrl && !busyAction && !!device && !!pairRecord,
185
- requestUSBAccess: selectUSBDevice,
186
- pairBrowser,
187
- startInstallation,
188
- stopRelay,
189
- clearError: () => setError(undefined),
190
- };
191
- }
192
-
193
- function noopLog() {
194
- // Intentionally empty. Consumers can pass a logger for progress messages.
195
- }
@@ -1,66 +0,0 @@
1
- # @limrun/ui/play-publish
2
-
3
- Headless building blocks for publishing an AAB asset from the Limrun
4
- registry to Google Play, with a browser-owned Google sign-in. No UI ships
5
- here; embedders render their own buttons and dialogs around the hook,
6
- same as `device-install`.
7
-
8
- The Google access token is minted in the browser via Google Identity
9
- Services (token model, no client secret) and sent to the registry once
10
- per publish. Limrun never stores it.
11
-
12
- ## Requirements
13
-
14
- - A Google OAuth **Web application** client ID whose authorized
15
- JavaScript origins include your app's origin.
16
- - The signed-in Google account needs release permission for the target
17
- app in Play Console, and the app listing must already exist.
18
- - The asset must be an AAB signed with the app's Play upload key.
19
-
20
- ## React
21
-
22
- ```tsx
23
- import { usePlaystorePublish } from '@limrun/ui/play-publish/react';
24
-
25
- const play = usePlaystorePublish({
26
- registryApiUrl: 'https://registry.limrun.com',
27
- token: limrunToken,
28
- organizationId: organizationTid,
29
- googleClientId: GOOGLE_OAUTH_CLIENT_ID,
30
- });
31
-
32
- // On dialog open, warm the sign-in script so the click stays popup-safe.
33
- // Optionally await the returned promise (true = ready) to gate the button:
34
- play.preloadGoogle();
35
-
36
- // Button handlers:
37
- await play.signInWithGoogle();
38
- await play.publish({ assetName: 'app-release.aab', packageName: 'com.example.app' });
39
-
40
- // Render from state: play.status, play.versionCode, play.error, play.errorCode
41
- ```
42
-
43
- `errorCode` carries the registry's machine-readable error code; the
44
- canonical value list lives on `PlaystorePublishError`'s doc comment and
45
- grows additively.
46
-
47
- Google access tokens expire after about an hour. A `permissionDenied`
48
- error long after sign-in usually means the token expired; offer "Sign in
49
- with Google" again rather than pointing users at Play Console
50
- permissions.
51
-
52
- ## Without React
53
-
54
- ```ts
55
- import { requestGoogleAccessToken, publishToPlaystore } from '@limrun/ui/play-publish';
56
-
57
- const accessToken = await requestGoogleAccessToken({ clientId: GOOGLE_OAUTH_CLIENT_ID });
58
- const { versionCode } = await publishToPlaystore({
59
- registryApiUrl,
60
- token,
61
- organizationId,
62
- accessToken,
63
- assetName: 'app-release.aab',
64
- packageName: 'com.example.app',
65
- });
66
- ```
@@ -1,12 +0,0 @@
1
- export {
2
- ANDROID_PUBLISHER_SCOPE,
3
- loadGoogleIdentityServices,
4
- requestGoogleAccessToken,
5
- type RequestGoogleAccessTokenInput,
6
- } from '../core/play-publish/google';
7
- export {
8
- publishToPlaystore,
9
- PlaystorePublishError,
10
- type PlaystorePublishInput,
11
- type PlaystorePublishResult,
12
- } from '../core/play-publish/publish';
@@ -1,208 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react';
2
- import { errorMessage } from '../core/errors';
3
- import {
4
- loadGoogleIdentityServices,
5
- PlaystorePublishError,
6
- publishToPlaystore,
7
- requestGoogleAccessToken,
8
- type PlaystorePublishInput,
9
- type PlaystorePublishResult,
10
- } from './index';
11
-
12
- export type UsePlaystorePublishOptions = Partial<
13
- Pick<PlaystorePublishInput, 'registryApiUrl' | 'token' | 'organizationId'>
14
- > & {
15
- /** Google OAuth Web application client ID used for sign-in. */
16
- googleClientId?: string;
17
- };
18
-
19
- export type PlaystorePublishStatus = 'idle' | 'signing-in' | 'ready' | 'publishing' | 'published' | 'error';
20
-
21
- export type PlaystorePublishRequest = Omit<
22
- PlaystorePublishInput,
23
- 'registryApiUrl' | 'token' | 'organizationId' | 'accessToken'
24
- >;
25
-
26
- export type UsePlaystorePublishResult = {
27
- status: PlaystorePublishStatus;
28
- isSignedIn: boolean;
29
- versionCode?: number;
30
- error?: string;
31
- /** Registry error code driving per-cause UX, e.g. versionCodeExists. */
32
- errorCode?: string;
33
- /**
34
- * Warm up the Google sign-in script (e.g. on dialog open) so
35
- * signInWithGoogle stays synchronous with the click. Resolves true once
36
- * the script is ready; await it to gate the sign-in button, or ignore
37
- * the result and rely on the blocked-popup error plus retry.
38
- */
39
- preloadGoogle: () => Promise<boolean>;
40
- signInWithGoogle: () => Promise<boolean>;
41
- publish: (request: PlaystorePublishRequest) => Promise<PlaystorePublishResult | undefined>;
42
- /**
43
- * Clears the outcome state (keeps the Google session). Does not cancel an
44
- * in-flight sign-in or publish: their completion is ignored status-wise
45
- * and new calls stay refused until the in-flight one settles.
46
- */
47
- reset: () => void;
48
- };
49
-
50
- export function usePlaystorePublish({
51
- registryApiUrl,
52
- token,
53
- organizationId,
54
- googleClientId,
55
- }: UsePlaystorePublishOptions): UsePlaystorePublishResult {
56
- const [status, setStatus] = useState<PlaystorePublishStatus>('idle');
57
- // isSignedIn derives from token possession so the two can never diverge,
58
- // even when a reset() during the popup makes the flow's status stale.
59
- const [accessToken, setAccessToken] = useState<string | undefined>();
60
- const [versionCode, setVersionCode] = useState<number | undefined>();
61
- const [error, setError] = useState<string | undefined>();
62
- const [errorCode, setErrorCode] = useState<string | undefined>();
63
- // The token lives in a ref so publish() right after signInWithGoogle()
64
- // in one handler sees it before React re-renders.
65
- const accessTokenRef = useRef<string | undefined>(undefined);
66
- // reset() bumps the generation so an abandoned in-flight call cannot
67
- // overwrite the state it settles into.
68
- const generationRef = useRef(0);
69
- const busyRef = useRef(false);
70
-
71
- const preloadGoogle = useCallback(
72
- () =>
73
- loadGoogleIdentityServices().then(
74
- () => true,
75
- () => false,
76
- ),
77
- [],
78
- );
79
-
80
- // Guard failures replace the whole outcome, like the main paths do: a
81
- // stale errorCode or versionCode must not render beside the new error.
82
- const failGuard = useCallback((message: string) => {
83
- setError(message);
84
- setErrorCode(undefined);
85
- setVersionCode(undefined);
86
- setStatus('error');
87
- }, []);
88
-
89
- const signInWithGoogle = useCallback(async () => {
90
- if (busyRef.current) {
91
- return false;
92
- }
93
- if (!googleClientId) {
94
- failGuard('Google OAuth client ID is not configured.');
95
- return false;
96
- }
97
- busyRef.current = true;
98
- const generation = generationRef.current;
99
- setStatus('signing-in');
100
- setError(undefined);
101
- setErrorCode(undefined);
102
- try {
103
- const nextToken = await requestGoogleAccessToken({ clientId: googleClientId });
104
- accessTokenRef.current = nextToken;
105
- setAccessToken(nextToken);
106
- if (generation === generationRef.current) {
107
- setStatus('ready');
108
- } else {
109
- // A reset() during the popup ran before the token landed and left
110
- // idle; upgrade it so status agrees with isSignedIn. Anything but
111
- // idle belongs to a newer flow and must not be stomped.
112
- setStatus((current) => (current === 'idle' ? 'ready' : current));
113
- }
114
- return true;
115
- } catch (caught) {
116
- if (generation === generationRef.current) {
117
- setError(errorMessage(caught));
118
- setStatus('error');
119
- }
120
- return false;
121
- } finally {
122
- busyRef.current = false;
123
- }
124
- }, [googleClientId]);
125
-
126
- const publish = useCallback(
127
- async (request: PlaystorePublishRequest) => {
128
- if (busyRef.current) {
129
- return undefined;
130
- }
131
- if (!registryApiUrl) {
132
- failGuard('Registry URL is not configured.');
133
- return undefined;
134
- }
135
- const googleToken = accessTokenRef.current;
136
- if (!googleToken) {
137
- failGuard('Sign in with Google before publishing.');
138
- return undefined;
139
- }
140
- busyRef.current = true;
141
- const generation = generationRef.current;
142
- setStatus('publishing');
143
- setError(undefined);
144
- setErrorCode(undefined);
145
- // A stale code from an earlier publish must not render as this
146
- // attempt's success next to an error state.
147
- setVersionCode(undefined);
148
- try {
149
- // Fields picked explicitly: spreading a wider-typed request object
150
- // could smuggle same-named credential fields or excess properties
151
- // into the request body.
152
- const result = await publishToPlaystore({
153
- registryApiUrl,
154
- token,
155
- organizationId,
156
- accessToken: googleToken,
157
- packageName: request.packageName,
158
- assetId: request.assetId,
159
- assetName: request.assetName,
160
- track: request.track,
161
- releaseStatus: request.releaseStatus,
162
- });
163
- if (generation === generationRef.current) {
164
- setVersionCode(result.versionCode);
165
- setStatus('published');
166
- }
167
- return result;
168
- } catch (caught) {
169
- if (generation === generationRef.current) {
170
- setError(errorMessage(caught));
171
- if (caught instanceof PlaystorePublishError) {
172
- setErrorCode(caught.code);
173
- }
174
- setStatus('error');
175
- }
176
- return undefined;
177
- } finally {
178
- busyRef.current = false;
179
- }
180
- },
181
- [registryApiUrl, token, organizationId],
182
- );
183
-
184
- const reset = useCallback(() => {
185
- generationRef.current += 1;
186
- setVersionCode(undefined);
187
- setError(undefined);
188
- setErrorCode(undefined);
189
- setStatus(accessTokenRef.current ? 'ready' : 'idle');
190
- }, []);
191
-
192
- // Memoized so the result works as a prop or dependency without
193
- // re-rendering consumers on unrelated parent renders.
194
- return useMemo(
195
- () => ({
196
- status,
197
- isSignedIn: accessToken !== undefined,
198
- versionCode,
199
- error,
200
- errorCode,
201
- preloadGoogle,
202
- signInWithGoogle,
203
- publish,
204
- reset,
205
- }),
206
- [status, accessToken, versionCode, error, errorCode, preloadGoogle, signInWithGoogle, publish, reset],
207
- );
208
- }