@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/dist/index.d.cts CHANGED
@@ -211,7 +211,7 @@ declare function useDeleteProfile(): _tanstack_react_query.UseMutationResult<{
211
211
  /** One account offered for selection — the element type of the accounts list. */
212
212
  type DiscoverableAccount = DiscoverableAccountList['data'][number];
213
213
  /** Why a connect attempt ended in `error` — actionable reasons for the host. */
214
- type ConnectErrorReason = 'popup_blocked' | 'auth_failed' | 'connection_not_found' | 'select_failed' | 'reauth_required';
214
+ type ConnectErrorReason = 'prepare_failed' | 'popup_blocked' | 'auth_failed' | 'connection_not_found' | 'select_failed' | 'reauth_required';
215
215
  /**
216
216
  * The single outcome of a connect attempt. `active` carries the activated
217
217
  * connection (so the host can call `onConnected`); `connected_pending` means the
@@ -338,8 +338,23 @@ interface UseConnectParams {
338
338
  profileId: string;
339
339
  /** The platform to connect (X, LinkedIn, Meta, …). */
340
340
  platform: ConnectablePlatform;
341
- /** Called once a connection is fully ACTIVE (an account is bound). */
341
+ /** Called once a connection is fully ACTIVE (an account is bound). The
342
+ * connections list is auto-refetched too, so you rarely need to act here. */
342
343
  onConnected?: (connection: Connection) => void;
344
+ /** Called when the attempt fails, with the typed reason. */
345
+ onError?: (reason: ConnectErrorReason) => void;
346
+ /** Called when the user closes the OAuth popup without finishing. The hook
347
+ * stays in the `cancelled` phase; call `reset()` to re-arm for another try. */
348
+ onCancelled?: () => void;
349
+ /**
350
+ * Pre-mint the Nango session on mount (default `true`). Keep it `true` for a
351
+ * dedicated "Connect X" button. Set it `false` for a MULTI-platform picker —
352
+ * then call `prepare()` on the platform button's `onPointerEnter`/`onFocus` so
353
+ * only the platform the user is about to click mints a session (not all of
354
+ * them on open). The popup still needs a pre-minted session, so prepare on
355
+ * intent, not on click.
356
+ */
357
+ prepareOnMount?: boolean;
343
358
  }
344
359
  /**
345
360
  * The connect flow's UI state. `connected_pending` is a TERMINAL success state —
@@ -373,10 +388,17 @@ interface UseConnectResult {
373
388
  /**
374
389
  * Start the OAuth flow. MUST be called directly in the user's click handler
375
390
  * (no `await` before it): it opens the OAuth popup synchronously, so the
376
- * browser keeps it inside the user gesture. A no-op until the session is ready
377
- * (state `preparing`) disable the button until `phase` is `idle`.
391
+ * browser keeps it inside the user gesture. If the session isn't ready yet
392
+ * (`phase` !== `idle`) it kicks `prepare()` and no-ops the popup, so the next
393
+ * click works — prepare on intent (hover/focus) to make the first click open.
378
394
  */
379
395
  start: () => void;
396
+ /**
397
+ * Mint the Nango session ahead of the click. Idempotent (a no-op if a session
398
+ * is already held or a mint is in flight). Only needed with
399
+ * `prepareOnMount: false` — call it on the button's `onPointerEnter`/`onFocus`.
400
+ */
401
+ prepare: () => void;
380
402
  /** When `phase` is `picking`, activate the connection with the chosen account. */
381
403
  select: (externalAccountId: string) => void;
382
404
  /** Return to a fresh, ready state (re-mints the session) — e.g. a "try again". */
@@ -397,7 +419,7 @@ interface UseConnectResult {
397
419
  * The hosted `/connect` page remains the fallback for callers NOT using this SDK
398
420
  * (a plain link to `hosted_connect_url`); this hook never redirects.
399
421
  */
400
- declare function useConnect({ profileId, platform, onConnected, }: UseConnectParams): UseConnectResult;
422
+ declare function useConnect({ profileId, platform, onConnected, onError, onCancelled, prepareOnMount, }: UseConnectParams): UseConnectResult;
401
423
  /**
402
424
  * List a profile's connected accounts. Pass a `filter` to narrow by `kind`
403
425
  * (`posting` = social, `ads`) or `status` — e.g. a composer fetches
@@ -496,10 +518,15 @@ interface ConnectRenderApi {
496
518
  state: ConnectState;
497
519
  /**
498
520
  * Begin connecting. Call this DIRECTLY from your button's `onClick` — it opens
499
- * the OAuth popup synchronously, so don't `await` anything before it. A no-op
500
- * until the session is ready (`state.phase === 'preparing'`).
521
+ * the OAuth popup synchronously, so don't `await` anything before it.
501
522
  */
502
523
  start: () => void;
524
+ /**
525
+ * Mint the session ahead of the click — only needed with
526
+ * `prepareOnMount={false}` (a multi-platform picker): call it on the button's
527
+ * `onPointerEnter`/`onFocus`.
528
+ */
529
+ prepare: () => void;
503
530
  /** When `state.phase === 'picking'`, activate with the chosen account id. */
504
531
  select: (externalAccountId: string) => void;
505
532
  /** Reset to a fresh, ready state (e.g. a "try again" after an error/cancel). */
@@ -512,6 +539,13 @@ interface ConnectProps {
512
539
  platform: ConnectablePlatform;
513
540
  /** Called once a connection is fully ACTIVE (an account is bound). */
514
541
  onConnected?: (connection: Connection) => void;
542
+ /** Called when the attempt fails, with the typed reason. */
543
+ onError?: (reason: ConnectErrorReason) => void;
544
+ /** Called when the user closes the OAuth popup without finishing. */
545
+ onCancelled?: () => void;
546
+ /** Pre-mint on mount (default `true`). Set `false` for a multi-platform picker
547
+ * and call `prepare()` on intent — see {@link UseConnectParams.prepareOnMount}. */
548
+ prepareOnMount?: boolean;
515
549
  /** Render your own button + picker + status from the flow state. */
516
550
  children: (api: ConnectRenderApi) => ReactNode;
517
551
  }
@@ -546,7 +580,7 @@ interface ConnectProps {
546
580
  * The trigger MUST call `start()` directly in the click (it opens the popup
547
581
  * synchronously). Mount `<Connect>` inside a `<PostrunProvider>`.
548
582
  */
549
- declare function Connect({ profileId, platform, onConnected, children, }: ConnectProps): ReactNode;
583
+ declare function Connect({ profileId, platform, onConnected, onError, onCancelled, prepareOnMount, children, }: ConnectProps): ReactNode;
550
584
 
551
585
  type MediaUploadStatus = 'idle' | 'uploading' | 'processing' | 'ready' | 'failed';
552
586
  interface MediaUploadOptions {
package/dist/index.d.ts CHANGED
@@ -211,7 +211,7 @@ declare function useDeleteProfile(): _tanstack_react_query.UseMutationResult<{
211
211
  /** One account offered for selection — the element type of the accounts list. */
212
212
  type DiscoverableAccount = DiscoverableAccountList['data'][number];
213
213
  /** Why a connect attempt ended in `error` — actionable reasons for the host. */
214
- type ConnectErrorReason = 'popup_blocked' | 'auth_failed' | 'connection_not_found' | 'select_failed' | 'reauth_required';
214
+ type ConnectErrorReason = 'prepare_failed' | 'popup_blocked' | 'auth_failed' | 'connection_not_found' | 'select_failed' | 'reauth_required';
215
215
  /**
216
216
  * The single outcome of a connect attempt. `active` carries the activated
217
217
  * connection (so the host can call `onConnected`); `connected_pending` means the
@@ -338,8 +338,23 @@ interface UseConnectParams {
338
338
  profileId: string;
339
339
  /** The platform to connect (X, LinkedIn, Meta, …). */
340
340
  platform: ConnectablePlatform;
341
- /** Called once a connection is fully ACTIVE (an account is bound). */
341
+ /** Called once a connection is fully ACTIVE (an account is bound). The
342
+ * connections list is auto-refetched too, so you rarely need to act here. */
342
343
  onConnected?: (connection: Connection) => void;
344
+ /** Called when the attempt fails, with the typed reason. */
345
+ onError?: (reason: ConnectErrorReason) => void;
346
+ /** Called when the user closes the OAuth popup without finishing. The hook
347
+ * stays in the `cancelled` phase; call `reset()` to re-arm for another try. */
348
+ onCancelled?: () => void;
349
+ /**
350
+ * Pre-mint the Nango session on mount (default `true`). Keep it `true` for a
351
+ * dedicated "Connect X" button. Set it `false` for a MULTI-platform picker —
352
+ * then call `prepare()` on the platform button's `onPointerEnter`/`onFocus` so
353
+ * only the platform the user is about to click mints a session (not all of
354
+ * them on open). The popup still needs a pre-minted session, so prepare on
355
+ * intent, not on click.
356
+ */
357
+ prepareOnMount?: boolean;
343
358
  }
344
359
  /**
345
360
  * The connect flow's UI state. `connected_pending` is a TERMINAL success state —
@@ -373,10 +388,17 @@ interface UseConnectResult {
373
388
  /**
374
389
  * Start the OAuth flow. MUST be called directly in the user's click handler
375
390
  * (no `await` before it): it opens the OAuth popup synchronously, so the
376
- * browser keeps it inside the user gesture. A no-op until the session is ready
377
- * (state `preparing`) disable the button until `phase` is `idle`.
391
+ * browser keeps it inside the user gesture. If the session isn't ready yet
392
+ * (`phase` !== `idle`) it kicks `prepare()` and no-ops the popup, so the next
393
+ * click works — prepare on intent (hover/focus) to make the first click open.
378
394
  */
379
395
  start: () => void;
396
+ /**
397
+ * Mint the Nango session ahead of the click. Idempotent (a no-op if a session
398
+ * is already held or a mint is in flight). Only needed with
399
+ * `prepareOnMount: false` — call it on the button's `onPointerEnter`/`onFocus`.
400
+ */
401
+ prepare: () => void;
380
402
  /** When `phase` is `picking`, activate the connection with the chosen account. */
381
403
  select: (externalAccountId: string) => void;
382
404
  /** Return to a fresh, ready state (re-mints the session) — e.g. a "try again". */
@@ -397,7 +419,7 @@ interface UseConnectResult {
397
419
  * The hosted `/connect` page remains the fallback for callers NOT using this SDK
398
420
  * (a plain link to `hosted_connect_url`); this hook never redirects.
399
421
  */
400
- declare function useConnect({ profileId, platform, onConnected, }: UseConnectParams): UseConnectResult;
422
+ declare function useConnect({ profileId, platform, onConnected, onError, onCancelled, prepareOnMount, }: UseConnectParams): UseConnectResult;
401
423
  /**
402
424
  * List a profile's connected accounts. Pass a `filter` to narrow by `kind`
403
425
  * (`posting` = social, `ads`) or `status` — e.g. a composer fetches
@@ -496,10 +518,15 @@ interface ConnectRenderApi {
496
518
  state: ConnectState;
497
519
  /**
498
520
  * Begin connecting. Call this DIRECTLY from your button's `onClick` — it opens
499
- * the OAuth popup synchronously, so don't `await` anything before it. A no-op
500
- * until the session is ready (`state.phase === 'preparing'`).
521
+ * the OAuth popup synchronously, so don't `await` anything before it.
501
522
  */
502
523
  start: () => void;
524
+ /**
525
+ * Mint the session ahead of the click — only needed with
526
+ * `prepareOnMount={false}` (a multi-platform picker): call it on the button's
527
+ * `onPointerEnter`/`onFocus`.
528
+ */
529
+ prepare: () => void;
503
530
  /** When `state.phase === 'picking'`, activate with the chosen account id. */
504
531
  select: (externalAccountId: string) => void;
505
532
  /** Reset to a fresh, ready state (e.g. a "try again" after an error/cancel). */
@@ -512,6 +539,13 @@ interface ConnectProps {
512
539
  platform: ConnectablePlatform;
513
540
  /** Called once a connection is fully ACTIVE (an account is bound). */
514
541
  onConnected?: (connection: Connection) => void;
542
+ /** Called when the attempt fails, with the typed reason. */
543
+ onError?: (reason: ConnectErrorReason) => void;
544
+ /** Called when the user closes the OAuth popup without finishing. */
545
+ onCancelled?: () => void;
546
+ /** Pre-mint on mount (default `true`). Set `false` for a multi-platform picker
547
+ * and call `prepare()` on intent — see {@link UseConnectParams.prepareOnMount}. */
548
+ prepareOnMount?: boolean;
515
549
  /** Render your own button + picker + status from the flow state. */
516
550
  children: (api: ConnectRenderApi) => ReactNode;
517
551
  }
@@ -546,7 +580,7 @@ interface ConnectProps {
546
580
  * The trigger MUST call `start()` directly in the click (it opens the popup
547
581
  * synchronously). Mount `<Connect>` inside a `<PostrunProvider>`.
548
582
  */
549
- declare function Connect({ profileId, platform, onConnected, children, }: ConnectProps): ReactNode;
583
+ declare function Connect({ profileId, platform, onConnected, onError, onCancelled, prepareOnMount, children, }: ConnectProps): ReactNode;
550
584
 
551
585
  type MediaUploadStatus = 'idle' | 'uploading' | 'processing' | 'ready' | 'failed';
552
586
  interface MediaUploadOptions {
package/dist/index.js CHANGED
@@ -303,19 +303,28 @@ var POLL_TIMEOUT_MS = 15e3;
303
303
  function useConnect({
304
304
  profileId,
305
305
  platform,
306
- onConnected
306
+ onConnected,
307
+ onError,
308
+ onCancelled,
309
+ prepareOnMount = true
307
310
  }) {
308
- const { client } = usePostrun();
311
+ const { client, queryClient } = usePostrun();
309
312
  const [state, setState] = useState({ phase: "preparing" });
310
313
  const [remintNonce, setRemintNonce] = useState(0);
311
314
  const sessionRef = useRef(null);
312
315
  const pickRef = useRef(null);
313
316
  const inFlightRef = useRef(false);
314
317
  const flowGenRef = useRef(0);
318
+ const preparingRef = useRef(false);
319
+ const prepareGenRef = useRef(0);
315
320
  const onConnectedRef = useRef(onConnected);
321
+ const onErrorRef = useRef(onError);
322
+ const onCancelledRef = useRef(onCancelled);
316
323
  useEffect(() => {
317
324
  onConnectedRef.current = onConnected;
318
- }, [onConnected]);
325
+ onErrorRef.current = onError;
326
+ onCancelledRef.current = onCancelled;
327
+ });
319
328
  const abandonFlow = useCallback(() => {
320
329
  flowGenRef.current += 1;
321
330
  inFlightRef.current = false;
@@ -323,12 +332,23 @@ function useConnect({
323
332
  pickRef.current = null;
324
333
  pick?.reject(new Error("connect flow abandoned"));
325
334
  }, []);
326
- useEffect(() => {
327
- let abandoned = false;
335
+ const prepare = useCallback(() => {
336
+ if (sessionRef.current || preparingRef.current) return;
337
+ preparingRef.current = true;
338
+ const gen = prepareGenRef.current;
328
339
  setState({ phase: "preparing" });
329
- sessionRef.current = null;
340
+ const failPrepare = () => {
341
+ preparingRef.current = false;
342
+ setState({ phase: "error", reason: "prepare_failed" });
343
+ onErrorRef.current?.("prepare_failed");
344
+ };
330
345
  connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
331
- if (abandoned || !data) return;
346
+ if (prepareGenRef.current !== gen) return;
347
+ if (!data) {
348
+ failPrepare();
349
+ return;
350
+ }
351
+ preparingRef.current = false;
332
352
  sessionRef.current = {
333
353
  token: data.connect_session_token,
334
354
  providerConfigKey: data.provider_config_key,
@@ -336,16 +356,28 @@ function useConnect({
336
356
  };
337
357
  setState({ phase: "idle" });
338
358
  }).catch(() => {
339
- if (!abandoned) setState({ phase: "error", reason: "auth_failed" });
359
+ if (prepareGenRef.current !== gen) return;
360
+ failPrepare();
340
361
  });
362
+ }, [client, profileId, platform]);
363
+ useEffect(() => {
364
+ prepareGenRef.current += 1;
365
+ preparingRef.current = false;
366
+ sessionRef.current = null;
367
+ setState({ phase: "preparing" });
368
+ if (prepareOnMount) prepare();
341
369
  return () => {
342
- abandoned = true;
370
+ prepareGenRef.current += 1;
343
371
  abandonFlow();
344
372
  };
345
- }, [client, profileId, platform, remintNonce, abandonFlow]);
373
+ }, [profileId, platform, remintNonce, prepareOnMount, prepare, abandonFlow]);
346
374
  const start = useCallback(() => {
347
375
  const session = sessionRef.current;
348
- if (!session || inFlightRef.current) return;
376
+ if (!session) {
377
+ prepare();
378
+ return;
379
+ }
380
+ if (inFlightRef.current) return;
349
381
  inFlightRef.current = true;
350
382
  const gen = flowGenRef.current;
351
383
  const isCurrent = () => flowGenRef.current === gen;
@@ -410,20 +442,24 @@ function useConnect({
410
442
  switch (outcome.status) {
411
443
  case "active":
412
444
  setState({ phase: "active", connection: outcome.connection });
445
+ void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
413
446
  onConnectedRef.current?.(outcome.connection);
414
447
  return;
415
448
  case "connected_pending":
416
449
  setState({ phase: "connected_pending" });
450
+ void queryClient.invalidateQueries({ queryKey: connectionKeys.lists() });
417
451
  return;
418
452
  case "cancelled":
419
453
  setState({ phase: "cancelled" });
454
+ onCancelledRef.current?.();
420
455
  return;
421
456
  case "error":
422
457
  setState({ phase: "error", reason: outcome.reason });
458
+ onErrorRef.current?.(outcome.reason);
423
459
  return;
424
460
  }
425
461
  });
426
- }, [client, profileId]);
462
+ }, [client, profileId, prepare, queryClient]);
427
463
  const select = useCallback((externalAccountId) => {
428
464
  const pick = pickRef.current;
429
465
  if (!pick) return;
@@ -434,7 +470,7 @@ function useConnect({
434
470
  const reset = useCallback(() => {
435
471
  setRemintNonce((n) => n + 1);
436
472
  }, []);
437
- return { state, start, select, reset };
473
+ return { state, start, prepare, select, reset };
438
474
  }
439
475
  function useConnections(profileId, filter) {
440
476
  const { client, queryClient } = usePostrun();
@@ -506,9 +542,19 @@ function Connect({
506
542
  profileId,
507
543
  platform,
508
544
  onConnected,
545
+ onError,
546
+ onCancelled,
547
+ prepareOnMount,
509
548
  children
510
549
  }) {
511
- const api = useConnect({ profileId, platform, onConnected });
550
+ const api = useConnect({
551
+ profileId,
552
+ platform,
553
+ onConnected,
554
+ onError,
555
+ onCancelled,
556
+ prepareOnMount
557
+ });
512
558
  return children(api);
513
559
  }
514
560
  var UploadError = class extends Error {