mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
SELF-1680: Starfall mobile push notifications (#1548)
* move to personal mcp * add new nova pin screen * rename screen * update nova route * unblock local dev building * rename nova to starfall * move to dev dependency * move to dependencies * add correct package * save wip * save wip * save wip fixes * rename self logo * fix screen logos * fix order * add starfall api to fetch push notification code * agent feedback * fix tests, minor agent feedback * abstract component * rename topic * re-add button props * fix linting
This commit is contained in:
3
app/src/assets/icons/checkmark_white.svg
Normal file
3
app/src/assets/icons/checkmark_white.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.0459 12.3135C0.360352 11.6221 0.0146484 10.9219 0.00878906 10.2129C0.00292969 9.50391 0.342773 8.80664 1.02832 8.12109L8.12988 1.02832C8.80957 0.342773 9.50391 0.00292969 10.2129 0.00878906C10.9277 0.0146484 11.6309 0.360352 12.3223 1.0459L19.3799 8.10352C20.0654 8.79492 20.4111 9.49805 20.417 10.2129C20.4229 10.9219 20.083 11.6191 19.3975 12.3047L12.3047 19.3975C11.6191 20.083 10.9219 20.4229 10.2129 20.417C9.50391 20.4111 8.80371 20.0654 8.1123 19.3799L1.0459 12.3135ZM9.24609 14.5723C9.42188 14.5723 9.58301 14.5312 9.72949 14.4492C9.88184 14.3613 10.0107 14.2412 10.1162 14.0889L14.2207 7.74316C14.2852 7.64355 14.3379 7.53809 14.3789 7.42676C14.4258 7.31543 14.4492 7.2041 14.4492 7.09277C14.4492 6.84668 14.3555 6.64746 14.168 6.49512C13.9863 6.34277 13.7783 6.2666 13.5439 6.2666C13.2334 6.2666 12.9727 6.43359 12.7617 6.76758L9.21973 12.4277L7.59375 10.3887C7.47656 10.2422 7.35938 10.1396 7.24219 10.0811C7.125 10.0166 6.99316 9.98438 6.84668 9.98438C6.60059 9.98438 6.39258 10.0723 6.22266 10.248C6.05273 10.418 5.96777 10.626 5.96777 10.8721C5.96777 10.9893 5.98828 11.1035 6.0293 11.2148C6.07617 11.3203 6.14062 11.4287 6.22266 11.54L8.34082 14.0889C8.46973 14.2529 8.60742 14.376 8.75391 14.458C8.90039 14.5342 9.06445 14.5723 9.24609 14.5723Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/assets/images/bg_starfall_push.png
Normal file
BIN
app/src/assets/images/bg_starfall_push.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
12
app/src/assets/logos/opera_minipay.svg
Normal file
12
app/src/assets/logos/opera_minipay.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
40
app/src/components/starfall/StarfallLogoHeader.tsx
Normal file
40
app/src/components/starfall/StarfallLogoHeader.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { View, XStack } from 'tamagui';
|
||||
|
||||
import { black, zinc800 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import CheckmarkIcon from '@/assets/icons/checkmark_white.svg';
|
||||
import OperaLogo from '@/assets/logos/opera_minipay.svg';
|
||||
import SelfLogo from '@/assets/logos/self.svg';
|
||||
|
||||
export const StarfallLogoHeader: React.FC = () => (
|
||||
<XStack gap={10} alignItems="center" marginBottom={20}>
|
||||
{/* Opera MiniPay logo */}
|
||||
<View width={46} height={46} borderRadius={3} overflow="hidden">
|
||||
<OperaLogo width={46} height={46} />
|
||||
</View>
|
||||
|
||||
{/* Checkmark icon */}
|
||||
<View width={32} height={32}>
|
||||
<CheckmarkIcon width={32} height={32} />
|
||||
</View>
|
||||
|
||||
{/* Self logo */}
|
||||
<View
|
||||
width={46}
|
||||
height={46}
|
||||
backgroundColor={black}
|
||||
borderRadius={3}
|
||||
borderWidth={1}
|
||||
borderColor={zinc800}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<SelfLogo width={28} height={28} />
|
||||
</View>
|
||||
</XStack>
|
||||
);
|
||||
62
app/src/components/starfall/StarfallPIN.tsx
Normal file
62
app/src/components/starfall/StarfallPIN.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { Text, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
export interface StarfallPINProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const StarfallPIN: React.FC<StarfallPINProps> = ({ code }) => {
|
||||
// Split the code into individual digits (expects 4 digits)
|
||||
const digits = code.split('').slice(0, 4);
|
||||
|
||||
// Pad with empty strings if less than 4 digits
|
||||
while (digits.length < 4) {
|
||||
digits.push('');
|
||||
}
|
||||
|
||||
return (
|
||||
<XStack
|
||||
gap={6}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
padding={4}
|
||||
borderRadius={12}
|
||||
borderWidth={1}
|
||||
borderColor="#52525b"
|
||||
backgroundColor="rgba(0, 0, 0, 0.4)"
|
||||
width="100%"
|
||||
>
|
||||
{digits.map((digit, index) => (
|
||||
<YStack
|
||||
key={index}
|
||||
flex={1}
|
||||
height={80}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
borderRadius={8}
|
||||
borderWidth={1}
|
||||
borderColor="rgba(255, 255, 255, 0.2)"
|
||||
paddingHorizontal={12}
|
||||
>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={32}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
letterSpacing={-1}
|
||||
lineHeight={32}
|
||||
>
|
||||
{digit}
|
||||
</Text>
|
||||
</YStack>
|
||||
))}
|
||||
</XStack>
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import documentsScreens from '@/navigation/documents';
|
||||
import homeScreens from '@/navigation/home';
|
||||
import onboardingScreens from '@/navigation/onboarding';
|
||||
import sharedScreens from '@/navigation/shared';
|
||||
import starfallScreens from '@/navigation/starfall';
|
||||
import verificationScreens from '@/navigation/verification';
|
||||
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
|
||||
import type { WebViewScreenParams } from '@/screens/shared/WebViewScreen';
|
||||
@@ -41,6 +42,7 @@ export const navigationScreens = {
|
||||
...verificationScreens,
|
||||
...accountScreens,
|
||||
...sharedScreens,
|
||||
...starfallScreens,
|
||||
...devScreens, // allow in production for testing
|
||||
};
|
||||
|
||||
@@ -158,6 +160,7 @@ export type RootStackParamList = Omit<
|
||||
Gratification: {
|
||||
points?: number;
|
||||
};
|
||||
StarfallPushCode: undefined;
|
||||
|
||||
// Home screens
|
||||
Home: {
|
||||
|
||||
18
app/src/navigation/starfall.ts
Normal file
18
app/src/navigation/starfall.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
|
||||
import StarfallPushCodeScreen from '@/screens/starfall/StarfallPushCodeScreen';
|
||||
|
||||
const starfallScreens = {
|
||||
StarfallPushCode: {
|
||||
screen: StarfallPushCodeScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
};
|
||||
|
||||
export default starfallScreens;
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot, dinotBold } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import LogoWhite from '@/assets/icons/logo_white.svg';
|
||||
import GratificationBg from '@/assets/images/gratification_bg.svg';
|
||||
import SelfLogo from '@/assets/logos/self.svg';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
const GratificationScreen: React.FC = () => {
|
||||
@@ -160,7 +160,7 @@ const GratificationScreen: React.FC = () => {
|
||||
>
|
||||
{/* Logo icon */}
|
||||
<View marginBottom={12} style={styles.logoContainer}>
|
||||
<LogoWhite width={37} height={37} />
|
||||
<SelfLogo width={37} height={37} />
|
||||
</View>
|
||||
|
||||
{/* Points display */}
|
||||
|
||||
@@ -660,11 +660,11 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
>
|
||||
<YStack gap="$2">
|
||||
<TopicToggleButton
|
||||
label="Nova"
|
||||
label="Starfall"
|
||||
isSubscribed={
|
||||
hasNotificationPermission && subscribedTopics.includes('nova')
|
||||
}
|
||||
onToggle={() => handleTopicToggle(['nova'], 'Nova')}
|
||||
onToggle={() => handleTopicToggle(['nova'], 'Starfall')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="General"
|
||||
@@ -675,7 +675,7 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
onToggle={() => handleTopicToggle(['general'], 'General')}
|
||||
/>
|
||||
<TopicToggleButton
|
||||
label="Both (Nova + General)"
|
||||
label="Both (Starfall + General)"
|
||||
isSubscribed={
|
||||
hasNotificationPermission &&
|
||||
subscribedTopics.includes('nova') &&
|
||||
|
||||
255
app/src/screens/starfall/StarfallPushCodeScreen.tsx
Normal file
255
app/src/screens/starfall/StarfallPushCodeScreen.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ImageBackground, StyleSheet } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { Text, View, YStack } from 'tamagui';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import {
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
green500,
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { advercase, dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import StarfallBackground from '@/assets/images/bg_starfall_push.png';
|
||||
import { StarfallLogoHeader } from '@/components/starfall/StarfallLogoHeader';
|
||||
import { StarfallPIN } from '@/components/starfall/StarfallPIN';
|
||||
import { confirmTap } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import { getOrGeneratePointsAddress } from '@/providers/authProvider';
|
||||
import { fetchPushCode } from '@/services/starfall/pushCodeService';
|
||||
|
||||
const DASH_CODE = '----';
|
||||
|
||||
const StarfallPushCodeScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleFetchCode = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
confirmTap();
|
||||
|
||||
const walletAddress = await getOrGeneratePointsAddress();
|
||||
const fetchedCode = await fetchPushCode(walletAddress);
|
||||
|
||||
setCode(fetchedCode);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch push code:', err);
|
||||
setError('Failed to generate code. Please try again.');
|
||||
setCode(null); // Clear stale code on error
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (copyTimeoutRef.current) {
|
||||
clearTimeout(copyTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleRetry = () => {
|
||||
handleFetchCode();
|
||||
};
|
||||
|
||||
const handleCopyCode = async () => {
|
||||
if (!code || code === DASH_CODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
confirmTap();
|
||||
await Clipboard.setString(code);
|
||||
setIsCopied(true);
|
||||
|
||||
// Clear any existing timeout before creating a new one
|
||||
if (copyTimeoutRef.current) {
|
||||
clearTimeout(copyTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Reset after 1.65 seconds
|
||||
copyTimeoutRef.current = setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
copyTimeoutRef.current = null;
|
||||
}, 1650);
|
||||
} catch (copyError) {
|
||||
console.error('Failed to copy to clipboard:', copyError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
confirmTap();
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black}>
|
||||
{/* Colorful background image */}
|
||||
<ImageBackground
|
||||
source={StarfallBackground}
|
||||
style={StyleSheet.absoluteFill}
|
||||
resizeMode="cover"
|
||||
>
|
||||
{/* Fade to black overlay - stronger at bottom */}
|
||||
<LinearGradient
|
||||
colors={['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0.6)', black]}
|
||||
locations={[0.1, 0.45, 0.6]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</ImageBackground>
|
||||
|
||||
{/* Content container */}
|
||||
<YStack flex={1} justifyContent="center" alignItems="center">
|
||||
{/* App logos section */}
|
||||
<StarfallLogoHeader />
|
||||
|
||||
{/* Title and content */}
|
||||
<YStack
|
||||
paddingHorizontal={20}
|
||||
paddingVertical={20}
|
||||
gap={12}
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<Text
|
||||
fontFamily={advercase}
|
||||
fontSize={28}
|
||||
fontWeight="400"
|
||||
color={white}
|
||||
textAlign="center"
|
||||
letterSpacing={1}
|
||||
>
|
||||
Your Starfall code awaits
|
||||
</Text>
|
||||
|
||||
<YStack gap={16} width="100%" alignItems="center">
|
||||
<View paddingHorizontal={40} width="100%">
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={14}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
textAlign="center"
|
||||
>
|
||||
Open Starfall in Opera MiniPay and enter this four digit code
|
||||
to continue your journey.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View width="100%">
|
||||
<StarfallPIN
|
||||
code={
|
||||
code === null || isLoading || error !== null
|
||||
? DASH_CODE
|
||||
: code
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={14}
|
||||
fontWeight="500"
|
||||
color="#ef4444"
|
||||
textAlign="center"
|
||||
>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
backgroundColor={black}
|
||||
style={{ backgroundColor: black }}
|
||||
>
|
||||
{/* Bottom buttons */}
|
||||
<YStack gap={10} width="100%">
|
||||
{/* Debug: Fetch code button or Retry button on error */}
|
||||
{error ? (
|
||||
<PrimaryButton
|
||||
onPress={handleRetry}
|
||||
disabled={isLoading}
|
||||
fontSize={16}
|
||||
style={{
|
||||
borderColor: '#374151',
|
||||
borderWidth: 1,
|
||||
borderRadius: 60,
|
||||
height: 46,
|
||||
paddingVertical: 0,
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onPress={handleFetchCode}
|
||||
disabled={isLoading}
|
||||
fontSize={16}
|
||||
style={{
|
||||
borderColor: '#374151',
|
||||
borderWidth: 1,
|
||||
borderRadius: 60,
|
||||
height: 46,
|
||||
paddingVertical: 0,
|
||||
}}
|
||||
>
|
||||
{isLoading ? 'Fetching...' : 'Fetch code'}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
|
||||
<PrimaryButton
|
||||
onPress={handleCopyCode}
|
||||
disabled={isCopied || !code || code === DASH_CODE || isLoading}
|
||||
fontSize={16}
|
||||
style={{
|
||||
backgroundColor: isCopied ? green500 : undefined,
|
||||
borderColor: '#374151',
|
||||
borderWidth: 1,
|
||||
borderRadius: 60,
|
||||
height: 46,
|
||||
paddingVertical: 0,
|
||||
}}
|
||||
>
|
||||
{isCopied ? 'Code copied!' : 'Copy code'}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton
|
||||
onPress={handleDismiss}
|
||||
textColor={black}
|
||||
fontSize={16}
|
||||
style={{ borderRadius: 60, height: 46, paddingVertical: 0 }}
|
||||
>
|
||||
Dismiss
|
||||
</SecondaryButton>
|
||||
</YStack>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarfallPushCodeScreen;
|
||||
67
app/src/services/starfall/pushCodeService.ts
Normal file
67
app/src/services/starfall/pushCodeService.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { POINTS_API_BASE_URL } from '@/services/points/constants';
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Fetches a one-time push code for the specified wallet address.
|
||||
* The code has a TTL of 30 minutes and refreshes with each call.
|
||||
*
|
||||
* @param walletAddress - The wallet address to generate a push code for
|
||||
* @returns The 4-digit push code as a string
|
||||
* @throws Error if the API request fails or times out
|
||||
*/
|
||||
export async function fetchPushCode(walletAddress: string): Promise<string> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, REQUEST_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${POINTS_API_BASE_URL}/push/wallet/${walletAddress}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
|
||||
// Clear timeout on successful response
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch push code: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const code = await response.json();
|
||||
|
||||
// The API returns a JSON string like "5932"
|
||||
if (typeof code !== 'string' || code.length !== 4) {
|
||||
throw new Error('Invalid push code format received from API');
|
||||
}
|
||||
|
||||
return code;
|
||||
} catch (error) {
|
||||
// Clear timeout on error
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Handle abort/timeout specifically
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.error('Push code request timed out');
|
||||
throw new Error(
|
||||
'Request timed out. Please check your connection and try again.',
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error fetching push code:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user