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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_ME
2 changes: 2 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
},
"plugins": [
"expo-router",
"@clerk/expo",
"expo-secure-store",
[
"expo-splash-screen",
{
Expand Down
10 changes: 8 additions & 2 deletions app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import "@/global.css";
import { Stack } from "expo-router";
import { useAuth } from "@clerk/expo";
import { Redirect, Stack } from "expo-router";

export default function RootLayout() {
export default function AuthLayout() {
const { isSignedIn, isLoaded } = useAuth();
if (!isLoaded) return null;
if (isSignedIn) {
return <Redirect href="/" />;
}
return <Stack screenOptions={{ headerShown: false }} />;
}
207 changes: 198 additions & 9 deletions app/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,201 @@
import { Link } from "expo-router";
import { useSignIn } from "@clerk/expo";
import { Link, useRouter } from "expo-router";
import { styled } from "nativewind";
import React from "react";
import {
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
Text,
TextInput,
View,
} from "react-native";
import { SafeAreaView as RNSafeAreaView } from "react-native-safe-area-context";

const SafeAreaView = styled(RNSafeAreaView) as any;

export default function SignIn() {
const { signIn, errors, fetchStatus } = useSignIn();
const router = useRouter();

const [emailAddress, setEmailAddress] = React.useState("");
const [password, setPassword] = React.useState("");
const [code, setCode] = React.useState("");

const handleSubmit = async () => {
const { error } = await signIn.password({
emailAddress,
password,
});
if (error) {
console.error(JSON.stringify(error, null, 2));
Comment on lines +31 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file list =="
git ls-files 'app/(auth)/sign-in.tsx'

echo
echo "== outline =="
ast-grep outline 'app/(auth)/sign-in.tsx' --view expanded || true

echo
echo "== relevant lines =="
cat -n 'app/(auth)/sign-in.tsx' | sed -n '1,220p'

echo
echo "== console usages in file =="
rg -n 'console\.(log|error)|__DEV__|session\?\.currentTask|JSON\.stringify\(error' 'app/(auth)/sign-in.tsx'

Repository: saikumarbt/react-native-recurly

Length of output: 8692


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Inspecting app/(auth)/sign-in.tsx...\n'
if [ -f 'app/(auth)/sign-in.tsx' ]; then
  nl -ba 'app/(auth)/sign-in.tsx' | sed -n '1,220p'
fi

Repository: saikumarbt/react-native-recurly

Length of output: 246


Avoid logging raw auth/session payloads. JSON.stringify(error, null, 2) and session?.currentTask can leak sensitive auth details into device logs; keep these messages generic and behind __DEV__. Also applies to app/(auth)/sign-in.tsx:39-40, 63-64.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(auth)/sign-in.tsx around lines 31 - 32, The sign-in flow is logging
sensitive auth/session data in `signIn` and related success/error paths, so
replace the raw JSON/stringified payloads and `session?.currentTask` output with
generic messages only. Update the logging in `app/(auth)/sign-in.tsx` around the
`error` handling and session usage to be gated behind `__DEV__`, and keep
production logs free of auth details.

return;
}

if (signIn.status === "complete") {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
console.log(session?.currentTask);
return;
}
router.replace("/");
},
});
} else if (signIn.status === "needs_client_trust") {
const emailCodeFactor = signIn.supportedSecondFactors.find(
(factor) => factor.strategy === "email_code",
);

if (emailCodeFactor) {
await signIn.mfa.sendEmailCode();
}
Comment on lines +46 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- app/(auth)/sign-in.tsx (outline) ---'
ast-grep outline 'app/(auth)/sign-in.tsx' --view expanded || true

echo
echo '--- relevant lines in app/(auth)/sign-in.tsx ---'
nl -ba 'app/(auth)/sign-in.tsx' | sed -n '1,180p'

echo
echo '--- search for needs_client_trust / sendEmailCode / verification screen ---'
rg -n 'needs_client_trust|sendEmailCode|email_code|verificationError|Enter the code sent to your email|auth-error' app -S || true

Repository: saikumarbt/react-native-recurly

Length of output: 411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- app/(auth)/sign-in.tsx (1-220 with line numbers) ---'
awk '{printf("%4d %s\n", NR, $0)}' 'app/(auth)/sign-in.tsx' | sed -n '1,220p'

echo
echo '--- targeted search in app/ ---'
rg -n 'needs_client_trust|sendEmailCode|email_code|verificationError|Enter the code sent to your email|auth-error' app -S || true

Repository: saikumarbt/react-native-recurly

Length of output: 9014


Guard the email-code challenge before switching to the verification screen.
If email_code isn’t offered or sendEmailCode() fails, the sign-in flow still lands on the verify step with no way to continue or recover.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(auth)/sign-in.tsx around lines 46 - 53, The sign-in flow in the
needs_client_trust branch can advance to the verification screen even when the
email-code challenge is unavailable or sendEmailCode() fails. Update the sign-in
handler around signIn.status, emailCodeFactor, and signIn.mfa.sendEmailCode() so
the verification step is reached only after confirming the email_code factor
exists and the challenge request succeeds; otherwise stop the transition and
surface a recoverable error or fallback path.

}
};

const handleVerify = async () => {
await signIn.mfa.verifyEmailCode({ code });

if (signIn.status === "complete") {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
console.log(session?.currentTask);
return;
}
router.replace("/");
},
});
}
};

if (signIn.status === "needs_client_trust") {
return (
<SafeAreaView className="auth-screen">
<View className="auth-content">
<Text className="auth-title">Verify your account</Text>
<Text className="auth-subtitle">
Enter the code sent to your email.
</Text>

<View className="auth-card">
<View className="auth-form">
<View className="auth-field">
<Text className="auth-label">Verification Code</Text>
<TextInput
className="auth-input"
value={code}
placeholder="Enter your verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors?.fields?.code && (
<Text className="auth-error">
{errors.fields.code.message}
</Text>
)}
</View>

<Pressable
className={`auth-button ${fetchStatus === "fetching" ? "auth-button-disabled" : ""}`}
onPress={handleVerify}
disabled={fetchStatus === "fetching"}
>
<Text className="auth-button-text">Verify</Text>
</Pressable>
</View>
</View>
</View>
</SafeAreaView>
);
}

import { Text, View } from "react-native";
const SignIn = () => {
return (
<View>
<Text>SignIn</Text>
<Link href="/(auth)/sign-up">Create Account</Link>
</View>
<SafeAreaView className="auth-screen">
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<ScrollView className="auth-scroll">
<View className="auth-content">
<View className="auth-brand-block">
<View className="auth-logo-wrap">
<View className="auth-logo-mark">
<Text className="auth-logo-mark-text">R</Text>
</View>
<View>
<Text className="auth-wordmark">Recurrly</Text>
<Text className="auth-wordmark-sub">SMART BILLING</Text>
</View>
</View>

<Text className="auth-title">Welcome back</Text>
<Text className="auth-subtitle">
Sign in to manage your subscriptions
</Text>
</View>
<View className="auth-card">
<View className="auth-form">
<View className="auth-field">
<Text className="auth-label">Email address</Text>
<TextInput
className="auth-input"
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={(emailAddress) =>
setEmailAddress(emailAddress)
}
keyboardType="email-address"
/>
{errors?.fields?.identifier && (
<Text className="auth-error">
{errors.fields.identifier.message}
</Text>
)}
</View>

<View className="auth-field">
<Text className="auth-label">Password</Text>
<TextInput
className="auth-input"
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors?.fields?.password && (
<Text className="auth-error">
{errors.fields.password.message}
</Text>
)}
</View>

<Pressable
className={`auth-button ${!emailAddress || !password || fetchStatus === "fetching" ? "auth-button-disabled" : ""}`}
onPress={handleSubmit}
disabled={
!emailAddress || !password || fetchStatus === "fetching"
}
>
<Text className="auth-button-text">Sign in</Text>
</Pressable>
</View>
</View>

<View className="auth-link-row">
<Text className="auth-link-copy">Don't have an account?</Text>
<Link href="/(auth)/sign-up">
<Text className="auth-link">Sign up</Text>
</Link>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
export default SignIn;
}
Loading