@ethora/chat-component-rn 26.6.12 → 26.6.13

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 (198) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/lib/commonjs/components/AuthForms/Login.js +2 -1
  4. package/lib/commonjs/components/AuthForms/Login.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatWrapper.js +14 -13
  8. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/LoginWrapper.js +2 -1
  10. package/lib/commonjs/components/MainComponents/LoginWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MediaMessage.js +17 -6
  12. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  14. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  16. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +2 -1
  18. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  19. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +5 -1
  20. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  21. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +10 -2
  22. package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  23. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  24. package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  25. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  26. package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  27. package/lib/commonjs/components/styled/AudioMessage.js +2 -1
  28. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  29. package/lib/commonjs/components/styled/MessageImage.js +34 -8
  30. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  31. package/lib/commonjs/components/styled/VideoMessage.js +7 -1
  32. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  33. package/lib/commonjs/context/xmppProvider.js +44 -18
  34. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  35. package/lib/commonjs/helpers/initXmppRooms.js +5 -1
  36. package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
  37. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +126 -12
  38. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  39. package/lib/commonjs/helpers/secureFileUrl.js +101 -0
  40. package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
  41. package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
  42. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  43. package/lib/commonjs/hooks/useFileToken.js +10 -0
  44. package/lib/commonjs/hooks/useFileToken.js.map +1 -0
  45. package/lib/commonjs/hooks/useSendMessage.js +23 -13
  46. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  47. package/lib/commonjs/main.js +25 -0
  48. package/lib/commonjs/main.js.map +1 -1
  49. package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
  50. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  51. package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
  52. package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
  53. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
  54. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  55. package/lib/commonjs/networking/api-requests/user.api.js +9 -9
  56. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  57. package/lib/commonjs/networking/apiClient.js +62 -112
  58. package/lib/commonjs/networking/apiClient.js.map +1 -1
  59. package/lib/commonjs/networking/authRefresh.js +323 -0
  60. package/lib/commonjs/networking/authRefresh.js.map +1 -0
  61. package/lib/commonjs/networking/xmppClient.js +33 -5
  62. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  63. package/lib/commonjs/roomStore/chatSettingsSlice.js +20 -2
  64. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  65. package/lib/module/components/AuthForms/Login.js +2 -1
  66. package/lib/module/components/AuthForms/Login.js.map +1 -1
  67. package/lib/module/components/MainComponents/ChatHeader.js +4 -1
  68. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  69. package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
  70. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  71. package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
  72. package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
  73. package/lib/module/components/MainComponents/MediaMessage.js +17 -6
  74. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  75. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
  76. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  77. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
  78. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  79. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +2 -1
  80. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  81. package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
  82. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  83. package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
  84. package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
  85. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
  86. package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
  87. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
  88. package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
  89. package/lib/module/components/styled/AudioMessage.js +2 -1
  90. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  91. package/lib/module/components/styled/MessageImage.js +34 -8
  92. package/lib/module/components/styled/MessageImage.js.map +1 -1
  93. package/lib/module/components/styled/VideoMessage.js +7 -1
  94. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  95. package/lib/module/context/xmppProvider.js +43 -18
  96. package/lib/module/context/xmppProvider.js.map +1 -1
  97. package/lib/module/helpers/initXmppRooms.js +5 -1
  98. package/lib/module/helpers/initXmppRooms.js.map +1 -1
  99. package/lib/module/helpers/resolveInitBeforeLoadUser.js +126 -11
  100. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  101. package/lib/module/helpers/secureFileUrl.js +91 -0
  102. package/lib/module/helpers/secureFileUrl.js.map +1 -0
  103. package/lib/module/hooks/useChatWrapperInit.js +3 -3
  104. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  105. package/lib/module/hooks/useFileToken.js +5 -0
  106. package/lib/module/hooks/useFileToken.js.map +1 -0
  107. package/lib/module/hooks/useSendMessage.js +24 -14
  108. package/lib/module/hooks/useSendMessage.js.map +1 -1
  109. package/lib/module/main.js +12 -1
  110. package/lib/module/main.js.map +1 -1
  111. package/lib/module/networking/api-requests/auth.api.js +96 -53
  112. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  113. package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
  114. package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
  115. package/lib/module/networking/api-requests/rooms.api.js +9 -9
  116. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  117. package/lib/module/networking/api-requests/user.api.js +8 -7
  118. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  119. package/lib/module/networking/apiClient.js +61 -113
  120. package/lib/module/networking/apiClient.js.map +1 -1
  121. package/lib/module/networking/authRefresh.js +312 -0
  122. package/lib/module/networking/authRefresh.js.map +1 -0
  123. package/lib/module/networking/xmppClient.js +33 -5
  124. package/lib/module/networking/xmppClient.js.map +1 -1
  125. package/lib/module/roomStore/chatSettingsSlice.js +20 -2
  126. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  127. package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
  128. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  129. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  130. package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
  131. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  132. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  133. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  134. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  135. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  136. package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
  137. package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
  138. package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
  139. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  140. package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
  141. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  142. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  143. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  144. package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
  145. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  146. package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
  147. package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
  148. package/lib/typescript/hooks/useFileToken.d.ts +2 -0
  149. package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
  150. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  151. package/lib/typescript/main.d.ts +6 -1
  152. package/lib/typescript/main.d.ts.map +1 -1
  153. package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
  154. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  155. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  156. package/lib/typescript/networking/apiClient.d.ts +12 -6
  157. package/lib/typescript/networking/apiClient.d.ts.map +1 -1
  158. package/lib/typescript/networking/authRefresh.d.ts +92 -0
  159. package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
  160. package/lib/typescript/networking/xmppClient.d.ts +6 -0
  161. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  162. package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
  163. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  164. package/lib/typescript/types/types.d.ts +17 -0
  165. package/lib/typescript/types/types.d.ts.map +1 -1
  166. package/package.json +1 -1
  167. package/src/components/AuthForms/Login.tsx +1 -0
  168. package/src/components/MainComponents/ChatHeader.tsx +4 -1
  169. package/src/components/MainComponents/ChatWrapper.tsx +14 -19
  170. package/src/components/MainComponents/LoginWrapper.tsx +1 -0
  171. package/src/components/MainComponents/MediaMessage.tsx +14 -4
  172. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
  173. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +8 -7
  174. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
  175. package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
  176. package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
  177. package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
  178. package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
  179. package/src/components/styled/AudioMessage.tsx +2 -1
  180. package/src/components/styled/MessageImage.tsx +45 -6
  181. package/src/components/styled/VideoMessage.tsx +10 -1
  182. package/src/context/xmppProvider.tsx +45 -20
  183. package/src/helpers/initXmppRooms.tsx +5 -1
  184. package/src/helpers/resolveInitBeforeLoadUser.ts +123 -11
  185. package/src/helpers/secureFileUrl.ts +103 -0
  186. package/src/hooks/useChatWrapperInit.ts +3 -3
  187. package/src/hooks/useFileToken.ts +7 -0
  188. package/src/hooks/useSendMessage.tsx +28 -13
  189. package/src/main.ts +22 -1
  190. package/src/networking/api-requests/auth.api.ts +125 -52
  191. package/src/networking/api-requests/roomMembers.api.ts +1 -1
  192. package/src/networking/api-requests/rooms.api.ts +9 -9
  193. package/src/networking/api-requests/user.api.ts +8 -7
  194. package/src/networking/apiClient.ts +68 -123
  195. package/src/networking/authRefresh.ts +435 -0
  196. package/src/networking/xmppClient.ts +37 -5
  197. package/src/roomStore/chatSettingsSlice.ts +28 -3
  198. package/src/types/types.ts +17 -0
@@ -1,4 +1,5 @@
1
- import http, { setBaseURL } from '../networking/apiClient';
1
+ import { setBaseURL } from '../networking/apiClient';
2
+ import { refreshAuthTokens, isRefreshFatalError } from '../networking/authRefresh';
2
3
  import { loginViaJwt } from '../networking/api-requests/auth.api';
3
4
  import { getMyUser } from '../networking/api-requests/user.api';
4
5
  import { IConfig, User } from '../types/types';
@@ -46,15 +47,109 @@ const normalizeUserForXmpp = (user?: User | null): User | null => {
46
47
  return { ...user, xmppUsername: normalizedXmppUsername };
47
48
  };
48
49
 
50
+ // Hermes has no atob/Buffer; hand-decode just enough base64url to read a
51
+ // JWT's `iat`. Claims are ASCII so the byte-per-char string is fine.
52
+ const B64_ALPHABET =
53
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
54
+
55
+ const readJwtIat = (token?: string | null): number => {
56
+ if (!token || typeof token !== 'string') {return 0;}
57
+ const part = token.split('.')[1];
58
+ if (!part) {return 0;}
59
+ try {
60
+ const clean = part.replace(/-/g, '+').replace(/_/g, '/');
61
+ let out = '';
62
+ let buffer = 0;
63
+ let bits = 0;
64
+ for (let i = 0; i < clean.length; i++) {
65
+ const idx = B64_ALPHABET.indexOf(clean[i]);
66
+ if (idx === -1) {continue;}
67
+ buffer = (buffer << 6) | idx;
68
+ bits += 6;
69
+ if (bits >= 8) {
70
+ bits -= 8;
71
+ out += String.fromCharCode((buffer >> bits) & 0xff);
72
+ }
73
+ }
74
+ const iat = JSON.parse(out)?.iat;
75
+ return typeof iat === 'number' ? iat : 0;
76
+ } catch {
77
+ return 0;
78
+ }
79
+ };
80
+
81
+ /**
82
+ * A `userLogin.user` is typically a snapshot the host captured at ITS
83
+ * login time and re-presents verbatim on every mount, while this SDK
84
+ * keeps rotating the session and persisting the newest pair to
85
+ * `ETHORA_USER`. Taking the snapshot as-is therefore discards the live
86
+ * session in favour of an ever-aging copy: media goes blank the moment
87
+ * the snapshot has no `fileToken` (secure-files URLs 401 without it),
88
+ * and once the snapshot's refresh token ages out entirely the account
89
+ * looks dead even though the SDK held a working session all along.
90
+ *
91
+ * So, for the SAME account only, prefer whichever refresh pair was
92
+ * minted later (JWT `iat` decides — a host that genuinely re-logged-in
93
+ * hands us newer tokens and wins), and fill in a missing `fileToken`
94
+ * from the persisted copy either way.
95
+ */
96
+ const adoptFresherPersistedSession = async (candidate: User): Promise<User> => {
97
+ try {
98
+ const stored = await asyncLocalStorage<User>(
99
+ localStorageConstants.ETHORA_USER
100
+ ).get();
101
+ if (!stored) {return candidate;}
102
+
103
+ const sameUser =
104
+ (candidate._id && stored._id && candidate._id === stored._id) ||
105
+ (candidate.xmppUsername &&
106
+ stored.xmppUsername &&
107
+ candidate.xmppUsername === stored.xmppUsername);
108
+ if (!sameUser) {return candidate;}
109
+
110
+ const merged = { ...candidate };
111
+
112
+ if (
113
+ stored.refreshToken &&
114
+ readJwtIat(stored.refreshToken) > readJwtIat(candidate.refreshToken)
115
+ ) {
116
+ merged.refreshToken = stored.refreshToken;
117
+ if (stored.token) {merged.token = stored.token;}
118
+ if (stored.fileToken) {merged.fileToken = stored.fileToken;}
119
+ }
120
+
121
+ if (!merged.fileToken && stored.fileToken) {
122
+ merged.fileToken = stored.fileToken;
123
+ }
124
+
125
+ return merged;
126
+ } catch {
127
+ return candidate;
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Bootstrap rotation.
133
+ *
134
+ * Was a second, independent `/users/login/refresh` caller that bypassed
135
+ * every lock in the SDK — and, worse, only wrote the rotated token to
136
+ * the store on the happy path, so several of the early-return branches
137
+ * below used to drop it. Under the backend's reuse detection a dropped
138
+ * rotation means the next launch presents a burned token and the
139
+ * session is killed.
140
+ *
141
+ * `refreshAuthTokens` persists the new pair before it resolves, so by
142
+ * the time this returns the rotation is safe no matter which branch the
143
+ * caller takes afterwards. The explicit token is required here: during
144
+ * bootstrap the candidate session isn't in the store yet.
145
+ */
49
146
  const refreshWithToken = async (refreshToken: string) => {
50
- const response = await http.post(
51
- '/users/login/refresh',
52
- {},
53
- { headers: { Authorization: refreshToken } }
54
- );
147
+ const result = await refreshAuthTokens({ refreshToken });
55
148
  return {
56
- token: response?.data?.token || '',
57
- refreshToken: response?.data?.refreshToken || refreshToken,
149
+ token: result.token,
150
+ refreshToken: result.refreshToken || refreshToken,
151
+ xmppPassword: result.xmppPassword || '',
152
+ fileToken: result.fileToken || '',
58
153
  };
59
154
  };
60
155
 
@@ -87,11 +182,15 @@ const tryHydrateViaMy = async (
87
182
 
88
183
  let workingToken = candidate?.token || '';
89
184
  let workingRefresh = candidate?.refreshToken || '';
185
+ let rotatedXmppPassword = '';
186
+ let workingFileToken = candidate?.fileToken || '';
90
187
 
91
188
  const candidateWithCurrentTokens = (): User => ({
92
189
  ...candidate,
93
190
  token: workingToken || candidate.token,
94
191
  refreshToken: workingRefresh || candidate.refreshToken,
192
+ xmppPassword: rotatedXmppPassword || candidate.xmppPassword,
193
+ fileToken: workingFileToken || candidate.fileToken,
95
194
  });
96
195
 
97
196
  const fallbackWithCreds = (): User | null => {
@@ -107,6 +206,8 @@ const tryHydrateViaMy = async (
107
206
  if (merged) {
108
207
  merged.token = workingToken || merged.token;
109
208
  merged.refreshToken = workingRefresh || merged.refreshToken;
209
+ merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
210
+ merged.fileToken = workingFileToken || merged.fileToken;
110
211
  }
111
212
  return merged;
112
213
  } catch (error) {
@@ -127,6 +228,8 @@ const tryHydrateViaMy = async (
127
228
  const refreshed = await refreshWithToken(workingRefresh);
128
229
  workingToken = refreshed.token;
129
230
  workingRefresh = refreshed.refreshToken;
231
+ rotatedXmppPassword = refreshed.xmppPassword || rotatedXmppPassword;
232
+ workingFileToken = refreshed.fileToken || workingFileToken;
130
233
 
131
234
  try {
132
235
  const myUser = await getMyUser({ token: workingToken, endpoint: myEndpoint });
@@ -136,6 +239,8 @@ const tryHydrateViaMy = async (
136
239
  if (merged) {
137
240
  merged.token = workingToken || merged.token;
138
241
  merged.refreshToken = workingRefresh || merged.refreshToken;
242
+ merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
243
+ merged.fileToken = workingFileToken || merged.fileToken;
139
244
  }
140
245
  return merged;
141
246
  } catch (myError) {
@@ -147,6 +252,11 @@ const tryHydrateViaMy = async (
147
252
  }
148
253
  } catch (error) {
149
254
  if (signal?.aborted || isAbortError(error)) {return null;}
255
+ // A fatal refresh verdict (reuse detected / token not found / stale
256
+ // with nothing newer around) carries no HTTP response, so it would
257
+ // otherwise fall through to `throw` — the session is simply dead,
258
+ // which for bootstrap means "nothing to restore".
259
+ if (isRefreshFatalError(error)) {return null;}
150
260
  if (isAuthError(error)) {return null;}
151
261
  throw error;
152
262
  }
@@ -162,13 +272,15 @@ export const resolveInitBeforeLoadUser = async (
162
272
  setBaseURL(config.baseUrl, config.customAppToken);
163
273
  }
164
274
 
165
- const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
275
+ const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
166
276
 
167
277
  // Priority 1: explicit userLogin
168
278
  const explicitUser = config?.userLogin?.enabled ? config?.userLogin?.user : null;
169
279
  if (explicitUser) {
170
280
  const candidate = normalizeUserForXmpp(explicitUser);
171
- if (candidate && hasXmppCredentials(candidate)) {return candidate;}
281
+ if (candidate && hasXmppCredentials(candidate)) {
282
+ return await adoptFresherPersistedSession(candidate);
283
+ }
172
284
 
173
285
  const hydrated = await tryHydrateViaMy(explicitUser, myEndpoint, signal).catch(
174
286
  () => null
@@ -275,7 +387,7 @@ export const refreshUserCredentialsForXmpp = async (
275
387
  }
276
388
  }
277
389
 
278
- const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
390
+ const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
279
391
 
280
392
  // Priority 2: redux store user (covers userLogin + customLogin +
281
393
  // anything previously persisted into chatSettingStore.user). The
@@ -0,0 +1,103 @@
1
+ import { store } from '../roomStore';
2
+ import { getCurrentBaseURL } from '../networking/apiClient';
3
+ import { refreshAuthTokens } from '../networking/authRefresh';
4
+
5
+ const SECURE_FILES_HOST_PREFIX = 'secure-files.';
6
+ const SECURE_MEDIA_PATH_PREFIX = '/secure-media/';
7
+
8
+ const FILE_TOKEN_PARAM = 'ft';
9
+
10
+ export const resolveFileUrl = (url?: string | null): string => {
11
+ if (!url) {return '';}
12
+ // Any scheme at all — http(s), data, file, content, blob.
13
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {return url;}
14
+ if (!url.startsWith('/')) {return url;}
15
+
16
+ const base = getCurrentBaseURL().replace(/\/+$/, '');
17
+ return base ? `${base}${url}` : url;
18
+ };
19
+
20
+ export const isSecureFileUrl = (url?: string | null): boolean => {
21
+ if (!url) {return false;}
22
+
23
+ const path = url.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, '');
24
+ if (path.startsWith(SECURE_MEDIA_PATH_PREFIX)) {return true;}
25
+
26
+ const match = /^https?:\/\/(?:[^@/]+@)?([^:/?#]+)/i.exec(url);
27
+ if (!match) {return false;}
28
+ return match[1].toLowerCase().startsWith(SECURE_FILES_HOST_PREFIX);
29
+ };
30
+
31
+ export const appendFileToken = (
32
+ url: string | null | undefined,
33
+ fileToken: string | null | undefined
34
+ ): string => {
35
+ if (!url) {return '';}
36
+
37
+ const resolved = resolveFileUrl(url);
38
+ if (!fileToken || !isSecureFileUrl(resolved)) {return resolved;}
39
+ url = resolved;
40
+
41
+ const hashIndex = url.indexOf('#');
42
+ const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
43
+ const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
44
+
45
+ const queryIndex = withoutHash.indexOf('?');
46
+ const base = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
47
+ const query = queryIndex === -1 ? '' : withoutHash.slice(queryIndex + 1);
48
+
49
+ const params = query
50
+ .split('&')
51
+ .filter(Boolean)
52
+ .filter((pair) => pair.split('=')[0] !== FILE_TOKEN_PARAM);
53
+
54
+ params.push(`${FILE_TOKEN_PARAM}=${encodeURIComponent(fileToken)}`);
55
+
56
+ return `${base}?${params.join('&')}${hash}`;
57
+ };
58
+
59
+ export const withFileToken = (url?: string | null): string =>
60
+ appendFileToken(
61
+ url,
62
+ store.getState().chatSettingStore?.user?.fileToken || ''
63
+ );
64
+
65
+ const RECOVERY_COOLDOWN_MS = 30_000;
66
+ let recoveryInFlight: Promise<boolean> | null = null;
67
+ let lastRecoveryAt = 0;
68
+
69
+ export const requestFileTokenRecovery = (): Promise<boolean> => {
70
+ if (recoveryInFlight) {return recoveryInFlight;}
71
+
72
+ const now = Date.now();
73
+ if (now - lastRecoveryAt < RECOVERY_COOLDOWN_MS) {
74
+ return Promise.resolve(false);
75
+ }
76
+ lastRecoveryAt = now;
77
+
78
+ const state = store.getState();
79
+ const refreshConfig = state.chatSettingStore?.config?.refreshTokens;
80
+ const user = state.chatSettingStore?.user;
81
+
82
+ if (!refreshConfig?.enabled) {return Promise.resolve(false);}
83
+
84
+ const run = async (): Promise<boolean> => {
85
+ if (!refreshConfig.refreshFunction && !user?.refreshToken) {return false;}
86
+
87
+ const tokens = await refreshAuthTokens();
88
+ return Boolean(tokens.fileToken);
89
+ };
90
+
91
+ recoveryInFlight = run()
92
+ .catch(() => false)
93
+ .finally(() => {
94
+ recoveryInFlight = null;
95
+ });
96
+
97
+ return recoveryInFlight;
98
+ };
99
+
100
+ export const __resetFileTokenRecoveryForTests = (): void => {
101
+ recoveryInFlight = null;
102
+ lastRecoveryAt = 0;
103
+ };
@@ -10,7 +10,7 @@ import { chatAutoEnterer } from '../helpers/chatAutoEnterer';
10
10
  import { initRoomsPresence } from '../helpers/initRoomsPresence';
11
11
  import { updatedChatLastTimestamps } from '../helpers/updatedChatLastTimestamps';
12
12
  import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
13
- import { refresh } from '../networking/apiClient';
13
+ import { refreshAuthTokensQuietly } from '../networking/authRefresh';
14
14
  import { setLangSource, setConfig } from '../roomStore/chatSettingsSlice';
15
15
  import { setIsLoading } from '../roomStore/roomsSlice';
16
16
  import { useRoomState } from './useRoomState';
@@ -179,7 +179,7 @@ const useChatWrapperInit = ({
179
179
  }
180
180
 
181
181
  {
182
- config?.refreshTokens?.enabled && refresh();
182
+ config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
183
183
  }
184
184
  } catch (error) {
185
185
  console.error(' Error during initialization', error);
@@ -233,7 +233,7 @@ const useChatWrapperInit = ({
233
233
  setConnectionLost(false);
234
234
  }
235
235
  {
236
- config?.refreshTokens?.enabled && refresh();
236
+ config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
237
237
  }
238
238
  }
239
239
  }
@@ -0,0 +1,7 @@
1
+ import { useSelector } from 'react-redux';
2
+ import { RootState } from '../roomStore';
3
+
4
+ export const useFileToken = (): string =>
5
+ useSelector(
6
+ (state: RootState) => state.chatSettingStore.user?.fileToken || ''
7
+ );
@@ -9,7 +9,7 @@ import {
9
9
  clearMessageFailure,
10
10
  markMessageFailed,
11
11
  } from '../roomStore/roomHeapSlice';
12
- import { uploadFileMultipart } from '../networking/api-requests/auth.api';
12
+ import { uploadFileV2 } from '../networking/api-requests/auth.api';
13
13
  import { enqueueOutboundSend } from '../networking/outboundQueue';
14
14
  import { RootState, store } from '../roomStore';
15
15
  import { getGlobalXmppClient } from '../utils/clientRegistry';
@@ -182,13 +182,34 @@ export const useSendMessage = (_configOverride?: IConfig) => {
182
182
  // PENDING_WATCHDOG_MS, flip the bubble to "failed → tap to
183
183
  // retry". We check at fire-time whether the message is still
184
184
  // pending and not already marked failed by the catch path.
185
- const watchdogTimer = setTimeout(() => {
185
+ // One-shot extension for the case below. Declared outside the
186
+ // timer so the rescheduled run can't extend again.
187
+ let watchdogExtended = false;
188
+ const runWatchdog = () => {
186
189
  const state = reduxStore.getState();
187
190
  const roomMsgs = state.rooms?.rooms?.[activeRoomJID]?.messages || [];
188
191
  const stillPending = roomMsgs.find(
189
192
  (m: any) => m.id === optimisticId && m.pending
190
193
  );
191
194
  const alreadyFailed = state.roomHeapSlice?.failedMessages?.[optimisticId];
195
+
196
+ // Don't call it a failure while the stream is still coming back.
197
+ // A reconnect tears the old client down and builds a new one, and
198
+ // any echo in flight during that window is simply lost — the
199
+ // message may well have been delivered (it has been observed
200
+ // arriving twice on the receiving side). Failing it here shows a
201
+ // red "tap to retry" under a message that actually went through.
202
+ // Give the reconnect one more window to produce an echo, once.
203
+ const streamOnline = getGlobalXmppClient()?.status === 'online';
204
+ if (stillPending && !alreadyFailed && !streamOnline && !watchdogExtended) {
205
+ watchdogExtended = true;
206
+ console.warn(
207
+ `Send watchdog — ${optimisticId} still pending but the stream is not online; extending one window`
208
+ );
209
+ watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
210
+ return;
211
+ }
212
+
192
213
  if (stillPending && !alreadyFailed) {
193
214
  console.warn(
194
215
  `Send watchdog fired — message ${optimisticId} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
@@ -211,7 +232,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
211
232
  messageType: 'text',
212
233
  });
213
234
  }
214
- }, PENDING_WATCHDOG_MS);
235
+ };
236
+
237
+ let watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
215
238
 
216
239
  try {
217
240
  // The single send closure, used both for the immediate send and for
@@ -467,19 +490,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
467
490
  name: data?.name || data?.fileName || `media_${Date.now()}`,
468
491
  };
469
492
 
470
- // Mirror the web client: a single multipart POST to /files/ with
471
- // the file under the plural "files" field. We send it via fetch
472
- // (not axios) because RN's fetch sets the multipart boundary on the
473
- // Content-Type header correctly. The previous axios fallback omitted
474
- // a valid boundary, so the server mis-parsed one file as many and
475
- // rejected the request with HTTP 413 "TOO_MANY_FILES". The only
476
- // retry is the documented singular-"file" fallback for deployments
477
- // that 500 on the plural field (bug #10).
478
493
  const tryUpload = async () => {
479
494
  const fd = new FormData();
480
495
  fd.append('files', fileBlob as any);
481
496
  try {
482
- return await uploadFileMultipart(fd);
497
+ return await uploadFileV2(fd, activeRoomJID);
483
498
  } catch (err: any) {
484
499
  if (err?.response?.status === 500) {
485
500
  console.warn(
@@ -488,7 +503,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
488
503
  );
489
504
  const fd2 = new FormData();
490
505
  fd2.append('file', fileBlob as any);
491
- return await uploadFileMultipart(fd2);
506
+ return await uploadFileV2(fd2, activeRoomJID);
492
507
  }
493
508
  throw err;
494
509
  }
package/src/main.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  *
8
8
  * <Chat
9
9
  * config={{
10
- * baseUrl: 'https://api.chat.ethora.com/v1',
10
+ * // The API ROOT — the SDK versions every path itself
11
+ * // (`/v1/...`, `/v2/...`). A legacy `.../v1` is accepted and
12
+ * // normalised, so existing integrations keep working.
13
+ * baseUrl: 'https://api.chat.ethora.com',
11
14
  * xmppSettings: { host: 'xmpp.chat.ethora.com' },
12
15
  * jwtLogin: { enabled: true, token: '<paste JWT>' },
13
16
  * initBeforeLoad: true,
@@ -66,3 +69,21 @@ export type {
66
69
  } from './types/types';
67
70
  export type { Iso639_1Codes } from './types/models/language.model';
68
71
  export type { TranslateMode } from './utils/translateModePolicy';
72
+
73
+ // Auth-token rotation. Exposed so hosts can drive a refresh themselves
74
+ // (it is deduped and shares the SDK's lock) and, more importantly, so
75
+ // they can tell a dead session from a transient failure: only
76
+ // `RefreshFatalError` means "log the user out". Every other rejection —
77
+ // network, 5xx, a lost REFRESH_IN_PROGRESS race — must leave the
78
+ // session alone.
79
+ export {
80
+ refreshAuthTokens,
81
+ refreshAuthTokensQuietly,
82
+ RefreshFatalError,
83
+ isRefreshFatalError,
84
+ } from './networking/authRefresh';
85
+ export type {
86
+ RefreshErrorCode,
87
+ RefreshResult,
88
+ RefreshOptions,
89
+ } from './networking/authRefresh';