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 CHANGED
@@ -1,4 +1,4 @@
1
- import {NotificationsPush, Networking, Storage, Session, Utils, PayWallLogic, Rating, AdJust, TrackingTransparency, Voice, MixPanel, Adapty, HomeActions, Facebook, Firebase, Legal} from "./src/libraries";
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.2",
4
- "description": "Apps SDK - Compatible with Expo SDK 54 + React 19 - create User pre url",
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();
@@ -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
- const REVIEW_INTERVAL = 1000 * 60 * 60 * 24 * 7; // 7 días
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
- showRatingDialog = async (force = false) => {
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
- if (force || (await this.requestReviewIfNeeded())) {
9
- if (await StoreReview.hasAction()) {
10
- if (await StoreReview.isAvailableAsync()) {
11
- await StoreReview.requestReview();
12
- return {
13
- success: true,
14
- shown: true,
15
- message: 'Review dialog shown successfully'
16
- };
17
- } else {
18
- return {
19
- success: false,
20
- shown: false,
21
- blocked: true,
22
- message: 'Review dialog not available (possibly blocked by OS)'
23
- };
24
- }
25
- } else {
26
- return {
27
- success: false,
28
- shown: false,
29
- blocked: true,
30
- message: 'Store review not supported on this device'
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
- } else {
34
- console.log("Rating requestReviewIfNeeded not needed");
35
- return {
36
- success: false,
37
- shown: false,
38
- tooSoon: true,
39
- message: 'Review request too soon (less than 7 days since last request)'
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.log("Error en requestReview", error);
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
- requestReviewIfNeeded = async () => {
54
- const lastReviewRequestString = await Storage.getData('lastReviewRequest');
55
- const lastReviewRequest = lastReviewRequestString ? Number(lastReviewRequestString) : 0;
56
- const now = Date.now();
57
- if (now - lastReviewRequest > REVIEW_INTERVAL) {
58
- const available = await StoreReview.isAvailableAsync();
59
- if (available) {
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
  }
@@ -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
- showRatingDialog(force?: boolean): Promise<void>;
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 {