@postrun/react 1.0.0 → 1.2.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/README.md CHANGED
@@ -100,9 +100,34 @@ function ConnectX({
100
100
  ```
101
101
 
102
102
  Prefer wiring it yourself? `useConnect({ profileId, platform, onConnected })`
103
- returns the same `{ state, start, select, reset }` — `<Connect>` is just a thin
104
- render-prop wrapper over it. The hosted `/connect` page remains available as a
105
- no-SDK fallback (link to the `hosted_connect_url` from a `POST .../connect`).
103
+ returns the same `{ state, start, prepare, select, reset }` — `<Connect>` is just
104
+ a thin render-prop wrapper over it. The hosted `/connect` page remains available
105
+ as a no-SDK fallback (link to the `hosted_connect_url` from a `POST .../connect`).
106
+
107
+ A successful connect auto-refetches your `useConnections` list, so the new account
108
+ shows up with no manual refetch. Prefer callbacks to reading `state.phase`? Use
109
+ `onSuccess()` (fires on success — `active` **or** `connected_pending`, ideal for
110
+ "close the dialog"), `onConnected(connection)` (only `active`, hands you the bound
111
+ connection), `onError(reason)`, and `onCancelled()`.
112
+
113
+ **Multi-platform picker?** A session is pre-minted on mount, which is ideal for a
114
+ dedicated button but would mint one per platform in a "pick a network" list. Set
115
+ `prepareOnMount={false}` and call `prepare()` on the button's intent
116
+ (`onPointerEnter`/`onFocus`) so only the platform the user is about to click mints:
117
+
118
+ ```tsx
119
+ <Connect profileId={id} platform="meta_ads" prepareOnMount={false} onConnected={refetch}>
120
+ {({ state, start, prepare, select }) =>
121
+ state.phase === 'picking' ? (
122
+ <Picker accounts={state.accounts} onPick={select} />
123
+ ) : (
124
+ <button onPointerEnter={prepare} onFocus={prepare} onClick={start}>
125
+ Connect Meta
126
+ </button>
127
+ )
128
+ }
129
+ </Connect>
130
+ ```
106
131
 
107
132
  ## License
108
133
 
package/dist/index.cjs CHANGED
@@ -314,19 +314,31 @@ var POLL_TIMEOUT_MS = 15e3;
314
314
  function useConnect({
315
315
  profileId,
316
316
  platform,
317
- onConnected
317
+ onConnected,
318
+ onSuccess,
319
+ onError,
320
+ onCancelled,
321
+ prepareOnMount = true
318
322
  }) {
319
- const { client } = usePostrun();
323
+ const { client, queryClient } = usePostrun();
320
324
  const [state, setState] = react.useState({ phase: "preparing" });
321
325
  const [remintNonce, setRemintNonce] = react.useState(0);
322
326
  const sessionRef = react.useRef(null);
323
327
  const pickRef = react.useRef(null);
324
328
  const inFlightRef = react.useRef(false);
325
329
  const flowGenRef = react.useRef(0);
330
+ const preparingRef = react.useRef(false);
331
+ const prepareGenRef = react.useRef(0);
326
332
  const onConnectedRef = react.useRef(onConnected);
333
+ const onSuccessRef = react.useRef(onSuccess);
334
+ const onErrorRef = react.useRef(onError);
335
+ const onCancelledRef = react.useRef(onCancelled);
327
336
  react.useEffect(() => {
328
337
  onConnectedRef.current = onConnected;
329
- }, [onConnected]);
338
+ onSuccessRef.current = onSuccess;
339
+ onErrorRef.current = onError;
340
+ onCancelledRef.current = onCancelled;
341
+ });
330
342
  const abandonFlow = react.useCallback(() => {
331
343
  flowGenRef.current += 1;
332
344
  inFlightRef.current = false;
@@ -334,12 +346,23 @@ function useConnect({
334
346
  pickRef.current = null;
335
347
  pick?.reject(new Error("connect flow abandoned"));
336
348
  }, []);
337
- react.useEffect(() => {
338
- let abandoned = false;
349
+ const prepare = react.useCallback(() => {
350
+ if (sessionRef.current || preparingRef.current) return;
351
+ preparingRef.current = true;
352
+ const gen = prepareGenRef.current;
339
353
  setState({ phase: "preparing" });
340
- sessionRef.current = null;
354
+ const failPrepare = () => {
355
+ preparingRef.current = false;
356
+ setState({ phase: "error", reason: "prepare_failed" });
357
+ onErrorRef.current?.("prepare_failed");
358
+ };
341
359
  js.connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
342
- if (abandoned || !data) return;
360
+ if (prepareGenRef.current !== gen) return;
361
+ if (!data) {
362
+ failPrepare();
363
+ return;
364
+ }
365
+ preparingRef.current = false;
343
366
  sessionRef.current = {
344
367
  token: data.connect_session_token,
345
368
  providerConfigKey: data.provider_config_key,
@@ -347,16 +370,28 @@ function useConnect({
347
370
  };
348
371
  setState({ phase: "idle" });
349
372
  }).catch(() => {
350
- if (!abandoned) setState({ phase: "error", reason: "auth_failed" });
373
+ if (prepareGenRef.current !== gen) return;
374
+ failPrepare();
351
375
  });
376
+ }, [client, profileId, platform]);
377
+ react.useEffect(() => {
378
+ prepareGenRef.current += 1;
379
+ preparingRef.current = false;
380
+ sessionRef.current = null;
381
+ setState({ phase: "preparing" });
382
+ if (prepareOnMount) prepare();
352
383
  return () => {
353
- abandoned = true;
384
+ prepareGenRef.current += 1;
354
385
  abandonFlow();
355
386
  };
356
- }, [client, profileId, platform, remintNonce, abandonFlow]);
387
+ }, [profileId, platform, remintNonce, prepareOnMount, prepare, abandonFlow]);
357
388
  const start = react.useCallback(() => {
358
389
  const session = sessionRef.current;
359
- if (!session || inFlightRef.current) return;
390
+ if (!session) {
391
+ prepare();
392
+ return;
393
+ }
394
+ if (inFlightRef.current) return;
360
395
  inFlightRef.current = true;
361
396
  const gen = flowGenRef.current;
362
397
  const isCurrent = () => flowGenRef.current === gen;
@@ -421,20 +456,26 @@ function useConnect({
421
456
  switch (outcome.status) {
422
457
  case "active":
423
458
  setState({ phase: "active", connection: outcome.connection });
459
+ void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
424
460
  onConnectedRef.current?.(outcome.connection);
461
+ onSuccessRef.current?.();
425
462
  return;
426
463
  case "connected_pending":
427
464
  setState({ phase: "connected_pending" });
465
+ void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
466
+ onSuccessRef.current?.();
428
467
  return;
429
468
  case "cancelled":
430
469
  setState({ phase: "cancelled" });
470
+ onCancelledRef.current?.();
431
471
  return;
432
472
  case "error":
433
473
  setState({ phase: "error", reason: outcome.reason });
474
+ onErrorRef.current?.(outcome.reason);
434
475
  return;
435
476
  }
436
477
  });
437
- }, [client, profileId]);
478
+ }, [client, profileId, prepare, queryClient]);
438
479
  const select = react.useCallback((externalAccountId) => {
439
480
  const pick = pickRef.current;
440
481
  if (!pick) return;
@@ -445,7 +486,7 @@ function useConnect({
445
486
  const reset = react.useCallback(() => {
446
487
  setRemintNonce((n) => n + 1);
447
488
  }, []);
448
- return { state, start, select, reset };
489
+ return { state, start, prepare, select, reset };
449
490
  }
450
491
  function useConnections(profileId, filter) {
451
492
  const { client, queryClient } = usePostrun();
@@ -517,9 +558,21 @@ function Connect({
517
558
  profileId,
518
559
  platform,
519
560
  onConnected,
561
+ onSuccess,
562
+ onError,
563
+ onCancelled,
564
+ prepareOnMount,
520
565
  children
521
566
  }) {
522
- const api = useConnect({ profileId, platform, onConnected });
567
+ const api = useConnect({
568
+ profileId,
569
+ platform,
570
+ onConnected,
571
+ onSuccess,
572
+ onError,
573
+ onCancelled,
574
+ prepareOnMount
575
+ });
523
576
  return children(api);
524
577
  }
525
578
  var UploadError = class extends Error {