Created
June 17, 2025 23:32
-
-
Save jmont96/cc2fc65c54de051ea2c614fc701d0170 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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