apps-sdk 2.1.2 → 2.1.3
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.
- package/index.js +2 -1
- package/package.json +2 -2
- package/src/libraries/Authentication.js +799 -0
- package/src/libraries/Rating.js +312 -50
- package/src/libraries/index.js +1 -0
- package/types/index.d.ts +39 -1
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {NotificationsPush, Networking, Storage, Session, Utils, PayWallLogic, Rating, AdJust, TrackingTransparency, Voice, MixPanel, Adapty, HomeActions, Facebook,
|
|
1
|
+
import {NotificationsPush, Networking, Storage, Session, Utils, PayWallLogic, Rating, AdJust, TrackingTransparency, Voice, MixPanel, Adapty, HomeActions, Facebook, Legal, Authentication} from "./src/libraries";
|
|
2
2
|
// import PayWall from "./src/components/PayWall"; // DEPRECATED: Use SDK.adaptyOnboarding or SDK.adapty.showPaywall() instead
|
|
3
3
|
import AdaptyOnboarding from "./src/components/AdaptyOnboarding";
|
|
4
4
|
|
|
@@ -65,4 +65,5 @@ export default {
|
|
|
65
65
|
facebook: Facebook,
|
|
66
66
|
firebase: Firebase,
|
|
67
67
|
legal: Legal,
|
|
68
|
+
authentication: Authentication,
|
|
68
69
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apps-sdk",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "Apps SDK - Compatible with Expo SDK 54 + React 19 -
|
|
3
|
+
"version": "2.1.3",
|
|
4
|
+
"description": "Apps SDK - Compatible with Expo SDK 54 + React 19 - add rating flow, auth modules",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "ASD",
|
|
7
7
|
"license": "ISC",
|
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatConnect Authentication Service Layer
|
|
3
|
+
* -----------------------------------------
|
|
4
|
+
* Handles all authentication-related API calls including:
|
|
5
|
+
* - Login (auth-user)
|
|
6
|
+
* - Registration (create-user)
|
|
7
|
+
* - User linking
|
|
8
|
+
* - Subscription checking
|
|
9
|
+
*
|
|
10
|
+
* API Base URLs:
|
|
11
|
+
* - Auth: https://bc1742.gways.org/auth/
|
|
12
|
+
* - ChatConnect: https://bc1742.gways.org/chatconnect/
|
|
13
|
+
* - User: https://bc1742.gways.org/user/
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import Storage from './Storage';
|
|
17
|
+
import Session from './Session';
|
|
18
|
+
import Networking from './Networking';
|
|
19
|
+
import config from '../../config';
|
|
20
|
+
|
|
21
|
+
const BASE_URL = 'https://bc1742.gways.org';
|
|
22
|
+
const AUTH_BASE_URL = `${BASE_URL}/auth`;
|
|
23
|
+
const CHATCONNECT_BASE_URL = `${BASE_URL}/chatconnect`;
|
|
24
|
+
const USER_BASE_URL = `${BASE_URL}/user`;
|
|
25
|
+
|
|
26
|
+
class Authentication {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.AUTH_API_BASE_URL = AUTH_BASE_URL;
|
|
29
|
+
this.USER_API_BASE_URL = USER_BASE_URL;
|
|
30
|
+
this.CHATCONNECT_API_BASE_URL = CHATCONNECT_BASE_URL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get website ID from config
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
getWebsiteId() {
|
|
38
|
+
return config.CONFIG_EXTRA?.website_id || 'chatconnect-web';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Helper to get app user ID from session
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
getAppUserId() {
|
|
46
|
+
try {
|
|
47
|
+
const userId = Session?.getUserID?.();
|
|
48
|
+
return userId ?? '';
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generic API call helper with retry logic
|
|
56
|
+
* @param {string} url - API endpoint URL
|
|
57
|
+
* @param {Object} payload - Request payload
|
|
58
|
+
* @param {Object} opts - Options (retries, timeoutMs, silent)
|
|
59
|
+
* @returns {Promise<Object>} API result
|
|
60
|
+
*/
|
|
61
|
+
async callApi(url, payload, opts = {}) {
|
|
62
|
+
const { retries = 2, timeoutMs = 15000 } = opts;
|
|
63
|
+
|
|
64
|
+
let attempt = 0;
|
|
65
|
+
let lastError = null;
|
|
66
|
+
|
|
67
|
+
while (attempt <= retries) {
|
|
68
|
+
try {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
71
|
+
|
|
72
|
+
const response = await Networking.request(url, payload);
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
|
|
75
|
+
// Handle error responses
|
|
76
|
+
if (response?.success === 0 || response?.success === false) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: response.msg || response.message || response.error || 'Request failed',
|
|
80
|
+
code: response.code,
|
|
81
|
+
raw: response,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract data from response
|
|
86
|
+
const data = response?.data ?? response;
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
data: data,
|
|
90
|
+
message: response?.msg || response?.message,
|
|
91
|
+
raw: response,
|
|
92
|
+
};
|
|
93
|
+
} catch (err) {
|
|
94
|
+
lastError = err;
|
|
95
|
+
const isAbort = err?.name === 'AbortError';
|
|
96
|
+
const transient =
|
|
97
|
+
isAbort ||
|
|
98
|
+
(err?.message && /(timeout|network|fetch failed|abort)/i.test(err.message));
|
|
99
|
+
|
|
100
|
+
if (!transient || attempt === retries) {
|
|
101
|
+
config.DEBUG_MODE && console.debug('API call failed:', url, err?.message);
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
error: err?.message || 'Unknown error',
|
|
105
|
+
raw: err,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Backoff before retry
|
|
110
|
+
await new Promise((res) => setTimeout(res, 400 * (attempt + 1)));
|
|
111
|
+
}
|
|
112
|
+
attempt++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
config.DEBUG_MODE && console.debug('API call failed after retries:', url, lastError?.message);
|
|
116
|
+
return { success: false, error: lastError?.message || 'Unknown error', raw: lastError };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Login with credentials (auth-user)
|
|
121
|
+
* POST https://bc1742.gways.org/auth/auth-user
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} payload - Login payload
|
|
124
|
+
* @param {string} payload.login - Email or username (or credential)
|
|
125
|
+
* @param {string} payload.password - Password
|
|
126
|
+
* @param {string} [payload.websiteid] - Optional website ID
|
|
127
|
+
* @returns {Promise<Object>} Login result
|
|
128
|
+
*/
|
|
129
|
+
async login(payload) {
|
|
130
|
+
// Handle both old and new payload formats
|
|
131
|
+
const loginValue = 'login' in payload ? payload.login : payload.credential;
|
|
132
|
+
|
|
133
|
+
const requestPayload = {
|
|
134
|
+
login: loginValue,
|
|
135
|
+
password: payload.password,
|
|
136
|
+
websiteid: ('websiteid' in payload ? payload.websiteid : undefined) || this.getWebsiteId(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const url = `${AUTH_BASE_URL}/auth-user`;
|
|
140
|
+
const result = await this.callApi(url, requestPayload);
|
|
141
|
+
|
|
142
|
+
// Check if we got a response but with success: 0 inside data
|
|
143
|
+
if (result.success && result.data && result.data.success === 0) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'user_not_found',
|
|
147
|
+
data: result.data,
|
|
148
|
+
raw: result.raw,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Store user data if login successful
|
|
153
|
+
if (result.success && result.data && result.data.success === 1) {
|
|
154
|
+
try {
|
|
155
|
+
await Storage.storeData('AUTH_USER_ID', result.data.us_id);
|
|
156
|
+
await Storage.storeData('AUTH_USER_LOGIN', result.data.us_login);
|
|
157
|
+
// Store credentials for auto-linking later
|
|
158
|
+
await Storage.storeData('AUTH_CREDENTIALS', JSON.stringify({
|
|
159
|
+
login: loginValue,
|
|
160
|
+
password: payload.password,
|
|
161
|
+
}));
|
|
162
|
+
// Handle metadata (can be array if empty or object if populated)
|
|
163
|
+
if (result.data.metadata && !Array.isArray(result.data.metadata)) {
|
|
164
|
+
await Storage.storeData('AUTH_USER_METADATA', JSON.stringify(result.data.metadata));
|
|
165
|
+
} else {
|
|
166
|
+
await Storage.removeData('AUTH_USER_METADATA');
|
|
167
|
+
}
|
|
168
|
+
if (result.data.subscription_data) {
|
|
169
|
+
await Storage.storeData('AUTH_SUBSCRIPTION', JSON.stringify(result.data.subscription_data));
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.warn('Failed to store auth data:', e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Register a new user account (create-user)
|
|
181
|
+
* POST https://bc1742.gways.org/chatconnect/create-user
|
|
182
|
+
*
|
|
183
|
+
* @param {Object} payload - Registration payload
|
|
184
|
+
* @param {string} payload.login - Email or username (or email field)
|
|
185
|
+
* @param {string} payload.password - Password
|
|
186
|
+
* @param {string} [payload.websiteid] - Optional website ID
|
|
187
|
+
* @returns {Promise<Object>} Registration result
|
|
188
|
+
*/
|
|
189
|
+
async register(payload) {
|
|
190
|
+
// Handle both old and new payload formats
|
|
191
|
+
const loginValue = 'login' in payload ? payload.login : payload.email;
|
|
192
|
+
|
|
193
|
+
const requestPayload = {
|
|
194
|
+
login: loginValue,
|
|
195
|
+
password: payload.password,
|
|
196
|
+
websiteid: ('websiteid' in payload ? payload.websiteid : undefined) || this.getWebsiteId(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const url = `${CHATCONNECT_BASE_URL}/create-user`;
|
|
200
|
+
const result = await this.callApi(url, requestPayload);
|
|
201
|
+
|
|
202
|
+
// Store user data and credentials on successful registration
|
|
203
|
+
if (result.success && result.data?.user) {
|
|
204
|
+
const userData = result.data.user;
|
|
205
|
+
try {
|
|
206
|
+
await Storage.storeData('AUTH_USER_ID', String(userData.us_id));
|
|
207
|
+
await Storage.storeData('AUTH_USER_LOGIN', userData.login);
|
|
208
|
+
await Storage.storeData('AUTH_CREDENTIALS', JSON.stringify({
|
|
209
|
+
login: loginValue,
|
|
210
|
+
password: payload.password,
|
|
211
|
+
}));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.warn('Failed to store user data:', e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Complete registration flow: create-user → link-user
|
|
222
|
+
*
|
|
223
|
+
* @param {Object} payload - Registration payload
|
|
224
|
+
* @param {string} payload.email - Email
|
|
225
|
+
* @param {string} payload.password - Password
|
|
226
|
+
* @returns {Promise<Object>} Complete registration result
|
|
227
|
+
*/
|
|
228
|
+
async registerAndLinkAccount(payload) {
|
|
229
|
+
// Step 1: Create user account
|
|
230
|
+
const createResult = await this.register({
|
|
231
|
+
login: payload.email.trim(),
|
|
232
|
+
password: payload.password,
|
|
233
|
+
websiteid: this.getWebsiteId(),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!createResult.success) {
|
|
237
|
+
if (createResult.code === 112) {
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
error: 'user_already_exists',
|
|
241
|
+
code: 112,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: createResult.error || 'registration_failed',
|
|
247
|
+
code: createResult.code,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const userData = createResult.data?.user || (createResult.raw?.data?.user);
|
|
252
|
+
|
|
253
|
+
if (!userData?.us_id) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: 'registration_failed',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Store the web user credentials
|
|
261
|
+
try {
|
|
262
|
+
await Storage.storeData('WEB_USER_ID', String(userData.us_id));
|
|
263
|
+
await Storage.storeData('WEB_USER_LOGIN', userData.login);
|
|
264
|
+
await Storage.storeData('AUTH_USER_ID', String(userData.us_id));
|
|
265
|
+
await Storage.storeData('AUTH_USER_LOGIN', userData.login);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.warn('Failed to store web user data:', e);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Step 2: Link web user with mobile app user
|
|
271
|
+
const appUserId = this.getAppUserId();
|
|
272
|
+
|
|
273
|
+
if (!appUserId) {
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
user: userData,
|
|
277
|
+
linked: false,
|
|
278
|
+
error: 'app_user_id_not_available',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const linkResult = await this.linkUser({
|
|
283
|
+
user_id: String(userData.us_id),
|
|
284
|
+
app_user_id: appUserId,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (!linkResult.success) {
|
|
288
|
+
if (linkResult.code === 111) {
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
user: userData,
|
|
292
|
+
linked: false,
|
|
293
|
+
error: 'web_user_not_found',
|
|
294
|
+
code: 111,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
success: true,
|
|
299
|
+
user: userData,
|
|
300
|
+
linked: false,
|
|
301
|
+
error: linkResult.error || 'link_failed',
|
|
302
|
+
code: linkResult.code,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const isLinked = true;
|
|
307
|
+
|
|
308
|
+
// Store linked status
|
|
309
|
+
try {
|
|
310
|
+
await Storage.storeData('ACCOUNT_LINKED', 'true');
|
|
311
|
+
await Storage.storeData('LINKED_WEB_USER_ID', String(userData.us_id));
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.warn('Failed to store linked status:', e);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
user: userData,
|
|
319
|
+
linked: isLinked,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Link a user account
|
|
325
|
+
* POST https://bc1742.gways.org/user/link
|
|
326
|
+
*
|
|
327
|
+
* @param {Object} payload - Link payload
|
|
328
|
+
* @param {string} payload.user_id - Web user ID
|
|
329
|
+
* @param {string} payload.app_user_id - Mobile app user ID
|
|
330
|
+
* @returns {Promise<Object>} Link result
|
|
331
|
+
*/
|
|
332
|
+
async linkUser(payload) {
|
|
333
|
+
const url = `${USER_BASE_URL}/link`;
|
|
334
|
+
const result = await this.callApi(url, payload);
|
|
335
|
+
|
|
336
|
+
if (!result.success) {
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
error: result.error,
|
|
340
|
+
code: result.code,
|
|
341
|
+
raw: result.raw,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const rawData = result.data;
|
|
346
|
+
|
|
347
|
+
if (Array.isArray(rawData) && rawData.length > 0) {
|
|
348
|
+
const userData = rawData[0];
|
|
349
|
+
|
|
350
|
+
// Store user data from link response
|
|
351
|
+
try {
|
|
352
|
+
if (userData.us_id) {
|
|
353
|
+
await Storage.storeData('AUTH_USER_ID', String(userData.us_id));
|
|
354
|
+
}
|
|
355
|
+
if (userData.us_login) {
|
|
356
|
+
await Storage.storeData('AUTH_USER_LOGIN', userData.us_login);
|
|
357
|
+
}
|
|
358
|
+
if (userData.metadata && !Array.isArray(userData.metadata)) {
|
|
359
|
+
await Storage.storeData('AUTH_USER_METADATA', JSON.stringify(userData.metadata));
|
|
360
|
+
}
|
|
361
|
+
if (userData.subscription_data) {
|
|
362
|
+
await Storage.storeData('AUTH_SUBSCRIPTION', JSON.stringify(userData.subscription_data));
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.warn('Failed to store user data from link response:', e);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
data: {
|
|
371
|
+
success: userData.success,
|
|
372
|
+
linked: userData.success === 1,
|
|
373
|
+
items: rawData,
|
|
374
|
+
},
|
|
375
|
+
raw: result.raw,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Handle legacy response format
|
|
380
|
+
if (rawData && typeof rawData === 'object' && 'success' in rawData) {
|
|
381
|
+
return {
|
|
382
|
+
success: true,
|
|
383
|
+
data: {
|
|
384
|
+
success: rawData.success ? 1 : 0,
|
|
385
|
+
linked: !!rawData.success,
|
|
386
|
+
},
|
|
387
|
+
raw: result.raw,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
data: {
|
|
394
|
+
success: 1,
|
|
395
|
+
linked: true,
|
|
396
|
+
},
|
|
397
|
+
raw: result.raw,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Fetch authenticated user profile using stored credentials
|
|
403
|
+
* @returns {Promise<Object>} User profile result
|
|
404
|
+
*/
|
|
405
|
+
async fetchUserProfile() {
|
|
406
|
+
try {
|
|
407
|
+
const credentialsJson = await Storage.getData('AUTH_CREDENTIALS');
|
|
408
|
+
if (!credentialsJson) {
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: 'no_stored_credentials',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const credentials = JSON.parse(credentialsJson);
|
|
416
|
+
if (!credentials.login || !credentials.password) {
|
|
417
|
+
return {
|
|
418
|
+
success: false,
|
|
419
|
+
error: 'invalid_stored_credentials',
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return await this.login({
|
|
424
|
+
login: credentials.login,
|
|
425
|
+
password: credentials.password,
|
|
426
|
+
websiteid: this.getWebsiteId(),
|
|
427
|
+
});
|
|
428
|
+
} catch (e) {
|
|
429
|
+
config.DEBUG_MODE && console.debug('Failed to fetch user profile:', e?.message);
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
error: 'fetch_profile_failed',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Check if user account is linked
|
|
439
|
+
* @returns {Promise<boolean>}
|
|
440
|
+
*/
|
|
441
|
+
async isAccountLinked() {
|
|
442
|
+
try {
|
|
443
|
+
const linked = await Storage.getData('ACCOUNT_LINKED');
|
|
444
|
+
if (linked === 'true') {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const userId = await Storage.getData('AUTH_USER_ID');
|
|
449
|
+
if (userId) {
|
|
450
|
+
await Storage.storeData('ACCOUNT_LINKED', 'true');
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return false;
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get stored auth user login (email)
|
|
462
|
+
* @returns {Promise<string|null>}
|
|
463
|
+
*/
|
|
464
|
+
async getStoredUserLogin() {
|
|
465
|
+
try {
|
|
466
|
+
return await Storage.getData('AUTH_USER_LOGIN');
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Check if user is linked
|
|
474
|
+
* POST https://bc1742.gways.org/user/check-link
|
|
475
|
+
*
|
|
476
|
+
* @param {Object} payload - Check link payload
|
|
477
|
+
* @param {string} payload.user_id - User ID
|
|
478
|
+
* @param {string} payload.app_user_id - App user ID
|
|
479
|
+
* @returns {Promise<Object>} Check link result
|
|
480
|
+
*/
|
|
481
|
+
async checkLink(payload) {
|
|
482
|
+
const url = `${USER_BASE_URL}/check-link`;
|
|
483
|
+
return this.callApi(url, payload);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Check user subscription status
|
|
488
|
+
* POST https://bc1742.gways.org/user/check-subs
|
|
489
|
+
*
|
|
490
|
+
* @param {Object} payload - Check subscription payload
|
|
491
|
+
* @param {string} payload.user_id - User ID
|
|
492
|
+
* @param {string} payload.app_user_id - App user ID
|
|
493
|
+
* @returns {Promise<Object>} Subscription status result
|
|
494
|
+
*/
|
|
495
|
+
async checkSubscription(payload) {
|
|
496
|
+
const url = `${USER_BASE_URL}/check-subs`;
|
|
497
|
+
return this.callApi(url, payload);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Check if a credential exists (stub - for backward compatibility)
|
|
502
|
+
* @param {Object} payload - Check credential payload
|
|
503
|
+
* @returns {Promise<Object>} Credential check result
|
|
504
|
+
*/
|
|
505
|
+
async checkCredential(payload) {
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
data: {
|
|
509
|
+
exists: true,
|
|
510
|
+
credentialType: payload.credentialType,
|
|
511
|
+
requiresPhone: false,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Validate phone number (basic client-side validation)
|
|
518
|
+
* @param {Object} payload - Validate phone payload
|
|
519
|
+
* @returns {Promise<Object>} Phone validation result
|
|
520
|
+
*/
|
|
521
|
+
async validatePhone(payload) {
|
|
522
|
+
const phone = payload.phoneNumber.replace(/\D/g, '');
|
|
523
|
+
const isValid = phone.length >= 6 && phone.length <= 15;
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
success: true,
|
|
527
|
+
data: {
|
|
528
|
+
valid: isValid,
|
|
529
|
+
formattedPhone: isValid ? `${payload.countryCode}${phone}` : undefined,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Request password reset (stub - to be implemented on backend)
|
|
536
|
+
* @param {Object} payload - Password reset request payload
|
|
537
|
+
* @returns {Promise<Object>} Password reset request result
|
|
538
|
+
*/
|
|
539
|
+
async requestPasswordReset(payload) {
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
data: {
|
|
543
|
+
sent: true,
|
|
544
|
+
destination: payload.credential,
|
|
545
|
+
expiresIn: 300,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Verify reset code (stub - to be implemented on backend)
|
|
552
|
+
* @param {Object} payload - Verify code payload
|
|
553
|
+
* @returns {Promise<Object>} Verification result
|
|
554
|
+
*/
|
|
555
|
+
async verifyResetCode(payload) {
|
|
556
|
+
const isValid = payload.code.length === 6 && /^\d+$/.test(payload.code);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
success: isValid,
|
|
560
|
+
data: isValid ? {
|
|
561
|
+
valid: true,
|
|
562
|
+
resetToken: payload.code,
|
|
563
|
+
} : undefined,
|
|
564
|
+
error: isValid ? undefined : 'Invalid code',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Reset password (stub - to be implemented on backend)
|
|
570
|
+
* @param {Object} payload - Reset password payload
|
|
571
|
+
* @returns {Promise<Object>} Password reset result
|
|
572
|
+
*/
|
|
573
|
+
async resetPassword(payload) {
|
|
574
|
+
return {
|
|
575
|
+
success: true,
|
|
576
|
+
data: {
|
|
577
|
+
success: true,
|
|
578
|
+
message: 'Password reset successfully',
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Resend verification code (stub - to be implemented on backend)
|
|
585
|
+
* @param {Object} payload - Resend code payload
|
|
586
|
+
* @returns {Promise<Object>} Resend code result
|
|
587
|
+
*/
|
|
588
|
+
async resendCode(payload) {
|
|
589
|
+
return {
|
|
590
|
+
success: true,
|
|
591
|
+
data: {
|
|
592
|
+
sent: true,
|
|
593
|
+
canResendAt: new Date(Date.now() + 60000).toISOString(),
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Validate QR code data
|
|
600
|
+
* @param {Object} payload - QR code validation payload
|
|
601
|
+
* @param {string} payload.qrData - QR code data
|
|
602
|
+
* @returns {Promise<Object>} QR code validation result
|
|
603
|
+
*/
|
|
604
|
+
async validateQrCode(payload) {
|
|
605
|
+
const qrData = payload.qrData.trim();
|
|
606
|
+
|
|
607
|
+
let userId;
|
|
608
|
+
|
|
609
|
+
if (/^\d+$/.test(qrData)) {
|
|
610
|
+
userId = qrData;
|
|
611
|
+
} else {
|
|
612
|
+
try {
|
|
613
|
+
const parsed = JSON.parse(qrData);
|
|
614
|
+
userId = parsed.user_id || parsed.userId || parsed.us_id;
|
|
615
|
+
} catch {
|
|
616
|
+
const match = qrData.match(/(?:user_id|us_id)[=:](\d+)/i);
|
|
617
|
+
if (match) {
|
|
618
|
+
userId = match[1];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!userId || !/^\d+$/.test(userId)) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
data: { valid: false },
|
|
627
|
+
error: 'Invalid QR code format',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
success: true,
|
|
633
|
+
data: {
|
|
634
|
+
valid: true,
|
|
635
|
+
user_id: userId,
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Link account using QR code
|
|
642
|
+
* POST https://bc1742.gways.org/user/link
|
|
643
|
+
*
|
|
644
|
+
* @param {Object} payload - QR link payload
|
|
645
|
+
* @param {string} payload.user_id - User ID from QR code
|
|
646
|
+
* @returns {Promise<Object>} QR link result
|
|
647
|
+
*/
|
|
648
|
+
async linkWithQrCode(payload) {
|
|
649
|
+
const appUserId = this.getAppUserId();
|
|
650
|
+
|
|
651
|
+
if (!appUserId) {
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
error: 'App user ID not available',
|
|
655
|
+
data: { linked: false },
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const token = payload.user_id?.trim();
|
|
660
|
+
|
|
661
|
+
if (!token) {
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
error: 'Token not provided',
|
|
665
|
+
data: { linked: false },
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
config.DEBUG_MODE && console.debug('[linkWithQrCode] Linking with token:', token);
|
|
670
|
+
|
|
671
|
+
const result = await this.linkUser({
|
|
672
|
+
user_id: token,
|
|
673
|
+
app_user_id: appUserId,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
if (result.success) {
|
|
677
|
+
try {
|
|
678
|
+
await Storage.storeData('ACCOUNT_LINKED', 'true');
|
|
679
|
+
await Storage.storeData('LINKED_WEB_USER_ID', token);
|
|
680
|
+
|
|
681
|
+
const userData = result.data?.items?.[0];
|
|
682
|
+
if (userData?.us_id) {
|
|
683
|
+
await Storage.storeData('AUTH_USER_ID', String(userData.us_id));
|
|
684
|
+
} else {
|
|
685
|
+
await Storage.storeData('AUTH_USER_ID', token);
|
|
686
|
+
}
|
|
687
|
+
} catch (e) {
|
|
688
|
+
console.warn('Failed to store linked status:', e);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const userData = result.data?.items?.[0];
|
|
692
|
+
const subscriptionActive = userData?.subscription_data?.subscription_active === true;
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
success: true,
|
|
696
|
+
data: {
|
|
697
|
+
linked: true,
|
|
698
|
+
message: 'Account linked successfully',
|
|
699
|
+
subscriptionActive,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Handle "already linked" as success
|
|
705
|
+
if (result.code === 115) {
|
|
706
|
+
try {
|
|
707
|
+
await Storage.storeData('ACCOUNT_LINKED', 'true');
|
|
708
|
+
await Storage.storeData('AUTH_USER_ID', token);
|
|
709
|
+
await Storage.storeData('LINKED_WEB_USER_ID', token);
|
|
710
|
+
} catch (e) {
|
|
711
|
+
console.warn('Failed to store linked status:', e);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
success: true,
|
|
716
|
+
data: {
|
|
717
|
+
linked: true,
|
|
718
|
+
message: 'Account already linked',
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
success: false,
|
|
725
|
+
error: result.error || 'Failed to link account',
|
|
726
|
+
data: { linked: false },
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Logout current user
|
|
732
|
+
* @returns {Promise<Object>} Logout result
|
|
733
|
+
*/
|
|
734
|
+
async logout() {
|
|
735
|
+
try {
|
|
736
|
+
await Storage.removeData('AUTH_USER_ID');
|
|
737
|
+
await Storage.removeData('AUTH_USER_LOGIN');
|
|
738
|
+
await Storage.removeData('AUTH_USER_METADATA');
|
|
739
|
+
await Storage.removeData('AUTH_SUBSCRIPTION');
|
|
740
|
+
} catch (e) {
|
|
741
|
+
console.warn('Failed to clear auth data:', e);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return { success: true, data: { success: true } };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Check if user is currently authenticated
|
|
749
|
+
* @returns {Promise<boolean>}
|
|
750
|
+
*/
|
|
751
|
+
async isAuthenticated() {
|
|
752
|
+
try {
|
|
753
|
+
const userId = await Storage.getData('AUTH_USER_ID');
|
|
754
|
+
return !!userId;
|
|
755
|
+
} catch {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get stored user ID
|
|
762
|
+
* @returns {Promise<string|null>}
|
|
763
|
+
*/
|
|
764
|
+
async getStoredUserId() {
|
|
765
|
+
try {
|
|
766
|
+
return await Storage.getData('AUTH_USER_ID');
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Get stored user metadata
|
|
774
|
+
* @returns {Promise<Object|null>}
|
|
775
|
+
*/
|
|
776
|
+
async getStoredUserMetadata() {
|
|
777
|
+
try {
|
|
778
|
+
const metadata = await Storage.getData('AUTH_USER_METADATA');
|
|
779
|
+
return metadata ? JSON.parse(metadata) : null;
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Get stored subscription data
|
|
787
|
+
* @returns {Promise<Object|null>}
|
|
788
|
+
*/
|
|
789
|
+
async getStoredSubscription() {
|
|
790
|
+
try {
|
|
791
|
+
const subscription = await Storage.getData('AUTH_SUBSCRIPTION');
|
|
792
|
+
return subscription ? JSON.parse(subscription) : null;
|
|
793
|
+
} catch {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export default new Authentication();
|
package/src/libraries/Rating.js
CHANGED
|
@@ -1,65 +1,327 @@
|
|
|
1
1
|
import * as StoreReview from 'expo-store-review';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
2
3
|
import Storage from './Storage';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Storage keys
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const STORAGE_KEYS = {
|
|
9
|
+
FIRST_AI_INTERACTION_DONE: 'RATING_FIRST_AI_INTERACTION_DONE',
|
|
10
|
+
USER_SENTIMENT_POSITIVE: 'RATING_USER_SENTIMENT_POSITIVE',
|
|
11
|
+
RATING_ATTEMPTS_COUNT: 'RATING_ATTEMPTS_COUNT',
|
|
12
|
+
FEEDBACK_SUBMITTED: 'RATING_FEEDBACK_SUBMITTED',
|
|
13
|
+
LAST_RATING_PROMPT_DATE: 'RATING_LAST_RATING_PROMPT_DATE',
|
|
14
|
+
DOWNLOAD_SHARE_PROMPT_SHOWN: 'RATING_DOWNLOAD_SHARE_PROMPT_SHOWN',
|
|
15
|
+
LOW_CREDITS_PROMPT_SHOWN: 'RATING_LOW_CREDITS_PROMPT_SHOWN',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const MAX_RATING_ATTEMPTS = 2; // Max native rating prompts shown lifetime
|
|
22
|
+
const RATING_DELAY_MS = 5000; // Delay before showing prompt after AI response
|
|
23
|
+
const LOW_CREDITS_THRESHOLD = 3; // Credits level considered "low"
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Rating class
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
5
28
|
class Rating {
|
|
6
|
-
|
|
29
|
+
|
|
30
|
+
// In-memory state (also persisted in storage)
|
|
31
|
+
_hasCompletedFirstInteraction = false;
|
|
32
|
+
_isShowingPrompt = false;
|
|
33
|
+
|
|
34
|
+
// UI callbacks — injected via configure()
|
|
35
|
+
_onShowSentimentPrompt = null; // (source: string) => void
|
|
36
|
+
_onShowFeedbackForm = null; // () => void
|
|
37
|
+
_onTrackEvent = null; // (eventName: string, props: object) => void
|
|
38
|
+
_onSubmitFeedback = null; // (text: string) => Promise<void> (optional app-level hook)
|
|
39
|
+
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
// configure — call once on app startup (after initialize) with UI hooks
|
|
42
|
+
// -----------------------------------------------------------------------
|
|
43
|
+
configure = ({
|
|
44
|
+
onShowSentimentPrompt,
|
|
45
|
+
onShowFeedbackForm,
|
|
46
|
+
onTrackEvent,
|
|
47
|
+
onSubmitFeedback,
|
|
48
|
+
} = {}) => {
|
|
49
|
+
if (onShowSentimentPrompt) this._onShowSentimentPrompt = onShowSentimentPrompt;
|
|
50
|
+
if (onShowFeedbackForm) this._onShowFeedbackForm = onShowFeedbackForm;
|
|
51
|
+
if (onTrackEvent) this._onTrackEvent = onTrackEvent;
|
|
52
|
+
if (onSubmitFeedback) this._onSubmitFeedback = onSubmitFeedback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// -----------------------------------------------------------------------
|
|
56
|
+
// initialize — load persisted state from storage
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
initialize = async () => {
|
|
7
59
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
const hasInteracted = await Storage.getData(STORAGE_KEYS.FIRST_AI_INTERACTION_DONE);
|
|
61
|
+
this._hasCompletedFirstInteraction = hasInteracted === 'true';
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('[RatingFlow] Failed to initialize:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// -----------------------------------------------------------------------
|
|
68
|
+
// onAIInteractionSuccess
|
|
69
|
+
// Called after every successful AI response.
|
|
70
|
+
// @param isUserInitiated — true if the user explicitly triggered the action
|
|
71
|
+
// -----------------------------------------------------------------------
|
|
72
|
+
onAIInteractionSuccess = async (isUserInitiated = false) => {
|
|
73
|
+
if (this._isShowingPrompt) return;
|
|
74
|
+
|
|
75
|
+
if (!this._hasCompletedFirstInteraction) {
|
|
76
|
+
// Only show on user-initiated interactions (not auto-generated intro/welcome)
|
|
77
|
+
if (!isUserInitiated) return;
|
|
78
|
+
|
|
79
|
+
await Storage.storeData(STORAGE_KEYS.FIRST_AI_INTERACTION_DONE, 'true');
|
|
80
|
+
this._hasCompletedFirstInteraction = true;
|
|
81
|
+
|
|
82
|
+
setTimeout(async () => {
|
|
83
|
+
const canShow = await this._checkBasicEligibility();
|
|
84
|
+
if (canShow) {
|
|
85
|
+
await this._updateLastShownDate();
|
|
86
|
+
this._showSentimentPrompt('ai_interaction');
|
|
87
|
+
}
|
|
88
|
+
}, RATING_DELAY_MS);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const userSaidYes = await Storage.getData(STORAGE_KEYS.USER_SENTIMENT_POSITIVE);
|
|
93
|
+
|
|
94
|
+
if (userSaidYes === 'true') {
|
|
95
|
+
setTimeout(async () => {
|
|
96
|
+
const canShow = await this._checkBasicEligibility();
|
|
97
|
+
const shownToday = await this._wasShownToday();
|
|
98
|
+
if (canShow && !shownToday && !this._isShowingPrompt) {
|
|
99
|
+
await this._updateLastShownDate();
|
|
100
|
+
this._trackEvent('rate_prompt_shown', { source: 'ai_interaction', type: 'direct_rating' });
|
|
101
|
+
await this.triggerNativeRating();
|
|
102
|
+
}
|
|
103
|
+
}, RATING_DELAY_MS);
|
|
104
|
+
} else {
|
|
105
|
+
setTimeout(async () => {
|
|
106
|
+
const canShow = await this._checkBasicEligibility();
|
|
107
|
+
const shownToday = await this._wasShownToday();
|
|
108
|
+
if (canShow && !shownToday && !this._isShowingPrompt) {
|
|
109
|
+
await this._updateLastShownDate();
|
|
110
|
+
this._showSentimentPrompt('ai_interaction');
|
|
32
111
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
112
|
+
}, RATING_DELAY_MS);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -----------------------------------------------------------------------
|
|
117
|
+
// onCreditsRunningLow
|
|
118
|
+
// Called after a successful action when credits are low.
|
|
119
|
+
// @param currentCredits — current credit count
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
onCreditsRunningLow = async (currentCredits) => {
|
|
122
|
+
if (this._isShowingPrompt) return;
|
|
123
|
+
if (currentCredits > LOW_CREDITS_THRESHOLD) return;
|
|
124
|
+
|
|
125
|
+
const canShow = await this._checkBasicEligibility();
|
|
126
|
+
if (!canShow) return;
|
|
127
|
+
|
|
128
|
+
const shownToday = await this._wasShownToday();
|
|
129
|
+
if (shownToday) return;
|
|
130
|
+
|
|
131
|
+
const userSaidYes = await Storage.getData(STORAGE_KEYS.USER_SENTIMENT_POSITIVE);
|
|
132
|
+
if (userSaidYes === 'true') {
|
|
133
|
+
await this._updateLastShownDate();
|
|
134
|
+
this._trackEvent('rate_prompt_shown', { source: 'credits_low', type: 'direct_rating' });
|
|
135
|
+
await this.triggerNativeRating();
|
|
136
|
+
} else {
|
|
137
|
+
const alreadyShown = await Storage.getData(STORAGE_KEYS.LOW_CREDITS_PROMPT_SHOWN);
|
|
138
|
+
if (alreadyShown === 'true') return;
|
|
139
|
+
await Storage.storeData(STORAGE_KEYS.LOW_CREDITS_PROMPT_SHOWN, 'true');
|
|
140
|
+
await this._updateLastShownDate();
|
|
141
|
+
this._showSentimentPrompt('credits_low');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
// onResultDownloadedOrShared
|
|
147
|
+
// Called when the user downloads or shares an AI result.
|
|
148
|
+
// @param actionType — 'download' | 'share'
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
onResultDownloadedOrShared = async (actionType) => {
|
|
151
|
+
if (this._isShowingPrompt) return;
|
|
152
|
+
|
|
153
|
+
const canShow = await this._checkBasicEligibility();
|
|
154
|
+
if (!canShow) return;
|
|
155
|
+
|
|
156
|
+
const shownToday = await this._wasShownToday();
|
|
157
|
+
if (shownToday) return;
|
|
158
|
+
|
|
159
|
+
const userSaidYes = await Storage.getData(STORAGE_KEYS.USER_SENTIMENT_POSITIVE);
|
|
160
|
+
if (userSaidYes === 'true') {
|
|
161
|
+
await this._updateLastShownDate();
|
|
162
|
+
this._trackEvent('rate_prompt_shown', { source: `result_${actionType}`, type: 'direct_rating' });
|
|
163
|
+
await this.triggerNativeRating();
|
|
164
|
+
} else {
|
|
165
|
+
const alreadyShown = await Storage.getData(STORAGE_KEYS.DOWNLOAD_SHARE_PROMPT_SHOWN);
|
|
166
|
+
if (alreadyShown === 'true') return;
|
|
167
|
+
await Storage.storeData(STORAGE_KEYS.DOWNLOAD_SHARE_PROMPT_SHOWN, 'true');
|
|
168
|
+
await this._updateLastShownDate();
|
|
169
|
+
this._showSentimentPrompt(`result_${actionType}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// triggerNativeRating — calls StoreReview.requestReview()
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
triggerNativeRating = async () => {
|
|
177
|
+
this._trackEvent('native_rating_prompt_called', { platform: Platform.OS });
|
|
178
|
+
try {
|
|
179
|
+
const attemptsStr = await Storage.getData(STORAGE_KEYS.RATING_ATTEMPTS_COUNT);
|
|
180
|
+
const newAttempts = (attemptsStr ? parseInt(attemptsStr, 10) : 0) + 1;
|
|
181
|
+
await Storage.storeData(STORAGE_KEYS.RATING_ATTEMPTS_COUNT, String(newAttempts));
|
|
182
|
+
await StoreReview.requestReview();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('[RatingFlow] Rating request failed:', error);
|
|
185
|
+
this._trackEvent('native_rating_api_error', { platform: Platform.OS, error: String(error) });
|
|
186
|
+
} finally {
|
|
187
|
+
this._isShowingPrompt = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -----------------------------------------------------------------------
|
|
192
|
+
// onUserRespondedYes — call from the sentiment prompt YES handler
|
|
193
|
+
// -----------------------------------------------------------------------
|
|
194
|
+
onUserRespondedYes = async () => {
|
|
195
|
+
this._trackEvent('rate_prompt_response', { response: 'yes' });
|
|
196
|
+
await Storage.storeData(STORAGE_KEYS.USER_SENTIMENT_POSITIVE, 'true');
|
|
197
|
+
await this.triggerNativeRating();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -----------------------------------------------------------------------
|
|
201
|
+
// onUserRespondedNo — call from the sentiment prompt NO handler
|
|
202
|
+
// -----------------------------------------------------------------------
|
|
203
|
+
onUserRespondedNo = () => {
|
|
204
|
+
this._trackEvent('rate_prompt_response', { response: 'no' });
|
|
205
|
+
this._showFeedbackForm();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// -----------------------------------------------------------------------
|
|
209
|
+
// submitFeedback — call when the user submits written feedback
|
|
210
|
+
// @param feedbackText — the feedback string
|
|
211
|
+
// -----------------------------------------------------------------------
|
|
212
|
+
submitFeedback = async (feedbackText) => {
|
|
213
|
+
try {
|
|
214
|
+
this._trackEvent('feedback_submitted', { feedback_text: feedbackText });
|
|
215
|
+
await Storage.storeData(STORAGE_KEYS.FEEDBACK_SUBMITTED, 'true');
|
|
216
|
+
if (this._onSubmitFeedback) await this._onSubmitFeedback(feedbackText);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error('[RatingFlow] Failed to submit feedback:', error);
|
|
219
|
+
throw error;
|
|
220
|
+
} finally {
|
|
221
|
+
this._isShowingPrompt = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// onFeedbackCanceled — call when the user dismisses the feedback form
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
onFeedbackCanceled = () => {
|
|
229
|
+
this._trackEvent('feedback_canceled');
|
|
230
|
+
this._isShowingPrompt = false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// -----------------------------------------------------------------------
|
|
234
|
+
// reset — clears all persisted rating state (for debugging/testing)
|
|
235
|
+
// -----------------------------------------------------------------------
|
|
236
|
+
reset = async () => {
|
|
237
|
+
await Promise.all(Object.values(STORAGE_KEYS).map(key => Storage.removeData(key)));
|
|
238
|
+
this._hasCompletedFirstInteraction = false;
|
|
239
|
+
this._isShowingPrompt = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// Legacy: showRatingDialog — kept for backwards compatibility
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
showRatingDialog = async (force = false) => {
|
|
246
|
+
try {
|
|
247
|
+
if (force) {
|
|
248
|
+
await this.triggerNativeRating();
|
|
249
|
+
return { success: true, shown: true };
|
|
41
250
|
}
|
|
251
|
+
const canShow = await this._checkBasicEligibility();
|
|
252
|
+
if (!canShow) return { success: false, shown: false, tooSoon: true };
|
|
253
|
+
await this.triggerNativeRating();
|
|
254
|
+
return { success: true, shown: true };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('[RatingFlow] showRatingDialog error:', error);
|
|
257
|
+
return { success: false, shown: false, error: true, message: error.message };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// =======================================================================
|
|
262
|
+
// Private helpers
|
|
263
|
+
// =======================================================================
|
|
264
|
+
|
|
265
|
+
_checkBasicEligibility = async () => {
|
|
266
|
+
try {
|
|
267
|
+
const isAvailable = await StoreReview.isAvailableAsync();
|
|
268
|
+
if (!isAvailable) return false;
|
|
269
|
+
|
|
270
|
+
const feedbackSubmitted = await Storage.getData(STORAGE_KEYS.FEEDBACK_SUBMITTED);
|
|
271
|
+
if (feedbackSubmitted === 'true') return false;
|
|
272
|
+
|
|
273
|
+
const attemptsStr = await Storage.getData(STORAGE_KEYS.RATING_ATTEMPTS_COUNT);
|
|
274
|
+
const attempts = attemptsStr ? parseInt(attemptsStr, 10) : 0;
|
|
275
|
+
if (attempts >= MAX_RATING_ATTEMPTS) return false;
|
|
276
|
+
|
|
277
|
+
return true;
|
|
42
278
|
} catch (error) {
|
|
43
|
-
console.
|
|
44
|
-
return
|
|
45
|
-
success: false,
|
|
46
|
-
shown: false,
|
|
47
|
-
error: true,
|
|
48
|
-
message: error.message
|
|
49
|
-
};
|
|
279
|
+
console.error('[RatingFlow] Error checking basic eligibility:', error);
|
|
280
|
+
return false;
|
|
50
281
|
}
|
|
51
282
|
}
|
|
52
283
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await StoreReview.requestReview();
|
|
61
|
-
await Storage.storeData('lastReviewRequest', now.toString());
|
|
284
|
+
_wasShownToday = async () => {
|
|
285
|
+
try {
|
|
286
|
+
const lastShownDate = await Storage.getData(STORAGE_KEYS.LAST_RATING_PROMPT_DATE);
|
|
287
|
+
if (lastShownDate) {
|
|
288
|
+
const lastDate = new Date(lastShownDate);
|
|
289
|
+
const now = new Date();
|
|
290
|
+
return lastDate.toDateString() === now.toDateString();
|
|
62
291
|
}
|
|
292
|
+
return false;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('[RatingFlow] Error checking last shown date:', error);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_updateLastShownDate = async () => {
|
|
300
|
+
await Storage.storeData(STORAGE_KEYS.LAST_RATING_PROMPT_DATE, new Date().toISOString());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_showSentimentPrompt = (source) => {
|
|
304
|
+
this._isShowingPrompt = true;
|
|
305
|
+
this._trackEvent('rate_prompt_shown', { source: source || 'ai_interaction' });
|
|
306
|
+
if (this._onShowSentimentPrompt) {
|
|
307
|
+
this._onShowSentimentPrompt(source);
|
|
308
|
+
} else {
|
|
309
|
+
console.warn('[RatingFlow] No sentiment prompt handler configured. Call AppsSDK.rating.configure().');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_showFeedbackForm = () => {
|
|
314
|
+
this._trackEvent('feedback_screen_shown');
|
|
315
|
+
if (this._onShowFeedbackForm) {
|
|
316
|
+
this._onShowFeedbackForm();
|
|
317
|
+
} else {
|
|
318
|
+
console.warn('[RatingFlow] No feedback form handler configured. Call AppsSDK.rating.configure().');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_trackEvent = (eventName, properties = {}) => {
|
|
323
|
+
if (this._onTrackEvent) {
|
|
324
|
+
this._onTrackEvent(eventName, { ...properties, timestamp: new Date().toISOString() });
|
|
63
325
|
}
|
|
64
326
|
}
|
|
65
327
|
}
|
package/src/libraries/index.js
CHANGED
|
@@ -14,3 +14,4 @@ export { default as HomeActions } from './QuickActions';
|
|
|
14
14
|
export { default as Facebook } from './Facebook';
|
|
15
15
|
export { default as Firebase } from './Firebase';
|
|
16
16
|
export { default as Legal } from './Legal';
|
|
17
|
+
export { default as Authentication } from './Authentication';
|
package/types/index.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ declare module 'apps-sdk' {
|
|
|
37
37
|
init(): Promise<void>;
|
|
38
38
|
setDebugMode(debugMode: boolean): void;
|
|
39
39
|
setConfigEndpoint(endpoint: string): void;
|
|
40
|
+
setUserCreateEndpoint(endpoint: string): void;
|
|
41
|
+
setBaseUrl(baseUrl: string): void;
|
|
42
|
+
getCredits(webappUrl: string, websiteId: string): Promise<number>;
|
|
43
|
+
getWebappBasePayload(websiteId: string, additionalData?: any): any;
|
|
40
44
|
initSession(): Promise<void>;
|
|
41
45
|
storeSessionStructure(): Promise<void>;
|
|
42
46
|
checkFirstOpen(): Promise<void>;
|
|
@@ -119,8 +123,42 @@ declare module 'apps-sdk' {
|
|
|
119
123
|
isSpecialEvent(eventKeyword: string): boolean;
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
export interface RatingConfigOptions {
|
|
127
|
+
/** Called when the sentiment prompt ("Are you enjoying the app?") should be shown. */
|
|
128
|
+
onShowSentimentPrompt: (source: string) => void;
|
|
129
|
+
/** Called when the feedback form should be shown (user said NO). */
|
|
130
|
+
onShowFeedbackForm: () => void;
|
|
131
|
+
/** Called to track analytics events. */
|
|
132
|
+
onTrackEvent: (eventName: string, properties: Record<string, any>) => void;
|
|
133
|
+
/** Optional: called after feedback text is submitted. */
|
|
134
|
+
onSubmitFeedback?: (feedbackText: string) => Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
export class Rating {
|
|
123
|
-
|
|
138
|
+
/** Register UI callbacks. Call once on app startup after initialize(). */
|
|
139
|
+
configure(options: RatingConfigOptions): void;
|
|
140
|
+
/** Load persisted state from storage. Call on app startup. */
|
|
141
|
+
initialize(): Promise<void>;
|
|
142
|
+
/** Trigger rating flow after a successful AI interaction. */
|
|
143
|
+
onAIInteractionSuccess(isUserInitiated?: boolean): Promise<void>;
|
|
144
|
+
/** Trigger rating flow when credits are running low. */
|
|
145
|
+
onCreditsRunningLow(currentCredits: number): Promise<void>;
|
|
146
|
+
/** Trigger rating flow after user downloads or shares a result. */
|
|
147
|
+
onResultDownloadedOrShared(actionType: 'download' | 'share'): Promise<void>;
|
|
148
|
+
/** Directly call StoreReview.requestReview(). */
|
|
149
|
+
triggerNativeRating(): Promise<void>;
|
|
150
|
+
/** Call this when the user taps YES on the sentiment prompt. */
|
|
151
|
+
onUserRespondedYes(): Promise<void>;
|
|
152
|
+
/** Call this when the user taps NO on the sentiment prompt. */
|
|
153
|
+
onUserRespondedNo(): void;
|
|
154
|
+
/** Submit written feedback text. */
|
|
155
|
+
submitFeedback(feedbackText: string): Promise<void>;
|
|
156
|
+
/** Call when the user dismisses the feedback form without submitting. */
|
|
157
|
+
onFeedbackCanceled(): void;
|
|
158
|
+
/** Reset all persisted rating state (for debugging/testing). */
|
|
159
|
+
reset(): Promise<void>;
|
|
160
|
+
/** @deprecated Legacy method — use the new flow methods instead. */
|
|
161
|
+
showRatingDialog(force?: boolean): Promise<{ success: boolean; shown: boolean; [key: string]: any }>;
|
|
124
162
|
}
|
|
125
163
|
|
|
126
164
|
export class NotificationsPush {
|