@postrun/react 0.2.0 → 1.0.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
@@ -3,8 +3,8 @@
3
3
  React provider and hooks for the [Postrun API](https://postrun.ai).
4
4
 
5
5
  Wrap your app once, then build your UI from hooks that handle the data fetching,
6
- caching, and orchestration for you — the hosted OAuth connect flow, the media
7
- upload pipeline, live status polling, and pagination.
6
+ caching, and orchestration for you — the embedded one-click OAuth connect flow,
7
+ the media upload pipeline, live status polling, and pagination.
8
8
 
9
9
  ```tsx
10
10
  import { PostrunProvider } from '@postrun/react';
@@ -32,9 +32,9 @@ calls `getToken` to supply it. The secret key never touches the browser.
32
32
  **Profiles** — `useProfiles` · `useProfilesInfinite` · `useProfile` ·
33
33
  `useCreateProfile` · `useUpdateProfile` · `useDeleteProfile`
34
34
 
35
- **Connections** — `useConnect` (hosted OAuth) · `useConnections` ·
36
- `useConnection` · `useDiscoverableAccounts` · `useSelectAccount` ·
37
- `useDisconnect`
35
+ **Connections** — `useConnect` (embedded one-click OAuth) · `Connect` (drop-in) ·
36
+ `useConnections` · `useConnection` · `useDiscoverableAccounts` ·
37
+ `useSelectAccount` · `useDisconnect`
38
38
 
39
39
  **Media** — `useMediaUpload` (signed upload → bytes → poll until ready) ·
40
40
  `useMedia` · `useUpdateMedia` · `useDeleteMedia`
@@ -50,6 +50,60 @@ settles, so a scheduled post visibly transitions with no manual refetch.
50
50
  Composite, opinionated UI (a post composer, a calendar grid) is **deliberately
51
51
  not shipped** — that's your product and your taste. Build it from the hooks above.
52
52
 
53
+ ## One-click connect
54
+
55
+ Connect a platform from **your own button**, in your own app — no redirect, no
56
+ second click. `<Connect>` runs the OAuth popup in-page and, for multi-account
57
+ platforms (Meta Ads, Facebook Pages), hands you the discoverable accounts to draw
58
+ your own picker. It's headless: you render every pixel.
59
+
60
+ ```tsx
61
+ import { Connect } from '@postrun/react';
62
+
63
+ function ConnectX({
64
+ profileId,
65
+ onConnected,
66
+ }: {
67
+ profileId: string;
68
+ onConnected: () => void; // e.g. refetch your connections list
69
+ }) {
70
+ return (
71
+ <Connect profileId={profileId} platform="x" onConnected={onConnected}>
72
+ {({ state, start, select, reset }) => {
73
+ if (state.phase === 'picking') {
74
+ return (
75
+ <ul>
76
+ {state.accounts.map((a) => (
77
+ <li key={a.external_account_id}>
78
+ <button onClick={() => select(a.external_account_id)}>
79
+ {a.name ?? a.external_account_id}
80
+ </button>
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ );
85
+ }
86
+ if (state.phase === 'error') {
87
+ return <button onClick={reset}>Try again</button>;
88
+ }
89
+ return (
90
+ // `start` MUST be called directly in the click — it opens the popup
91
+ // synchronously, so the browser keeps it inside the user gesture.
92
+ <button onClick={start} disabled={state.phase !== 'idle'}>
93
+ {state.phase === 'active' ? 'Connected ✓' : 'Connect X'}
94
+ </button>
95
+ );
96
+ }}
97
+ </Connect>
98
+ );
99
+ }
100
+ ```
101
+
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`).
106
+
53
107
  ## License
54
108
 
55
109
  [Apache-2.0](../../LICENSE) — use it freely in your product, with an explicit
package/dist/index.cjs CHANGED
@@ -4,8 +4,10 @@
4
4
  var react = require('react');
5
5
  var reactQuery = require('@tanstack/react-query');
6
6
  var js = require('@postrun/js');
7
- var pRetry = require('p-retry');
7
+ var Nango = require('@nangohq/frontend');
8
8
  var pWaitFor = require('p-wait-for');
9
+ var pLimit = require('p-limit');
10
+ var pRetry = require('p-retry');
9
11
  var axios = require('axios');
10
12
  var reactTweet = require('react-tweet');
11
13
  var fi = require('react-icons/fi');
@@ -15,8 +17,10 @@ var lu = require('react-icons/lu');
15
17
 
16
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
19
 
18
- var pRetry__default = /*#__PURE__*/_interopDefault(pRetry);
20
+ var Nango__default = /*#__PURE__*/_interopDefault(Nango);
19
21
  var pWaitFor__default = /*#__PURE__*/_interopDefault(pWaitFor);
22
+ var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
23
+ var pRetry__default = /*#__PURE__*/_interopDefault(pRetry);
20
24
  var axios__default = /*#__PURE__*/_interopDefault(axios);
21
25
  var twitterText__default = /*#__PURE__*/_interopDefault(twitterText);
22
26
 
@@ -203,29 +207,245 @@ function useDeleteProfile() {
203
207
  queryClient
204
208
  );
205
209
  }
206
-
207
- // src/navigate.ts
208
- function navigate(url) {
209
- window.location.assign(url);
210
+ var PENDING = { status: "connected_pending" };
211
+ var CANCELLED = { status: "cancelled" };
212
+ var active = (connection) => ({
213
+ status: "active",
214
+ connection
215
+ });
216
+ var failed = (reason) => ({
217
+ status: "error",
218
+ reason
219
+ });
220
+ function outcomeForAuthError(error) {
221
+ switch (error.type) {
222
+ case "window_closed":
223
+ return CANCELLED;
224
+ case "blocked_by_browser":
225
+ return failed("popup_blocked");
226
+ case "missing_auth_token":
227
+ case "invalid_host_url":
228
+ case "missing_credentials":
229
+ case "connection_test_failed":
230
+ case "missing_connect_session_token":
231
+ case "connection_validation_failed":
232
+ case "resource_capped":
233
+ case "unknown_error":
234
+ return failed("auth_failed");
235
+ }
236
+ }
237
+ async function grant(authorize) {
238
+ try {
239
+ return { ok: true, connectionId: await authorize() };
240
+ } catch (error) {
241
+ if (error instanceof Nango.AuthError) {
242
+ return { ok: false, outcome: outcomeForAuthError(error) };
243
+ }
244
+ return { ok: false, outcome: failed("auth_failed") };
245
+ }
246
+ }
247
+ async function awaitGrantedConnection(deps, nangoConnectionId) {
248
+ try {
249
+ return await pWaitFor__default.default(
250
+ async () => {
251
+ const rows = await deps.listByNangoConnectionId(nangoConnectionId);
252
+ const match = rows[0];
253
+ return match ? pWaitFor__default.default.resolveWith(match) : false;
254
+ },
255
+ { interval: deps.pollIntervalMs, timeout: deps.pollTimeoutMs }
256
+ );
257
+ } catch (error) {
258
+ if (error instanceof pWaitFor.TimeoutError) {
259
+ return null;
260
+ }
261
+ throw error;
262
+ }
263
+ }
264
+ async function bindPendingConnection(deps, connection) {
265
+ let accounts;
266
+ try {
267
+ accounts = await deps.discoverAccounts(connection.id);
268
+ } catch (error) {
269
+ if (error instanceof js.PostrunError && error.code === "not_implemented") {
270
+ return PENDING;
271
+ }
272
+ throw error;
273
+ }
274
+ if (accounts.length === 0) {
275
+ return PENDING;
276
+ }
277
+ const chosen = await deps.chooseAccount(accounts);
278
+ try {
279
+ return active(await deps.selectAccount(connection.id, chosen));
280
+ } catch (error) {
281
+ if (error instanceof js.PostrunError) {
282
+ return error.code === "connection_reauth_required" ? failed("reauth_required") : failed("select_failed");
283
+ }
284
+ throw error;
285
+ }
286
+ }
287
+ async function runEmbeddedConnect(deps) {
288
+ const granted = await grant(deps.authorize);
289
+ if (!granted.ok) {
290
+ return granted.outcome;
291
+ }
292
+ let connection;
293
+ try {
294
+ connection = await awaitGrantedConnection(deps, granted.connectionId);
295
+ } catch {
296
+ return failed("connection_not_found");
297
+ }
298
+ if (connection === null) {
299
+ return PENDING;
300
+ }
301
+ if (connection.external_account_id !== null) {
302
+ return active(connection);
303
+ }
304
+ try {
305
+ return await bindPendingConnection(deps, connection);
306
+ } catch {
307
+ return failed("select_failed");
308
+ }
210
309
  }
211
310
 
212
311
  // src/connections.ts
213
- function useConnect() {
214
- const { client, queryClient } = usePostrun();
215
- return reactQuery.useMutation(
216
- {
217
- mutationFn: async ({ profileId, platform }) => {
218
- const session = (await js.connectionsConnect({
312
+ var POLL_INTERVAL_MS = 1500;
313
+ var POLL_TIMEOUT_MS = 15e3;
314
+ function useConnect({
315
+ profileId,
316
+ platform,
317
+ onConnected
318
+ }) {
319
+ const { client } = usePostrun();
320
+ const [state, setState] = react.useState({ phase: "preparing" });
321
+ const [remintNonce, setRemintNonce] = react.useState(0);
322
+ const sessionRef = react.useRef(null);
323
+ const pickRef = react.useRef(null);
324
+ const inFlightRef = react.useRef(false);
325
+ const flowGenRef = react.useRef(0);
326
+ const onConnectedRef = react.useRef(onConnected);
327
+ react.useEffect(() => {
328
+ onConnectedRef.current = onConnected;
329
+ }, [onConnected]);
330
+ const abandonFlow = react.useCallback(() => {
331
+ flowGenRef.current += 1;
332
+ inFlightRef.current = false;
333
+ const pick = pickRef.current;
334
+ pickRef.current = null;
335
+ pick?.reject(new Error("connect flow abandoned"));
336
+ }, []);
337
+ react.useEffect(() => {
338
+ let abandoned = false;
339
+ setState({ phase: "preparing" });
340
+ sessionRef.current = null;
341
+ js.connectionsConnect({ client, path: { id: profileId }, body: { platform } }).then(({ data }) => {
342
+ if (abandoned || !data) return;
343
+ sessionRef.current = {
344
+ token: data.connect_session_token,
345
+ providerConfigKey: data.provider_config_key,
346
+ host: data.nango_host
347
+ };
348
+ setState({ phase: "idle" });
349
+ }).catch(() => {
350
+ if (!abandoned) setState({ phase: "error", reason: "auth_failed" });
351
+ });
352
+ return () => {
353
+ abandoned = true;
354
+ abandonFlow();
355
+ };
356
+ }, [client, profileId, platform, remintNonce, abandonFlow]);
357
+ const start = react.useCallback(() => {
358
+ const session = sessionRef.current;
359
+ if (!session || inFlightRef.current) return;
360
+ inFlightRef.current = true;
361
+ const gen = flowGenRef.current;
362
+ const isCurrent = () => flowGenRef.current === gen;
363
+ setState({ phase: "connecting" });
364
+ void runEmbeddedConnect({
365
+ // Nango lives INSIDE `authorize` so a SYNCHRONOUS throw (invalid host /
366
+ // missing token — the Nango SDK throws `AuthError` synchronously) becomes a
367
+ // promise rejection that `grant()` maps to `auth_failed`, never an uncaught
368
+ // throw escaping the click and wedging `inFlightRef`. Gesture timing still
369
+ // holds: `authorize()` is invoked SYNCHRONOUSLY down the
370
+ // start → runEmbeddedConnect → grant chain (each `await`'s operand is
371
+ // evaluated before it suspends), so `nango.auth()`'s `window.open` fires
372
+ // inside the user gesture, with no `await` before it.
373
+ authorize: async () => {
374
+ const nango = new Nango__default.default({
375
+ host: session.host,
376
+ connectSessionToken: session.token
377
+ });
378
+ const result = await nango.auth(session.providerConfigKey, {
379
+ detectClosedAuthWindow: true
380
+ });
381
+ return result.connectionId;
382
+ },
383
+ chooseAccount: (accounts) => new Promise((resolve, reject) => {
384
+ if (!isCurrent()) {
385
+ reject(new Error("connect flow abandoned"));
386
+ return;
387
+ }
388
+ pickRef.current = { resolve, reject };
389
+ setState({ phase: "picking", accounts });
390
+ }),
391
+ listByNangoConnectionId: async (nangoConnectionId) => {
392
+ const { data } = await js.connectionsListByProfile({
219
393
  client,
220
394
  path: { id: profileId },
221
- body: { platform }
222
- })).data;
223
- navigate(session.hosted_connect_url);
224
- return session;
395
+ query: { nango_connection_id: nangoConnectionId }
396
+ });
397
+ return data?.data ?? [];
398
+ },
399
+ discoverAccounts: async (connectionId) => {
400
+ const { data } = await js.connectionsListAccounts({
401
+ client,
402
+ path: { id: connectionId }
403
+ });
404
+ return data?.data ?? [];
405
+ },
406
+ selectAccount: async (connectionId, externalAccountId) => {
407
+ const { data } = await js.connectionsSelect({
408
+ client,
409
+ path: { id: connectionId },
410
+ body: { external_account_id: externalAccountId }
411
+ });
412
+ if (!data) throw new Error("select returned no connection");
413
+ return data;
414
+ },
415
+ pollIntervalMs: POLL_INTERVAL_MS,
416
+ pollTimeoutMs: POLL_TIMEOUT_MS
417
+ }).then((outcome) => {
418
+ if (!isCurrent()) return;
419
+ inFlightRef.current = false;
420
+ pickRef.current = null;
421
+ switch (outcome.status) {
422
+ case "active":
423
+ setState({ phase: "active", connection: outcome.connection });
424
+ onConnectedRef.current?.(outcome.connection);
425
+ return;
426
+ case "connected_pending":
427
+ setState({ phase: "connected_pending" });
428
+ return;
429
+ case "cancelled":
430
+ setState({ phase: "cancelled" });
431
+ return;
432
+ case "error":
433
+ setState({ phase: "error", reason: outcome.reason });
434
+ return;
225
435
  }
226
- },
227
- queryClient
228
- );
436
+ });
437
+ }, [client, profileId]);
438
+ const select = react.useCallback((externalAccountId) => {
439
+ const pick = pickRef.current;
440
+ if (!pick) return;
441
+ pickRef.current = null;
442
+ setState({ phase: "connecting" });
443
+ pick.resolve(externalAccountId);
444
+ }, []);
445
+ const reset = react.useCallback(() => {
446
+ setRemintNonce((n) => n + 1);
447
+ }, []);
448
+ return { state, start, select, reset };
229
449
  }
230
450
  function useConnections(profileId, filter) {
231
451
  const { client, queryClient } = usePostrun();
@@ -291,6 +511,17 @@ function useDisconnect() {
291
511
  queryClient
292
512
  );
293
513
  }
514
+
515
+ // src/Connect.tsx
516
+ function Connect({
517
+ profileId,
518
+ platform,
519
+ onConnected,
520
+ children
521
+ }) {
522
+ const api = useConnect({ profileId, platform, onConnected });
523
+ return children(api);
524
+ }
294
525
  var UploadError = class extends Error {
295
526
  status;
296
527
  constructor(status, message) {
@@ -331,17 +562,7 @@ async function uploadBytes(target, file, options = {}) {
331
562
  }
332
563
 
333
564
  // src/media.ts
334
- var DOCUMENT_MIME = /^application\/(pdf|msword|vnd\.(openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation)|ms-powerpoint))$/;
335
- function inferKind(contentType) {
336
- if (contentType === "image/gif") return "gif";
337
- if (contentType.startsWith("image/")) return "image";
338
- if (contentType.startsWith("video/")) return "video";
339
- if (DOCUMENT_MIME.test(contentType)) return "document";
340
- throw new Error(
341
- `Could not infer media kind from "${contentType}". Pass { kind } explicitly.`
342
- );
343
- }
344
- async function pollUntilSettled(client, id, signal) {
565
+ async function pollUntilSettled(client, id, signal, onTick) {
345
566
  let latest;
346
567
  await pWaitFor__default.default(
347
568
  async () => {
@@ -349,6 +570,7 @@ async function pollUntilSettled(client, id, signal) {
349
570
  throw new DOMException("Upload aborted", "AbortError");
350
571
  }
351
572
  latest = (await js.mediaGet({ client, path: { id } })).data;
573
+ onTick?.(latest);
352
574
  return latest.status === "ready" || latest.status === "failed";
353
575
  },
354
576
  { interval: 1500, timeout: 3e5 }
@@ -358,93 +580,134 @@ async function pollUntilSettled(client, id, signal) {
358
580
  }
359
581
  return latest;
360
582
  }
361
- function useMediaUpload() {
362
- const { client, queryClient } = usePostrun();
363
- const [status, setStatus] = react.useState("idle");
364
- const [progress, setProgress] = react.useState(0);
365
- const [media, setMedia] = react.useState(null);
366
- const [error, setError] = react.useState(null);
367
- const abortRef = react.useRef(null);
368
- const upload = react.useCallback(
369
- async (file, options) => {
370
- const contentType = options.contentType || file.type;
371
- if (!contentType) {
372
- throw new Error(
373
- "Could not determine the file's content type. Pass { contentType } explicitly."
374
- );
375
- }
376
- const kind = options.kind ?? inferKind(contentType);
377
- abortRef.current?.abort();
378
- const controller = new AbortController();
379
- abortRef.current = controller;
380
- setStatus("uploading");
381
- setProgress(0);
382
- setMedia(null);
383
- setError(null);
384
- try {
385
- const created = (await js.mediaCreate({
386
- client,
387
- body: {
388
- profile_id: options.profileId,
389
- kind,
390
- content_type: contentType,
391
- targets: options.targets,
392
- raw: options.raw,
393
- alt_text: options.altText,
394
- external_id: options.externalId,
395
- metadata: options.metadata
583
+ async function runUpload(client, file, options, signal, callbacks) {
584
+ const created = (await js.mediaCreate({
585
+ client,
586
+ body: {
587
+ profile_id: options.profileId,
588
+ kind: options.kind,
589
+ content_type: options.contentType,
590
+ targets: options.targets,
591
+ raw: options.raw,
592
+ alt_text: options.altText,
593
+ external_id: options.externalId,
594
+ metadata: options.metadata
595
+ }
596
+ })).data;
597
+ if (created.upload) {
598
+ const target = created.upload;
599
+ await pRetry__default.default(
600
+ async () => {
601
+ try {
602
+ await uploadBytes(target, file, {
603
+ onProgress: callbacks.onProgress,
604
+ signal
605
+ });
606
+ } catch (uploadError) {
607
+ if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
608
+ throw new pRetry.AbortError(uploadError);
396
609
  }
397
- })).data;
398
- if (created.upload) {
399
- const target = created.upload;
400
- await pRetry__default.default(
401
- async () => {
402
- try {
403
- await uploadBytes(target, file, {
404
- onProgress: setProgress,
405
- signal: controller.signal
406
- });
407
- } catch (uploadError) {
408
- if (uploadError instanceof UploadError && uploadError.status >= 400 && uploadError.status < 500) {
409
- throw new pRetry.AbortError(uploadError);
410
- }
411
- throw uploadError;
412
- }
413
- },
414
- { retries: 3, signal: controller.signal }
415
- );
416
- }
417
- setStatus("processing");
418
- const settled = await pollUntilSettled(
419
- client,
420
- created.id,
421
- controller.signal
422
- );
423
- queryClient.setQueryData(mediaKeys.detail(created.id), settled);
424
- void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
425
- setMedia(settled);
426
- setStatus(settled.status === "failed" ? "failed" : "ready");
427
- return settled;
428
- } catch (caught) {
429
- setError(caught);
430
- setStatus("failed");
431
- throw caught;
432
- } finally {
433
- if (abortRef.current === controller) {
434
- abortRef.current = null;
610
+ throw uploadError;
435
611
  }
612
+ },
613
+ { retries: 3, signal }
614
+ );
615
+ }
616
+ callbacks.onProcessing();
617
+ return pollUntilSettled(client, created.id, signal, callbacks.onPoll);
618
+ }
619
+ function toFileArray(files) {
620
+ if (files instanceof File) return [files];
621
+ return Array.from(files);
622
+ }
623
+ function useMediaUpload(options) {
624
+ const { client, queryClient } = usePostrun();
625
+ const [items, setItems] = react.useState([]);
626
+ const controllers = react.useRef(/* @__PURE__ */ new Map());
627
+ const limitRef = react.useRef(null);
628
+ if (!limitRef.current) {
629
+ limitRef.current = pLimit__default.default(options?.concurrency ?? 3);
630
+ }
631
+ const patch = react.useCallback(
632
+ (id, changes) => {
633
+ setItems(
634
+ (current) => current.map(
635
+ (item) => item.id === id ? { ...item, ...changes } : item
636
+ )
637
+ );
638
+ },
639
+ []
640
+ );
641
+ const add = react.useCallback(
642
+ (files, uploadOptions) => {
643
+ const queued = toFileArray(files).map((file) => ({
644
+ id: crypto.randomUUID(),
645
+ file,
646
+ status: "uploading",
647
+ progress: 0,
648
+ media: null,
649
+ error: null
650
+ }));
651
+ setItems((current) => [...current, ...queued]);
652
+ const limit = limitRef.current;
653
+ if (!limit) {
654
+ return Promise.resolve([]);
436
655
  }
656
+ const settlements = queued.map((item) => {
657
+ const controller = new AbortController();
658
+ controllers.current.set(item.id, controller);
659
+ return limit(() => {
660
+ if (controller.signal.aborted) {
661
+ throw new DOMException("Upload aborted", "AbortError");
662
+ }
663
+ return runUpload(client, item.file, uploadOptions, controller.signal, {
664
+ onProgress: (progress) => patch(item.id, { progress }),
665
+ onProcessing: () => patch(item.id, { status: "processing" }),
666
+ // Live server progress (stage + percent) each poll tick.
667
+ onPoll: (media) => patch(item.id, { media })
668
+ });
669
+ }).then((settled) => {
670
+ patch(item.id, {
671
+ status: settled.status === "failed" ? "failed" : "ready",
672
+ media: settled,
673
+ progress: 1
674
+ });
675
+ queryClient.setQueryData(mediaKeys.detail(settled.id), settled);
676
+ void queryClient.invalidateQueries({ queryKey: mediaKeys.lists() });
677
+ return settled;
678
+ }).catch((error) => {
679
+ if (controller.signal.aborted) {
680
+ return null;
681
+ }
682
+ patch(item.id, { status: "failed", error });
683
+ return null;
684
+ }).finally(() => {
685
+ controllers.current.delete(item.id);
686
+ });
687
+ });
688
+ return Promise.all(settlements).then(
689
+ (results) => results.filter((result) => result !== null)
690
+ );
437
691
  },
438
- [client, queryClient]
692
+ [client, queryClient, patch]
439
693
  );
440
- const cancel = react.useCallback(() => abortRef.current?.abort(), []);
694
+ const remove = react.useCallback((id) => {
695
+ controllers.current.get(id)?.abort();
696
+ controllers.current.delete(id);
697
+ setItems((current) => current.filter((item) => item.id !== id));
698
+ }, []);
441
699
  const reset = react.useCallback(() => {
442
- setStatus("idle");
443
- setProgress(0);
444
- setMedia(null);
445
- setError(null);
700
+ controllers.current.forEach((controller) => controller.abort());
701
+ controllers.current.clear();
702
+ setItems([]);
446
703
  }, []);
447
- return { upload, cancel, reset, status, progress, media, error };
704
+ const ready = items.flatMap(
705
+ (item) => item.status === "ready" && item.media ? [item.media] : []
706
+ );
707
+ const isUploading = items.some(
708
+ (item) => item.status === "uploading" || item.status === "processing"
709
+ );
710
+ return { items, ready, isUploading, add, remove, reset };
448
711
  }
449
712
  function useMedia(id) {
450
713
  const { client, queryClient } = usePostrun();
@@ -1326,6 +1589,7 @@ function LinkedInPostPreviewImpl({
1326
1589
  }
1327
1590
  var LinkedInPostPreview = react.memo(LinkedInPostPreviewImpl);
1328
1591
 
1592
+ exports.Connect = Connect;
1329
1593
  exports.LinkedInPostPreview = LinkedInPostPreview;
1330
1594
  exports.PostrunProvider = PostrunProvider;
1331
1595
  exports.UploadError = UploadError;