feat: add expo mobile application source code
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { storage } from './storage';
|
||||
import * as StoreReview from 'expo-store-review';
|
||||
|
||||
const ACTIONS_COUNT_KEY = 'user_actions_count';
|
||||
const LAST_PROMPT_DATE_KEY = 'last_review_prompt_date';
|
||||
|
||||
export const ActionTracker = {
|
||||
/**
|
||||
* Increment action count and check if we should show review prompt
|
||||
*/
|
||||
async trackAction(minActions: number = 10, enabled: boolean = true) {
|
||||
if (!enabled) return;
|
||||
|
||||
try {
|
||||
const currentCountStr = await storage.get(ACTIONS_COUNT_KEY);
|
||||
const currentCount = parseInt(currentCountStr || '0', 10) + 1;
|
||||
|
||||
await storage.save(ACTIONS_COUNT_KEY, currentCount.toString());
|
||||
|
||||
if (currentCount >= minActions) {
|
||||
const lastPrompt = await storage.get(LAST_PROMPT_DATE_KEY);
|
||||
const now = new Date().getTime();
|
||||
|
||||
// Only prompt once every 30 days
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
if (!lastPrompt || (now - parseInt(lastPrompt, 10)) > thirtyDays) {
|
||||
if (await StoreReview.isAvailableAsync()) {
|
||||
await StoreReview.requestReview();
|
||||
await storage.save(LAST_PROMPT_DATE_KEY, now.toString());
|
||||
// Reset counter after successful prompt
|
||||
await storage.save(ACTIONS_COUNT_KEY, '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ActionTracker] Error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiService } from '../services/api';
|
||||
|
||||
class Logger {
|
||||
private logs: string[] = [];
|
||||
private maxLogs = 50;
|
||||
|
||||
log(message: string, type: 'info' | 'error' | 'sync' | 'success' = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const entry = `[${timestamp}] [${type.toUpperCase()}] ${message}`;
|
||||
this.logs.unshift(entry);
|
||||
|
||||
if (this.logs.length > this.maxLogs) {
|
||||
this.logs.pop();
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
ApiService.reportError(message, 'error').catch(() => {});
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.log(entry);
|
||||
}
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return this.logs;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logs = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const DebugLogger = new Logger();
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Platform } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
/**
|
||||
* 🚀 Unified Storage System for biiproject
|
||||
* Handles both Native (AsyncStorage/SecureStore) and Web (LocalStorage) seamlessly.
|
||||
*/
|
||||
const SENSITIVE_KEYS = ['user_token', 'saved_pass', 'biometric_credentials'];
|
||||
|
||||
// In-memory fallback for environments where storage is unavailable (e.g. some web modes or broken native modules)
|
||||
const memoryStorage: Record<string, string> = {};
|
||||
|
||||
export const storage = {
|
||||
save: async (key: string, value: string) => {
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(key, value);
|
||||
} else {
|
||||
memoryStorage[key] = value;
|
||||
}
|
||||
} else {
|
||||
if (SENSITIVE_KEYS.includes(key)) {
|
||||
// SecureStore might fail if biometrics are not configured or on some Android versions
|
||||
try {
|
||||
await SecureStore.setItemAsync(key, value);
|
||||
} catch (e) {
|
||||
console.warn(`[Storage] SecureStore failed for ${key}, falling back to AsyncStorage`, e);
|
||||
await AsyncStorage.setItem(key, value);
|
||||
}
|
||||
} else {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage Save Error [${key}]:`, error);
|
||||
memoryStorage[key] = value; // Last resort
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get: async (key: string) => {
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
return memoryStorage[key] || null;
|
||||
} else {
|
||||
if (SENSITIVE_KEYS.includes(key)) {
|
||||
try {
|
||||
const val = await SecureStore.getItemAsync(key);
|
||||
if (val) return val;
|
||||
} catch (e) {
|
||||
console.warn(`[Storage] SecureStore read failed for ${key}`, e);
|
||||
}
|
||||
// Check fallback
|
||||
return await AsyncStorage.getItem(key);
|
||||
} else {
|
||||
return await AsyncStorage.getItem(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Storage Get Error [${key}]:`, error);
|
||||
return memoryStorage[key] || null;
|
||||
}
|
||||
},
|
||||
|
||||
remove: async (key: string) => {
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
delete memoryStorage[key];
|
||||
} else {
|
||||
if (SENSITIVE_KEYS.includes(key)) {
|
||||
try {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
} catch (e) {
|
||||
console.warn(`[Storage] SecureStore delete failed for ${key}`, e);
|
||||
}
|
||||
await AsyncStorage.removeItem(key);
|
||||
} else {
|
||||
await AsyncStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Storage Remove Error [${key}]:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user