@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,25 @@
1
+ import React from 'react';
2
+ import { Text, StyleSheet, Linking, Pressable } from 'react-native';
3
+ import { Colors } from '../../../theme/ThemeContext';
4
+
5
+ interface UrlButtonProps {
6
+ url: string;
7
+ children: React.ReactNode;
8
+ style?: object;
9
+ }
10
+
11
+ export default function UrlButton({ url, children, style }: UrlButtonProps) {
12
+ return (
13
+ <Pressable onPress={() => Linking.openURL(url)} style={style} accessibilityRole="link">
14
+ {children}
15
+ </Pressable>
16
+ );
17
+ }
18
+
19
+ export function UrlButtonInline({ url, children }: { url: string; children: React.ReactNode }) {
20
+ return (
21
+ <Text onPress={() => Linking.openURL(url)} accessibilityRole="link">
22
+ {children}
23
+ </Text>
24
+ );
25
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * FlatList with built-in pull-to-refresh.
3
+ * Ported from squad-demo/src/components/ux/scroll/RefreshableFlatList.tsx.
4
+ */
5
+ import React, { useCallback, useState } from 'react';
6
+ import { FlatList, RefreshControl, type FlatListProps } from 'react-native';
7
+ import { Colors } from '../../../theme/ThemeContext';
8
+
9
+ interface RefreshableFlatListProps<T> extends FlatListProps<T> {
10
+ onRefresh: () => Promise<void>;
11
+ }
12
+
13
+ export default function RefreshableFlatList<T>({ onRefresh, ...props }: RefreshableFlatListProps<T>) {
14
+ const [refreshing, setRefreshing] = useState(false);
15
+
16
+ const handleRefresh = useCallback(async () => {
17
+ setRefreshing(true);
18
+ await onRefresh();
19
+ setRefreshing(false);
20
+ }, [onRefresh]);
21
+
22
+ return (
23
+ <FlatList
24
+ {...props}
25
+ refreshControl={
26
+ <RefreshControl
27
+ refreshing={refreshing}
28
+ onRefresh={handleRefresh}
29
+ tintColor={Colors.white}
30
+ colors={[Colors.purple1]}
31
+ />
32
+ }
33
+ />
34
+ );
35
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shape components.
3
+ * Ported from squad-demo/src/components/ux/shapes/.
4
+ */
5
+ import React from 'react';
6
+ import { View, StyleSheet } from 'react-native';
7
+ import { Colors } from '../../../theme/ThemeContext';
8
+
9
+ export function Circle({ size = 48, color = Colors.purple1, children }: { size?: number; color?: string; children?: React.ReactNode }) {
10
+ return (
11
+ <View style={[styles.circle, { width: size, height: size, borderRadius: size / 2, backgroundColor: color }]}>
12
+ {children}
13
+ </View>
14
+ );
15
+ }
16
+
17
+ export function HorizontalLine({ color = Colors.gray3, thickness = StyleSheet.hairlineWidth, marginVertical = 0 }: { color?: string; thickness?: number; marginVertical?: number }) {
18
+ return <View style={{ height: thickness, backgroundColor: color, marginVertical }} />;
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ circle: { justifyContent: 'center', alignItems: 'center', overflow: 'hidden' },
23
+ });
@@ -0,0 +1,28 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Text, TextProps, StyleSheet } from 'react-native';
3
+
4
+ interface TypographyProps extends TextProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ function createTypography(baseStyle: object) {
9
+ return function TypographyComponent({ children, style, ...props }: TypographyProps) {
10
+ return (
11
+ <Text style={[baseStyle, style]} {...props}>
12
+ {children}
13
+ </Text>
14
+ );
15
+ };
16
+ }
17
+
18
+ export const TitleLarge = createTypography({ fontSize: 56, lineHeight: 66, fontWeight: '500' });
19
+ export const TitleRegular = createTypography({ fontSize: 32, lineHeight: 42, fontWeight: '500' });
20
+ export const TitleMedium = createTypography({ fontSize: 24, lineHeight: 36, fontWeight: '500' });
21
+ export const TitleSmall = createTypography({ fontSize: 18, lineHeight: 27, fontWeight: '700' });
22
+ export const TitleTiny = createTypography({ fontSize: 16, lineHeight: 24, fontWeight: '500' });
23
+ export const SubtitleSmall = createTypography({ fontSize: 14, lineHeight: 21, fontWeight: '500' });
24
+ export const BodyRegular = createTypography({ fontSize: 16, lineHeight: 24 });
25
+ export const BodyMedium = createTypography({ fontSize: 14, lineHeight: 21 });
26
+ export const BodySmall = createTypography({ fontSize: 12, lineHeight: 18 });
27
+ export const ButtonLarge = createTypography({ fontSize: 16 });
28
+ export const ButtonSmall = createTypography({ fontSize: 14 });
@@ -0,0 +1,75 @@
1
+ import React, { memo } from 'react';
2
+ import { View, Text, StyleSheet, StyleProp, ViewStyle } from 'react-native';
3
+ import { Image } from 'expo-image';
4
+ import { Colors } from '../../../theme/ThemeContext';
5
+
6
+ interface UserImageProps {
7
+ imageUrl?: string | null;
8
+ displayName?: string | null;
9
+ size?: number;
10
+ style?: StyleProp<ViewStyle>;
11
+ borderColor?: string;
12
+ }
13
+
14
+ function UserImage({
15
+ imageUrl,
16
+ displayName,
17
+ size = 48,
18
+ style,
19
+ borderColor,
20
+ }: UserImageProps) {
21
+ const initials = displayName
22
+ ? displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
23
+ : '?';
24
+
25
+ const containerStyle = [
26
+ styles.container,
27
+ {
28
+ width: size,
29
+ height: size,
30
+ borderRadius: size / 2,
31
+ ...(borderColor && { borderWidth: 2, borderColor }),
32
+ },
33
+ style,
34
+ ];
35
+
36
+ const a11yLabel = displayName ? `${displayName}'s profile photo` : 'User profile photo';
37
+
38
+ if (imageUrl) {
39
+ return (
40
+ <View style={containerStyle} accessibilityLabel={a11yLabel} accessibilityRole="image">
41
+ <Image
42
+ source={{ uri: imageUrl }}
43
+ style={{ width: size, height: size, borderRadius: size / 2 }}
44
+ contentFit="cover"
45
+ transition={200}
46
+ />
47
+ </View>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <View style={[containerStyle, styles.placeholder]} accessibilityLabel={a11yLabel} accessibilityRole="image">
53
+ <Text style={[styles.initials, { fontSize: size * 0.36 }]} accessibilityElementsHidden>
54
+ {initials}
55
+ </Text>
56
+ </View>
57
+ );
58
+ }
59
+
60
+ export default memo(UserImage);
61
+
62
+ const styles = StyleSheet.create({
63
+ container: {
64
+ overflow: 'hidden',
65
+ },
66
+ placeholder: {
67
+ backgroundColor: Colors.purple1,
68
+ justifyContent: 'center',
69
+ alignItems: 'center',
70
+ },
71
+ initials: {
72
+ color: Colors.white,
73
+ fontWeight: '600',
74
+ },
75
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * UserImage variant components.
3
+ * Ported from squad-demo/src/components/ux/user-image/*.
4
+ */
5
+ import React, { memo } from 'react';
6
+ import { View, Text, StyleSheet, Pressable } from 'react-native';
7
+ import { useNavigation } from '@react-navigation/native';
8
+ import UserImage from './UserImage';
9
+ import { Colors } from '../../../theme/ThemeContext';
10
+
11
+ // --- ImageCircle ---
12
+ export function ImageCircle({ uri, size = 48 }: { uri?: string; size?: number }) {
13
+ return <UserImage imageUrl={uri} displayName="" size={size} />;
14
+ }
15
+
16
+ // --- UserImageWithEmoji ---
17
+ export function UserImageWithEmoji({
18
+ imageUrl, displayName, size = 48, emoji,
19
+ }: { imageUrl?: string; displayName?: string; size?: number; emoji?: string }) {
20
+ return (
21
+ <View>
22
+ <UserImage imageUrl={imageUrl} displayName={displayName} size={size} />
23
+ {emoji && (
24
+ <View style={[styles.emojiBadge, { bottom: -2, right: -2 }]}>
25
+ <Text style={styles.emojiText}>{emoji}</Text>
26
+ </View>
27
+ )}
28
+ </View>
29
+ );
30
+ }
31
+
32
+ // --- CommunityImageWithEmoji ---
33
+ export function CommunityImageWithEmoji({
34
+ imageUrl, name, emoji, size = 48,
35
+ }: { imageUrl?: string; name?: string; emoji?: string; size?: number }) {
36
+ return <UserImageWithEmoji imageUrl={imageUrl} displayName={name} size={size} emoji={emoji} />;
37
+ }
38
+
39
+ // --- FounderImageWithEmoji ---
40
+ export function FounderImageWithEmoji({
41
+ imageUrl, displayName, size = 48,
42
+ }: { imageUrl?: string; displayName?: string; size?: number }) {
43
+ return <UserImageWithEmoji imageUrl={imageUrl} displayName={displayName} size={size} emoji="star" />;
44
+ }
45
+
46
+ // --- UserImageWithTag ---
47
+ export function UserImageWithTag({
48
+ imageUrl, displayName, tag, tagColor, size = 48,
49
+ }: { imageUrl?: string; displayName?: string; tag: string; tagColor?: string; size?: number }) {
50
+ return (
51
+ <View>
52
+ <UserImage imageUrl={imageUrl} displayName={displayName} size={size} />
53
+ <View style={[styles.tagBadge, { backgroundColor: tagColor ?? Colors.purple1 }]}>
54
+ <Text style={styles.tagText}>{tag}</Text>
55
+ </View>
56
+ </View>
57
+ );
58
+ }
59
+
60
+ // --- WithProfileLink ---
61
+ export function WithProfileLink({
62
+ userId, children,
63
+ }: { userId: string; children: React.ReactNode }) {
64
+ const navigation = useNavigation();
65
+ return (
66
+ <Pressable onPress={() => (navigation as any).navigate('Profile', { userId })} accessibilityRole="link">
67
+ {children}
68
+ </Pressable>
69
+ );
70
+ }
71
+
72
+ // --- WithMessagingLink ---
73
+ export function WithMessagingLink({
74
+ connectionId, children,
75
+ }: { connectionId: string; children: React.ReactNode }) {
76
+ const navigation = useNavigation();
77
+ return (
78
+ <Pressable onPress={() => (navigation as any).navigate('Messaging', { connectionId })} accessibilityRole="link">
79
+ {children}
80
+ </Pressable>
81
+ );
82
+ }
83
+
84
+ // --- WithMessagingOrProfileLink ---
85
+ export function WithMessagingOrProfileLink({
86
+ userId, connectionId, children,
87
+ }: { userId: string; connectionId?: string; children: React.ReactNode }) {
88
+ const navigation = useNavigation();
89
+ const handlePress = () => {
90
+ if (connectionId) {
91
+ (navigation as any).navigate('Messaging', { connectionId });
92
+ } else {
93
+ (navigation as any).navigate('Profile', { userId });
94
+ }
95
+ };
96
+ return <Pressable onPress={handlePress} accessibilityRole="link">{children}</Pressable>;
97
+ }
98
+
99
+ const styles = StyleSheet.create({
100
+ emojiBadge: { position: 'absolute', backgroundColor: Colors.gray2, borderRadius: 10, padding: 2 },
101
+ emojiText: { fontSize: 12 },
102
+ tagBadge: { position: 'absolute', bottom: -4, right: -4, borderRadius: 8, paddingHorizontal: 6, paddingVertical: 2, borderWidth: 2, borderColor: Colors.gray9 },
103
+ tagText: { color: Colors.white, fontSize: 8, fontWeight: '700' },
104
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Wallet/Coupon components.
3
+ * Ported from squad-demo/src/components/wallet/*.
4
+ */
5
+ import React, { memo } from 'react';
6
+ import { View, Text, StyleSheet, Pressable, Share } from 'react-native';
7
+ import { Colors } from '../../theme/ThemeContext';
8
+ import Button from '../ux/buttons/Button';
9
+ import { BodySmall, BodyRegular, TitleSmall, SubtitleSmall } from '../ux/text/Typography';
10
+
11
+ // --- CouponCard ---
12
+ export const CouponCard = memo(function CouponCard({
13
+ code, title, description, discount, brandName, isRedeemed, onRedeem, onShare, primaryColor = Colors.purple1,
14
+ }: {
15
+ code: string; title: string; description?: string; discount?: string;
16
+ brandName?: string; isRedeemed: boolean; onRedeem?: () => void; onShare?: () => void; primaryColor?: string;
17
+ }) {
18
+ return (
19
+ <View style={[styles.couponCard, isRedeemed && styles.couponRedeemed]}>
20
+ <View style={styles.couponLeft}>
21
+ {brandName && <BodySmall style={styles.brandName}>{brandName}</BodySmall>}
22
+ <TitleSmall style={styles.couponTitle}>{title}</TitleSmall>
23
+ {description && <BodySmall style={styles.couponDesc} numberOfLines={2}>{description}</BodySmall>}
24
+ {discount && <SubtitleSmall style={[styles.couponDiscount, { color: primaryColor }]}>{discount}</SubtitleSmall>}
25
+ </View>
26
+ <View style={styles.couponRight}>
27
+ {isRedeemed ? (
28
+ <View style={styles.redeemedBadge}><BodySmall style={styles.redeemedText}>Redeemed</BodySmall></View>
29
+ ) : (
30
+ <View style={styles.couponActions}>
31
+ <Button style={[styles.redeemBtn, { backgroundColor: primaryColor }]} onPress={onRedeem}>
32
+ <Text style={styles.redeemText}>Redeem</Text>
33
+ </Button>
34
+ {onShare && (
35
+ <Button style={styles.shareBtn} onPress={onShare}>
36
+ <Text style={styles.shareText}>Share</Text>
37
+ </Button>
38
+ )}
39
+ </View>
40
+ )}
41
+ </View>
42
+ </View>
43
+ );
44
+ });
45
+
46
+ // --- CouponShareCard ---
47
+ export function CouponShareCard({ code, title, discount, onShare }: { code: string; title: string; discount?: string; onShare: () => void }) {
48
+ return (
49
+ <Pressable style={styles.shareCard} onPress={onShare}>
50
+ <TitleSmall style={styles.shareCardTitle}>{title}</TitleSmall>
51
+ {discount && <BodyRegular style={styles.shareCardDiscount}>{discount}</BodyRegular>}
52
+ <BodySmall style={styles.shareCardCode}>{code}</BodySmall>
53
+ <BodySmall style={styles.shareCardHint}>Tap to share</BodySmall>
54
+ </Pressable>
55
+ );
56
+ }
57
+
58
+ // --- WalletHeader ---
59
+ export function WalletHeader({ balance, primaryColor = Colors.purple1 }: { balance: number; primaryColor?: string }) {
60
+ return (
61
+ <View style={[styles.walletHeader, { borderColor: primaryColor }]}>
62
+ <BodySmall style={styles.balanceLabel}>Your Balance</BodySmall>
63
+ <Text style={[styles.balanceAmount, { color: primaryColor }]}>{balance}</Text>
64
+ <BodySmall style={styles.balanceUnit}>points</BodySmall>
65
+ </View>
66
+ );
67
+ }
68
+
69
+ // --- RedeemBottomSheet ---
70
+ export function RedeemBottomSheet({ coupon, onRedeem, onDismiss }: {
71
+ coupon: { code: string; title: string; description?: string }; onRedeem: () => void; onDismiss: () => void;
72
+ }) {
73
+ return (
74
+ <View style={styles.redeemSheet}>
75
+ <TitleSmall style={styles.redeemTitle}>{coupon.title}</TitleSmall>
76
+ {coupon.description && <BodyRegular style={styles.redeemDesc}>{coupon.description}</BodyRegular>}
77
+ <BodySmall style={styles.redeemCode}>Code: {coupon.code}</BodySmall>
78
+ <Button style={styles.redeemConfirmBtn} onPress={onRedeem}>
79
+ <Text style={styles.redeemConfirmText}>Confirm Redemption</Text>
80
+ </Button>
81
+ </View>
82
+ );
83
+ }
84
+
85
+ const styles = StyleSheet.create({
86
+ couponCard: { flexDirection: 'row', backgroundColor: Colors.gray2, borderRadius: 12, padding: 16, gap: 12 },
87
+ couponRedeemed: { opacity: 0.6 },
88
+ couponLeft: { flex: 1 },
89
+ couponRight: { justifyContent: 'center' },
90
+ brandName: { color: Colors.gray6, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 4 },
91
+ couponTitle: { color: Colors.white, marginBottom: 4 },
92
+ couponDesc: { color: Colors.gray6, marginBottom: 4 },
93
+ couponDiscount: { fontWeight: '600' },
94
+ couponActions: { gap: 4 },
95
+ redeemBtn: { paddingVertical: 8, paddingHorizontal: 16, borderRadius: 16 },
96
+ redeemText: { color: Colors.gray1, fontSize: 13, fontWeight: '600' },
97
+ shareBtn: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 12, borderWidth: 1, borderColor: Colors.gray5, alignItems: 'center' },
98
+ shareText: { color: Colors.white, fontSize: 12 },
99
+ redeemedBadge: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 12, backgroundColor: Colors.gray3 },
100
+ redeemedText: { color: Colors.gray6, fontSize: 12 },
101
+ shareCard: { backgroundColor: Colors.gray2, borderRadius: 12, padding: 16, alignItems: 'center' },
102
+ shareCardTitle: { color: Colors.white, marginBottom: 4 },
103
+ shareCardDiscount: { color: Colors.purple1, marginBottom: 8 },
104
+ shareCardCode: { color: Colors.gray6, letterSpacing: 2 },
105
+ shareCardHint: { color: Colors.gray6, marginTop: 4 },
106
+ walletHeader: { padding: 24, borderRadius: 16, borderWidth: 1.5, backgroundColor: Colors.gray2, alignItems: 'center' },
107
+ balanceLabel: { color: Colors.gray6, marginBottom: 4 },
108
+ balanceAmount: { fontSize: 48, fontWeight: '700', lineHeight: 56 },
109
+ balanceUnit: { color: Colors.gray6 },
110
+ redeemSheet: { padding: 24 },
111
+ redeemTitle: { color: Colors.white, marginBottom: 8 },
112
+ redeemDesc: { color: Colors.gray6, marginBottom: 12 },
113
+ redeemCode: { color: Colors.gray6, marginBottom: 20 },
114
+ redeemConfirmBtn: { paddingVertical: 14, borderRadius: 24, backgroundColor: Colors.purple1, alignItems: 'center' },
115
+ redeemConfirmText: { color: Colors.gray1, fontWeight: '600' },
116
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * AuthContext — provides auth state to the React tree.
3
+ * Ported from squad-demo/src/contexts/AuthContext.tsx.
4
+ */
5
+ import React, { createContext, useContext, useEffect, useState } from 'react';
6
+ import type { User } from '@squad-sports/core';
7
+
8
+ interface AuthContextValue {
9
+ isAuthenticated: boolean;
10
+ isLoading: boolean;
11
+ user: User | null;
12
+ accessToken: string | null;
13
+ }
14
+
15
+ const AuthCtx = createContext<AuthContextValue>({
16
+ isAuthenticated: false,
17
+ isLoading: true,
18
+ user: null,
19
+ accessToken: null,
20
+ });
21
+
22
+ export function useAuth(): AuthContextValue {
23
+ return useContext(AuthCtx);
24
+ }
25
+
26
+ export function AuthContextProvider({
27
+ children,
28
+ user,
29
+ accessToken,
30
+ isLoading,
31
+ }: {
32
+ children: React.ReactNode;
33
+ user: User | null;
34
+ accessToken: string | null;
35
+ isLoading: boolean;
36
+ }) {
37
+ const value: AuthContextValue = {
38
+ isAuthenticated: !!accessToken && !!user,
39
+ isLoading,
40
+ user,
41
+ accessToken,
42
+ };
43
+
44
+ return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
45
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * EventProcessor context — provides EventProcessor instance to the React tree.
3
+ * Ported from squad-demo/src/contexts/EventProcessorContext.tsx + EventProcessorSetup.tsx.
4
+ */
5
+ import React, { createContext, useContext, useEffect } from 'react';
6
+ import { EventProcessor } from '../realtime/EventProcessor';
7
+ import { useApiClient } from '../SquadProvider';
8
+
9
+ const EventProcessorCtx = createContext<EventProcessor>(EventProcessor.shared);
10
+
11
+ export function useEventProcessor(): EventProcessor {
12
+ return useContext(EventProcessorCtx);
13
+ }
14
+
15
+ /**
16
+ * Sets up EventProcessor with the current API client.
17
+ * Wire this into the provider chain to auto-connect SSE.
18
+ */
19
+ export function EventProcessorSetup({ children }: { children: React.ReactNode }) {
20
+ const apiClient = useApiClient();
21
+
22
+ useEffect(() => {
23
+ const processor = EventProcessor.shared;
24
+ processor.setApiClient(apiClient);
25
+
26
+ if (apiClient.currentToken) {
27
+ processor.setShouldAllowEvents(true);
28
+ processor.connect();
29
+ }
30
+
31
+ return () => {
32
+ // Don't disconnect — EventProcessor is a singleton
33
+ };
34
+ }, [apiClient]);
35
+
36
+ return (
37
+ <EventProcessorCtx.Provider value={EventProcessor.shared}>
38
+ {children}
39
+ </EventProcessorCtx.Provider>
40
+ );
41
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * PlayerQueueContext — manages a queue of audio players so only one plays at a time.
3
+ * Ported from squad-demo/src/contexts/player-queue.tsx + player-index.tsx.
4
+ */
5
+ import React, { createContext, useContext, useCallback, useRef, useState } from 'react';
6
+ import { Audio } from 'expo-av';
7
+
8
+ interface PlayerQueueItem {
9
+ id: string;
10
+ sound: Audio.Sound;
11
+ }
12
+
13
+ interface PlayerQueueContextValue {
14
+ /** Register a player and get its ID */
15
+ register: (id: string, sound: Audio.Sound) => void;
16
+ /** Unregister a player */
17
+ unregister: (id: string) => void;
18
+ /** Play a specific player (pauses all others) */
19
+ play: (id: string) => void;
20
+ /** Pause the currently playing player */
21
+ pause: () => void;
22
+ /** Stop all players */
23
+ stopAll: () => void;
24
+ /** Currently playing player ID */
25
+ activeId: string | null;
26
+ }
27
+
28
+ const PlayerQueueCtx = createContext<PlayerQueueContextValue>({
29
+ register: () => {},
30
+ unregister: () => {},
31
+ play: () => {},
32
+ pause: () => {},
33
+ stopAll: () => {},
34
+ activeId: null,
35
+ });
36
+
37
+ export function usePlayerQueue() {
38
+ return useContext(PlayerQueueCtx);
39
+ }
40
+
41
+ export function PlayerQueueProvider({ children }: { children: React.ReactNode }) {
42
+ const players = useRef(new Map<string, Audio.Sound>());
43
+ const [activeId, setActiveId] = useState<string | null>(null);
44
+
45
+ const register = useCallback((id: string, sound: Audio.Sound) => {
46
+ players.current.set(id, sound);
47
+ }, []);
48
+
49
+ const unregister = useCallback((id: string) => {
50
+ players.current.delete(id);
51
+ if (activeId === id) setActiveId(null);
52
+ }, [activeId]);
53
+
54
+ const play = useCallback(async (id: string) => {
55
+ // Pause all others
56
+ for (const [playerId, sound] of players.current) {
57
+ if (playerId !== id) {
58
+ try { await sound.pauseAsync(); } catch {}
59
+ }
60
+ }
61
+
62
+ // Play target
63
+ const sound = players.current.get(id);
64
+ if (sound) {
65
+ try {
66
+ await Audio.setAudioModeAsync({ playsInSilentModeIOS: true });
67
+ await sound.playAsync();
68
+ setActiveId(id);
69
+ } catch {}
70
+ }
71
+ }, []);
72
+
73
+ const pause = useCallback(async () => {
74
+ if (activeId) {
75
+ const sound = players.current.get(activeId);
76
+ if (sound) {
77
+ try { await sound.pauseAsync(); } catch {}
78
+ }
79
+ setActiveId(null);
80
+ }
81
+ }, [activeId]);
82
+
83
+ const stopAll = useCallback(async () => {
84
+ for (const sound of players.current.values()) {
85
+ try { await sound.stopAsync(); } catch {}
86
+ }
87
+ setActiveId(null);
88
+ }, []);
89
+
90
+ return (
91
+ <PlayerQueueCtx.Provider value={{ register, unregister, play, pause, stopAll, activeId }}>
92
+ {children}
93
+ </PlayerQueueCtx.Provider>
94
+ );
95
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Auth hook — provides auth state from Recoil atoms.
3
+ * Ported from squad-demo/src/hooks/useAuth.ts.
4
+ */
5
+ import { useRecoilValue } from 'recoil';
6
+ import { reActiveAccessToken, reActiveUserId, reAuthHydrated } from '../state/session';
7
+ import { reUserCache } from '../state/user';
8
+ import type { User } from '@squad-sports/core';
9
+
10
+ export function useAuth() {
11
+ const accessToken = useRecoilValue(reActiveAccessToken);
12
+ const userId = useRecoilValue(reActiveUserId);
13
+ const hydrated = useRecoilValue(reAuthHydrated);
14
+ const user = useRecoilValue(reUserCache);
15
+
16
+ return {
17
+ isAuthenticated: !!accessToken && !!userId,
18
+ isLoading: !hydrated,
19
+ accessToken,
20
+ userId,
21
+ user: user as User | null,
22
+ };
23
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Data refresh hook — marks data stale and triggers Recoil refresh.
3
+ * Ported from squad-demo/src/hooks/useDataRefresh.ts.
4
+ */
5
+ import { useCallback } from 'react';
6
+ import { useSetRecoilState } from 'recoil';
7
+ import { reLastRefreshed, reRefreshing } from '../state/sync/refresh';
8
+
9
+ export function useDataRefresh() {
10
+ const setLastRefreshed = useSetRecoilState(reLastRefreshed);
11
+ const setRefreshing = useSetRecoilState(reRefreshing);
12
+
13
+ const refreshAllData = useCallback(() => {
14
+ setRefreshing(true);
15
+ setLastRefreshed(Date.now());
16
+ // The refresh propagates to all DependableAtoms that subscribe to reRefresh
17
+ setTimeout(() => setRefreshing(false), 500);
18
+ }, [setLastRefreshed, setRefreshing]);
19
+
20
+ const checkForStaleData = useCallback(async (): Promise<boolean> => {
21
+ // Check if any data is older than 5 minutes
22
+ return false; // Simplified — full implementation checks cache ages
23
+ }, []);
24
+
25
+ const clearStaleMarkers = useCallback(() => {
26
+ // Clear stale data flags
27
+ }, []);
28
+
29
+ return { refreshAllData, checkForStaleData, clearStaleMarkers };
30
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * EventProcessor hook.
3
+ * Ported from squad-demo/src/hooks/useEventProcessor.ts.
4
+ */
5
+ import { useEventProcessor } from '../contexts/EventProcessorContext';
6
+ export { useEventProcessor };