@dubsdotapp/expo 0.5.19 → 0.5.20
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/dist/index.js +67 -88
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +67 -88
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ui/AuthGate.tsx +107 -112
package/package.json
CHANGED
package/src/ui/AuthGate.tsx
CHANGED
|
@@ -71,7 +71,7 @@ export function AuthGate({
|
|
|
71
71
|
appName = 'Dubs',
|
|
72
72
|
accentColor,
|
|
73
73
|
}: AuthGateProps) {
|
|
74
|
-
const { client, pushEnabled } = useDubs();
|
|
74
|
+
const { client, pushEnabled, uiConfig } = useDubs();
|
|
75
75
|
const auth = useAuth();
|
|
76
76
|
const [phase, setPhase] = useState<'init' | 'active'>('init');
|
|
77
77
|
const [registrationPhase, setRegistrationPhase] = useState(false);
|
|
@@ -105,12 +105,20 @@ export function AuthGate({
|
|
|
105
105
|
if (auth.status === 'needsRegistration') setRegistrationPhase(true);
|
|
106
106
|
}, [auth.status]);
|
|
107
107
|
|
|
108
|
-
// Show push setup after new registration completes (not restored sessions)
|
|
108
|
+
// Show push setup after new registration completes (not restored sessions).
|
|
109
|
+
// Gated on pushConfigured.android — if the developer hasn't uploaded FCM credentials
|
|
110
|
+
// for this app in the dev portal, push can't be delivered, so don't prompt.
|
|
109
111
|
useEffect(() => {
|
|
110
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
pushEnabled &&
|
|
114
|
+
uiConfig.pushConfigured?.android &&
|
|
115
|
+
auth.status === 'authenticated' &&
|
|
116
|
+
registrationPhase &&
|
|
117
|
+
!isRestoredSession
|
|
118
|
+
) {
|
|
111
119
|
setShowPushSetup(true);
|
|
112
120
|
}
|
|
113
|
-
}, [pushEnabled, auth.status, registrationPhase, isRestoredSession]);
|
|
121
|
+
}, [pushEnabled, uiConfig.pushConfigured?.android, auth.status, registrationPhase, isRestoredSession]);
|
|
114
122
|
|
|
115
123
|
useEffect(() => {
|
|
116
124
|
if (auth.token) onSaveToken(auth.token);
|
|
@@ -137,16 +145,15 @@ export function AuthGate({
|
|
|
137
145
|
return <DefaultLoadingScreen status="authenticating" appName={appName} accentColor={accentColor} />;
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
148
|
+
// Keep the registration screen mounted through the push step so the transition
|
|
149
|
+
// from step 3 (referral) → step 4 (push) animates as a continuation of the same flow.
|
|
150
|
+
const inRegistrationFlow =
|
|
151
|
+
registrationPhase &&
|
|
152
|
+
(auth.status === 'needsRegistration' ||
|
|
153
|
+
auth.status === 'registering' ||
|
|
154
|
+
(auth.status === 'authenticated' && showPushSetup));
|
|
155
|
+
|
|
156
|
+
if (auth.status === 'authenticated' && !inRegistrationFlow) {
|
|
150
157
|
return (
|
|
151
158
|
<AuthContext.Provider value={auth}>
|
|
152
159
|
{pushEnabled && <PushTokenRestorer />}
|
|
@@ -155,7 +162,7 @@ export function AuthGate({
|
|
|
155
162
|
);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
if (
|
|
165
|
+
if (inRegistrationFlow) {
|
|
159
166
|
const isRegistering = auth.status === 'registering';
|
|
160
167
|
const regError = auth.status === 'error' ? auth.error : null;
|
|
161
168
|
if (renderRegistration) {
|
|
@@ -169,6 +176,8 @@ export function AuthGate({
|
|
|
169
176
|
client={client}
|
|
170
177
|
appName={appName}
|
|
171
178
|
accentColor={accentColor}
|
|
179
|
+
pushStepActive={showPushSetup}
|
|
180
|
+
onPushComplete={() => setShowPushSetup(false)}
|
|
172
181
|
/>
|
|
173
182
|
);
|
|
174
183
|
}
|
|
@@ -296,9 +305,17 @@ function DefaultRegistrationScreen({
|
|
|
296
305
|
client,
|
|
297
306
|
appName,
|
|
298
307
|
accentColor,
|
|
299
|
-
|
|
308
|
+
pushStepActive,
|
|
309
|
+
onPushComplete,
|
|
310
|
+
}: RegistrationScreenProps & {
|
|
311
|
+
appName: string;
|
|
312
|
+
accentColor?: string;
|
|
313
|
+
pushStepActive?: boolean;
|
|
314
|
+
onPushComplete?: () => void;
|
|
315
|
+
}) {
|
|
300
316
|
const t = useDubsTheme();
|
|
301
317
|
const accent = accentColor || t.accent;
|
|
318
|
+
const push = usePushNotifications();
|
|
302
319
|
|
|
303
320
|
// ── Shared state ──
|
|
304
321
|
const [step, setStep] = useState(0);
|
|
@@ -563,11 +580,84 @@ function DefaultRegistrationScreen({
|
|
|
563
580
|
</View>
|
|
564
581
|
);
|
|
565
582
|
|
|
583
|
+
// Advance to the push step when the parent signals (after auth completes).
|
|
584
|
+
// Uses the same animateToStep transition as steps 0→1→2 so it feels like
|
|
585
|
+
// a continuation, not a separate modal swap.
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
if (pushStepActive && step !== 3) {
|
|
588
|
+
animateToStep(3);
|
|
589
|
+
}
|
|
590
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
591
|
+
}, [pushStepActive]);
|
|
592
|
+
|
|
593
|
+
// ── Step 4: Push Notifications ──
|
|
594
|
+
const handleEnablePush = async () => {
|
|
595
|
+
try { await push.register(); } catch {}
|
|
596
|
+
onPushComplete?.();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const renderPushStep = () => (
|
|
600
|
+
<View style={s.stepContainer}>
|
|
601
|
+
<View style={s.stepTop}>
|
|
602
|
+
<Text style={[s.title, { color: t.text }]}>Enable Notifications</Text>
|
|
603
|
+
<Text style={[s.subtitle, { color: t.textMuted }]}>
|
|
604
|
+
Stay in the loop with real-time updates
|
|
605
|
+
</Text>
|
|
606
|
+
<StepIndicator currentStep={3} />
|
|
607
|
+
|
|
608
|
+
<View style={pushStyles.iconContainer}>
|
|
609
|
+
<View style={[pushStyles.bellCircle, { backgroundColor: accent + '20' }]}>
|
|
610
|
+
<Text style={[pushStyles.bellIcon, { color: accent }]}>{'🔔'}</Text>
|
|
611
|
+
</View>
|
|
612
|
+
</View>
|
|
613
|
+
|
|
614
|
+
<View style={pushStyles.benefitsList}>
|
|
615
|
+
<Text style={[pushStyles.benefitsHeader, { color: t.text }]}>
|
|
616
|
+
Get real-time updates when:
|
|
617
|
+
</Text>
|
|
618
|
+
{[
|
|
619
|
+
'A fight you picked on goes LIVE',
|
|
620
|
+
'Your pick wins or loses',
|
|
621
|
+
'Final results and rankings',
|
|
622
|
+
].map((item, i) => (
|
|
623
|
+
<View key={i} style={pushStyles.benefitRow}>
|
|
624
|
+
<View style={[pushStyles.bulletDot, { backgroundColor: accent }]} />
|
|
625
|
+
<Text style={[pushStyles.benefitText, { color: t.textMuted }]}>{item}</Text>
|
|
626
|
+
</View>
|
|
627
|
+
))}
|
|
628
|
+
</View>
|
|
629
|
+
</View>
|
|
630
|
+
|
|
631
|
+
<View style={s.bottomRow}>
|
|
632
|
+
<TouchableOpacity
|
|
633
|
+
style={[s.secondaryBtn, { borderColor: t.border }]}
|
|
634
|
+
onPress={onPushComplete}
|
|
635
|
+
activeOpacity={0.7}
|
|
636
|
+
>
|
|
637
|
+
<Text style={[s.secondaryBtnText, { color: t.textMuted }]}>Maybe Later</Text>
|
|
638
|
+
</TouchableOpacity>
|
|
639
|
+
<TouchableOpacity
|
|
640
|
+
style={[s.primaryBtn, { backgroundColor: accent, flex: 1, opacity: push.loading ? 0.7 : 1 }]}
|
|
641
|
+
onPress={handleEnablePush}
|
|
642
|
+
disabled={push.loading}
|
|
643
|
+
activeOpacity={0.8}
|
|
644
|
+
>
|
|
645
|
+
{push.loading ? (
|
|
646
|
+
<ActivityIndicator color="#FFFFFF" size="small" />
|
|
647
|
+
) : (
|
|
648
|
+
<Text style={s.primaryBtnText}>Enable Notifications</Text>
|
|
649
|
+
)}
|
|
650
|
+
</TouchableOpacity>
|
|
651
|
+
</View>
|
|
652
|
+
</View>
|
|
653
|
+
);
|
|
654
|
+
|
|
566
655
|
const renderStep = () => {
|
|
567
656
|
switch (step) {
|
|
568
657
|
case 0: return renderAvatarStep();
|
|
569
658
|
case 1: return renderUsernameStep();
|
|
570
659
|
case 2: return renderReferralStep();
|
|
660
|
+
case 3: return renderPushStep();
|
|
571
661
|
default: return null;
|
|
572
662
|
}
|
|
573
663
|
};
|
|
@@ -598,7 +688,7 @@ function DefaultRegistrationScreen({
|
|
|
598
688
|
// ── Push Token Restorer (silent, zero UI) ──
|
|
599
689
|
// Runs inside DubsContext + AuthGate when user is authenticated.
|
|
600
690
|
// Silently re-registers the push token for returning users who already granted permission.
|
|
601
|
-
// Does NOT prompt — the prompt only happens in
|
|
691
|
+
// Does NOT prompt — the prompt only happens in step 4 of DefaultRegistrationScreen during onboarding.
|
|
602
692
|
|
|
603
693
|
function PushTokenRestorer() {
|
|
604
694
|
const push = usePushNotifications();
|
|
@@ -614,101 +704,6 @@ function PushTokenRestorer() {
|
|
|
614
704
|
return null;
|
|
615
705
|
}
|
|
616
706
|
|
|
617
|
-
// ── Push Setup Screen (Step 3 of onboarding) ──
|
|
618
|
-
|
|
619
|
-
function PushSetupScreen({
|
|
620
|
-
accentColor,
|
|
621
|
-
appName,
|
|
622
|
-
onComplete,
|
|
623
|
-
}: {
|
|
624
|
-
accentColor?: string;
|
|
625
|
-
appName: string;
|
|
626
|
-
onComplete: () => void;
|
|
627
|
-
}) {
|
|
628
|
-
const t = useDubsTheme();
|
|
629
|
-
const accent = accentColor || t.accent;
|
|
630
|
-
const push = usePushNotifications();
|
|
631
|
-
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
632
|
-
const slideAnim = useRef(new Animated.Value(30)).current;
|
|
633
|
-
|
|
634
|
-
useEffect(() => {
|
|
635
|
-
Animated.parallel([
|
|
636
|
-
Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
637
|
-
Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true }),
|
|
638
|
-
]).start();
|
|
639
|
-
}, [fadeAnim, slideAnim]);
|
|
640
|
-
|
|
641
|
-
const handleEnable = async () => {
|
|
642
|
-
await push.register();
|
|
643
|
-
onComplete();
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
const benefits = [
|
|
647
|
-
'A fight you picked on goes LIVE',
|
|
648
|
-
'Your pick wins or loses',
|
|
649
|
-
'Final results and rankings',
|
|
650
|
-
];
|
|
651
|
-
|
|
652
|
-
return (
|
|
653
|
-
<View style={[s.container, { backgroundColor: t.background }]}>
|
|
654
|
-
<Animated.View
|
|
655
|
-
style={[
|
|
656
|
-
s.stepContainer,
|
|
657
|
-
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] },
|
|
658
|
-
]}
|
|
659
|
-
>
|
|
660
|
-
<View style={s.stepTop}>
|
|
661
|
-
<Text style={[s.title, { color: t.text }]}>Enable Notifications</Text>
|
|
662
|
-
<Text style={[s.subtitle, { color: t.textMuted }]}>
|
|
663
|
-
Stay in the loop with real-time updates
|
|
664
|
-
</Text>
|
|
665
|
-
<StepIndicator currentStep={3} />
|
|
666
|
-
|
|
667
|
-
<View style={pushStyles.iconContainer}>
|
|
668
|
-
<View style={[pushStyles.bellCircle, { backgroundColor: accent + '20' }]}>
|
|
669
|
-
<Text style={[pushStyles.bellIcon, { color: accent }]}>{'\uD83D\uDD14'}</Text>
|
|
670
|
-
</View>
|
|
671
|
-
</View>
|
|
672
|
-
|
|
673
|
-
<View style={pushStyles.benefitsList}>
|
|
674
|
-
<Text style={[pushStyles.benefitsHeader, { color: t.text }]}>
|
|
675
|
-
Get real-time updates when:
|
|
676
|
-
</Text>
|
|
677
|
-
{benefits.map((item, i) => (
|
|
678
|
-
<View key={i} style={pushStyles.benefitRow}>
|
|
679
|
-
<View style={[pushStyles.bulletDot, { backgroundColor: accent }]} />
|
|
680
|
-
<Text style={[pushStyles.benefitText, { color: t.textMuted }]}>{item}</Text>
|
|
681
|
-
</View>
|
|
682
|
-
))}
|
|
683
|
-
</View>
|
|
684
|
-
</View>
|
|
685
|
-
|
|
686
|
-
<View style={s.bottomRow}>
|
|
687
|
-
<TouchableOpacity
|
|
688
|
-
style={[s.secondaryBtn, { borderColor: t.border }]}
|
|
689
|
-
onPress={onComplete}
|
|
690
|
-
activeOpacity={0.7}
|
|
691
|
-
>
|
|
692
|
-
<Text style={[s.secondaryBtnText, { color: t.textMuted }]}>Maybe Later</Text>
|
|
693
|
-
</TouchableOpacity>
|
|
694
|
-
<TouchableOpacity
|
|
695
|
-
style={[s.primaryBtn, { backgroundColor: accent, flex: 1, opacity: push.loading ? 0.7 : 1 }]}
|
|
696
|
-
onPress={handleEnable}
|
|
697
|
-
disabled={push.loading}
|
|
698
|
-
activeOpacity={0.8}
|
|
699
|
-
>
|
|
700
|
-
{push.loading ? (
|
|
701
|
-
<ActivityIndicator color="#FFFFFF" size="small" />
|
|
702
|
-
) : (
|
|
703
|
-
<Text style={s.primaryBtnText}>Enable Notifications</Text>
|
|
704
|
-
)}
|
|
705
|
-
</TouchableOpacity>
|
|
706
|
-
</View>
|
|
707
|
-
</Animated.View>
|
|
708
|
-
</View>
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
707
|
const pushStyles = StyleSheet.create({
|
|
713
708
|
iconContainer: { alignItems: 'center', marginVertical: 24 },
|
|
714
709
|
bellCircle: { width: 100, height: 100, borderRadius: 50, justifyContent: 'center', alignItems: 'center' },
|