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