Skip to content

Instantly share code, notes, and snippets.

@jmont96
Created June 17, 2025 23:32
Show Gist options
  • Save jmont96/cc2fc65c54de051ea2c614fc701d0170 to your computer and use it in GitHub Desktop.
Save jmont96/cc2fc65c54de051ea2c614fc701d0170 to your computer and use it in GitHub Desktop.
import { SolanaNet, User } from "@phantasia/model-interfaces";
import {
getAccessToken,
isConnected,
isNotCreated,
User as PrivyUser,
useEmbeddedSolanaWallet,
useEmbeddedWalletStateChange,
usePrivy,
} from "@privy-io/expo";
import React, {
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import {
AuthContextInitialState,
AuthContextProps,
getUser,
loginWallet,
Singletons,
updateUser,
useSweepWallet,
} from "react-common";
import {
resetSession,
setTokenId,
setUserEmail,
setUserNickname,
} from "react-native-crisp-chat-sdk";
export const DEMO_AUTH_TOKEN = "demo";
export const DEMO_EMAIL = "[email protected]";
export const AuthContext = React.createContext<AuthContextProps>(
AuthContextInitialState
);
export function AuthContextProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [authToken, setAuthToken] = useState<string | null>(null);
const [initializing, setInitializing] = useState<boolean>(true);
const { logout, isReady, user: privyUser } = usePrivy();
const solanaWallet = useEmbeddedSolanaWallet();
const getWalletProvider = useCallback(() => {
if (!solanaWallet || !solanaWallet.getProvider)
return Promise.resolve(null);
return solanaWallet.getProvider();
}, [solanaWallet]);
const wallet = useSweepWallet({
user,
getProvider: getWalletProvider,
authToken,
solanaNet: process.env.EXPO_PUBLIC_SOLANA_NET as SolanaNet,
});
useEmbeddedWalletStateChange({
onStateChange: (state) => {
const { status } = state;
switch (status) {
case "not-created":
if (isNotCreated(solanaWallet))
solanaWallet.create().catch((e) => console.error(e));
}
},
});
const createWallets = useCallback(
async (u: PrivyUser) => {
return u.linked_accounts.find(
(account) =>
account.type === "wallet" &&
account.wallet_client_type === "privy" &&
account.chain_type === "solana"
)?.address;
},
[solanaWallet, privyUser]
);
const refreshToken = useCallback(async () => {
const token = await getAccessToken();
setAuthToken(token);
return token ?? "";
}, []);
const handleSignIn = useCallback(
async (u: PrivyUser, address: string | undefined) => {
if (user) return;
const token = await getAccessToken();
try {
if (!token || !u) throw new Error("Login Failed");
if (!address) throw new Error("No user signed in");
const { data } = await loginWallet(token);
setTokenId(data.walletPubkey);
setUserEmail(data.email ?? "");
setUserNickname(data.username);
setUser(data);
setAuthToken(token);
setInitializing(false);
} catch (e: any) {
Singletons.toastService.error("Error", e?.response?.data?.msg);
}
},
[user]
);
const handleDemoSignIn = useCallback(async () => {
try {
const res = await loginWallet(DEMO_AUTH_TOKEN);
setUser(res.data);
setAuthToken(DEMO_AUTH_TOKEN);
} catch (e) {
// @ts-ignore
Singletons.toastService.error("Error", e?.message);
}
}, []);
const refreshUser = useCallback(async () => {
const u = await getUser(authToken ?? "", refreshToken);
setUser(u.data);
}, [authToken, refreshToken]);
const handleSignOut = useCallback(async () => {
await logout();
setUser(null);
setAuthToken(null);
resetSession();
}, []);
const handleUpdateDatabaseUser = useCallback(
async (fields: Partial<User>) => {
try {
const u = await updateUser(fields, authToken ?? "", refreshToken);
setUser(u.data);
} catch (e) {
console.error(e);
}
},
[authToken, refreshToken]
);
useEffect(() => {
if (isReady && privyUser && isConnected(solanaWallet))
createWallets(privyUser).then((address) =>
handleSignIn(privyUser, address ?? "")
);
if (isReady && !privyUser) setInitializing(false);
}, [isReady, privyUser, solanaWallet]);
return (
<AuthContext.Provider
value={{
user,
refreshUser,
login: () => {},
handleSignOut,
handleUpdateDatabaseUser,
initializing,
authToken,
handleDemoSignIn,
wallet,
refreshToken,
}}
>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
import logo from "@/assets/images/logos/logo-text-white.png";
import { Input2 } from "@/atoms/Inputs";
import LoadingSpinner from "@/atoms/LoadingSpinner";
import { Text1 } from "@/atoms/Text";
import { StyledButton, StyledText } from "@/atoms/buttons/Buttons";
import { DEMO_EMAIL, useAuth } from "@/context/AuthContext";
import { platformIsIos } from "@/utils";
import Feather from "@expo/vector-icons/Feather";
import { hasError, useLoginWithEmail } from "@privy-io/expo";
import React, { useCallback, useState } from "react";
import { Image, KeyboardAvoidingView, View } from "react-native";
export function MobileLoginForm() {
const [email, setEmail] = useState("");
const { sendCode, loginWithCode } = useLoginWithEmail();
return (
<>
<KeyboardAvoidingView
behavior={platformIsIos() ? "padding" : "height"}
className={"bg-black flex-1"}
>
<View
className={
"w-full flex-1 bg-black flex items-center justify-center p-12 py-20 space-y-8"
}
>
<Image
source={logo}
resizeMode={"contain"}
className={"h-6 my-auto"}
/>
<EnterEmailState
sendCode={sendCode}
email={email}
setEmail={setEmail}
/>
<EnterCodeState loginWithCode={loginWithCode} email={email} />
<ErrorState />
<LoadingState />
</View>
</KeyboardAvoidingView>
</>
);
}
function EnterEmailState({
email,
setEmail,
sendCode,
}: {
email: string;
setEmail: (e: string) => void;
sendCode: (args: { email: string }) => Promise<any>;
}) {
const { handleDemoSignIn } = useAuth();
const { state } = useLoginWithEmail();
const handleSubmit = useCallback(async () => {
try {
if (
email.toLowerCase().includes(DEMO_EMAIL.toLowerCase()) &&
handleDemoSignIn
)
handleDemoSignIn();
else await sendCode({ email });
} catch (e) {
console.error(e);
}
}, [email, handleDemoSignIn]);
const { status } = state;
if (status !== "initial") return null;
return (
<>
<Input2
autoCapitalize={false}
onChange={(text: string) => setEmail(text)}
placeholder={"Enter your email for a 2fa code"}
value={email}
autoCorrect={false}
className={"w-full"}
keyboardType={"email-address"}
/>
<StyledButton
className={"h-14 bg-red-500 rounded-2xl w-full mt-6"}
onPress={handleSubmit}
>
<Text1 className={"text-white text-sm font-medium"}>
Send code to sign in
</Text1>
</StyledButton>
</>
);
}
function EnterCodeState({
email,
loginWithCode,
}: {
email: string;
loginWithCode: (args: {
code: string;
email?: string;
}) => Promise<any | undefined>;
}) {
const [code, setCode] = useState("");
const { state } = useLoginWithEmail();
const { status } = state;
if (status !== "awaiting-code-input") return null;
return (
<>
<Text1 className={"text-white text-base mb-6"}>
Check your email, {email}, for a 6-digit sign-in code
</Text1>
<Input2
value={code}
onChange={(text: string) => setCode(text)}
placeholder="6 Digit Code"
keyboardType={"number-pad"}
/>
<StyledButton
className={"h-14 bg-red-500 rounded-2xl w-full mt-6"}
onPress={() => loginWithCode({ code })}
>
<Text1 className={"text-white text-sm font-medium"}>Sign in</Text1>
</StyledButton>
</>
);
}
function LoadingState() {
const { state } = useLoginWithEmail();
const { status } = state;
if (status !== "sending-code" && status !== "submitting-code") return null;
const message =
status === "sending-code" ? "Sending code..." : "Signing you in...";
return (
<View className={"flex items-center justify-center"}>
<LoadingSpinner color={"#FFFFFF"} />
<StyledText className={"mt-4 text-xs text-zinc-300"}>
{message}
</StyledText>
</View>
);
}
function ErrorState() {
const { state } = useLoginWithEmail();
// @ts-ignore
if (!hasError(state)) return null;
return (
<View className={"flex items-center justify-center"}>
<Feather name={"x-octagon"} color={"#dc2626"} size={32} />
<StyledText
className={"mt-4 text-sm font-normal text-center text-red-300"}
>
{state.error.message}
</StyledText>
<StyledText className={"mt-4 text-sm font-normal text-center text-white"}>
An error occurred. Please restart the app and try again
</StyledText>
</View>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment