@sendoracloud/sdk-react-native 1.2.0 → 1.4.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.
@@ -31,7 +31,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var contact_widget_exports = {};
32
32
  __export(contact_widget_exports, {
33
33
  ContactWidget: () => ContactWidget,
34
- useContactWidget: () => useContactWidget
34
+ TicketHistory: () => TicketHistory,
35
+ useContactWidget: () => useContactWidget,
36
+ useTicketHistory: () => useTicketHistory
35
37
  });
36
38
  module.exports = __toCommonJS(contact_widget_exports);
37
39
  var React = __toESM(require("react"), 1);
@@ -45,6 +47,8 @@ function ContactWidget({
45
47
  theme = "auto",
46
48
  prefillName,
47
49
  prefillEmail,
50
+ prefillUserId,
51
+ lockEmail,
48
52
  onClose
49
53
  }) {
50
54
  const didFire = React.useRef(false);
@@ -55,6 +59,8 @@ function ContactWidget({
55
59
  const params = new URLSearchParams({ widgetId, theme });
56
60
  if (prefillName) params.set("prefillName", prefillName);
57
61
  if (prefillEmail) params.set("prefillEmail", prefillEmail);
62
+ if (prefillUserId) params.set("prefillUserId", prefillUserId);
63
+ if (lockEmail && prefillEmail) params.set("lockEmail", "1");
58
64
  const uri = `${workerHost}/embed/contact?${params.toString()}`;
59
65
  const handleClose = (result) => {
60
66
  if (didFire.current) return;
@@ -103,25 +109,114 @@ function useContactWidget(widgetId, defaults) {
103
109
  const [lastResult, setLastResult] = React.useState(null);
104
110
  const open = React.useCallback(() => setVisible(true), []);
105
111
  const close = React.useCallback(() => setVisible(false), []);
112
+ const sdkUser = defaults?.user ?? null;
113
+ const resolvedName = defaults?.prefillName ?? sdkUser?.name ?? void 0;
114
+ const resolvedEmail = defaults?.prefillEmail ?? sdkUser?.email ?? void 0;
115
+ const resolvedUserId = defaults?.prefillUserId ?? sdkUser?.id ?? void 0;
116
+ const resolvedLockEmail = defaults?.lockEmail ?? (sdkUser != null && sdkUser.email != null && sdkUser.emailVerified && !sdkUser.isAnonymous);
106
117
  const View2 = React.useCallback(
107
118
  () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
108
119
  ContactWidget,
109
120
  {
110
- ...defaults,
121
+ workerHost: defaults?.workerHost,
122
+ theme: defaults?.theme,
111
123
  widgetId,
112
124
  visible,
125
+ prefillName: resolvedName,
126
+ prefillEmail: resolvedEmail,
127
+ prefillUserId: resolvedUserId,
128
+ lockEmail: resolvedLockEmail,
113
129
  onClose: (r) => {
114
130
  setLastResult(r);
115
131
  setVisible(false);
116
132
  }
117
133
  }
118
134
  ),
119
- [defaults, widgetId, visible]
135
+ [defaults, widgetId, visible, resolvedName, resolvedEmail, resolvedUserId, resolvedLockEmail]
120
136
  );
121
137
  return { open, close, View: View2, lastResult };
122
138
  }
139
+ function TicketHistory({
140
+ visible,
141
+ widgetId,
142
+ accessToken,
143
+ workerHost = "https://go.sendoracloud.com",
144
+ theme = "auto",
145
+ onClose
146
+ }) {
147
+ const didFire = React.useRef(false);
148
+ React.useEffect(() => {
149
+ if (visible) didFire.current = false;
150
+ }, [visible]);
151
+ if (!widgetId || !accessToken) return null;
152
+ const params = new URLSearchParams({ widgetId, theme });
153
+ const uri = `${workerHost}/embed/tickets?${params.toString()}#access=${encodeURIComponent(accessToken)}`;
154
+ const handleClose = (result) => {
155
+ if (didFire.current) return;
156
+ didFire.current = true;
157
+ onClose(result);
158
+ };
159
+ const onNav = (req) => {
160
+ const url = req.url || "";
161
+ if (url.startsWith("sendora://close")) {
162
+ handleClose({ ticketId: "", portalUrl: null, submitted: false });
163
+ return false;
164
+ }
165
+ if (!url.startsWith("https://") && !url.startsWith("about:")) return false;
166
+ return true;
167
+ };
168
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
169
+ import_react_native.Modal,
170
+ {
171
+ visible,
172
+ animationType: "slide",
173
+ presentationStyle: "pageSheet",
174
+ onRequestClose: () => handleClose({ ticketId: "", portalUrl: null, submitted: false }),
175
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: { flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
176
+ import_react_native_webview.WebView,
177
+ {
178
+ source: { uri },
179
+ onShouldStartLoadWithRequest: onNav,
180
+ originWhitelist: ["https://*", "about:*", "sendora://*"],
181
+ setSupportMultipleWindows: false,
182
+ javaScriptEnabled: true,
183
+ domStorageEnabled: true,
184
+ allowsBackForwardNavigationGestures: false
185
+ }
186
+ ) })
187
+ }
188
+ );
189
+ }
190
+ function useTicketHistory(widgetId, getAccessToken, defaults) {
191
+ const [visible, setVisible] = React.useState(false);
192
+ const [token, setToken] = React.useState(null);
193
+ const open = React.useCallback(async () => {
194
+ const t = await getAccessToken();
195
+ if (!t) return;
196
+ setToken(t);
197
+ setVisible(true);
198
+ }, [getAccessToken]);
199
+ const close = React.useCallback(() => setVisible(false), []);
200
+ const View2 = React.useCallback(
201
+ () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
202
+ TicketHistory,
203
+ {
204
+ widgetId,
205
+ visible,
206
+ accessToken: token,
207
+ workerHost: defaults?.workerHost,
208
+ theme: defaults?.theme,
209
+ onClose: () => setVisible(false)
210
+ }
211
+ ),
212
+ [widgetId, visible, token, defaults]
213
+ );
214
+ return { open, close, View: View2 };
215
+ }
123
216
  // Annotate the CommonJS export names for ESM import in node:
124
217
  0 && (module.exports = {
125
218
  ContactWidget,
126
- useContactWidget
219
+ TicketHistory,
220
+ useContactWidget,
221
+ useTicketHistory
127
222
  });
@@ -44,6 +44,20 @@ interface ContactWidgetProps {
44
44
  prefillName?: string;
45
45
  /** Pre-fill email field for signed-in users. */
46
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;
47
61
  /**
48
62
  * Fired when the user closes the sheet — either after a successful
49
63
  * submit (`submitted = true`) or via the close button
@@ -51,7 +65,7 @@ interface ContactWidgetProps {
51
65
  */
52
66
  onClose: (result: ContactWidgetResult) => void;
53
67
  }
54
- declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
68
+ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, prefillUserId, lockEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
55
69
  /**
56
70
  * Helper hook so the host doesn't have to wire visibility state by
57
71
  * hand.
@@ -59,12 +73,66 @@ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillNa
59
73
  * const contact = useContactWidget("widget-uuid");
60
74
  * <Button onPress={contact.open} title="Contact us" />
61
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 });
62
86
  */
63
- declare function useContactWidget(widgetId: string, defaults?: Omit<ContactWidgetProps, "visible" | "widgetId" | "onClose">): {
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
+ }): {
64
97
  open: () => void;
65
98
  close: () => void;
66
99
  View: () => React.ReactElement | null;
67
100
  lastResult: ContactWidgetResult | null;
68
101
  };
102
+ interface TicketHistoryProps {
103
+ /** Whether the modal sheet is visible. */
104
+ visible: boolean;
105
+ /** Widget UUID that scopes the org. */
106
+ widgetId: string;
107
+ /**
108
+ * Current SDK access token (from `SendoraCloud.auth.getAccessToken()`).
109
+ * Passed in the URL hash fragment so it never appears in server
110
+ * logs / referer headers. Embed scrubs the hash on load.
111
+ * The widget is no-op when this is null/empty.
112
+ */
113
+ accessToken: string | null | undefined;
114
+ workerHost?: string;
115
+ theme?: "auto" | "light" | "dark";
116
+ onClose: (result: ContactWidgetResult) => void;
117
+ }
118
+ declare function TicketHistory({ visible, widgetId, accessToken, workerHost, theme, onClose, }: TicketHistoryProps): React.ReactElement | null;
119
+ /**
120
+ * Convenience hook for the ticket history view. Takes the SDK's
121
+ * `getAccessToken` helper (deferred — called lazily on open) so the
122
+ * host doesn't have to thread token state by hand.
123
+ *
124
+ * import SendoraCloud from "@sendoracloud/sdk-react-native";
125
+ * const history = useTicketHistory("widget-uuid", () => SendoraCloud.auth.getAccessToken());
126
+ * <Button onPress={history.open} title="My tickets" />
127
+ * <history.View />
128
+ */
129
+ declare function useTicketHistory(widgetId: string, getAccessToken: () => Promise<string | null>, defaults?: {
130
+ workerHost?: string;
131
+ theme?: "auto" | "light" | "dark";
132
+ }): {
133
+ open: () => void;
134
+ close: () => void;
135
+ View: () => React.ReactElement | null;
136
+ };
69
137
 
70
- export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, useContactWidget };
138
+ export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, TicketHistory, type TicketHistoryProps, useContactWidget, useTicketHistory };
@@ -44,6 +44,20 @@ interface ContactWidgetProps {
44
44
  prefillName?: string;
45
45
  /** Pre-fill email field for signed-in users. */
46
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;
47
61
  /**
48
62
  * Fired when the user closes the sheet — either after a successful
49
63
  * submit (`submitted = true`) or via the close button
@@ -51,7 +65,7 @@ interface ContactWidgetProps {
51
65
  */
52
66
  onClose: (result: ContactWidgetResult) => void;
53
67
  }
54
- declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
68
+ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillName, prefillEmail, prefillUserId, lockEmail, onClose, }: ContactWidgetProps): React.ReactElement | null;
55
69
  /**
56
70
  * Helper hook so the host doesn't have to wire visibility state by
57
71
  * hand.
@@ -59,12 +73,66 @@ declare function ContactWidget({ visible, widgetId, workerHost, theme, prefillNa
59
73
  * const contact = useContactWidget("widget-uuid");
60
74
  * <Button onPress={contact.open} title="Contact us" />
61
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 });
62
86
  */
63
- declare function useContactWidget(widgetId: string, defaults?: Omit<ContactWidgetProps, "visible" | "widgetId" | "onClose">): {
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
+ }): {
64
97
  open: () => void;
65
98
  close: () => void;
66
99
  View: () => React.ReactElement | null;
67
100
  lastResult: ContactWidgetResult | null;
68
101
  };
102
+ interface TicketHistoryProps {
103
+ /** Whether the modal sheet is visible. */
104
+ visible: boolean;
105
+ /** Widget UUID that scopes the org. */
106
+ widgetId: string;
107
+ /**
108
+ * Current SDK access token (from `SendoraCloud.auth.getAccessToken()`).
109
+ * Passed in the URL hash fragment so it never appears in server
110
+ * logs / referer headers. Embed scrubs the hash on load.
111
+ * The widget is no-op when this is null/empty.
112
+ */
113
+ accessToken: string | null | undefined;
114
+ workerHost?: string;
115
+ theme?: "auto" | "light" | "dark";
116
+ onClose: (result: ContactWidgetResult) => void;
117
+ }
118
+ declare function TicketHistory({ visible, widgetId, accessToken, workerHost, theme, onClose, }: TicketHistoryProps): React.ReactElement | null;
119
+ /**
120
+ * Convenience hook for the ticket history view. Takes the SDK's
121
+ * `getAccessToken` helper (deferred — called lazily on open) so the
122
+ * host doesn't have to thread token state by hand.
123
+ *
124
+ * import SendoraCloud from "@sendoracloud/sdk-react-native";
125
+ * const history = useTicketHistory("widget-uuid", () => SendoraCloud.auth.getAccessToken());
126
+ * <Button onPress={history.open} title="My tickets" />
127
+ * <history.View />
128
+ */
129
+ declare function useTicketHistory(widgetId: string, getAccessToken: () => Promise<string | null>, defaults?: {
130
+ workerHost?: string;
131
+ theme?: "auto" | "light" | "dark";
132
+ }): {
133
+ open: () => void;
134
+ close: () => void;
135
+ View: () => React.ReactElement | null;
136
+ };
69
137
 
70
- export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, useContactWidget };
138
+ export { ContactWidget, type ContactWidgetProps, type ContactWidgetResult, TicketHistory, type TicketHistoryProps, useContactWidget, useTicketHistory };
@@ -10,6 +10,8 @@ function ContactWidget({
10
10
  theme = "auto",
11
11
  prefillName,
12
12
  prefillEmail,
13
+ prefillUserId,
14
+ lockEmail,
13
15
  onClose
14
16
  }) {
15
17
  const didFire = React.useRef(false);
@@ -20,6 +22,8 @@ function ContactWidget({
20
22
  const params = new URLSearchParams({ widgetId, theme });
21
23
  if (prefillName) params.set("prefillName", prefillName);
22
24
  if (prefillEmail) params.set("prefillEmail", prefillEmail);
25
+ if (prefillUserId) params.set("prefillUserId", prefillUserId);
26
+ if (lockEmail && prefillEmail) params.set("lockEmail", "1");
23
27
  const uri = `${workerHost}/embed/contact?${params.toString()}`;
24
28
  const handleClose = (result) => {
25
29
  if (didFire.current) return;
@@ -68,24 +72,113 @@ function useContactWidget(widgetId, defaults) {
68
72
  const [lastResult, setLastResult] = React.useState(null);
69
73
  const open = React.useCallback(() => setVisible(true), []);
70
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);
71
80
  const View2 = React.useCallback(
72
81
  () => /* @__PURE__ */ jsx(
73
82
  ContactWidget,
74
83
  {
75
- ...defaults,
84
+ workerHost: defaults?.workerHost,
85
+ theme: defaults?.theme,
76
86
  widgetId,
77
87
  visible,
88
+ prefillName: resolvedName,
89
+ prefillEmail: resolvedEmail,
90
+ prefillUserId: resolvedUserId,
91
+ lockEmail: resolvedLockEmail,
78
92
  onClose: (r) => {
79
93
  setLastResult(r);
80
94
  setVisible(false);
81
95
  }
82
96
  }
83
97
  ),
84
- [defaults, widgetId, visible]
98
+ [defaults, widgetId, visible, resolvedName, resolvedEmail, resolvedUserId, resolvedLockEmail]
85
99
  );
86
100
  return { open, close, View: View2, lastResult };
87
101
  }
102
+ function TicketHistory({
103
+ visible,
104
+ widgetId,
105
+ accessToken,
106
+ workerHost = "https://go.sendoracloud.com",
107
+ theme = "auto",
108
+ onClose
109
+ }) {
110
+ const didFire = React.useRef(false);
111
+ React.useEffect(() => {
112
+ if (visible) didFire.current = false;
113
+ }, [visible]);
114
+ if (!widgetId || !accessToken) return null;
115
+ const params = new URLSearchParams({ widgetId, theme });
116
+ const uri = `${workerHost}/embed/tickets?${params.toString()}#access=${encodeURIComponent(accessToken)}`;
117
+ const handleClose = (result) => {
118
+ if (didFire.current) return;
119
+ didFire.current = true;
120
+ onClose(result);
121
+ };
122
+ const onNav = (req) => {
123
+ const url = req.url || "";
124
+ if (url.startsWith("sendora://close")) {
125
+ handleClose({ ticketId: "", portalUrl: null, submitted: false });
126
+ return false;
127
+ }
128
+ if (!url.startsWith("https://") && !url.startsWith("about:")) return false;
129
+ return true;
130
+ };
131
+ return /* @__PURE__ */ jsx(
132
+ Modal,
133
+ {
134
+ visible,
135
+ animationType: "slide",
136
+ presentationStyle: "pageSheet",
137
+ onRequestClose: () => handleClose({ ticketId: "", portalUrl: null, submitted: false }),
138
+ children: /* @__PURE__ */ jsx(View, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
139
+ WebView,
140
+ {
141
+ source: { uri },
142
+ onShouldStartLoadWithRequest: onNav,
143
+ originWhitelist: ["https://*", "about:*", "sendora://*"],
144
+ setSupportMultipleWindows: false,
145
+ javaScriptEnabled: true,
146
+ domStorageEnabled: true,
147
+ allowsBackForwardNavigationGestures: false
148
+ }
149
+ ) })
150
+ }
151
+ );
152
+ }
153
+ function useTicketHistory(widgetId, getAccessToken, defaults) {
154
+ const [visible, setVisible] = React.useState(false);
155
+ const [token, setToken] = React.useState(null);
156
+ const open = React.useCallback(async () => {
157
+ const t = await getAccessToken();
158
+ if (!t) return;
159
+ setToken(t);
160
+ setVisible(true);
161
+ }, [getAccessToken]);
162
+ const close = React.useCallback(() => setVisible(false), []);
163
+ const View2 = React.useCallback(
164
+ () => /* @__PURE__ */ jsx(
165
+ TicketHistory,
166
+ {
167
+ widgetId,
168
+ visible,
169
+ accessToken: token,
170
+ workerHost: defaults?.workerHost,
171
+ theme: defaults?.theme,
172
+ onClose: () => setVisible(false)
173
+ }
174
+ ),
175
+ [widgetId, visible, token, defaults]
176
+ );
177
+ return { open, close, View: View2 };
178
+ }
88
179
  export {
89
180
  ContactWidget,
90
- useContactWidget
181
+ TicketHistory,
182
+ useContactWidget,
183
+ useTicketHistory
91
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",