@croacroa/react-native-template 2.0.1 → 3.2.0
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/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -0
- package/README.md +446 -399
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -0
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -23
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -27
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +64 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -0
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -175
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Payment and subscription type definitions
|
|
3
|
+
* Defines the adapter interface, product/purchase models, and subscription types
|
|
4
|
+
* for the payment system.
|
|
5
|
+
* @module services/payments/types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Product Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/** The type of in-app product */
|
|
13
|
+
export type ProductType = "consumable" | "non_consumable" | "subscription";
|
|
14
|
+
|
|
15
|
+
/** Subscription billing period */
|
|
16
|
+
export type SubscriptionPeriod = "weekly" | "monthly" | "quarterly" | "yearly";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a purchasable product from the app store.
|
|
20
|
+
*/
|
|
21
|
+
export interface Product {
|
|
22
|
+
/** Unique product identifier (matches store product ID) */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Display title of the product */
|
|
25
|
+
title: string;
|
|
26
|
+
/** Short description of the product */
|
|
27
|
+
description: string;
|
|
28
|
+
/** Numeric price in the smallest currency unit */
|
|
29
|
+
price: number;
|
|
30
|
+
/** Localized price string for display (e.g. "$9.99") */
|
|
31
|
+
priceString: string;
|
|
32
|
+
/** ISO 4217 currency code (e.g. "USD") */
|
|
33
|
+
currency: string;
|
|
34
|
+
/** The type of product */
|
|
35
|
+
type: ProductType;
|
|
36
|
+
/** Billing period for subscription products */
|
|
37
|
+
subscriptionPeriod?: SubscriptionPeriod;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Purchase Types
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents a completed purchase transaction.
|
|
46
|
+
*/
|
|
47
|
+
export interface Purchase {
|
|
48
|
+
/** Unique purchase/transaction identifier */
|
|
49
|
+
id: string;
|
|
50
|
+
/** The product that was purchased */
|
|
51
|
+
productId: string;
|
|
52
|
+
/** ISO 8601 date string of the transaction */
|
|
53
|
+
transactionDate: string;
|
|
54
|
+
/** Optional receipt data for server-side validation */
|
|
55
|
+
transactionReceipt?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Subscription Types
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/** Current status of a user's subscription */
|
|
63
|
+
export type SubscriptionStatus =
|
|
64
|
+
| "active"
|
|
65
|
+
| "expired"
|
|
66
|
+
| "cancelled"
|
|
67
|
+
| "grace_period"
|
|
68
|
+
| "none";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detailed information about a user's subscription.
|
|
72
|
+
*/
|
|
73
|
+
export interface SubscriptionInfo {
|
|
74
|
+
/** Current subscription status */
|
|
75
|
+
status: SubscriptionStatus;
|
|
76
|
+
/** The subscribed product ID, or null if no subscription */
|
|
77
|
+
productId: string | null;
|
|
78
|
+
/** ISO 8601 expiration date, or null if no subscription */
|
|
79
|
+
expiresAt: string | null;
|
|
80
|
+
/** Whether the subscription will auto-renew */
|
|
81
|
+
willRenew: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Adapter Interface
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Interface that all payment adapters must implement.
|
|
90
|
+
* Swap adapters to switch between providers (RevenueCat, Qonversion, etc.)
|
|
91
|
+
* without changing application code.
|
|
92
|
+
*/
|
|
93
|
+
export interface PaymentAdapter {
|
|
94
|
+
/**
|
|
95
|
+
* Initialize the payment provider.
|
|
96
|
+
* Called once when the app starts.
|
|
97
|
+
*/
|
|
98
|
+
initialize(): Promise<void>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetch available products by their store IDs.
|
|
102
|
+
*
|
|
103
|
+
* @param ids - Array of product identifiers to fetch
|
|
104
|
+
* @returns Array of available products
|
|
105
|
+
*/
|
|
106
|
+
getProducts(ids: string[]): Promise<Product[]>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initiate a purchase for the given product.
|
|
110
|
+
*
|
|
111
|
+
* @param productId - The product to purchase
|
|
112
|
+
* @returns The completed purchase record
|
|
113
|
+
* @throws Error if the purchase fails or is cancelled
|
|
114
|
+
*/
|
|
115
|
+
purchase(productId: string): Promise<Purchase>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Restore previously completed purchases.
|
|
119
|
+
* Useful when a user reinstalls the app or switches devices.
|
|
120
|
+
*
|
|
121
|
+
* @returns Array of restored purchases
|
|
122
|
+
*/
|
|
123
|
+
restorePurchases(): Promise<Purchase[]>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the current subscription status for the user.
|
|
127
|
+
*
|
|
128
|
+
* @returns Current subscription information
|
|
129
|
+
*/
|
|
130
|
+
getSubscriptionStatus(): Promise<SubscriptionInfo>;
|
|
131
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Centralized permission management service
|
|
3
|
+
* Provides a unified API for checking, requesting, and tracking permissions
|
|
4
|
+
* across different Expo modules.
|
|
5
|
+
* @module services/permissions/permission-manager
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Platform, Linking } from "react-native";
|
|
9
|
+
import { Camera } from "expo-camera";
|
|
10
|
+
import * as Location from "expo-location";
|
|
11
|
+
import * as Contacts from "expo-contacts";
|
|
12
|
+
import * as MediaLibrary from "expo-media-library";
|
|
13
|
+
import * as Notifications from "expo-notifications";
|
|
14
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
15
|
+
|
|
16
|
+
import { STORAGE_KEYS } from "@/constants/config";
|
|
17
|
+
import type {
|
|
18
|
+
PermissionType,
|
|
19
|
+
PermissionResult,
|
|
20
|
+
PermissionStatus,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
/** AsyncStorage key prefix for tracking asked permissions */
|
|
24
|
+
const PERMISSION_ASKED_PREFIX = STORAGE_KEYS.PERMISSION_PREFIX;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize native Expo permission status to our unified PermissionStatus.
|
|
28
|
+
* Handles the various status strings returned by different Expo modules.
|
|
29
|
+
*
|
|
30
|
+
* @param nativeStatus - The status string from an Expo permission response
|
|
31
|
+
* @param canAskAgain - Whether the system allows re-requesting the permission
|
|
32
|
+
* @returns Normalized PermissionStatus
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeStatus(
|
|
35
|
+
nativeStatus: string,
|
|
36
|
+
canAskAgain: boolean
|
|
37
|
+
): PermissionStatus {
|
|
38
|
+
switch (nativeStatus) {
|
|
39
|
+
case "granted":
|
|
40
|
+
return "granted";
|
|
41
|
+
case "undetermined":
|
|
42
|
+
return "undetermined";
|
|
43
|
+
case "denied":
|
|
44
|
+
return canAskAgain ? "denied" : "blocked";
|
|
45
|
+
default:
|
|
46
|
+
return "undetermined";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Permission handler interface for each permission type.
|
|
52
|
+
* Each handler provides check and request methods that return
|
|
53
|
+
* a normalized PermissionResult.
|
|
54
|
+
*/
|
|
55
|
+
interface PermissionHandler {
|
|
56
|
+
check: () => Promise<PermissionResult>;
|
|
57
|
+
request: () => Promise<PermissionResult>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Map of permission handlers for each supported PermissionType.
|
|
62
|
+
* Each handler wraps the corresponding Expo module's permission API.
|
|
63
|
+
*/
|
|
64
|
+
const permissionHandlers: Record<PermissionType, PermissionHandler> = {
|
|
65
|
+
camera: {
|
|
66
|
+
check: async () => {
|
|
67
|
+
const result = await Camera.getCameraPermissionsAsync();
|
|
68
|
+
return {
|
|
69
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
70
|
+
canAskAgain: result.canAskAgain,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
request: async () => {
|
|
74
|
+
const result = await Camera.requestCameraPermissionsAsync();
|
|
75
|
+
return {
|
|
76
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
77
|
+
canAskAgain: result.canAskAgain,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
location: {
|
|
83
|
+
check: async () => {
|
|
84
|
+
const result = await Location.getForegroundPermissionsAsync();
|
|
85
|
+
return {
|
|
86
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
87
|
+
canAskAgain: result.canAskAgain,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
request: async () => {
|
|
91
|
+
const result = await Location.requestForegroundPermissionsAsync();
|
|
92
|
+
return {
|
|
93
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
94
|
+
canAskAgain: result.canAskAgain,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
locationAlways: {
|
|
100
|
+
check: async () => {
|
|
101
|
+
const result = await Location.getBackgroundPermissionsAsync();
|
|
102
|
+
return {
|
|
103
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
104
|
+
canAskAgain: result.canAskAgain,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
request: async () => {
|
|
108
|
+
const result = await Location.requestBackgroundPermissionsAsync();
|
|
109
|
+
return {
|
|
110
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
111
|
+
canAskAgain: result.canAskAgain,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
contacts: {
|
|
117
|
+
check: async () => {
|
|
118
|
+
const result = await Contacts.getPermissionsAsync();
|
|
119
|
+
return {
|
|
120
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
121
|
+
canAskAgain: result.canAskAgain,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
request: async () => {
|
|
125
|
+
const result = await Contacts.requestPermissionsAsync();
|
|
126
|
+
return {
|
|
127
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
128
|
+
canAskAgain: result.canAskAgain,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
mediaLibrary: {
|
|
134
|
+
check: async () => {
|
|
135
|
+
const result = await MediaLibrary.getPermissionsAsync();
|
|
136
|
+
return {
|
|
137
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
138
|
+
canAskAgain: result.canAskAgain,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
request: async () => {
|
|
142
|
+
const result = await MediaLibrary.requestPermissionsAsync();
|
|
143
|
+
return {
|
|
144
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
145
|
+
canAskAgain: result.canAskAgain,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
microphone: {
|
|
151
|
+
check: async () => {
|
|
152
|
+
const result = await Camera.getMicrophonePermissionsAsync();
|
|
153
|
+
return {
|
|
154
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
155
|
+
canAskAgain: result.canAskAgain,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
request: async () => {
|
|
159
|
+
const result = await Camera.requestMicrophonePermissionsAsync();
|
|
160
|
+
return {
|
|
161
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
162
|
+
canAskAgain: result.canAskAgain,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
notifications: {
|
|
168
|
+
check: async () => {
|
|
169
|
+
const result = await Notifications.getPermissionsAsync();
|
|
170
|
+
return {
|
|
171
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
172
|
+
canAskAgain: result.canAskAgain,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
request: async () => {
|
|
176
|
+
const result = await Notifications.requestPermissionsAsync();
|
|
177
|
+
return {
|
|
178
|
+
status: normalizeStatus(result.status, result.canAskAgain),
|
|
179
|
+
canAskAgain: result.canAskAgain,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Centralized permission manager.
|
|
187
|
+
* Provides a unified API for all permission operations across the app.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { PermissionManager } from '@/services/permissions/permission-manager';
|
|
192
|
+
*
|
|
193
|
+
* // Check camera permission
|
|
194
|
+
* const result = await PermissionManager.check('camera');
|
|
195
|
+
* if (result.status === 'granted') {
|
|
196
|
+
* // Camera is available
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* // Request notification permission
|
|
200
|
+
* const notifResult = await PermissionManager.request('notifications');
|
|
201
|
+
* if (notifResult.status === 'blocked') {
|
|
202
|
+
* await PermissionManager.openSettings();
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export const PermissionManager = {
|
|
207
|
+
/**
|
|
208
|
+
* Check the current status of a permission without requesting it.
|
|
209
|
+
*
|
|
210
|
+
* @param type - The permission type to check
|
|
211
|
+
* @returns The current permission result
|
|
212
|
+
*/
|
|
213
|
+
async check(type: PermissionType): Promise<PermissionResult> {
|
|
214
|
+
try {
|
|
215
|
+
const handler = permissionHandlers[type];
|
|
216
|
+
return await handler.check();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`[PermissionManager] Failed to check ${type}:`, error);
|
|
219
|
+
return { status: "undetermined", canAskAgain: true };
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Request a permission from the user.
|
|
225
|
+
* Records that the permission has been asked in AsyncStorage.
|
|
226
|
+
*
|
|
227
|
+
* @param type - The permission type to request
|
|
228
|
+
* @returns The permission result after the request
|
|
229
|
+
*/
|
|
230
|
+
async request(type: PermissionType): Promise<PermissionResult> {
|
|
231
|
+
try {
|
|
232
|
+
const handler = permissionHandlers[type];
|
|
233
|
+
const result = await handler.request();
|
|
234
|
+
|
|
235
|
+
// Track that we've asked for this permission
|
|
236
|
+
await AsyncStorage.setItem(`${PERMISSION_ASKED_PREFIX}${type}`, "true");
|
|
237
|
+
|
|
238
|
+
return result;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`[PermissionManager] Failed to request ${type}:`, error);
|
|
241
|
+
return { status: "undetermined", canAskAgain: true };
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Open the device settings so the user can manually change permissions.
|
|
247
|
+
* On iOS, opens the app-specific settings page.
|
|
248
|
+
* On Android, uses Linking.openSettings() to open app info.
|
|
249
|
+
*/
|
|
250
|
+
async openSettings(): Promise<void> {
|
|
251
|
+
try {
|
|
252
|
+
if (Platform.OS === "ios") {
|
|
253
|
+
await Linking.openURL("app-settings:");
|
|
254
|
+
} else {
|
|
255
|
+
await Linking.openSettings();
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("[PermissionManager] Failed to open settings:", error);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check whether a permission has been previously asked.
|
|
264
|
+
* Useful for determining whether to show a pre-permission explanation
|
|
265
|
+
* before the system dialog.
|
|
266
|
+
*
|
|
267
|
+
* @param type - The permission type to check
|
|
268
|
+
* @returns Whether the permission has been previously requested
|
|
269
|
+
*/
|
|
270
|
+
async hasBeenAsked(type: PermissionType): Promise<boolean> {
|
|
271
|
+
try {
|
|
272
|
+
const value = await AsyncStorage.getItem(
|
|
273
|
+
`${PERMISSION_ASKED_PREFIX}${type}`
|
|
274
|
+
);
|
|
275
|
+
return value === "true";
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(
|
|
278
|
+
`[PermissionManager] Failed to check if ${type} was asked:`,
|
|
279
|
+
error
|
|
280
|
+
);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Permission types and default configurations
|
|
3
|
+
* Centralized type definitions for the permission management system.
|
|
4
|
+
* @module services/permissions/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Supported permission types across the app.
|
|
9
|
+
* Maps to corresponding Expo permission modules.
|
|
10
|
+
*/
|
|
11
|
+
export type PermissionType =
|
|
12
|
+
| "camera"
|
|
13
|
+
| "location"
|
|
14
|
+
| "locationAlways"
|
|
15
|
+
| "contacts"
|
|
16
|
+
| "mediaLibrary"
|
|
17
|
+
| "microphone"
|
|
18
|
+
| "notifications";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalized permission status across platforms.
|
|
22
|
+
* - 'undetermined': Permission has not been requested yet
|
|
23
|
+
* - 'granted': Permission has been granted
|
|
24
|
+
* - 'denied': Permission was denied but can be requested again
|
|
25
|
+
* - 'blocked': Permission was denied and cannot be requested again (must open Settings)
|
|
26
|
+
*/
|
|
27
|
+
export type PermissionStatus =
|
|
28
|
+
| "undetermined"
|
|
29
|
+
| "granted"
|
|
30
|
+
| "denied"
|
|
31
|
+
| "blocked";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result of a permission check or request
|
|
35
|
+
*/
|
|
36
|
+
export interface PermissionResult {
|
|
37
|
+
/** Normalized permission status */
|
|
38
|
+
status: PermissionStatus;
|
|
39
|
+
/** Whether the system will show the permission dialog if requested again */
|
|
40
|
+
canAskAgain: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for permission request UI
|
|
45
|
+
*/
|
|
46
|
+
export interface PermissionConfig {
|
|
47
|
+
/** Title shown in the permission request UI */
|
|
48
|
+
title: string;
|
|
49
|
+
/** Descriptive message explaining why the permission is needed */
|
|
50
|
+
message: string;
|
|
51
|
+
/** Ionicons icon name to display */
|
|
52
|
+
icon: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default UI configurations for each permission type.
|
|
57
|
+
* These can be overridden per-usage via the usePermission hook or PermissionGate component.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_PERMISSION_CONFIGS: Record<
|
|
60
|
+
PermissionType,
|
|
61
|
+
PermissionConfig
|
|
62
|
+
> = {
|
|
63
|
+
camera: {
|
|
64
|
+
title: "Camera Access",
|
|
65
|
+
message: "We need access to your camera to take photos and scan codes.",
|
|
66
|
+
icon: "camera-outline",
|
|
67
|
+
},
|
|
68
|
+
location: {
|
|
69
|
+
title: "Location Access",
|
|
70
|
+
message:
|
|
71
|
+
"We need access to your location to provide location-based features.",
|
|
72
|
+
icon: "location-outline",
|
|
73
|
+
},
|
|
74
|
+
locationAlways: {
|
|
75
|
+
title: "Background Location",
|
|
76
|
+
message:
|
|
77
|
+
"We need background location access to provide continuous location-based services.",
|
|
78
|
+
icon: "navigate-outline",
|
|
79
|
+
},
|
|
80
|
+
contacts: {
|
|
81
|
+
title: "Contacts Access",
|
|
82
|
+
message:
|
|
83
|
+
"We need access to your contacts to help you connect with friends.",
|
|
84
|
+
icon: "people-outline",
|
|
85
|
+
},
|
|
86
|
+
mediaLibrary: {
|
|
87
|
+
title: "Photo Library Access",
|
|
88
|
+
message:
|
|
89
|
+
"We need access to your photo library to let you select and save photos.",
|
|
90
|
+
icon: "images-outline",
|
|
91
|
+
},
|
|
92
|
+
microphone: {
|
|
93
|
+
title: "Microphone Access",
|
|
94
|
+
message:
|
|
95
|
+
"We need access to your microphone for audio recording and voice features.",
|
|
96
|
+
icon: "mic-outline",
|
|
97
|
+
},
|
|
98
|
+
notifications: {
|
|
99
|
+
title: "Push Notifications",
|
|
100
|
+
message:
|
|
101
|
+
"We need permission to send you notifications about important updates and messages.",
|
|
102
|
+
icon: "notifications-outline",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview WebSocket and real-time communication type definitions
|
|
3
|
+
* Provides shared types for the WebSocket manager and React hooks.
|
|
4
|
+
* @module services/realtime/types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Possible states of a WebSocket connection.
|
|
9
|
+
* - 'connecting': Initial connection attempt in progress
|
|
10
|
+
* - 'connected': Connection is open and ready to send/receive
|
|
11
|
+
* - 'disconnected': Connection is closed (either intentionally or due to error)
|
|
12
|
+
* - 'reconnecting': Attempting to re-establish a lost connection
|
|
13
|
+
*/
|
|
14
|
+
export type ConnectionStatus =
|
|
15
|
+
| "connecting"
|
|
16
|
+
| "connected"
|
|
17
|
+
| "disconnected"
|
|
18
|
+
| "reconnecting";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A typed WebSocket message with metadata.
|
|
22
|
+
* All messages sent or received through the WebSocket system use this envelope.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam T - The shape of the message payload
|
|
25
|
+
*/
|
|
26
|
+
export interface WebSocketMessage<T = unknown> {
|
|
27
|
+
/** Message type identifier (e.g. 'chat:message', 'presence_join') */
|
|
28
|
+
type: string;
|
|
29
|
+
/** Optional channel this message belongs to */
|
|
30
|
+
channel?: string;
|
|
31
|
+
/** The message payload */
|
|
32
|
+
payload: T;
|
|
33
|
+
/** ISO 8601 timestamp of when the message was created */
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for the WebSocket connection.
|
|
39
|
+
*/
|
|
40
|
+
export interface WebSocketConfig {
|
|
41
|
+
/** WebSocket server URL (ws:// or wss://) */
|
|
42
|
+
url: string;
|
|
43
|
+
/**
|
|
44
|
+
* Async function that returns an auth token.
|
|
45
|
+
* If provided, the token is appended as a query parameter on connect.
|
|
46
|
+
*/
|
|
47
|
+
getToken?: () => Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Whether to automatically reconnect on unexpected disconnection.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
autoReconnect?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Maximum number of reconnection attempts before giving up.
|
|
55
|
+
* @default 10
|
|
56
|
+
*/
|
|
57
|
+
maxReconnectAttempts?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Base delay in milliseconds for exponential backoff reconnection.
|
|
60
|
+
* The actual delay is `min(baseDelay * 2^attempt, 30000)`.
|
|
61
|
+
* @default 1000
|
|
62
|
+
*/
|
|
63
|
+
reconnectBaseDelay?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Interval in milliseconds between heartbeat (ping) messages.
|
|
66
|
+
* @default 30000
|
|
67
|
+
*/
|
|
68
|
+
heartbeatInterval?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Timeout in milliseconds for the initial connection attempt.
|
|
71
|
+
* @default 10000
|
|
72
|
+
*/
|
|
73
|
+
connectionTimeout?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handler callback for typed WebSocket messages.
|
|
78
|
+
*
|
|
79
|
+
* @typeParam T - The shape of the message payload
|
|
80
|
+
*/
|
|
81
|
+
export type MessageHandler<T = unknown> = (
|
|
82
|
+
message: WebSocketMessage<T>
|
|
83
|
+
) => void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handler callback for connection status changes.
|
|
87
|
+
*/
|
|
88
|
+
export type StatusHandler = (status: ConnectionStatus) => void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* User presence information for real-time presence tracking.
|
|
92
|
+
*/
|
|
93
|
+
export interface PresenceUser {
|
|
94
|
+
/** Unique user identifier */
|
|
95
|
+
id: string;
|
|
96
|
+
/** Optional display name */
|
|
97
|
+
name?: string;
|
|
98
|
+
/** ISO 8601 timestamp of the user's last known activity */
|
|
99
|
+
lastSeen: string;
|
|
100
|
+
}
|