@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
@@ -18,8 +18,9 @@ export async function loginEmail(email: string, password: string) {
18
18
  user: User;
19
19
  refreshToken: string;
20
20
  token: string;
21
+ fileToken?: string;
21
22
  }>(
22
- '/users/login-with-email',
23
+ '/v1/users/login-with-email',
23
24
  {
24
25
  email,
25
26
  password,
@@ -29,6 +30,16 @@ export async function loginEmail(email: string, password: string) {
29
30
 
30
31
  console.log('loginEmail res', res.data);
31
32
 
33
+ // The fileToken arrives at the TOP level of the login response, next to
34
+ // token/refreshToken — not inside `user`. Folding it in here is what
35
+ // makes secure media (`secure-files.*`, uploaded via /v2/files/secure)
36
+ // render at all: without it every such image 403s and the bubble comes
37
+ // up blank.
38
+ res.data.user = {
39
+ ...res.data.user,
40
+ fileToken: res.data.fileToken || '',
41
+ };
42
+
32
43
  return res;
33
44
  }
34
45
 
@@ -39,7 +50,7 @@ export function loginSocial(
39
50
  authToken: string = 'authToken'
40
51
  ) {
41
52
  return http.post<any>(
42
- '/users/login',
53
+ '/v1/users/login',
43
54
  {
44
55
  idToken,
45
56
  accessToken,
@@ -58,7 +69,7 @@ export function registerSocial(
58
69
  signUpPlan?: string
59
70
  ) {
60
71
  return http.post(
61
- '/users',
72
+ '/v1/users',
62
73
  {
63
74
  idToken,
64
75
  accessToken,
@@ -72,7 +83,7 @@ export function registerSocial(
72
83
 
73
84
  export function checkEmailExist(email: string) {
74
85
  return http.get(
75
- '/users/checkEmail/' + email,
86
+ '/v1/users/checkEmail/' + email,
76
87
 
77
88
  { headers: { Authorization: getCurrentAppToken() } }
78
89
  );
@@ -84,7 +95,8 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
84
95
  user: User;
85
96
  refreshToken: string;
86
97
  token: string;
87
- }>('/users/client', null, { headers: { 'x-custom-token': clientToken } });
98
+ fileToken?: string;
99
+ }>('/v1/users/client', null, { headers: { 'x-custom-token': clientToken } });
88
100
 
89
101
  console.log('✅ loginViaJwt: Received response', {
90
102
  hasUser: !!response.data.user,
@@ -103,6 +115,8 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
103
115
  ...response.data.user,
104
116
  refreshToken: response.data.refreshToken,
105
117
  token: response.data.token,
118
+ // Top-level, same as loginEmail — see the note there.
119
+ fileToken: response.data.fileToken || '',
106
120
  };
107
121
 
108
122
  return user;
@@ -131,7 +145,7 @@ export async function loginViaJwt(clientToken: string): Promise<User> {
131
145
 
132
146
  export function uploadFile(formData: FormData) {
133
147
  const token = store.getState().chatSettingStore?.user?.token ?? '';
134
- return http.post('/files/', formData, {
148
+ return http.post('/v1/files/', formData, {
135
149
  headers: {
136
150
  Authorization: token,
137
151
  Accept: '*/*',
@@ -144,79 +158,91 @@ export function uploadFile(formData: FormData) {
144
158
  });
145
159
  }
146
160
 
147
- const UPLOAD_PATH = '/files/';
148
-
149
- /**
150
- * Multipart upload of a picked file, sent over `XMLHttpRequest`.
151
- *
152
- * The transport matters here, and both obvious alternatives are wrong:
153
- *
154
- * - `fetch`. Expo SDK 54 shipped a WinterCG-compliant fetch and SDK 57
155
- * installs it as the GLOBAL fetch. It accepts only `Blob` / `File` /
156
- * string form parts, so React Native's own `{ uri, type, name }` part
157
- * — which is exactly what an image/document picker hands us — is
158
- * rejected with "Unsupported FormDataPart implementation", surfacing
159
- * to the caller as ERR_NETWORK. A host can set
160
- * `EXPO_PUBLIC_USE_RN_FETCH=1`, but that opts their whole app out of
161
- * Expo's fetch just to satisfy this SDK; not a cost we get to impose
162
- * on a consumer.
163
- * - axios. It forces its own `Content-Type: multipart/form-data`
164
- * WITHOUT a boundary, so the server mis-parses one file as many and
165
- * answers HTTP 413 TOO_MANY_FILES (bug #10 — this is why the upload
166
- * was moved off axios in the first place).
167
- *
168
- * `XMLHttpRequest` avoids both. Expo replaces `fetch`, not `XHR`, so the
169
- * request still goes through React Native's native networking module,
170
- * which understands the `{ uri, type, name }` part, streams the file
171
- * straight from disk (a 50MB video never lands in JS memory) and writes
172
- * its own `Content-Type` with a valid boundary — which is precisely why
173
- * we must NOT set that header ourselves below.
174
- */
175
- export function uploadFileMultipart(formData: FormData): Promise<{ data: any }> {
161
+ const SECURE_UPLOAD_PATH = '/v2/files/secure';
162
+ const LEGACY_UPLOAD_PATH = '/v1/files/';
163
+
164
+ const cloneFormData = (formData: FormData): FormData => {
165
+ const copy = new FormData();
166
+ const source = formData as any;
167
+
168
+ if (Array.isArray(source?._parts)) {
169
+ for (const [key, value] of source._parts) {
170
+ copy.append(key, value);
171
+ }
172
+ return copy;
173
+ }
174
+
175
+ if (typeof source?.forEach === 'function') {
176
+ source.forEach((value: any, key: string) => copy.append(key, value));
177
+ return copy;
178
+ }
179
+
180
+ if (typeof source?.getParts === 'function') {
181
+ for (const part of source.getParts()) {
182
+ // `headers` is derived by getParts() and must not be copied back —
183
+ // RN regenerates it from the value.
184
+ const { fieldName, string, headers: _headers, ...file } = part;
185
+ copy.append(fieldName, string !== undefined ? string : file);
186
+ }
187
+ }
188
+
189
+ return copy;
190
+ };
191
+
192
+ function postMultipart(
193
+ url: string,
194
+ body: FormData,
195
+ path: string
196
+ ): Promise<{ data: any }> {
176
197
  const token = store.getState().chatSettingStore?.user?.token ?? '';
177
- const baseUrl = getCurrentBaseURL();
178
- const url = `${baseUrl.replace(/\/$/, '')}${UPLOAD_PATH}`;
179
198
 
180
199
  pushLog('http', `→ POST ${url} (xhr)`, {
181
- headers: { Authorization: token ? token.slice(0, 16) + '…' : '[empty]', Accept: '*/*' },
200
+ path,
201
+ baseUrl: getCurrentBaseURL() || '[empty]',
202
+ headers: {
203
+ Authorization: token ? token.slice(0, 16) + '…' : '[empty]',
204
+ Accept: '*/*',
205
+ },
182
206
  });
183
207
 
184
208
  return new Promise((resolve, reject) => {
185
209
  const xhr = new XMLHttpRequest();
186
210
 
187
211
  const failNetwork = (reason: string) => {
188
- pushLog('http', `← ERR xhr ${UPLOAD_PATH}`, { reason });
212
+ pushLog('http', `← ERR xhr ${path}`, { reason, url });
189
213
  const err: any = new Error(reason);
190
214
  err.code = 'ERR_NETWORK';
191
- err.config = { url: UPLOAD_PATH };
215
+ err.config = { url: path };
192
216
  reject(err);
193
217
  };
194
218
 
195
219
  xhr.onload = () => {
196
220
  const status = xhr.status;
197
- let body: any = null;
221
+ let responseBody: any = null;
198
222
  try {
199
- body = JSON.parse(xhr.responseText);
223
+ responseBody = JSON.parse(xhr.responseText);
200
224
  } catch {
201
- body = xhr.responseText || null;
225
+ responseBody = xhr.responseText || null;
202
226
  }
203
227
 
204
228
  if (status < 200 || status >= 300) {
205
- pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, body);
229
+ pushLog('http', `← ${status} POST ${path} (xhr)`, responseBody);
206
230
  const err: any = new Error(`Upload failed: ${status}`);
207
231
  // Shaped like an axios error so callers' `err.response.status`
208
232
  // checks keep working — notably useSendMessage's retry with the
209
233
  // singular "file" field on 500.
210
- err.response = { status, statusText: xhr.statusText, data: body };
211
- err.config = { url: UPLOAD_PATH };
234
+ err.response = { status, statusText: xhr.statusText, data: responseBody };
235
+ err.config = { url: path };
212
236
  reject(err);
213
237
  return;
214
238
  }
215
239
 
216
- pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, {
217
- resultsCount: Array.isArray(body?.results) ? body.results.length : 'n/a',
240
+ pushLog('http', `← ${status} POST ${path} (xhr)`, {
241
+ resultsCount: Array.isArray(responseBody?.results)
242
+ ? responseBody.results.length
243
+ : 'n/a',
218
244
  });
219
- resolve({ data: body });
245
+ resolve({ data: responseBody });
220
246
  };
221
247
 
222
248
  xhr.onerror = () => failNetwork('Network request failed');
@@ -226,8 +252,55 @@ export function uploadFileMultipart(formData: FormData): Promise<{ data: any }>
226
252
  xhr.open('POST', url);
227
253
  xhr.setRequestHeader('Authorization', token);
228
254
  xhr.setRequestHeader('Accept', '*/*');
229
- // Deliberately no Content-Type — see the note above: RN fills it in
230
- // along with the multipart boundary it generated.
231
- xhr.send(formData as any);
255
+ // Deliberately no Content-Type — see the note above.
256
+ xhr.send(body as any);
232
257
  });
233
258
  }
259
+
260
+ const canFallBackToLegacyUpload = (error: unknown): boolean => {
261
+ const status = (error as { response?: { status?: number } })?.response
262
+ ?.status;
263
+ if (typeof status !== 'number') {return true;}
264
+ return status !== 401 && status !== 413;
265
+ };
266
+
267
+ let secureUploadUnavailable = false;
268
+
269
+ export async function uploadFileV2(
270
+ formData: FormData,
271
+ activeRoomJID: string
272
+ ): Promise<{ data: any }> {
273
+ const chatName = (activeRoomJID || '').split('@')[0];
274
+ const apiRoot = getCurrentBaseURL().replace(/\/$/, '');
275
+
276
+ if (!secureUploadUnavailable && chatName) {
277
+ const body = cloneFormData(formData);
278
+ body.append('chatName', chatName);
279
+
280
+ try {
281
+ return await postMultipart(
282
+ `${apiRoot}${SECURE_UPLOAD_PATH}`,
283
+ body,
284
+ SECURE_UPLOAD_PATH
285
+ );
286
+ } catch (error) {
287
+ if (!canFallBackToLegacyUpload(error)) {throw error;}
288
+
289
+ secureUploadUnavailable = true;
290
+ console.warn(
291
+ `[chat] ${SECURE_UPLOAD_PATH} unreachable, falling back to ${LEGACY_UPLOAD_PATH} for this session`,
292
+ error
293
+ );
294
+ }
295
+ }
296
+
297
+ return postMultipart(
298
+ `${apiRoot}${LEGACY_UPLOAD_PATH}`,
299
+ formData,
300
+ LEGACY_UPLOAD_PATH
301
+ );
302
+ }
303
+
304
+ export const __resetSecureUploadLatchForTests = (): void => {
305
+ secureUploadUnavailable = false;
306
+ };
@@ -7,7 +7,7 @@ export async function getUserByXmppUsername(
7
7
  ): Promise<RoomMember | null> {
8
8
  try {
9
9
  const res = await http.get<{ result: RoomMember }>(
10
- `/apps/users/${xmppUsername}`,
10
+ `/v1/apps/users/${xmppUsername}`,
11
11
  {
12
12
  headers: { Authorization: token },
13
13
  }
@@ -181,7 +181,7 @@ export async function getRooms(): Promise<{ items: ApiRoom[] }> {
181
181
 
182
182
  getRoomsInFlightToken = token;
183
183
  getRoomsInFlight = (async () => {
184
- const response = await http.get('/chats/my', {
184
+ const response = await http.get('/v1/chats/my', {
185
185
  headers: { Authorization: token },
186
186
  });
187
187
  lastGetRoomsResponse = response.data;
@@ -206,7 +206,7 @@ export async function getRoomByName(chatName: string): Promise<ApiRoom> {
206
206
  const token = store.getState().chatSettingStore.user.token || '';
207
207
 
208
208
  try {
209
- const response = await http.get(`/chats/my/${chatName}`, {
209
+ const response = await http.get(`/v1/chats/my/${chatName}`, {
210
210
  headers: {
211
211
  Authorization: token,
212
212
  },
@@ -221,7 +221,7 @@ export async function postRoom(data: PostRoom) {
221
221
  const token = store.getState().chatSettingStore.user.token || '';
222
222
 
223
223
  try {
224
- const response = await http.post('/chats', data, {
224
+ const response = await http.post('/v1/chats', data, {
225
225
  headers: {
226
226
  Authorization: token,
227
227
  },
@@ -240,7 +240,7 @@ export async function postPrivateRoom(
240
240
 
241
241
  try {
242
242
  const response = await http.post(
243
- '/chats/private',
243
+ '/v1/chats/private',
244
244
  { username },
245
245
  {
246
246
  headers: {
@@ -260,7 +260,7 @@ export async function postReportRoom(data: PostReportRoom) {
260
260
 
261
261
  try {
262
262
  const response = await http.post(
263
- `/chats/reports/${chatName}`,
263
+ `/v1/chats/reports/${chatName}`,
264
264
  { category, text },
265
265
  {
266
266
  headers: {
@@ -282,7 +282,7 @@ export async function postAddRoomMember(
282
282
 
283
283
  try {
284
284
  const response = await http.post(
285
- `/chats/users-access`,
285
+ `/v1/chats/users-access`,
286
286
  { chatName, members },
287
287
  {
288
288
  headers: {
@@ -301,7 +301,7 @@ export async function deleteRoomMember(data: DeleteRoomMember) {
301
301
  const token = store.getState().chatSettingStore.user.token || '';
302
302
 
303
303
  try {
304
- const response = await http.delete(`/chats/users-access`, {
304
+ const response = await http.delete(`/v1/chats/users-access`, {
305
305
  headers: {
306
306
  Authorization: token,
307
307
  },
@@ -320,7 +320,7 @@ export async function deleteRoom(name: string) {
320
320
  const token = store.getState().chatSettingStore.user.token || '';
321
321
 
322
322
  try {
323
- const response = await http.delete('/chats', {
323
+ const response = await http.delete('/v1/chats', {
324
324
  headers: {
325
325
  Authorization: token,
326
326
  },
@@ -360,7 +360,7 @@ export async function createChatCall(
360
360
 
361
361
  try {
362
362
  await http.post(
363
- `/chats/call/create/${chatName}`,
363
+ `/v1/chats/call/create/${chatName}`,
364
364
  { kind },
365
365
  {
366
366
  headers: {
@@ -1,4 +1,5 @@
1
1
  import http from '../apiClient';
2
+ import { normalizeApiPath } from '../apiClient';
2
3
  import { store } from '../../roomStore';
3
4
  import { User } from '../../types/types';
4
5
 
@@ -10,7 +11,7 @@ interface GetMyUserOptions {
10
11
  export async function getMyUser(options?: GetMyUserOptions): Promise<User> {
11
12
  const token =
12
13
  options?.token || store.getState().chatSettingStore.user.token || '';
13
- const endpoint = options?.endpoint || '/users/my';
14
+ const endpoint = normalizeApiPath(options?.endpoint) || '/v1/users/my';
14
15
 
15
16
  const response = await http.get(endpoint, {
16
17
  headers: { Authorization: token },
@@ -24,7 +25,7 @@ export async function getMyUser(options?: GetMyUserOptions): Promise<User> {
24
25
 
25
26
  export function getDocuments(walletAddress: string) {
26
27
  const token = store.getState().chatSettingStore.user.token || '';
27
- return http.get(`/docs/${walletAddress}`, {
28
+ return http.get(`/v1/docs/${walletAddress}`, {
28
29
  headers: {
29
30
  Authorization: `Bearer ${token}`,
30
31
  },
@@ -33,7 +34,7 @@ export function getDocuments(walletAddress: string) {
33
34
 
34
35
  export function getExportMyData() {
35
36
  const token = store.getState().chatSettingStore.user.token || '';
36
- return http.get('/users/exportData', {
37
+ return http.get('/v1/users/exportData', {
37
38
  headers: {
38
39
  Authorization: token,
39
40
  responseType: 'arraybuffer',
@@ -43,7 +44,7 @@ export function getExportMyData() {
43
44
 
44
45
  export function deleteDocument(fileId: string) {
45
46
  const token = store.getState().chatSettingStore.user.token || '';
46
- return http.delete(`/files/${fileId}`, {
47
+ return http.delete(`/v1/files/${fileId}`, {
47
48
  headers: {
48
49
  Authorization: token,
49
50
  },
@@ -51,18 +52,18 @@ export function deleteDocument(fileId: string) {
51
52
  }
52
53
 
53
54
  export function deleteMe() {
54
- return http.delete('/users');
55
+ return http.delete('/v1/users');
55
56
  }
56
57
 
57
58
  export function updateMe(data: any) {
58
- return http.put('/users', data);
59
+ return http.put('/v1/users', data);
59
60
  }
60
61
 
61
62
  export async function updateProfile(fd: FormData): Promise<{ user: User }> {
62
63
  const token = store.getState().chatSettingStore.user.token || '';
63
64
 
64
65
  try {
65
- const response = await http.put('/users', fd, {
66
+ const response = await http.put('/v1/users', fd, {
66
67
  headers: {
67
68
  Authorization: token,
68
69
  'Content-Type': 'multipart/form-data',
@@ -1,8 +1,13 @@
1
1
  import axios from 'axios';
2
2
  import { store } from '../roomStore';
3
3
 
4
- import { logout, refreshTokens } from '../roomStore/chatSettingsSlice';
4
+ import { logout } from '../roomStore/chatSettingsSlice';
5
5
  import { installAxiosCapture } from '../utils/devLogger';
6
+ import {
7
+ refreshAuthTokens,
8
+ isRefreshFatalError,
9
+ RefreshResult,
10
+ } from './authRefresh';
6
11
 
7
12
  // Per product-code-policy: no compiled-in Ethora endpoints or tokens.
8
13
  // Consumers must call `setBaseURL(baseUrl, appToken)` (or pass
@@ -21,10 +26,21 @@ const http = axios.create({
21
26
  // `http` instance.
22
27
  installAxiosCapture(http);
23
28
 
29
+ const stripApiVersion = (baseUrl: string): string =>
30
+ baseUrl.replace(/\/+$/, '').replace(/\/v\d+$/, '');
31
+
32
+ export const normalizeApiPath = (path?: string): string | undefined => {
33
+ if (!path) {return path;}
34
+ if (/^https?:\/\//i.test(path)) {return path;}
35
+ if (/^\/v\d+\//.test(path)) {return path;}
36
+ return `/v1${path.startsWith('/') ? '' : '/'}${path}`;
37
+ };
38
+
24
39
  export function setBaseURL(baseUrl?: string, appToken?: string) {
25
- if (baseUrl && baseUrl !== currentBaseURL) {
26
- currentBaseURL = baseUrl;
27
- http.defaults.baseURL = baseUrl;
40
+ const normalized = baseUrl ? stripApiVersion(baseUrl) : baseUrl;
41
+ if (normalized && normalized !== currentBaseURL) {
42
+ currentBaseURL = normalized;
43
+ http.defaults.baseURL = normalized;
28
44
  }
29
45
  if (appToken && appToken !== currentAppToken) {
30
46
  currentAppToken = appToken;
@@ -39,149 +55,78 @@ export function getCurrentBaseURL() {
39
55
  return currentBaseURL;
40
56
  }
41
57
 
42
- export function refresh(): Promise<{
43
- data: { refreshToken: string; token: string };
44
- }> {
45
- return new Promise((resolve, reject) => {
46
- const user = store.getState().chatSettingStore.user;
47
- try {
48
- http
49
- .post(
50
- '/users/login/refresh',
51
- {},
52
- { headers: { Authorization: user.refreshToken } }
53
- )
54
- .then((response) => {
55
- store.dispatch(
56
- refreshTokens({
57
- token: response.data.token,
58
- refreshToken: response.data.refreshToken,
59
- })
60
- );
61
- // BUGFIX: previously the `.then` only dispatched and never
62
- // resolved, so every `await refresh()` callsite hung forever
63
- // on the happy path. Resolve with the axios response shape so
64
- // the interceptor can read `tokens.data.token` to retry.
65
- resolve(response);
66
- })
67
- .catch((error) => {
68
- reject(error);
69
- });
70
- } catch (error) {
71
- // BUGFIX: previously the outer-try catch dispatched logout but
72
- // never rejected, so a synchronous throw from `http.post` (e.g.
73
- // a bad URL) would hang the promise. Reject explicitly.
74
- console.log('errr');
75
- store.dispatch(logout());
76
- reject(error);
77
- }
78
- });
58
+ /**
59
+ * @deprecated Import `refreshAuthTokens` from `./authRefresh` instead.
60
+ *
61
+ * Kept as a thin forwarder so existing call sites keep working while
62
+ * they migrate. Declared as a function (not `const refresh =
63
+ * refreshAuthTokens`) on purpose: this module and `authRefresh` import
64
+ * each other, and a const would capture the binding before the other
65
+ * module finished evaluating.
66
+ */
67
+ export function refresh(): Promise<RefreshResult> {
68
+ return refreshAuthTokens();
79
69
  }
80
70
 
81
- let isRefreshing = false;
82
- let failedQueue: any[] = [];
83
-
84
- const addRequestToQueue = (config: any) => {
85
- return new Promise((resolve, reject) => {
86
- failedQueue.push({ resolve, reject, config });
87
- });
88
- };
89
-
90
- const processQueue = (newAccessToken: string) => {
91
- for (const request of failedQueue) {
92
- if (newAccessToken) {
93
- request.config.headers.Authorization = newAccessToken;
94
- }
95
-
96
- request.resolve(http(request.config));
97
- }
98
-
99
- failedQueue = [];
100
- };
101
-
102
71
  http.interceptors.response.use(
103
72
  (response) => response,
104
73
  async (error) => {
105
- // BUGFIX: the original condition was `if (!enabled)` — inverted, so
106
- // turning `refreshTokens.enabled: true` actually DISABLED the refresh
107
- // path on 401. Use the correct sense.
108
- if (!store.getState().chatSettingStore?.config?.refreshTokens?.enabled) {
109
- // BUGFIX: previously this branch fell off the end of the async
110
- // function and returned `undefined`, which axios treats as a
111
- // resolved-with-undefined response. Callers expecting an error
112
- // got `undefined` instead, silently swallowing every failure
113
- // when the refresh path isn't configured. Re-reject so the
114
- // original error reaches the caller.
115
- return Promise.reject(error);
116
- }
117
-
118
74
  const originalRequest = error.config;
119
75
  const status = error.response?.status;
120
76
 
121
77
  // Refresh paths only fire on 401. Anything else passes through.
122
- if (!error.response || status !== 401) {
78
+ if (!error.response || status !== 401 || !originalRequest) {
123
79
  return Promise.reject(error);
124
80
  }
125
81
 
82
+ // Never retry twice, and never touch the auth endpoints themselves —
83
+ // both would recurse. (`_retry` was set but never checked before,
84
+ // so a request could bounce between 401 and replay indefinitely.)
126
85
  if (
127
- store.getState().chatSettingStore?.config?.refreshTokens?.refreshFunction
86
+ originalRequest._retry ||
87
+ originalRequest.url === '/v1/users/login/refresh' ||
88
+ originalRequest.url === '/v1/users/login'
128
89
  ) {
129
- // Consumer-supplied refresh function. Call it, dispatch the new
130
- // tokens, retry the original request.
131
- try {
132
- const fn = store.getState().chatSettingStore?.config?.refreshTokens?.refreshFunction;
133
- if (!fn) return Promise.reject(error);
134
- const result = await fn();
135
- const { refreshToken: newRefreshToken, accessToken } = result || { refreshToken: '', accessToken: '' };
136
- store.dispatch(
137
- refreshTokens({
138
- token: accessToken,
139
- refreshToken: newRefreshToken || '',
140
- })
141
- );
142
- // BUGFIX: previously this branch returned `undefined` after
143
- // dispatching, so the original request was never retried and
144
- // the caller resolved with undefined. Stamp the new token on
145
- // the original config and replay.
146
- if (originalRequest?.headers) {
147
- originalRequest.headers.Authorization = accessToken;
148
- }
149
- return http(originalRequest);
150
- } catch (refreshErr) {
151
- return Promise.reject(refreshErr);
152
- }
90
+ return Promise.reject(error);
153
91
  }
154
92
 
155
- // Built-in `/users/login/refresh` path. Skip if the failing request
156
- // IS the refresh / login itself — otherwise we'd loop forever.
157
- if (
158
- originalRequest.url === '/users/login/refresh' ||
159
- originalRequest.url === '/users/login'
160
- ) {
93
+ const refreshConfig =
94
+ store.getState().chatSettingStore?.config?.refreshTokens;
95
+
96
+ // BUGFIX (kept): the original condition here was inverted, so
97
+ // `refreshTokens.enabled: true` actually DISABLED the refresh path.
98
+ if (!refreshConfig?.enabled) {
161
99
  return Promise.reject(error);
162
100
  }
163
101
 
164
- originalRequest._retry = true;
165
-
166
- if (isRefreshing) {
167
- const retryOriginalRequest = addRequestToQueue(originalRequest);
168
- return retryOriginalRequest;
102
+ // Nothing to rotate with — reject rather than firing a doomed
103
+ // refresh that would look like a bad-token event to the backend.
104
+ const hasRefreshableSession = Boolean(
105
+ refreshConfig.refreshFunction ||
106
+ store.getState().chatSettingStore.user?.refreshToken
107
+ );
108
+ if (!hasRefreshableSession) {
109
+ return Promise.reject(error);
169
110
  }
170
111
 
171
- isRefreshing = true;
112
+ originalRequest._retry = true;
113
+
172
114
  try {
173
- const tokens = await refresh();
174
- // (was: console.log('tokens', tokens) — dumped the full response
175
- // incl. access/refresh/ws tokens on every refresh; removed.)
176
- isRefreshing = false;
177
- originalRequest.headers.Authorization = tokens.data.token;
178
- processQueue(tokens.data.token);
115
+ // One shared rotation for every 401 in flight — `refreshAuthTokens`
116
+ // dedupes internally, which replaces the old isRefreshing/
117
+ // failedQueue machinery entirely.
118
+ const tokens = await refreshAuthTokens();
119
+ originalRequest.headers = originalRequest.headers || {};
120
+ originalRequest.headers.Authorization = tokens.token;
179
121
  return http(originalRequest);
180
122
  } catch (refreshErr) {
181
- isRefreshing = false;
182
- // BUGFIX: previously this returned the error (resolving the
183
- // promise with the error as value) instead of rejecting, so
184
- // axios callers got the error as a "successful" response body.
123
+ // Only a genuinely dead session logs the user out. A network
124
+ // blip, a 5xx, or a REFRESH_IN_PROGRESS race must leave the
125
+ // session alone — logging out on those is exactly the mass-logout
126
+ // failure mode the new backend scheme would otherwise cause.
127
+ if (isRefreshFatalError(refreshErr)) {
128
+ store.dispatch(logout());
129
+ }
185
130
  return Promise.reject(refreshErr);
186
131
  }
187
132
  }