@squad-sports/react-native 1.3.1

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +46 -0
  4. package/src/SquadExperience.tsx +200 -0
  5. package/src/SquadProvider.tsx +232 -0
  6. package/src/SquadSportsSDK.ts +286 -0
  7. package/src/__tests__/DeepLinkHandler.test.ts +101 -0
  8. package/src/__tests__/ErrorBoundary.test.tsx +161 -0
  9. package/src/__tests__/EventProcessor.test.ts +241 -0
  10. package/src/__tests__/PushNotificationHandler.test.ts +91 -0
  11. package/src/__tests__/SecureStorage.test.ts +62 -0
  12. package/src/__tests__/SquadSportsSDK.test.ts +278 -0
  13. package/src/__tests__/VerificationCooldown.test.ts +153 -0
  14. package/src/components/ErrorBoundary.tsx +129 -0
  15. package/src/components/SOTD/SOTDComponents.tsx +101 -0
  16. package/src/components/audio/AudioPlayerRow.tsx +189 -0
  17. package/src/components/audio/recording/AudioRecording.tsx +232 -0
  18. package/src/components/communities/CommunityComponents.tsx +78 -0
  19. package/src/components/dialogs/AllDialogs.tsx +123 -0
  20. package/src/components/dialogs/ConfirmDialog.tsx +77 -0
  21. package/src/components/dialogs/PermissionDialog.tsx +132 -0
  22. package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
  23. package/src/components/events/EventComponents.tsx +93 -0
  24. package/src/components/feed/ChatBannerCard.tsx +94 -0
  25. package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
  26. package/src/components/feed/FreestyleCard.tsx +119 -0
  27. package/src/components/feed/InterstitialOverlay.tsx +190 -0
  28. package/src/components/feed/PollCard.tsx +158 -0
  29. package/src/components/feed/SponsoredContentCard.tsx +118 -0
  30. package/src/components/freestyle/FreestyleComponents.tsx +148 -0
  31. package/src/components/index.ts +42 -0
  32. package/src/components/message/MessageCard.tsx +166 -0
  33. package/src/components/message/MessageComponents.tsx +143 -0
  34. package/src/components/poll/PollComponents.tsx +226 -0
  35. package/src/components/sentinels/Sentinels.tsx +175 -0
  36. package/src/components/squad/FeedSquad.tsx +54 -0
  37. package/src/components/toasts/Toasts.tsx +88 -0
  38. package/src/components/ux/RootUXComponents.tsx +157 -0
  39. package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
  40. package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
  41. package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
  42. package/src/components/ux/buttons/Button.tsx +37 -0
  43. package/src/components/ux/buttons/InfoButton.tsx +24 -0
  44. package/src/components/ux/buttons/XButton.tsx +27 -0
  45. package/src/components/ux/carousel/Carousel.tsx +134 -0
  46. package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
  47. package/src/components/ux/errors/ErrorHint.tsx +46 -0
  48. package/src/components/ux/inputs/CodeInput.tsx +121 -0
  49. package/src/components/ux/inputs/DatePicker.tsx +76 -0
  50. package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
  51. package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
  52. package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
  53. package/src/components/ux/inputs/TextInput.tsx +58 -0
  54. package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
  55. package/src/components/ux/layout/BlurOverlay.tsx +26 -0
  56. package/src/components/ux/layout/CrossFade.tsx +30 -0
  57. package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
  58. package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
  59. package/src/components/ux/layout/NetworkBanner.tsx +64 -0
  60. package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
  61. package/src/components/ux/layout/RefreshControl.tsx +7 -0
  62. package/src/components/ux/layout/Screen.tsx +31 -0
  63. package/src/components/ux/layout/ScreenHeader.tsx +89 -0
  64. package/src/components/ux/layout/TabBar.tsx +39 -0
  65. package/src/components/ux/layout/Toast.tsx +116 -0
  66. package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
  67. package/src/components/ux/navigation/BackButton.tsx +29 -0
  68. package/src/components/ux/navigation/LinkButton.tsx +21 -0
  69. package/src/components/ux/navigation/UrlButton.tsx +25 -0
  70. package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
  71. package/src/components/ux/shapes/Shapes.tsx +23 -0
  72. package/src/components/ux/text/Typography.tsx +28 -0
  73. package/src/components/ux/user-image/UserImage.tsx +75 -0
  74. package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
  75. package/src/components/wallet/WalletComponents.tsx +116 -0
  76. package/src/contexts/AuthContext.tsx +45 -0
  77. package/src/contexts/EventProcessorContext.tsx +41 -0
  78. package/src/contexts/PlayerQueueContext.tsx +95 -0
  79. package/src/hooks/useAuth.ts +23 -0
  80. package/src/hooks/useDataRefresh.ts +30 -0
  81. package/src/hooks/useEventProcessor.ts +6 -0
  82. package/src/hooks/useImageOptimization.ts +59 -0
  83. package/src/hooks/useOnboardingStepGuard.ts +36 -0
  84. package/src/hooks/usePendingNavigation.ts +26 -0
  85. package/src/hooks/useSquadData.ts +84 -0
  86. package/src/hooks/useUserCreated.ts +25 -0
  87. package/src/hooks/useUserUpdate.ts +25 -0
  88. package/src/hooks/useViewabilityTracker.ts +40 -0
  89. package/src/index.ts +109 -0
  90. package/src/navigation/SquadNavigator.tsx +262 -0
  91. package/src/realtime/DeepLinkHandler.ts +113 -0
  92. package/src/realtime/EventProcessor.ts +313 -0
  93. package/src/realtime/NetworkMonitor.ts +84 -0
  94. package/src/realtime/OfflineQueue.ts +133 -0
  95. package/src/realtime/PushNotificationHandler.ts +125 -0
  96. package/src/realtime/useRealtimeSync.ts +84 -0
  97. package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
  98. package/src/screens/auth/EnterCodeScreen.tsx +253 -0
  99. package/src/screens/auth/EnterEmailScreen.tsx +234 -0
  100. package/src/screens/auth/LandingScreen.tsx +90 -0
  101. package/src/screens/auth/LoginScreen.tsx +126 -0
  102. package/src/screens/events/EventScreen.tsx +163 -0
  103. package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
  104. package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
  105. package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
  106. package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
  107. package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
  108. package/src/screens/home/HomeScreen.tsx +365 -0
  109. package/src/screens/home/slivers/SquadCircle.tsx +77 -0
  110. package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
  111. package/src/screens/invite/InviteScreen.tsx +175 -0
  112. package/src/screens/messaging/MessagingScreen.tsx +360 -0
  113. package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
  114. package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
  115. package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
  116. package/src/screens/polls/PollResponseScreen.tsx +229 -0
  117. package/src/screens/polls/PollSummationScreen.tsx +78 -0
  118. package/src/screens/profile/ProfileScreen.tsx +234 -0
  119. package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
  120. package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
  121. package/src/screens/settings/EditProfileScreen.tsx +154 -0
  122. package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
  123. package/src/screens/settings/SettingsScreen.tsx +194 -0
  124. package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
  125. package/src/screens/wallet/WalletScreen.tsx +174 -0
  126. package/src/services/AuthStateManager.ts +93 -0
  127. package/src/services/NavigationService.ts +40 -0
  128. package/src/services/UserDataManager.ts +59 -0
  129. package/src/services/UserUpdateService.ts +31 -0
  130. package/src/services/VerificationStateManager.ts +41 -0
  131. package/src/squad-line/CallScreen.tsx +158 -0
  132. package/src/squad-line/IncomingCallOverlay.tsx +113 -0
  133. package/src/squad-line/SquadLineClient.ts +327 -0
  134. package/src/squad-line/useSquadLine.ts +80 -0
  135. package/src/state/audio.ts +38 -0
  136. package/src/state/client.ts +26 -0
  137. package/src/state/communities.ts +45 -0
  138. package/src/state/contacts.ts +28 -0
  139. package/src/state/device-info.ts +22 -0
  140. package/src/state/events.ts +16 -0
  141. package/src/state/features.ts +57 -0
  142. package/src/state/index.ts +121 -0
  143. package/src/state/invitations.ts +16 -0
  144. package/src/state/modal-keys.ts +63 -0
  145. package/src/state/modal-queue.ts +104 -0
  146. package/src/state/navigation.ts +34 -0
  147. package/src/state/permissions.ts +43 -0
  148. package/src/state/session.ts +223 -0
  149. package/src/state/squaddie-of-the-day.ts +21 -0
  150. package/src/state/sync/crdt.ts +70 -0
  151. package/src/state/sync/dependable.ts +213 -0
  152. package/src/state/sync/feed-v2.ts +42 -0
  153. package/src/state/sync/messages.ts +44 -0
  154. package/src/state/sync/offline-support.ts +42 -0
  155. package/src/state/sync/polls.ts +37 -0
  156. package/src/state/sync/refresh.ts +24 -0
  157. package/src/state/sync/squad-v2.ts +25 -0
  158. package/src/state/ui.ts +36 -0
  159. package/src/state/user.ts +46 -0
  160. package/src/state/wallet.ts +26 -0
  161. package/src/storage/SecureStorage.ts +77 -0
  162. package/src/theme/ThemeContext.tsx +159 -0
  163. package/src/types/modules.d.ts +165 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Poll sync atoms.
3
+ * Ported from squad-demo/src/atoms/sync/polls.ts.
4
+ */
5
+ import { atom, atomFamily } from 'recoil';
6
+ import type { PollFeed, PollResponse, PollResponseReaction } from '@squad-sports/core';
7
+ import { type CrdtArray } from './crdt';
8
+
9
+ // Initial poll feed from API
10
+ export const reInitialPollFeed = atom<PollFeed | null>({
11
+ key: 'squad-sdk:polls:active:initial:v2',
12
+ default: null,
13
+ });
14
+
15
+ // Real-time poll updates
16
+ export const rePollUpdates = atom<CrdtArray<string, unknown>>({
17
+ key: 'squad-sdk:poll-feed:updates:v2',
18
+ default: new Map(),
19
+ });
20
+
21
+ // Poll response updates per poll
22
+ export const rePollResponseUpdates = atomFamily<CrdtArray<string, PollResponse>, string>({
23
+ key: 'squad-sdk:poll:responses:updates:v2',
24
+ default: new Map(),
25
+ });
26
+
27
+ // Poll response reaction updates
28
+ export const rePollResponseReactionsUpdates = atomFamily<CrdtArray<string, PollResponseReaction>, string>({
29
+ key: 'squad-sdk:poll:response:reactions:updates',
30
+ default: new Map(),
31
+ });
32
+
33
+ // Poll nudge updates
34
+ export const rePollNudgesUpdates = atomFamily<CrdtArray<string, unknown>, string>({
35
+ key: 'squad-sdk:poll:nudges:updates',
36
+ default: new Map(),
37
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Refresh trigger atom.
3
+ * Ported from squad-demo/src/atoms/sync/refresh.ts.
4
+ */
5
+ import { atom } from 'recoil';
6
+ import { DependableSelector } from './dependable';
7
+
8
+ export const reLastRefreshed = atom<number>({
9
+ key: 'squad-sdk:last-refreshed',
10
+ default: Date.now(),
11
+ });
12
+
13
+ export const reRefresh = new DependableSelector<number>({
14
+ key: 'squad-sdk:refresh',
15
+ get: ({ get }) => get(reLastRefreshed),
16
+ set: ({ set }, newValue) => {
17
+ set(reLastRefreshed, typeof newValue === 'number' ? newValue : Date.now());
18
+ },
19
+ });
20
+
21
+ export const reRefreshing = atom<boolean>({
22
+ key: 'squad-sdk:refreshing',
23
+ default: false,
24
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Squad/connections sync atoms.
3
+ * Ported from squad-demo/src/atoms/sync/squad-v2.ts.
4
+ */
5
+ import { atom } from 'recoil';
6
+ import type { Squad, Connection } from '@squad-sports/core';
7
+ import { type CrdtArray } from './crdt';
8
+
9
+ // Initial connections from API
10
+ export const reInitialConnections = atom<Squad | null>({
11
+ key: 'squad-sdk:squad:initial',
12
+ default: null,
13
+ });
14
+
15
+ // Real-time connection updates
16
+ export const reConnectionUpdates = atom<CrdtArray<string, Connection>>({
17
+ key: 'squad-sdk:squad:updates',
18
+ default: new Map(),
19
+ });
20
+
21
+ // Connection by user ID
22
+ export const reConnectionWithUser = atom<Map<string, Connection>>({
23
+ key: 'squad-sdk:squad:connectionByUser',
24
+ default: new Map(),
25
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * UI state atoms.
3
+ * Ported from squad-demo/src/atoms/loading-overlay.ts + keyboard.ts + networkBannerState.ts + networkStateAtom.ts.
4
+ */
5
+ import { atom } from 'recoil';
6
+
7
+ // Loading overlay
8
+ export const loadingOverlayVisible = atom<boolean>({
9
+ key: 'squad-sdk:loading-overlay:visible',
10
+ default: false,
11
+ });
12
+
13
+ export const loadingOverlayText = atom<string>({
14
+ key: 'squad-sdk:loading-overlay:text',
15
+ default: '',
16
+ });
17
+
18
+ // Keyboard
19
+ export const aKeyboardOpen = atom<boolean>({
20
+ key: 'squad-sdk:keyboard:open',
21
+ default: false,
22
+ });
23
+
24
+ // Network banner
25
+ export const networkBannerVisible = atom<boolean>({
26
+ key: 'squad-sdk:network:bannerVisible',
27
+ default: false,
28
+ });
29
+
30
+ export const networkStateAtom = atom<{
31
+ isConnected: boolean;
32
+ isInternetReachable: boolean | null;
33
+ }>({
34
+ key: 'squad-sdk:network:state',
35
+ default: { isConnected: true, isInternetReachable: true },
36
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * User state atoms.
3
+ * Ported from squad-demo/src/atoms/user.ts + user-tag.ts + sync/user-v2.ts.
4
+ */
5
+ import { atom, atomFamily, selectorFamily } from 'recoil';
6
+ import type { User } from '@squad-sports/core';
7
+ import { type CrdtArray, type CrdtArrayItem } from './sync/crdt';
8
+
9
+ // Logged-in user cache
10
+ export const reUserCache = atom<User | null>({
11
+ key: 'squad-sdk:user:cache',
12
+ default: null,
13
+ });
14
+
15
+ export const reLoggedInUserLoaded = atom<boolean>({
16
+ key: 'squad-sdk:user:loaded',
17
+ default: false,
18
+ });
19
+
20
+ // Per-user updates from SSE
21
+ export const userUpdates = atomFamily<CrdtArrayItem<User> | null, string>({
22
+ key: 'squad-sdk:user:updates',
23
+ default: null,
24
+ });
25
+
26
+ // User by ID (selector family)
27
+ export const reUser = selectorFamily<User | null, string>({
28
+ key: 'squad-sdk:user:byId',
29
+ get: (id) => ({ get }) => {
30
+ const cache = get(reUserCache);
31
+ if (cache?.id === id) return cache;
32
+ return null; // Would need API call for other users
33
+ },
34
+ });
35
+
36
+ // User tag
37
+ export const reUserTag = atom<string | null>({
38
+ key: 'squad-sdk:user:tag',
39
+ default: null,
40
+ });
41
+
42
+ // Sophia (AI assistant)
43
+ export const reSophiaUser = atom<User | null>({
44
+ key: 'squad-sdk:user:sophia',
45
+ default: null,
46
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Wallet and coupon atoms.
3
+ * Ported from squad-demo/src/atoms/wallet.ts.
4
+ */
5
+ import { atom } from 'recoil';
6
+ import type { Wallet, Coupons, Brands } from '@squad-sports/core';
7
+
8
+ export const reWallet = atom<Wallet | null>({
9
+ key: 'squad-sdk:wallet:data',
10
+ default: null,
11
+ });
12
+
13
+ export const reCoupons = atom<Coupons | null>({
14
+ key: 'squad-sdk:wallet:coupons',
15
+ default: null,
16
+ });
17
+
18
+ export const reBrands = atom<Brands | null>({
19
+ key: 'squad-sdk:wallet:brands',
20
+ default: null,
21
+ });
22
+
23
+ export const reWalletRefresh = atom<number>({
24
+ key: 'squad-sdk:wallet:refresh',
25
+ default: 0,
26
+ });
@@ -0,0 +1,77 @@
1
+ import type { StorageAdapter } from '@squad-sports/core';
2
+
3
+ /**
4
+ * Secure storage adapter for React Native.
5
+ * Uses expo-secure-store (encrypted) when available, falls back to AsyncStorage.
6
+ * Sensitive keys (tokens, user IDs) are always stored securely.
7
+ */
8
+ export class SecureStorageAdapter implements StorageAdapter {
9
+ private secureStore: any = null;
10
+ private asyncStorage: any = null;
11
+ private initialized = false;
12
+
13
+ private static readonly SECURE_KEYS = new Set([
14
+ 'SQUAD_SDK_AUTH_TOKEN',
15
+ 'SQUAD_SDK_AUTH_USER_ID',
16
+ 'SQUAD_SDK_AUTH_EMAIL',
17
+ 'SQUAD_SDK_AUTH_PHONE',
18
+ 'SQUAD_SDK_AUTH_COMMUNITY_ID',
19
+ 'SQUAD_SDK_AUTH_PARTNER_ID',
20
+ ]);
21
+
22
+ private async init(): Promise<void> {
23
+ if (this.initialized) return;
24
+ try {
25
+ this.secureStore = (await import('expo-secure-store'));
26
+ } catch {
27
+ // expo-secure-store not available
28
+ }
29
+ try {
30
+ this.asyncStorage = (await import('@react-native-async-storage/async-storage')).default;
31
+ } catch {
32
+ // AsyncStorage not available
33
+ }
34
+ this.initialized = true;
35
+ }
36
+
37
+ private isSecureKey(key: string): boolean {
38
+ return SecureStorageAdapter.SECURE_KEYS.has(key);
39
+ }
40
+
41
+ async getItem(key: string): Promise<string | null> {
42
+ await this.init();
43
+ if (this.isSecureKey(key) && this.secureStore) {
44
+ try {
45
+ return await this.secureStore.getItemAsync(key);
46
+ } catch {
47
+ // Fall through to AsyncStorage
48
+ }
49
+ }
50
+ return this.asyncStorage?.getItem(key) ?? null;
51
+ }
52
+
53
+ async setItem(key: string, value: string): Promise<void> {
54
+ await this.init();
55
+ if (this.isSecureKey(key) && this.secureStore) {
56
+ try {
57
+ await this.secureStore.setItemAsync(key, value);
58
+ return;
59
+ } catch {
60
+ // Fall through to AsyncStorage
61
+ }
62
+ }
63
+ await this.asyncStorage?.setItem(key, value);
64
+ }
65
+
66
+ async removeItem(key: string): Promise<void> {
67
+ await this.init();
68
+ if (this.isSecureKey(key) && this.secureStore) {
69
+ try {
70
+ await this.secureStore.deleteItemAsync(key);
71
+ } catch {}
72
+ }
73
+ try {
74
+ await this.asyncStorage?.removeItem(key);
75
+ } catch {}
76
+ }
77
+ }
@@ -0,0 +1,159 @@
1
+ import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';
2
+
3
+ /**
4
+ * Theme interface ported from squad-demo/src/theme/theme.tsx.
5
+ */
6
+ export interface Theme {
7
+ screenBackground: string;
8
+ screenBackgroundTransparency: string;
9
+ bottomSheetHandleIndicator: string;
10
+ offWhiteTransparency?: string;
11
+ recordingButtons?: string;
12
+ emojiKeyboardHeader: string;
13
+ buttonColor: string;
14
+ buttonText: string;
15
+ primaryColor: string;
16
+ secondaryColor: string;
17
+ tertiaryColor: string;
18
+ isDarkMode: boolean;
19
+ }
20
+
21
+ export interface ColorScheme {
22
+ primaryTextColor: string;
23
+ disabledGrey: string;
24
+ primaryGreyColor: string;
25
+ secondaryGrey: string;
26
+ primaryWhiteColor: string;
27
+ secondaryTextColor: string;
28
+ transparentBGWhite: string;
29
+ transparentBGBlack: string;
30
+ errorColor: string;
31
+ backgroundWhite: string;
32
+ backgroundBlack: string;
33
+ primaryPurple: string;
34
+ }
35
+
36
+ export const Colors = {
37
+ blackBackground: '#1D1D1D',
38
+ squadCircle: '#FAFAFA',
39
+ transparent: 'rgba(0,0,0,0)',
40
+ black: '#0A0A0A',
41
+ white: '#ffffff',
42
+ gray1: '#151515',
43
+ gray2: '#212121',
44
+ gray3: '#353535',
45
+ gray4: '#444444',
46
+ gray5: '#3D3D3D',
47
+ gray6: '#8A8A8A',
48
+ gray7: '#DFE5EB',
49
+ gray8: '#DCDCDC',
50
+ gray9: '#111111',
51
+ gray10: '#8A8A8A',
52
+ gray11: '#D9D9D9',
53
+ gray12: '#0B0B0B',
54
+ purple1: '#6E82E7',
55
+ purple2: '#2E46C9',
56
+ purple3: '#566BD7',
57
+ orange1: '#FF955C',
58
+ orange2: '#E9785C',
59
+ green: '#11EC0F',
60
+ gold: '#D1C282',
61
+ apricot: '#FF955C',
62
+ red: '#FF4478',
63
+ blue: 'rgb(0,98,255)',
64
+ lightGray: '#F2F2F2',
65
+ } as const;
66
+
67
+ const baseThemeColors: ColorScheme = {
68
+ primaryTextColor: Colors.gray1,
69
+ disabledGrey: Colors.gray11,
70
+ primaryGreyColor: Colors.gray6,
71
+ secondaryGrey: Colors.gray5,
72
+ primaryWhiteColor: Colors.white,
73
+ secondaryTextColor: Colors.gray2,
74
+ transparentBGWhite: 'rgba(0,0,0, 0.05)',
75
+ transparentBGBlack: 'rgba(255,255,255, 0.05)',
76
+ errorColor: Colors.orange1,
77
+ backgroundWhite: '#F5F6F8',
78
+ backgroundBlack: '#000000',
79
+ primaryPurple: Colors.purple1,
80
+ };
81
+
82
+ export const defaultTheme: Theme = {
83
+ screenBackground: Colors.gray9,
84
+ screenBackgroundTransparency: 'rgba(17,17,17, 0.8)',
85
+ bottomSheetHandleIndicator: 'rgba(255, 255, 255, 0.14)',
86
+ emojiKeyboardHeader: '#5F5E5F',
87
+ buttonColor: Colors.purple1,
88
+ buttonText: Colors.black,
89
+ primaryColor: Colors.black,
90
+ secondaryColor: Colors.gray2,
91
+ tertiaryColor: Colors.purple1,
92
+ isDarkMode: true,
93
+ };
94
+
95
+ /**
96
+ * Build a theme from community colors, falling back to defaults.
97
+ */
98
+ export function buildCommunityTheme(
99
+ primaryColor?: string,
100
+ secondaryColor?: string,
101
+ ): Theme {
102
+ return {
103
+ ...defaultTheme,
104
+ primaryColor: primaryColor ?? defaultTheme.primaryColor,
105
+ secondaryColor: secondaryColor ?? defaultTheme.secondaryColor,
106
+ buttonColor: primaryColor ?? defaultTheme.buttonColor,
107
+ tertiaryColor: secondaryColor ?? defaultTheme.tertiaryColor,
108
+ };
109
+ }
110
+
111
+ // --- Context ---
112
+
113
+ interface ThemeContextValue {
114
+ theme: Theme;
115
+ colors: typeof Colors;
116
+ baseThemeColors: ColorScheme;
117
+ switchTheme: (theme: Theme) => void;
118
+ }
119
+
120
+ const ThemeCtx = createContext<ThemeContextValue>({
121
+ theme: defaultTheme,
122
+ colors: Colors,
123
+ baseThemeColors,
124
+ switchTheme: () => {},
125
+ });
126
+
127
+ export function useTheme(): ThemeContextValue {
128
+ return useContext(ThemeCtx);
129
+ }
130
+
131
+ interface ThemeProviderProps {
132
+ children: React.ReactNode;
133
+ communityColor?: string;
134
+ communitySecondaryColor?: string;
135
+ }
136
+
137
+ export function ThemeProvider({
138
+ children,
139
+ communityColor,
140
+ communitySecondaryColor,
141
+ }: ThemeProviderProps) {
142
+ const initialTheme = useMemo(
143
+ () => buildCommunityTheme(communityColor, communitySecondaryColor),
144
+ [communityColor, communitySecondaryColor],
145
+ );
146
+
147
+ const [theme, setTheme] = useState<Theme>(initialTheme);
148
+
149
+ const switchTheme = useCallback((newTheme: Theme) => {
150
+ setTheme(newTheme);
151
+ }, []);
152
+
153
+ const value = useMemo(
154
+ () => ({ theme, colors: Colors, baseThemeColors, switchTheme }),
155
+ [theme, switchTheme],
156
+ );
157
+
158
+ return <ThemeCtx.Provider value={value}>{children}</ThemeCtx.Provider>;
159
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Type declarations for optional peer dependencies.
3
+ * These modules are dynamically imported and may not be installed in all environments.
4
+ */
5
+
6
+ declare module 'expo-image-manipulator' {
7
+ export enum SaveFormat {
8
+ JPEG = 'jpeg',
9
+ PNG = 'png',
10
+ WEBP = 'webp',
11
+ }
12
+
13
+ export interface ImageResult {
14
+ uri: string;
15
+ width: number;
16
+ height: number;
17
+ base64?: string;
18
+ }
19
+
20
+ export interface Action {
21
+ resize?: { width?: number; height?: number };
22
+ rotate?: number;
23
+ flip?: { vertical?: boolean; horizontal?: boolean };
24
+ crop?: { originX: number; originY: number; width: number; height: number };
25
+ }
26
+
27
+ export interface SaveOptions {
28
+ compress?: number;
29
+ format?: SaveFormat;
30
+ base64?: boolean;
31
+ }
32
+
33
+ export function manipulateAsync(
34
+ uri: string,
35
+ actions: Action[],
36
+ saveOptions?: SaveOptions,
37
+ ): Promise<ImageResult>;
38
+ }
39
+
40
+ declare module 'expo-notifications' {
41
+ export interface PermissionResponse {
42
+ status: string;
43
+ granted: boolean;
44
+ expires: string;
45
+ canAskAgain: boolean;
46
+ ios?: Record<string, unknown>;
47
+ }
48
+
49
+ export interface ExpoPushToken {
50
+ type: string;
51
+ data: string;
52
+ }
53
+
54
+ export function getPermissionsAsync(): Promise<PermissionResponse>;
55
+ export function requestPermissionsAsync(): Promise<PermissionResponse>;
56
+ export function getExpoPushTokenAsync(options?: { projectId?: string }): Promise<ExpoPushToken>;
57
+ export function addNotificationResponseReceivedListener(
58
+ listener: (response: any) => void,
59
+ ): { remove: () => void };
60
+ export function addNotificationReceivedListener(
61
+ listener: (notification: any) => void,
62
+ ): { remove: () => void };
63
+ }
64
+
65
+ declare module 'expo-camera' {
66
+ export class Camera {
67
+ static requestCameraPermissionsAsync(): Promise<{ status: string; granted: boolean }>;
68
+ static getCameraPermissionsAsync(): Promise<{ status: string; granted: boolean }>;
69
+ }
70
+
71
+ export interface CameraProps {
72
+ type?: any;
73
+ onBarCodeScanned?: (data: { type: string; data: string }) => void;
74
+ style?: any;
75
+ children?: any;
76
+ }
77
+
78
+ export default class CameraComponent extends React.Component<CameraProps> {}
79
+ }
80
+
81
+ declare module 'expo-clipboard' {
82
+ export function setStringAsync(text: string): Promise<void>;
83
+ export function getStringAsync(): Promise<string>;
84
+ export function hasStringAsync(): Promise<boolean>;
85
+ }
86
+
87
+ declare module '@react-native-community/netinfo' {
88
+ export interface NetInfoState {
89
+ type: string;
90
+ isConnected: boolean | null;
91
+ isInternetReachable: boolean | null;
92
+ details: Record<string, unknown> | null;
93
+ }
94
+
95
+ export type NetInfoChangeHandler = (state: NetInfoState) => void;
96
+
97
+ interface NetInfoStatic {
98
+ addEventListener(listener: NetInfoChangeHandler): () => void;
99
+ fetch(): Promise<NetInfoState>;
100
+ }
101
+
102
+ const NetInfo: NetInfoStatic;
103
+ export default NetInfo;
104
+ }
105
+
106
+ declare module 'expo-secure-store' {
107
+ export function getItemAsync(key: string): Promise<string | null>;
108
+ export function setItemAsync(key: string, value: string): Promise<void>;
109
+ export function deleteItemAsync(key: string): Promise<void>;
110
+ }
111
+
112
+ declare module '@twilio/voice-react-native-sdk' {
113
+ export class Voice {
114
+ register(token: string): Promise<void>;
115
+ connect(token: string, options?: Record<string, unknown>): Promise<any>;
116
+ on(event: string, handler: (...args: any[]) => void): this;
117
+ off(event: string, handler: (...args: any[]) => void): this;
118
+ }
119
+ }
120
+
121
+ declare module 'react-native-date-picker' {
122
+ import type { ComponentType } from 'react';
123
+
124
+ interface DatePickerProps {
125
+ date: Date;
126
+ onDateChange: (date: Date) => void;
127
+ mode?: 'date' | 'time' | 'datetime';
128
+ open?: boolean;
129
+ onConfirm?: (date: Date) => void;
130
+ onCancel?: () => void;
131
+ [key: string]: any;
132
+ }
133
+
134
+ const DatePicker: ComponentType<DatePickerProps>;
135
+ export default DatePicker;
136
+ }
137
+
138
+ declare module 'react-native-sse' {
139
+ interface EventSourceOptions {
140
+ headers?: Record<string, string>;
141
+ method?: string;
142
+ body?: string;
143
+ pollingInterval?: number;
144
+ }
145
+
146
+ type EventSourceListener = (event: { type: string; data?: string; lastEventId?: string }) => void;
147
+
148
+ class EventSource {
149
+ constructor(url: string, options?: EventSourceOptions);
150
+ addEventListener(type: string, listener: EventSourceListener): void;
151
+ removeEventListener(type: string, listener: EventSourceListener): void;
152
+ close(): void;
153
+ readonly readyState: number;
154
+ static readonly CONNECTING: number;
155
+ static readonly OPEN: number;
156
+ static readonly CLOSED: number;
157
+ }
158
+
159
+ export default EventSource;
160
+ }
161
+
162
+ // Augment Blob to include arrayBuffer (available in newer environments)
163
+ interface Blob {
164
+ arrayBuffer(): Promise<ArrayBuffer>;
165
+ }