apps-sdk 2.1.2 → 2.1.4

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.
@@ -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';