@appsupportkit/support-sdk 0.1.2 → 0.2.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.
Files changed (94) hide show
  1. package/lib/commonjs/components/BottomSheet.js +67 -47
  2. package/lib/commonjs/components/BottomSheet.js.map +1 -1
  3. package/lib/commonjs/components/LauncherButton.js +177 -0
  4. package/lib/commonjs/components/LauncherButton.js.map +1 -0
  5. package/lib/commonjs/components/SupportButton.js +54 -176
  6. package/lib/commonjs/components/SupportButton.js.map +1 -1
  7. package/lib/commonjs/components/icons.js +74 -23
  8. package/lib/commonjs/components/icons.js.map +1 -1
  9. package/lib/commonjs/components/primitives.js +183 -38
  10. package/lib/commonjs/components/primitives.js.map +1 -1
  11. package/lib/commonjs/components/screens/ComposeScreen.js +115 -58
  12. package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
  13. package/lib/commonjs/components/screens/HomeScreen.js +170 -115
  14. package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
  15. package/lib/commonjs/components/screens/ThreadScreen.js +222 -108
  16. package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/theme.js +136 -17
  19. package/lib/commonjs/theme.js.map +1 -1
  20. package/lib/commonjs/types.js +7 -0
  21. package/lib/commonjs/types.js.map +1 -1
  22. package/lib/commonjs/unread.js +92 -0
  23. package/lib/commonjs/unread.js.map +1 -0
  24. package/lib/commonjs/useSupportThreads.js +286 -0
  25. package/lib/commonjs/useSupportThreads.js.map +1 -0
  26. package/lib/commonjs/utils.js +33 -0
  27. package/lib/commonjs/utils.js.map +1 -1
  28. package/lib/module/components/BottomSheet.js +68 -48
  29. package/lib/module/components/BottomSheet.js.map +1 -1
  30. package/lib/module/components/LauncherButton.js +172 -0
  31. package/lib/module/components/LauncherButton.js.map +1 -0
  32. package/lib/module/components/SupportButton.js +57 -179
  33. package/lib/module/components/SupportButton.js.map +1 -1
  34. package/lib/module/components/icons.js +72 -22
  35. package/lib/module/components/icons.js.map +1 -1
  36. package/lib/module/components/primitives.js +179 -39
  37. package/lib/module/components/primitives.js.map +1 -1
  38. package/lib/module/components/screens/ComposeScreen.js +118 -61
  39. package/lib/module/components/screens/ComposeScreen.js.map +1 -1
  40. package/lib/module/components/screens/HomeScreen.js +172 -117
  41. package/lib/module/components/screens/HomeScreen.js.map +1 -1
  42. package/lib/module/components/screens/ThreadScreen.js +228 -112
  43. package/lib/module/components/screens/ThreadScreen.js.map +1 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/theme.js +135 -16
  46. package/lib/module/theme.js.map +1 -1
  47. package/lib/module/types.js +7 -0
  48. package/lib/module/types.js.map +1 -1
  49. package/lib/module/unread.js +83 -0
  50. package/lib/module/unread.js.map +1 -0
  51. package/lib/module/useSupportThreads.js +281 -0
  52. package/lib/module/useSupportThreads.js.map +1 -0
  53. package/lib/module/utils.js +30 -0
  54. package/lib/module/utils.js.map +1 -1
  55. package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
  56. package/lib/typescript/components/LauncherButton.d.ts +14 -0
  57. package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
  58. package/lib/typescript/components/SupportButton.d.ts +3 -3
  59. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  60. package/lib/typescript/components/icons.d.ts +13 -2
  61. package/lib/typescript/components/icons.d.ts.map +1 -1
  62. package/lib/typescript/components/primitives.d.ts +35 -4
  63. package/lib/typescript/components/primitives.d.ts.map +1 -1
  64. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
  65. package/lib/typescript/components/screens/HomeScreen.d.ts +3 -2
  66. package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
  67. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
  68. package/lib/typescript/index.d.ts +1 -1
  69. package/lib/typescript/index.d.ts.map +1 -1
  70. package/lib/typescript/theme.d.ts +44 -3
  71. package/lib/typescript/theme.d.ts.map +1 -1
  72. package/lib/typescript/types.d.ts +22 -0
  73. package/lib/typescript/types.d.ts.map +1 -1
  74. package/lib/typescript/unread.d.ts +22 -0
  75. package/lib/typescript/unread.d.ts.map +1 -0
  76. package/lib/typescript/useSupportThreads.d.ts +28 -0
  77. package/lib/typescript/useSupportThreads.d.ts.map +1 -0
  78. package/lib/typescript/utils.d.ts +5 -0
  79. package/lib/typescript/utils.d.ts.map +1 -1
  80. package/package.json +2 -2
  81. package/src/components/BottomSheet.tsx +61 -43
  82. package/src/components/LauncherButton.tsx +217 -0
  83. package/src/components/SupportButton.tsx +52 -196
  84. package/src/components/icons.tsx +77 -19
  85. package/src/components/primitives.tsx +217 -36
  86. package/src/components/screens/ComposeScreen.tsx +131 -61
  87. package/src/components/screens/HomeScreen.tsx +168 -117
  88. package/src/components/screens/ThreadScreen.tsx +289 -119
  89. package/src/index.tsx +1 -0
  90. package/src/theme.ts +165 -18
  91. package/src/types.ts +24 -0
  92. package/src/unread.ts +91 -0
  93. package/src/useSupportThreads.ts +372 -0
  94. package/src/utils.ts +33 -0
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.latestAdminMessageAt = latestAdminMessageAt;
7
+ exports.pruneWatermarks = pruneWatermarks;
8
+ exports.reconcileThread = reconcileThread;
9
+ exports.reconcileThreads = reconcileThreads;
10
+ exports.sumUnread = sumUnread;
11
+ exports.unreadSince = unreadSince;
12
+ // Unread accounting.
13
+ //
14
+ // The server counts a thread's unread as "admin messages newer than the
15
+ // user's read receipt". That number is authoritative, but it is always a
16
+ // little behind the device: between the moment the user opens a thread and
17
+ // the moment `mark-read` lands, every already-in-flight poll still reports
18
+ // the old count — which is what made the badge flicker back on after being
19
+ // cleared, and what made a thread the user was actively reading accumulate
20
+ // unread replies.
21
+ //
22
+ // So the client keeps its own watermark per thread: the `created_at` of the
23
+ // newest admin message the user has actually seen. Those are *server*
24
+ // timestamps, so nothing here compares against the device clock.
25
+ //
26
+ // Displayed unread is then the stricter of the two counts, which is correct
27
+ // in both directions:
28
+ //
29
+ // read here, receipt not written yet → local watermark wins (0)
30
+ // read on another device → server count wins (0)
31
+
32
+ /** Watermark map: thread id → `created_at` (ms) of the newest seen reply. */
33
+
34
+ /** `created_at` (ms) of the newest admin message, or 0 if there are none. */
35
+ function latestAdminMessageAt(thread) {
36
+ let latest = 0;
37
+ for (const message of thread.messages) {
38
+ if (message.sender !== 'admin') continue;
39
+ const at = Date.parse(message.created_at);
40
+ if (Number.isFinite(at) && at > latest) latest = at;
41
+ }
42
+ return latest;
43
+ }
44
+
45
+ /** Admin messages in `thread` newer than `watermark`. */
46
+ function unreadSince(thread, watermark) {
47
+ let count = 0;
48
+ for (const message of thread.messages) {
49
+ if (message.sender !== 'admin') continue;
50
+ const at = Date.parse(message.created_at);
51
+ if (Number.isFinite(at) && at > watermark) count += 1;
52
+ }
53
+ return count;
54
+ }
55
+
56
+ /**
57
+ * Returns `thread` with `unread_count` narrowed by what this device knows
58
+ * the user has read. Identity is preserved when nothing changes so lists
59
+ * stay referentially stable between polls.
60
+ */
61
+ function reconcileThread(thread, watermarks) {
62
+ const watermark = watermarks.get(thread.id);
63
+ if (watermark === undefined || thread.unread_count === 0) return thread;
64
+ const unread = Math.min(thread.unread_count, unreadSince(thread, watermark));
65
+ return unread === thread.unread_count ? thread : {
66
+ ...thread,
67
+ unread_count: unread
68
+ };
69
+ }
70
+ function reconcileThreads(threads, watermarks) {
71
+ return threads.map(thread => reconcileThread(thread, watermarks));
72
+ }
73
+ function sumUnread(threads) {
74
+ return threads.reduce((total, thread) => total + thread.unread_count, 0);
75
+ }
76
+
77
+ /**
78
+ * Drops watermarks the server has caught up on (its own count is already 0)
79
+ * and ones for threads that no longer exist, so the map can't grow without
80
+ * bound over a long session.
81
+ */
82
+ function pruneWatermarks(watermarks, serverThreads) {
83
+ const keep = new Map();
84
+ for (const thread of serverThreads) {
85
+ const watermark = watermarks.get(thread.id);
86
+ if (watermark !== undefined && thread.unread_count > 0) {
87
+ keep.set(thread.id, watermark);
88
+ }
89
+ }
90
+ return keep;
91
+ }
92
+ //# sourceMappingURL=unread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["latestAdminMessageAt","thread","latest","message","messages","sender","at","Date","parse","created_at","Number","isFinite","unreadSince","watermark","count","reconcileThread","watermarks","get","id","undefined","unread_count","unread","Math","min","reconcileThreads","threads","map","sumUnread","reduce","total","pruneWatermarks","serverThreads","keep","Map","set"],"sourceRoot":"../../src","sources":["unread.ts"],"mappings":";;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;;AAGA;AACO,SAASA,oBAAoBA,CAACC,MAAc,EAAU;EAC3D,IAAIC,MAAM,GAAG,CAAC;EACd,KAAK,MAAMC,OAAO,IAAIF,MAAM,CAACG,QAAQ,EAAE;IACrC,IAAID,OAAO,CAACE,MAAM,KAAK,OAAO,EAAE;IAChC,MAAMC,EAAE,GAAGC,IAAI,CAACC,KAAK,CAACL,OAAO,CAACM,UAAU,CAAC;IACzC,IAAIC,MAAM,CAACC,QAAQ,CAACL,EAAE,CAAC,IAAIA,EAAE,GAAGJ,MAAM,EAAEA,MAAM,GAAGI,EAAE;EACrD;EACA,OAAOJ,MAAM;AACf;;AAEA;AACO,SAASU,WAAWA,CAACX,MAAc,EAAEY,SAAiB,EAAU;EACrE,IAAIC,KAAK,GAAG,CAAC;EACb,KAAK,MAAMX,OAAO,IAAIF,MAAM,CAACG,QAAQ,EAAE;IACrC,IAAID,OAAO,CAACE,MAAM,KAAK,OAAO,EAAE;IAChC,MAAMC,EAAE,GAAGC,IAAI,CAACC,KAAK,CAACL,OAAO,CAACM,UAAU,CAAC;IACzC,IAAIC,MAAM,CAACC,QAAQ,CAACL,EAAE,CAAC,IAAIA,EAAE,GAAGO,SAAS,EAAEC,KAAK,IAAI,CAAC;EACvD;EACA,OAAOA,KAAK;AACd;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,eAAeA,CAC7Bd,MAAc,EACde,UAA0B,EAClB;EACR,MAAMH,SAAS,GAAGG,UAAU,CAACC,GAAG,CAAChB,MAAM,CAACiB,EAAE,CAAC;EAC3C,IAAIL,SAAS,KAAKM,SAAS,IAAIlB,MAAM,CAACmB,YAAY,KAAK,CAAC,EAAE,OAAOnB,MAAM;EACvE,MAAMoB,MAAM,GAAGC,IAAI,CAACC,GAAG,CAACtB,MAAM,CAACmB,YAAY,EAAER,WAAW,CAACX,MAAM,EAAEY,SAAS,CAAC,CAAC;EAC5E,OAAOQ,MAAM,KAAKpB,MAAM,CAACmB,YAAY,GAAGnB,MAAM,GAAG;IAAE,GAAGA,MAAM;IAAEmB,YAAY,EAAEC;EAAO,CAAC;AACtF;AAEO,SAASG,gBAAgBA,CAC9BC,OAAiB,EACjBT,UAA0B,EAChB;EACV,OAAOS,OAAO,CAACC,GAAG,CAAEzB,MAAM,IAAKc,eAAe,CAACd,MAAM,EAAEe,UAAU,CAAC,CAAC;AACrE;AAEO,SAASW,SAASA,CAACF,OAAiB,EAAU;EACnD,OAAOA,OAAO,CAACG,MAAM,CAAC,CAACC,KAAK,EAAE5B,MAAM,KAAK4B,KAAK,GAAG5B,MAAM,CAACmB,YAAY,EAAE,CAAC,CAAC;AAC1E;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASU,eAAeA,CAC7Bd,UAA0B,EAC1Be,aAAuB,EACP;EAChB,MAAMC,IAAoB,GAAG,IAAIC,GAAG,CAAC,CAAC;EACtC,KAAK,MAAMhC,MAAM,IAAI8B,aAAa,EAAE;IAClC,MAAMlB,SAAS,GAAGG,UAAU,CAACC,GAAG,CAAChB,MAAM,CAACiB,EAAE,CAAC;IAC3C,IAAIL,SAAS,KAAKM,SAAS,IAAIlB,MAAM,CAACmB,YAAY,GAAG,CAAC,EAAE;MACtDY,IAAI,CAACE,GAAG,CAACjC,MAAM,CAACiB,EAAE,EAAEL,SAAS,CAAC;IAChC;EACF;EACA,OAAOmB,IAAI;AACb","ignoreList":[]}
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.IDLE_POLL_MS = void 0;
7
+ exports.useSupportThreads = useSupportThreads;
8
+ var _react = require("react");
9
+ var _reactNative = require("react-native");
10
+ var _api = require("./api");
11
+ var _unread = require("./unread");
12
+ // Threads + unread state for the SDK: fetching, poll cadence, and read
13
+ // tracking. Kept out of SupportButton so the rules below live in one place.
14
+ //
15
+ // Three invariants make the badge trustworthy:
16
+ //
17
+ // 1. Responses are applied in issue order (`appliedSeq`), so a slow poll
18
+ // can never overwrite a newer one.
19
+ // 2. The badge-only endpoint returns a bare number that cannot be
20
+ // reconciled against local reads, so its answer is dropped when a read
21
+ // happened while it was in flight (`readEpoch`), and it isn't used at
22
+ // all while any `mark-read` is still unconfirmed — the full endpoint is
23
+ // polled instead, since that payload *can* be reconciled.
24
+ // 3. `mark-read` failures are retried instead of swallowed, and the local
25
+ // watermark holds the UI correct in the meantime.
26
+
27
+ // Poll cadence by context: fast where latency is felt (staring at a thread
28
+ // waiting for a reply), slow when the sheet is closed — and the idle tick
29
+ // uses the count-only endpoint, so it costs ~18 bytes instead of the full
30
+ // thread list.
31
+ const THREAD_POLL_MS = 5_000;
32
+ const SHEET_POLL_MS = 15_000;
33
+ const IDLE_POLL_MS = exports.IDLE_POLL_MS = 60_000;
34
+ const MIN_POLL_MS = 3_000;
35
+
36
+ // A failed mark-read used to be dropped on the floor, which resurrected the
37
+ // badge on the next poll. Retry a few times with backoff; anything still
38
+ // unconfirmed after that is picked up by the next poll tick or foreground.
39
+ const MARK_READ_MAX_ATTEMPTS = 4;
40
+ const MARK_READ_BACKOFF_MS = 1_500;
41
+
42
+ /**
43
+ * Load failures the user can act on say so; the rest stay neutral. A 401 is
44
+ * an integration mistake — the end user retrying forever won't fix a key
45
+ * that doesn't match the app — so the real cause is logged for whoever is
46
+ * running the build instead of hidden behind "couldn't load".
47
+ */
48
+ function loadErrorMessage(e) {
49
+ if (e instanceof _api.SupportApiError) {
50
+ if (e.offline) return "You're offline — check your connection";
51
+ if (e.status === 401 || e.status === 403) {
52
+ if (__DEV__) {
53
+ console.error('SupportButton: the support API rejected this app (401). Check that ' + '`apiKey` is the key shown when the app was registered and that ' + '`appName` matches that app exactly.');
54
+ }
55
+ return 'Support is unavailable right now';
56
+ }
57
+ if (e.status === 429) return 'Too many requests — try again in a bit';
58
+ }
59
+ return "Couldn't load your conversations";
60
+ }
61
+ function useSupportThreads({
62
+ config,
63
+ revenuecatId,
64
+ enabled,
65
+ sheetOpen,
66
+ activeThreadId,
67
+ idlePollMs
68
+ }) {
69
+ const [threads, setThreads] = (0, _react.useState)(null);
70
+ const [totalUnread, setTotalUnread] = (0, _react.useState)(0);
71
+ const [loading, setLoading] = (0, _react.useState)(false);
72
+ const [refreshing, setRefreshing] = (0, _react.useState)(false);
73
+ const [error, setError] = (0, _react.useState)(null);
74
+ const mounted = (0, _react.useRef)(true);
75
+ /** Last server payload, before local read reconciliation. */
76
+ const rawThreads = (0, _react.useRef)(null);
77
+ const watermarks = (0, _react.useRef)(new Map());
78
+ const reads = (0, _react.useRef)(new Map());
79
+ const requestSeq = (0, _react.useRef)(0);
80
+ const appliedSeq = (0, _react.useRef)(0);
81
+ const readEpoch = (0, _react.useRef)(0);
82
+ const activeThreadIdRef = (0, _react.useRef)(activeThreadId);
83
+ (0, _react.useEffect)(() => {
84
+ activeThreadIdRef.current = activeThreadId;
85
+ }, [activeThreadId]);
86
+ (0, _react.useEffect)(() => {
87
+ mounted.current = true;
88
+ const timers = reads.current;
89
+ return () => {
90
+ mounted.current = false;
91
+ for (const attempt of timers.values()) {
92
+ if (attempt.timer !== null) clearTimeout(attempt.timer);
93
+ }
94
+ timers.clear();
95
+ };
96
+ }, []);
97
+ const publish = (0, _react.useCallback)(() => {
98
+ const raw = rawThreads.current;
99
+ if (raw === null) return;
100
+ const reconciled = (0, _unread.reconcileThreads)(raw, watermarks.current);
101
+ setThreads(reconciled);
102
+ setTotalUnread((0, _unread.sumUnread)(reconciled));
103
+ }, []);
104
+
105
+ /** POSTs the receipt, retrying transient failures with backoff. */
106
+ const sendRead = (0, _react.useCallback)(threadId => {
107
+ const attempt = reads.current.get(threadId);
108
+ if (!attempt || attempt.inFlight !== null) return;
109
+ const covers = attempt.target;
110
+ attempt.inFlight = covers;
111
+ attempt.timer = null;
112
+ (0, _api.markRead)(config, {
113
+ queryId: threadId,
114
+ revenuecatId
115
+ }).then(() => {
116
+ if (!mounted.current) return;
117
+ const current = reads.current.get(threadId);
118
+ if (!current) return;
119
+ current.inFlight = null;
120
+ // A reply that landed while this request was in flight isn't
121
+ // covered by the receipt it just wrote — send one more.
122
+ if (current.target > covers) {
123
+ current.attempts = 0;
124
+ sendRead(threadId);
125
+ return;
126
+ }
127
+ reads.current.delete(threadId);
128
+ // The receipt now exists server-side, so any response still in
129
+ // flight predates it and must not be trusted for unread counts.
130
+ readEpoch.current += 1;
131
+ }).catch(() => {
132
+ if (!mounted.current) return;
133
+ const current = reads.current.get(threadId);
134
+ if (!current) return;
135
+ current.inFlight = null;
136
+ current.attempts += 1;
137
+ if (current.attempts >= MARK_READ_MAX_ATTEMPTS) return;
138
+ current.timer = setTimeout(() => sendRead(threadId), MARK_READ_BACKOFF_MS * 2 ** (current.attempts - 1));
139
+ });
140
+ }, [config, revenuecatId]);
141
+
142
+ /** Re-drives receipts that gave up earlier (next poll tick / foreground). */
143
+ const flushReads = (0, _react.useCallback)(() => {
144
+ for (const [threadId, attempt] of reads.current) {
145
+ if (attempt.inFlight !== null || attempt.timer !== null) continue;
146
+ attempt.attempts = 0;
147
+ sendRead(threadId);
148
+ }
149
+ }, [sendRead]);
150
+
151
+ /**
152
+ * Records that the user has seen every reply currently in `threadId` and
153
+ * persists it. Idempotent: re-marking a thread with nothing new is a
154
+ * no-op, so the 5s in-thread poll doesn't spam the endpoint.
155
+ */
156
+ const markThreadRead = (0, _react.useCallback)(threadId => {
157
+ const thread = rawThreads.current?.find(t => t.id === threadId);
158
+ if (!thread) return;
159
+ const latest = (0, _unread.latestAdminMessageAt)(thread);
160
+ if (latest === 0) return;
161
+ if (latest <= (watermarks.current.get(threadId) ?? 0)) return;
162
+ watermarks.current.set(threadId, latest);
163
+ // Invalidate the bare counts of any request already in flight.
164
+ readEpoch.current += 1;
165
+ publish();
166
+ const attempt = reads.current.get(threadId);
167
+ if (attempt) attempt.target = latest;else {
168
+ reads.current.set(threadId, {
169
+ target: latest,
170
+ inFlight: null,
171
+ attempts: 0,
172
+ timer: null
173
+ });
174
+ }
175
+ sendRead(threadId);
176
+ }, [publish, sendRead]);
177
+ const fetchThreads = (0, _react.useCallback)(async mode => {
178
+ if (!enabled) return;
179
+ const seq = requestSeq.current += 1;
180
+ // With nothing cached there is no list to pull down, so a retry shows
181
+ // the skeleton rather than a spinner nobody can see.
182
+ const blank = rawThreads.current === null;
183
+ const spinner = mode === 'user' && !blank;
184
+ const skeleton = mode !== 'background' && blank;
185
+ if (spinner) setRefreshing(true);
186
+ if (skeleton) setLoading(true);
187
+ try {
188
+ const data = await (0, _api.getThreads)(config, revenuecatId);
189
+ if (!mounted.current) return;
190
+ // A slower earlier request must never clobber a newer answer.
191
+ if (seq <= appliedSeq.current) return;
192
+ appliedSeq.current = seq;
193
+ rawThreads.current = data.threads;
194
+ watermarks.current = (0, _unread.pruneWatermarks)(watermarks.current, data.threads);
195
+ setError(null);
196
+ publish();
197
+
198
+ // Replies that arrived while the user is sitting in the thread are
199
+ // read on arrival — otherwise the badge climbed while they read.
200
+ const active = activeThreadIdRef.current;
201
+ if (active !== null) markThreadRead(active);
202
+ flushReads();
203
+ } catch (e) {
204
+ if (!mounted.current) return;
205
+ // A failed background poll must not blow away cached threads.
206
+ if (rawThreads.current === null) setError(loadErrorMessage(e));
207
+ } finally {
208
+ if (mounted.current) {
209
+ if (spinner) setRefreshing(false);
210
+ if (skeleton) setLoading(false);
211
+ }
212
+ }
213
+ }, [config, enabled, revenuecatId, publish, markThreadRead, flushReads]);
214
+
215
+ /** Badge-only refresh — the cheap endpoint, used while idle. */
216
+ const fetchCount = (0, _react.useCallback)(async () => {
217
+ if (!enabled) return;
218
+ const seq = requestSeq.current += 1;
219
+ const epoch = readEpoch.current;
220
+ try {
221
+ const data = await (0, _api.getUnreadCount)(config, revenuecatId);
222
+ if (!mounted.current) return;
223
+ // This payload is just a number: there is no way to subtract reads
224
+ // that happened while it was in flight, so discard it instead.
225
+ if (readEpoch.current !== epoch) return;
226
+ if (seq <= appliedSeq.current) return;
227
+ appliedSeq.current = seq;
228
+ setTotalUnread(data.total_unread);
229
+ } catch {
230
+ // Best effort: the next tick or the next sheet open reconciles.
231
+ }
232
+ }, [config, enabled, revenuecatId]);
233
+
234
+ // Initial load.
235
+ (0, _react.useEffect)(() => {
236
+ void fetchThreads('initial');
237
+ }, [fetchThreads]);
238
+
239
+ // Opening the sheet always shows fresh data.
240
+ (0, _react.useEffect)(() => {
241
+ if (sheetOpen) void fetchThreads('background');
242
+ }, [sheetOpen, fetchThreads]);
243
+
244
+ // Opening a thread marks it read immediately, before the next poll.
245
+ (0, _react.useEffect)(() => {
246
+ if (activeThreadId !== null) markThreadRead(activeThreadId);
247
+ }, [activeThreadId, markThreadRead]);
248
+
249
+ // Cadence follows what the user is looking at; the app returning to the
250
+ // foreground always ticks.
251
+ (0, _react.useEffect)(() => {
252
+ if (!enabled) return;
253
+ const idle = !sheetOpen;
254
+ const intervalMs = idle ? idlePollMs : activeThreadId !== null ? THREAD_POLL_MS : SHEET_POLL_MS;
255
+ const tick = () => {
256
+ // While a receipt is unconfirmed the count-only endpoint would report
257
+ // replies the user has already read, so pay for the full payload.
258
+ if (idle && reads.current.size === 0) void fetchCount();else void fetchThreads('background');
259
+ };
260
+
261
+ // Clamped so a typo like `pollIntervalMs={30}` can't hammer the API;
262
+ // 0 (or negative) disables idle polling entirely.
263
+ const timer = intervalMs > 0 ? setInterval(tick, Math.max(intervalMs, MIN_POLL_MS)) : null;
264
+ const appStateSub = _reactNative.AppState.addEventListener('change', state => {
265
+ if (state === 'active') tick();
266
+ });
267
+ return () => {
268
+ if (timer !== null) clearInterval(timer);
269
+ appStateSub.remove();
270
+ };
271
+ }, [enabled, sheetOpen, activeThreadId, idlePollMs, fetchThreads, fetchCount]);
272
+ const refresh = (0, _react.useCallback)(() => {
273
+ void fetchThreads('user');
274
+ }, [fetchThreads]);
275
+ const reload = (0, _react.useCallback)(() => fetchThreads('background'), [fetchThreads]);
276
+ return {
277
+ threads,
278
+ totalUnread,
279
+ loading,
280
+ refreshing,
281
+ error,
282
+ refresh,
283
+ reload
284
+ };
285
+ }
286
+ //# sourceMappingURL=useSupportThreads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactNative","_api","_unread","THREAD_POLL_MS","SHEET_POLL_MS","IDLE_POLL_MS","exports","MIN_POLL_MS","MARK_READ_MAX_ATTEMPTS","MARK_READ_BACKOFF_MS","loadErrorMessage","e","SupportApiError","offline","status","__DEV__","console","error","useSupportThreads","config","revenuecatId","enabled","sheetOpen","activeThreadId","idlePollMs","threads","setThreads","useState","totalUnread","setTotalUnread","loading","setLoading","refreshing","setRefreshing","setError","mounted","useRef","rawThreads","watermarks","Map","reads","requestSeq","appliedSeq","readEpoch","activeThreadIdRef","useEffect","current","timers","attempt","values","timer","clearTimeout","clear","publish","useCallback","raw","reconciled","reconcileThreads","sumUnread","sendRead","threadId","get","inFlight","covers","target","markRead","queryId","then","attempts","delete","catch","setTimeout","flushReads","markThreadRead","thread","find","t","id","latest","latestAdminMessageAt","set","fetchThreads","mode","seq","blank","spinner","skeleton","data","getThreads","pruneWatermarks","active","fetchCount","epoch","getUnreadCount","total_unread","idle","intervalMs","tick","size","setInterval","Math","max","appStateSub","AppState","addEventListener","state","clearInterval","remove","refresh","reload"],"sourceRoot":"../../src","sources":["useSupportThreads.ts"],"mappings":"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","ignoreList":[]}
@@ -3,7 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.dayLabel = dayLabel;
6
7
  exports.formatDate = formatDate;
8
+ exports.formatTime = formatTime;
9
+ exports.isSameDay = isSameDay;
7
10
  exports.relativeTime = relativeTime;
8
11
  exports.validateProps = validateProps;
9
12
  /** "2h ago" relative time — mirrors the web dashboard's formatting. */
@@ -31,6 +34,36 @@ function formatDate(iso) {
31
34
  });
32
35
  }
33
36
 
37
+ /** "14:32" in the device's locale — shown under a group of messages. */
38
+ function formatTime(iso) {
39
+ return new Date(iso).toLocaleTimeString(undefined, {
40
+ hour: 'numeric',
41
+ minute: '2-digit'
42
+ });
43
+ }
44
+ function isSameDay(a, b) {
45
+ const first = new Date(a);
46
+ const second = new Date(b);
47
+ return first.getFullYear() === second.getFullYear() && first.getMonth() === second.getMonth() && first.getDate() === second.getDate();
48
+ }
49
+
50
+ /** Date separator label: Today / Yesterday / "Mon, 5 Jan". */
51
+ function dayLabel(iso) {
52
+ const now = new Date();
53
+ if (isSameDay(iso, now.toISOString())) return 'Today';
54
+ const yesterday = new Date(now.getTime() - 86_400_000);
55
+ if (isSameDay(iso, yesterday.toISOString())) return 'Yesterday';
56
+ const date = new Date(iso);
57
+ return date.toLocaleDateString(undefined, {
58
+ weekday: 'short',
59
+ month: 'short',
60
+ day: 'numeric',
61
+ ...(date.getFullYear() === now.getFullYear() ? {} : {
62
+ year: 'numeric'
63
+ })
64
+ });
65
+ }
66
+
34
67
  /** Dev-mode prop validation with actionable messages. */
35
68
  function validateProps(props) {
36
69
  if (typeof props.appName !== 'string' || props.appName.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"names":["relativeTime","iso","then","Date","getTime","seconds","Math","max","floor","now","minutes","hours","days","weeks","months","formatDate","toLocaleDateString","undefined","year","month","day","validateProps","props","appName","length","apiKey","supabaseUrl","test","user","name","isPaid","revenuecatId","appVersion"],"sourceRoot":"../../src","sources":["utils.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["relativeTime","iso","then","Date","getTime","seconds","Math","max","floor","now","minutes","hours","days","weeks","months","formatDate","toLocaleDateString","undefined","year","month","day","formatTime","toLocaleTimeString","hour","minute","isSameDay","a","b","first","second","getFullYear","getMonth","getDate","dayLabel","toISOString","yesterday","date","weekday","validateProps","props","appName","length","apiKey","supabaseUrl","test","user","name","isPaid","revenuecatId","appVersion"],"sourceRoot":"../../src","sources":["utils.ts"],"mappings":"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","ignoreList":[]}
@@ -3,15 +3,20 @@
3
3
  // Bottom sheet built on the built-in Modal + Animated APIs.
4
4
  //
5
5
  // Why no sheet library: the SDK needs exactly one detent, a drag handle, a
6
- // backdrop fade, and a 180ms ease-out slide — all achievable with core APIs.
6
+ // backdrop fade, and a short ease-out slide — all achievable with core APIs.
7
7
  // Skipping reanimated/gesture-handler keeps the package free of native
8
8
  // dependencies and install friction for consumers.
9
- import { useCallback, useEffect, useRef } from 'react';
9
+ //
10
+ // `visible` drives a single animation path: the sheet stays mounted until
11
+ // its exit finishes, so closing from the header button, the backdrop, the
12
+ // hardware back button, or a drag all animate identically — and a reopen
13
+ // always starts from off-screen instead of appearing already in place.
14
+ import { useCallback, useEffect, useRef, useState } from 'react';
10
15
  import { Animated, Easing, Modal, PanResponder, Pressable, useWindowDimensions, View } from 'react-native';
11
16
  import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
12
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
- const SLIDE_MS = 180; // motion.slow — ease-out, no springs, no bounce.
14
-
18
+ const SLIDE_MS = 260;
19
+ const FADE_MS = 200;
15
20
  export function BottomSheet({
16
21
  visible,
17
22
  onClose,
@@ -21,45 +26,57 @@ export function BottomSheet({
21
26
  const {
22
27
  height: windowHeight
23
28
  } = useWindowDimensions();
24
- const sheetHeight = Math.min(windowHeight * 0.82, 640);
29
+ const sheetHeight = Math.min(windowHeight * 0.88, 720);
30
+ const [rendered, setRendered] = useState(false);
25
31
  const translateY = useRef(new Animated.Value(sheetHeight)).current;
26
32
  const backdrop = useRef(new Animated.Value(0)).current;
27
- const animateIn = useCallback(() => {
28
- Animated.parallel([Animated.timing(translateY, {
29
- toValue: 0,
30
- duration: SLIDE_MS,
31
- easing: Easing.out(Easing.cubic),
32
- useNativeDriver: true
33
- }), Animated.timing(backdrop, {
34
- toValue: 1,
35
- duration: SLIDE_MS,
36
- easing: Easing.out(Easing.cubic),
37
- useNativeDriver: true
38
- })]).start();
39
- }, [translateY, backdrop]);
40
- const animateOut = useCallback(done => {
41
- Animated.parallel([Animated.timing(translateY, {
42
- toValue: sheetHeight,
43
- duration: SLIDE_MS,
44
- easing: Easing.out(Easing.cubic),
45
- useNativeDriver: true
46
- }), Animated.timing(backdrop, {
47
- toValue: 0,
48
- duration: SLIDE_MS,
49
- easing: Easing.out(Easing.cubic),
50
- useNativeDriver: true
51
- })]).start(({
52
- finished
53
- }) => {
54
- if (finished) done();
55
- });
56
- }, [translateY, backdrop, sheetHeight]);
33
+ /** Whether the entrance has already played for the current open. */
34
+ const shown = useRef(false);
35
+ useEffect(() => {
36
+ if (visible) setRendered(true);
37
+ }, [visible]);
57
38
  useEffect(() => {
58
- if (visible) animateIn();
59
- }, [visible, animateIn]);
39
+ if (!rendered) return;
40
+ if (visible && !shown.current) {
41
+ shown.current = true;
42
+ translateY.setValue(sheetHeight);
43
+ Animated.parallel([Animated.timing(translateY, {
44
+ toValue: 0,
45
+ duration: SLIDE_MS,
46
+ easing: Easing.bezier(0.16, 1, 0.3, 1),
47
+ useNativeDriver: true
48
+ }), Animated.timing(backdrop, {
49
+ toValue: 1,
50
+ duration: FADE_MS,
51
+ easing: Easing.out(Easing.cubic),
52
+ useNativeDriver: true
53
+ })]).start();
54
+ return;
55
+ }
56
+ if (!visible && shown.current) {
57
+ shown.current = false;
58
+ // Starts from wherever the sheet is now, so a drag that crosses the
59
+ // dismiss threshold continues rather than jumping.
60
+ Animated.parallel([Animated.timing(translateY, {
61
+ toValue: sheetHeight,
62
+ duration: theme.motion.slow,
63
+ easing: Easing.in(Easing.cubic),
64
+ useNativeDriver: true
65
+ }), Animated.timing(backdrop, {
66
+ toValue: 0,
67
+ duration: theme.motion.slow,
68
+ easing: Easing.in(Easing.cubic),
69
+ useNativeDriver: true
70
+ })]).start(({
71
+ finished
72
+ }) => {
73
+ if (finished) setRendered(false);
74
+ });
75
+ }
76
+ }, [rendered, visible, sheetHeight, translateY, backdrop, theme.motion.slow]);
60
77
  const requestClose = useCallback(() => {
61
- animateOut(onClose);
62
- }, [animateOut, onClose]);
78
+ onClose();
79
+ }, [onClose]);
63
80
 
64
81
  // Drag handle: follow the finger downward; past 25% (or a flick), dismiss.
65
82
  const panResponder = useRef(PanResponder.create({
@@ -74,14 +91,14 @@ export function BottomSheet({
74
91
  } else {
75
92
  Animated.timing(translateY, {
76
93
  toValue: 0,
77
- duration: SLIDE_MS,
94
+ duration: theme.motion.slow,
78
95
  easing: Easing.out(Easing.cubic),
79
96
  useNativeDriver: true
80
97
  }).start();
81
98
  }
82
99
  }
83
100
  })).current;
84
- if (!visible) return null;
101
+ if (!rendered) return null;
85
102
  return /*#__PURE__*/_jsx(Modal, {
86
103
  visible: true,
87
104
  transparent: true,
@@ -101,7 +118,7 @@ export function BottomSheet({
101
118
  right: 0,
102
119
  bottom: 0,
103
120
  left: 0,
104
- backgroundColor: 'rgba(0,0,0,0.5)',
121
+ backgroundColor: 'rgba(0,0,0,0.55)',
105
122
  opacity: backdrop
106
123
  },
107
124
  children: /*#__PURE__*/_jsx(Pressable, {
@@ -114,27 +131,30 @@ export function BottomSheet({
114
131
  }), /*#__PURE__*/_jsxs(Animated.View, {
115
132
  style: {
116
133
  height: sheetHeight,
117
- backgroundColor: theme.colors.overlay,
134
+ backgroundColor: theme.colors.bg,
118
135
  borderTopLeftRadius: theme.radii.sheet,
119
136
  borderTopRightRadius: theme.radii.sheet,
120
137
  borderWidth: 1,
121
138
  borderBottomWidth: 0,
122
139
  borderColor: theme.colors.border,
140
+ overflow: 'hidden',
123
141
  transform: [{
124
142
  translateY
125
- }]
143
+ }],
144
+ ...theme.shadows.sheet
126
145
  },
127
146
  children: [/*#__PURE__*/_jsx(View, {
128
147
  ...panResponder.panHandlers,
129
148
  style: {
130
149
  alignItems: 'center',
131
- paddingVertical: 10
150
+ paddingTop: 10,
151
+ paddingBottom: 6
132
152
  },
133
153
  children: /*#__PURE__*/_jsx(View, {
134
154
  style: {
135
- width: 32,
136
- height: 4,
137
- borderRadius: 2,
155
+ width: 40,
156
+ height: 5,
157
+ borderRadius: 3,
138
158
  backgroundColor: theme.colors.borderStrong
139
159
  }
140
160
  })