项目文件夹

文件
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

136 行
4.8 KiB
TypeScript

import * as StoreReview from 'expo-store-review';
import { MMKV } from 'react-native-mmkv';
import { Modal } from '@/modal';
import { t } from '@/text';
import { AsyncLock } from './lock';
import {
trackReviewPromptShown,
trackReviewPromptResponse,
trackReviewStoreShown,
trackReviewRetryScheduled
} from '@/track';
import { sync } from '@/sync/sync';
import { storage as syncStorage } from '@/sync/storage';
import { Platform } from 'react-native';
const localStorage = new MMKV();
const LOCAL_KEYS = {
STORE_REVIEW_LAST_SHOWN: 'review_store_last_shown',
DECLINED_AT: 'review_declined_at',
} as const;
const RETRY_DAYS = 30;
const STORE_REVIEW_RETRY_DAYS = 7; // Allow store review again after a week
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const lock = new AsyncLock();
export function requestReview() {
if (Platform.OS === 'web') {
return;
}
lock.inLock(async () => {
try {
// Check if store review is available
const isAvailable = await StoreReview.isAvailableAsync();
if (!isAvailable) {
console.log('Store review is not available on this platform');
return;
}
const settings = syncStorage.getState().settings;
// Check if we should show store review directly (user already answered and liked the app)
if (settings.reviewPromptAnswered && settings.reviewPromptLikedApp) {
// Check if enough time has passed since last store review
const lastShownStr = localStorage.getString(LOCAL_KEYS.STORE_REVIEW_LAST_SHOWN);
if (lastShownStr) {
const lastShown = new Date(lastShownStr);
const now = new Date();
const daysSinceShown = (now.getTime() - lastShown.getTime()) / DAY_IN_MS;
if (daysSinceShown < STORE_REVIEW_RETRY_DAYS) {
// Not enough time has passed since last store review
return;
}
}
await StoreReview.requestReview();
trackReviewStoreShown();
localStorage.set(LOCAL_KEYS.STORE_REVIEW_LAST_SHOWN, new Date().toISOString());
return;
}
// Check if user already answered the prompt (synced across devices)
if (settings.reviewPromptAnswered) {
// User already answered on another device, don't ask again
return;
}
// Check if user previously declined and if it's been 30 days (local check)
const declinedAtStr = localStorage.getString(LOCAL_KEYS.DECLINED_AT);
if (declinedAtStr) {
const declinedAt = new Date(declinedAtStr);
const now = new Date();
const daysSinceDeclined = (now.getTime() - declinedAt.getTime()) / DAY_IN_MS;
if (daysSinceDeclined < RETRY_DAYS) {
// Not enough time has passed since last decline
return;
}
}
// Pre-ask if they like the app (only shown once, ever)
trackReviewPromptShown();
const likesApp = await Modal.confirm(
t('review.enjoyingApp'),
t('review.feedbackPrompt'),
{
confirmText: t('review.yesILoveIt'),
cancelText: t('review.notReally'),
}
);
trackReviewPromptResponse(likesApp);
// Store the answer in sync settings (synced across devices)
sync.applySettings({
reviewPromptAnswered: true,
reviewPromptLikedApp: likesApp,
});
if (!likesApp) {
// User doesn't like the app, store the timestamp locally
localStorage.set(LOCAL_KEYS.DECLINED_AT, new Date().toISOString());
// Track that we'll retry in 30 days
trackReviewRetryScheduled(RETRY_DAYS);
return;
}
// Request the actual store review directly
await StoreReview.requestReview();
trackReviewStoreShown();
// Mark when we last showed the store review
localStorage.set(LOCAL_KEYS.STORE_REVIEW_LAST_SHOWN, new Date().toISOString());
} catch (error) {
console.error('Error requesting review:', error);
}
});
}
// Optional: Reset review state for testing
export function resetReviewState(): void {
localStorage.delete(LOCAL_KEYS.DECLINED_AT);
localStorage.delete(LOCAL_KEYS.STORE_REVIEW_LAST_SHOWN);
// Reset sync settings
sync.applySettings({
reviewPromptAnswered: false,
reviewPromptLikedApp: null,
});
}