@sendoracloud/sdk-react-native 1.1.0 → 1.3.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.
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/contact-widget.tsx
31
+ var contact_widget_exports = {};
32
+ __export(contact_widget_exports, {
33
+ ContactWidget: () => ContactWidget,
34
+ useContactWidget: () => useContactWidget
35
+ });
36
+ module.exports = __toCommonJS(contact_widget_exports);
37
+ var React = __toESM(require("react"), 1);
38
+ var import_react_native = require("react-native");
39
+ var import_react_native_webview = require("react-native-webview");
40
+ var import_jsx_runtime = require("react/jsx-runtime");
41
+ function ContactWidget({
42
+ visible,
43
+ widgetId,
44
+ workerHost = "https://go.sendoracloud.com",
45
+ theme = "auto",
46
+ prefillName,
47
+ prefillEmail,
48
+ prefillUserId,
49
+ lockEmail,
50
+ onClose
51
+ }) {
52
+ const didFire = React.useRef(false);
53
+ React.useEffect(() => {
54
+ if (visible) didFire.current = false;
55
+ }, [visible]);
56
+ if (!widgetId) return null;
57
+ const params = new URLSearchParams({ widgetId, theme });
58
+ if (prefillName) params.set("prefillName", prefillName);
59
+ if (prefillEmail) params.set("prefillEmail", prefillEmail);
60
+ if (prefillUserId) params.set("prefillUserId", prefillUserId);
61
+ if (lockEmail && prefillEmail) params.set("lockEmail", "1");
62
+ const uri = `${workerHost}/embed/contact?${params.toString()}`;
63
+ const handleClose = (result) => {
64
+ if (didFire.current) return;
65
+ didFire.current = true;
66
+ onClose(result);
67
+ };
68
+ const onNav = (req) => {
69
+ const url = req.url || "";
70
+ if (url.startsWith("sendora://close")) {
71
+ const queryStart = url.indexOf("?");
72
+ const params2 = queryStart >= 0 ? new URLSearchParams(url.slice(queryStart + 1)) : new URLSearchParams();
73
+ const ticketId = params2.get("ticketId") || "";
74
+ const portalUrl = params2.get("portalUrl") || null;
75
+ handleClose({ ticketId, portalUrl, submitted: ticketId !== "" });
76
+ return false;
77
+ }
78
+ if (!url.startsWith("https://") && !url.startsWith("about:")) {
79
+ return false;
80
+ }
81
+ return true;
82
+ };
83
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
84
+ import_react_native.Modal,
85
+ {
86
+ visible,
87
+ animationType: "slide",
88
+ presentationStyle: "pageSheet",
89
+ onRequestClose: () => handleClose({ ticketId: "", portalUrl: null, submitted: false }),
90
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
91
+ import_react_native_webview.WebView,
92
+ {
93
+ source: { uri },
94
+ onShouldStartLoadWithRequest: onNav,
95
+ originWhitelist: ["https://*", "about:*", "sendora://*"],
96
+ setSupportMultipleWindows: false,
97
+ javaScriptEnabled: true,
98
+ domStorageEnabled: true,
99
+ allowsBackForwardNavigationGestures: false
100
+ }
101
+ ) })
102
+ }
103
+ );
104
+ }
105
+ function useContactWidget(widgetId, defaults) {
106
+ const [visible, setVisible] = React.useState(false);
107
+ const [lastResult, setLastResult] = React.useState(null);
108
+ const open = React.useCallback(() => setVisible(true), []);
109
+ const close = React.useCallback(() => setVisible(false), []);
110
+ const sdkUser = defaults?.user ?? null;
111
+ const resolvedName = defaults?.prefillName ?? sdkUser?.name ?? void 0;
112
+ const resolvedEmail = defaults?.prefillEmail ?? sdkUser?.email ?? void 0;
113
+ const resolvedUserId = defaults?.prefillUserId ?? sdkUser?.id ?? void 0;
114
+ const resolvedLockEmail = defaults?.lockEmail ?? (sdkUser != null && sdkUser.email != null && sdkUser.emailVerified && !sdkUser.isAnonymous);
115
+ const View2 = React.useCallback(
116
+ () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
117
+ ContactWidget,
118
+ {
119
+ workerHost: defaults?.workerHost,
120
+ theme: defaults?.theme,
121
+ widgetId,
122
+ visible,
123
+ prefillName: resolvedName,
124
+ prefillEmail: resolvedEmail,
125
+ prefillUserId: resolvedUserId,
126
+ lockEmail: resolvedLockEmail,
127
+ onClose: (r) => {
128
+ setLastResult(r);
129
+ setVisible(false);
130
+ }
131
+ }
132
+ ),
133
+ [defaults, widgetId, visible, resolvedName, resolvedEmail, resolvedUserId, resolvedLockEmail]
134
+ );
135
+ return { open, close, View: View2, lastResult };
136
+ }
137
+ // Annotate the CommonJS export names for ESM import in node:
138
+ 0 && (module.exports = {
139
+ ContactWidget,
140
+ useContactWidget
141
+ });
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ export { WebViewMessageEvent } from 'react-native-webview';
3
+
4
+ /**
5
+ * Wave 66 — ContactWidget for React Native apps.
6
+ *
7
+ * Renders the worker-hosted embed at
8
+ * `https://go.sendoracloud.com/embed/contact?widgetId=…` inside a
9
+ * `<WebView>` from `react-native-webview`. The form posts to
10
+ * `POST /widgets/:id/submit` server-side.
11
+ *
12
+ * On submit success the embed page navigates to
13
+ * `sendora://close?ticketId=…&portalUrl=…`. WebView's
14
+ * `onShouldStartLoadWithRequest` catches the scheme + fires
15
+ * `onClose(result)` so the host can hide the modal.
16
+ *
17
+ * Single source of truth = the worker route. UI updates = redeploy
18
+ * worker. Zero SDK release.
19
+ *
20
+ * Import from subpath so customers without react-native-webview can
21
+ * still use the main SDK without Metro choking on a missing dep:
22
+ *
23
+ * import { ContactWidget } from "@sendoracloud/sdk-react-native/contact-widget";
24
+ */
25
+
26
+ interface ContactWidgetResult {
27
+ /** UUID of the ticket created server-side, or empty if user closed without submitting. */
28
+ ticketId: string;
29
+ /** Operator's tracking-portal URL string, or null. */
30
+ portalUrl: string | null;
31
+ /** True if the user submitted the form; false if they closed without submitting. */
32
+ submitted: boolean;
33
+ }
34
+ interface ContactWidgetProps {
35
+ /** Whether the modal sheet is visible. */
36
+ visible: boolean;
37
+ /** UUID of the widget configured in the Sendora dashboard. */
38
+ widgetId: string;
39
+ /** Worker host. Defaults to production. */
40
+ workerHost?: string;
41
+ /** `"auto"` | `"light"` | `"dark"`. Default `"auto"`. */
42
+ theme?: "auto" | "light" | "dark";
43
+ /** Pre-fill name field for signed-in users. */
44
+ prefillName?: string;
45
+ /** Pre-fill email field for signed-in users. */
46
+ prefillEmail?: string;
47
+ /**
48
+ * Wave 68 — stitch the ticket to the same profile as analytics
49
+ * events. Pass the signed-in user's id (anon or identified). Hint
50
+ * only, not an auth boundary — backend stores it without verifying
51
+ * because the WebView is in customer-app code.
52
+ */
53
+ prefillUserId?: string;
54
+ /**
55
+ * When true (and `prefillEmail` is set) the email field is hidden +
56
+ * read-only. Use for identified users so the agent inbox can trust
57
+ * the email field came from the customer's verified account, not
58
+ * free text.
59
+ */
60
+ lockEmail?: boolean;
61
+ /**
62
+ * Fired when the user closes the sheet — either after a successful
63
+ * submit (`submitted = true`) or via the close button
64
+ * (`submitted = false`). Host should set `visible = false` here.
65
+ */
66
+ onClose: (result: ContactWidgetResult) => void;
67
+ }
68
+ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, prefillUserId, lockEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
69
+ /**
70
+ * Helper hook so the host doesn't have to wire visibility state by
71
+ * hand.
72
+ *
73
+ * const contact = useContactWidget("widget-uuid");
74
+ * <Button onPress={contact.open} title="Contact us" />
75
+ * <contact.View />
76
+ *
77
+ * Auto-resolves identity from the SDK when the host passes the
78
+ * current `AuthUser` via the `user` default. Identified users get the
79
+ * locked-email mode + userId attached automatically. Anonymous SDK
80
+ * rows get the userId attached without locking. No user = standard
81
+ * form.
82
+ *
83
+ * import SendoraCloud from "@sendoracloud/sdk-react-native";
84
+ * const user = SendoraCloud.auth.getCurrentUser();
85
+ * const contact = useContactWidget("widget-uuid", { user });
86
+ */
87
+ declare function useContactWidget(widgetId: string, defaults?: Omit<ContactWidgetProps, "visible" | "widgetId" | "onClose"> & {
88
+ /** Pass `SendoraCloud.auth.getCurrentUser()` here to auto-stitch identity. */
89
+ user?: {
90
+ id: string;
91
+ email: string | null;
92
+ emailVerified: boolean;
93
+ name: string | null;
94
+ isAnonymous: boolean;
95
+ } | null;
96
+ }): {
97
+ open: () => void;
98
+ close: () => void;
99
+ View: () => React.ReactElement | null;
100
+ lastResult: ContactWidgetResult | null;
101
+ };
102
+
103
+ export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, useContactWidget };
@@ -0,0 +1,103 @@
1
+ import * as React from 'react';
2
+ export { WebViewMessageEvent } from 'react-native-webview';
3
+
4
+ /**
5
+ * Wave 66 — ContactWidget for React Native apps.
6
+ *
7
+ * Renders the worker-hosted embed at
8
+ * `https://go.sendoracloud.com/embed/contact?widgetId=…` inside a
9
+ * `<WebView>` from `react-native-webview`. The form posts to
10
+ * `POST /widgets/:id/submit` server-side.
11
+ *
12
+ * On submit success the embed page navigates to
13
+ * `sendora://close?ticketId=…&portalUrl=…`. WebView's
14
+ * `onShouldStartLoadWithRequest` catches the scheme + fires
15
+ * `onClose(result)` so the host can hide the modal.
16
+ *
17
+ * Single source of truth = the worker route. UI updates = redeploy
18
+ * worker. Zero SDK release.
19
+ *
20
+ * Import from subpath so customers without react-native-webview can
21
+ * still use the main SDK without Metro choking on a missing dep:
22
+ *
23
+ * import { ContactWidget } from "@sendoracloud/sdk-react-native/contact-widget";
24
+ */
25
+
26
+ interface ContactWidgetResult {
27
+ /** UUID of the ticket created server-side, or empty if user closed without submitting. */
28
+ ticketId: string;
29
+ /** Operator's tracking-portal URL string, or null. */
30
+ portalUrl: string | null;
31
+ /** True if the user submitted the form; false if they closed without submitting. */
32
+ submitted: boolean;
33
+ }
34
+ interface ContactWidgetProps {
35
+ /** Whether the modal sheet is visible. */
36
+ visible: boolean;
37
+ /** UUID of the widget configured in the Sendora dashboard. */
38
+ widgetId: string;
39
+ /** Worker host. Defaults to production. */
40
+ workerHost?: string;
41
+ /** `"auto"` | `"light"` | `"dark"`. Default `"auto"`. */
42
+ theme?: "auto" | "light" | "dark";
43
+ /** Pre-fill name field for signed-in users. */
44
+ prefillName?: string;
45
+ /** Pre-fill email field for signed-in users. */
46
+ prefillEmail?: string;
47
+ /**
48
+ * Wave 68 — stitch the ticket to the same profile as analytics
49
+ * events. Pass the signed-in user's id (anon or identified). Hint
50
+ * only, not an auth boundary — backend stores it without verifying
51
+ * because the WebView is in customer-app code.
52
+ */
53
+ prefillUserId?: string;
54
+ /**
55
+ * When true (and `prefillEmail` is set) the email field is hidden +
56
+ * read-only. Use for identified users so the agent inbox can trust
57
+ * the email field came from the customer's verified account, not
58
+ * free text.
59
+ */
60
+ lockEmail?: boolean;
61
+ /**
62
+ * Fired when the user closes the sheet — either after a successful
63
+ * submit (`submitted = true`) or via the close button
64
+ * (`submitted = false`). Host should set `visible = false` here.
65
+ */
66
+ onClose: (result: ContactWidgetResult) => void;
67
+ }
68
+ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, prefillUserId, lockEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
69
+ /**
70
+ * Helper hook so the host doesn't have to wire visibility state by
71
+ * hand.
72
+ *
73
+ * const contact = useContactWidget("widget-uuid");
74
+ * <Button onPress={contact.open} title="Contact us" />
75
+ * <contact.View />
76
+ *
77
+ * Auto-resolves identity from the SDK when the host passes the
78
+ * current `AuthUser` via the `user` default. Identified users get the
79
+ * locked-email mode + userId attached automatically. Anonymous SDK
80
+ * rows get the userId attached without locking. No user = standard
81
+ * form.
82
+ *
83
+ * import SendoraCloud from "@sendoracloud/sdk-react-native";
84
+ * const user = SendoraCloud.auth.getCurrentUser();
85
+ * const contact = useContactWidget("widget-uuid", { user });
86
+ */
87
+ declare function useContactWidget(widgetId: string, defaults?: Omit<ContactWidgetProps, "visible" | "widgetId" | "onClose"> & {
88
+ /** Pass `SendoraCloud.auth.getCurrentUser()` here to auto-stitch identity. */
89
+ user?: {
90
+ id: string;
91
+ email: string | null;
92
+ emailVerified: boolean;
93
+ name: string | null;
94
+ isAnonymous: boolean;
95
+ } | null;
96
+ }): {
97
+ open: () => void;
98
+ close: () => void;
99
+ View: () => React.ReactElement | null;
100
+ lastResult: ContactWidgetResult | null;
101
+ };
102
+
103
+ export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, useContactWidget };
@@ -0,0 +1,105 @@
1
+ // src/contact-widget.tsx
2
+ import * as React from "react";
3
+ import { Modal, View } from "react-native";
4
+ import { WebView } from "react-native-webview";
5
+ import { jsx } from "react/jsx-runtime";
6
+ function ContactWidget({
7
+ visible,
8
+ widgetId,
9
+ workerHost = "https://go.sendoracloud.com",
10
+ theme = "auto",
11
+ prefillName,
12
+ prefillEmail,
13
+ prefillUserId,
14
+ lockEmail,
15
+ onClose
16
+ }) {
17
+ const didFire = React.useRef(false);
18
+ React.useEffect(() => {
19
+ if (visible) didFire.current = false;
20
+ }, [visible]);
21
+ if (!widgetId) return null;
22
+ const params = new URLSearchParams({ widgetId, theme });
23
+ if (prefillName) params.set("prefillName", prefillName);
24
+ if (prefillEmail) params.set("prefillEmail", prefillEmail);
25
+ if (prefillUserId) params.set("prefillUserId", prefillUserId);
26
+ if (lockEmail && prefillEmail) params.set("lockEmail", "1");
27
+ const uri = `${workerHost}/embed/contact?${params.toString()}`;
28
+ const handleClose = (result) => {
29
+ if (didFire.current) return;
30
+ didFire.current = true;
31
+ onClose(result);
32
+ };
33
+ const onNav = (req) => {
34
+ const url = req.url || "";
35
+ if (url.startsWith("sendora://close")) {
36
+ const queryStart = url.indexOf("?");
37
+ const params2 = queryStart >= 0 ? new URLSearchParams(url.slice(queryStart + 1)) : new URLSearchParams();
38
+ const ticketId = params2.get("ticketId") || "";
39
+ const portalUrl = params2.get("portalUrl") || null;
40
+ handleClose({ ticketId, portalUrl, submitted: ticketId !== "" });
41
+ return false;
42
+ }
43
+ if (!url.startsWith("https://") && !url.startsWith("about:")) {
44
+ return false;
45
+ }
46
+ return true;
47
+ };
48
+ return /* @__PURE__ */ jsx(
49
+ Modal,
50
+ {
51
+ visible,
52
+ animationType: "slide",
53
+ presentationStyle: "pageSheet",
54
+ onRequestClose: () => handleClose({ ticketId: "", portalUrl: null, submitted: false }),
55
+ children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
56
+ WebView,
57
+ {
58
+ source: { uri },
59
+ onShouldStartLoadWithRequest: onNav,
60
+ originWhitelist: ["https://*", "about:*", "sendora://*"],
61
+ setSupportMultipleWindows: false,
62
+ javaScriptEnabled: true,
63
+ domStorageEnabled: true,
64
+ allowsBackForwardNavigationGestures: false
65
+ }
66
+ ) })
67
+ }
68
+ );
69
+ }
70
+ function useContactWidget(widgetId, defaults) {
71
+ const [visible, setVisible] = React.useState(false);
72
+ const [lastResult, setLastResult] = React.useState(null);
73
+ const open = React.useCallback(() => setVisible(true), []);
74
+ const close = React.useCallback(() => setVisible(false), []);
75
+ const sdkUser = defaults?.user ?? null;
76
+ const resolvedName = defaults?.prefillName ?? sdkUser?.name ?? void 0;
77
+ const resolvedEmail = defaults?.prefillEmail ?? sdkUser?.email ?? void 0;
78
+ const resolvedUserId = defaults?.prefillUserId ?? sdkUser?.id ?? void 0;
79
+ const resolvedLockEmail = defaults?.lockEmail ?? (sdkUser != null && sdkUser.email != null && sdkUser.emailVerified && !sdkUser.isAnonymous);
80
+ const View2 = React.useCallback(
81
+ () => /* @__PURE__ */ jsx(
82
+ ContactWidget,
83
+ {
84
+ workerHost: defaults?.workerHost,
85
+ theme: defaults?.theme,
86
+ widgetId,
87
+ visible,
88
+ prefillName: resolvedName,
89
+ prefillEmail: resolvedEmail,
90
+ prefillUserId: resolvedUserId,
91
+ lockEmail: resolvedLockEmail,
92
+ onClose: (r) => {
93
+ setLastResult(r);
94
+ setVisible(false);
95
+ }
96
+ }
97
+ ),
98
+ [defaults, widgetId, visible, resolvedName, resolvedEmail, resolvedUserId, resolvedLockEmail]
99
+ );
100
+ return { open, close, View: View2, lastResult };
101
+ }
102
+ export {
103
+ ContactWidget,
104
+ useContactWidget
105
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push token registration requires a Dev Client (EAS Build or `npx expo prebuild`).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -11,6 +11,11 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
+ },
15
+ "./contact-widget": {
16
+ "types": "./dist/contact-widget.d.ts",
17
+ "import": "./dist/contact-widget.js",
18
+ "require": "./dist/contact-widget.cjs"
14
19
  }
15
20
  },
16
21
  "files": [
@@ -32,7 +37,8 @@
32
37
  },
33
38
  "peerDependencies": {
34
39
  "@react-native-async-storage/async-storage": ">=1.17.0",
35
- "react-native": ">=0.70.0"
40
+ "react-native": ">=0.70.0",
41
+ "react-native-webview": ">=13.0.0"
36
42
  },
37
43
  "peerDependenciesMeta": {
38
44
  "@react-native-async-storage/async-storage": {
@@ -40,13 +46,19 @@
40
46
  },
41
47
  "react-native": {
42
48
  "optional": false
49
+ },
50
+ "react-native-webview": {
51
+ "optional": true
43
52
  }
44
53
  },
45
54
  "scripts": {
46
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
55
+ "build": "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
47
56
  "typecheck": "tsc --noEmit"
48
57
  },
49
58
  "devDependencies": {
59
+ "@types/react": "^19.2.16",
60
+ "react-native": "^0.85.3",
61
+ "react-native-webview": "^13.16.1",
50
62
  "tsup": "^8.0.0",
51
63
  "typescript": "^5.0.0"
52
64
  },