Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions app/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
OnboardingPage,
OnboardingPagination,
} from '@/components/Onboarding';
import PasskeyFaqModal from '@/components/PasskeyFaqModal';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { path } from '@/constants/path';
Expand All @@ -47,13 +48,15 @@ export default function Onboarding() {
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
const [currentIndex, setCurrentIndex] = useState(0);
const [showRecoveryLink, setShowRecoveryLink] = useState(false);
const [showPasskeyFaq, setShowPasskeyFaq] = useState(false);
const scrollX = useSharedValue(0);

// Responsive layout for small screens (iPhone SE)
const isSmallScreen = screenHeight < 700;
// Fixed button area height - content area uses flex: 1 to fill remaining space.
// Grows to fit the account-recovery prompt that appears after a failed login.
const buttonAreaHeight = (isSmallScreen ? 200 : 240) + (showRecoveryLink ? 44 : 0);
// Grows to fit the help prompt (Passkey FAQs / account recovery) shown after a failed login,
// which can wrap onto two lines on smaller screens.
const buttonAreaHeight = (isSmallScreen ? 200 : 240) + (showRecoveryLink ? 64 : 0);

// Track screen width as shared value for use in worklets
const widthSV = useSharedValue(screenWidth);
Expand Down Expand Up @@ -125,10 +128,15 @@ export default function Onboarding() {
const filteredOnboardingData = ONBOARDING_DATA.filter(slide => slide?.platform !== false);

// Surfaced below the Login button after a failed login attempt so users who
// can't authenticate (e.g. lost passkey) can start account recovery.
// can't authenticate (e.g. lost passkey) can read the Passkey FAQs or start
// account recovery.
const recoveryLink = showRecoveryLink ? (
<View className="flex-row flex-wrap items-center justify-center">
<Text className="text-sm text-white/60">Have trouble logging in? </Text>
<Text className="text-sm text-white/60">Have trouble logging in? See our </Text>
<Pressable onPress={() => setShowPasskeyFaq(true)} className="web:hover:opacity-70">
<Text className="text-sm font-bold text-white">Passkey FAQs</Text>
</Pressable>
<Text className="text-sm text-white/60"> or </Text>
<Pressable
onPress={handleRecoverAccount}
className="flex-row items-center web:hover:opacity-70"
Expand Down Expand Up @@ -230,6 +238,8 @@ export default function Onboarding() {
</View>
</SafeAreaView>
</AnimatedGradientBackground>

<PasskeyFaqModal isOpen={showPasskeyFaq} onOpenChange={setShowPasskeyFaq} />
</View>
);
}
Expand Down Expand Up @@ -330,6 +340,8 @@ export default function Onboarding() {
</View>
</View>
</View>

<PasskeyFaqModal isOpen={showPasskeyFaq} onOpenChange={setShowPasskeyFaq} />
</View>
);
}
46 changes: 46 additions & 0 deletions components/PasskeyFaqModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { View } from 'react-native';
import LottieView from 'lottie-react-native';

import { FAQ } from '@/components/FAQ';
import ResponsiveModal, { ModalState } from '@/components/ResponsiveModal';
import { Text } from '@/components/ui/text';
import passkeyFaqs from '@/constants/passkey-faqs';

const MODAL_STATE: ModalState = { name: 'passkey-faq', number: 1 };
const CLOSE_STATE: ModalState = { name: 'close', number: 0 };

interface PasskeyFaqModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}

const PasskeyFaqModal = ({ isOpen, onOpenChange }: PasskeyFaqModalProps) => {
return (
<ResponsiveModal
currentModal={MODAL_STATE}
previousModal={CLOSE_STATE}
isOpen={isOpen}
onOpenChange={onOpenChange}
trigger={null}
contentKey="passkey-faq"
contentClassName="md:max-w-md"
shouldAnimate={false}
>
<View className="gap-6">
<View className="items-center">
<LottieView
source={require('@/assets/animations/vault.json')}
autoPlay
loop
style={{ width: 140, height: 140 }}
resizeMode="contain"
/>
</View>
<Text className="native:text-2xl text-xl font-semibold">Passkey FAQ</Text>
<FAQ faqs={passkeyFaqs} />
</View>
</ResponsiveModal>
);
};

export default PasskeyFaqModal;
26 changes: 26 additions & 0 deletions constants/passkey-faqs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Faq } from '@/lib/types';

const passkeyFaqs: Faq[] = [
{
question: 'What is a passkey and why is it more secure than a password?',
answer:
"A passkey lets you log into Solid using your device's biometrics (Face ID, fingerprint) or PIN instead of a password. It's stored locally on your device (never with Solid's) and is phishing-resistant by design, making it significantly harder to steal or compromise than a traditional password.",
},
{
question: "Why can't I see the passkey option when logging in?",
answer:
"Passkeys require a compatible device and browser, make sure you're on a recent version of iOS, Android, Chrome, or Safari.",
},
{
question: 'Can I use my passkey across multiple devices?',
answer:
"Yes, if you use a password manager that supports passkeys (like iCloud Keychain or Google Password Manager), your passkey syncs across your devices automatically. Otherwise, you'll need to create a separate passkey on each new device.",
},
{
question: 'What happens if I lose my device?',
answer:
"Your passkey is tied to your device, not your account. If you lose access to your phone, visit the Passkey Recovery link on the login screen, enter your email, and we'll send you a verification code to regain access. Once in, you can set up a new passkey on your new device.",
},
];

export default passkeyFaqs;
Loading