From 94784a783c02af3b6d9e3533b50521bd6dcac603 Mon Sep 17 00:00:00 2001 From: Dustin Do Date: Wed, 11 Sep 2024 07:07:27 +0700 Subject: [PATCH] feat(mobile): track user auth events (#302) --- .vscode/settings.json | 1 + apps/mobile/app/(app)/(tabs)/_layout.tsx | 2 +- apps/mobile/app/(auth)/login.tsx | 37 ++++++++++++-- apps/mobile/app/_layout.tsx | 4 +- apps/mobile/components/auth/auth-email.tsx | 10 +++- apps/mobile/components/auth/auth-social.tsx | 54 ++++++++++++++++----- apps/mobile/mutations/user.ts | 8 ++- 7 files changed, 95 insertions(+), 21 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5e2658fe..f87f9c04 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,6 +17,7 @@ "EXCHANGERATES", "hono", "openai", + "posthog", "svix", "tanstack" ] diff --git a/apps/mobile/app/(app)/(tabs)/_layout.tsx b/apps/mobile/app/(app)/(tabs)/_layout.tsx index 65ad7c71..f94efa2d 100644 --- a/apps/mobile/app/(app)/(tabs)/_layout.tsx +++ b/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -17,11 +17,11 @@ export default function TabLayout() { const { user } = useUser() const { width } = useWindowDimensions() const { bottom } = useSafeAreaInsets() - const posthog = usePostHog() useEffect(() => { if (!user) { + posthog.reset() return } posthog.identify(user?.id, { diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx index f279e722..2af31463 100644 --- a/apps/mobile/app/(auth)/login.tsx +++ b/apps/mobile/app/(auth)/login.tsx @@ -11,12 +11,33 @@ import { Trans, t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { Link } from 'expo-router' import { MailIcon } from 'lucide-react-native' -import { useState } from 'react' +import { usePostHog } from 'posthog-react-native' +import { useCallback, useState } from 'react' import { Linking, ScrollView, View } from 'react-native' +type Strategy = 'email_code' | 'oauth_google' | 'oauth_apple' + export default function LoginScreen() { const [withEmail, setWithEmail] = useState(false) const { i18n } = useLingui() + const posthog = usePostHog() + + const handleSignedUp = useCallback( + (strategy: Strategy, userId?: string) => { + posthog.identify(userId) + posthog.capture('user_signed_up', { strategy }) + }, + [posthog], + ) + + const handleSignedIn = useCallback( + (strategy: Strategy, userId?: string) => { + posthog.identify(userId) + posthog.capture('user_signed_up', { strategy }) + }, + [posthog], + ) + return ( - - + + - {withEmail && } + {withEmail && ( + + )} diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index c7a040ef..86295b91 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -149,11 +149,13 @@ function RootLayout() { return ( void + onSignedIn: (strategy: 'email_code', userId?: string) => void +}) { const { i18n } = useLingui() const authEmailForm = useForm({ @@ -108,6 +114,7 @@ export function AuthEmail() { email: signUpAttempt.emailAddress!, name: signUpAttempt.firstName ?? '', }) + onSignedUp('email_code', signUpAttempt.id) } else { console.error(signUpAttempt) } @@ -119,6 +126,7 @@ export function AuthEmail() { if (signInAttempt.status === 'complete') { await setActiveSignIn({ session: signInAttempt.createdSessionId }) // signed in + onSignedIn('email_code', signInAttempt.id) } else { console.error(signInAttempt) } diff --git a/apps/mobile/components/auth/auth-social.tsx b/apps/mobile/components/auth/auth-social.tsx index dfccab60..c759b59c 100644 --- a/apps/mobile/components/auth/auth-social.tsx +++ b/apps/mobile/components/auth/auth-social.tsx @@ -9,33 +9,45 @@ import { GoogleLogo } from '../svg-assets/google-logo' import { Button } from '../ui/button' import { Text } from '../ui/text' +type Strategy = 'oauth_google' | 'oauth_apple' + type AuthSocialProps = { label: string icon: React.ComponentType - strategy: 'oauth_google' | 'oauth_apple' + strategy: Strategy + onSignedUp: (strategy: Strategy, userId?: string) => void + onSignedIn: (strategy: Strategy, userId?: string) => void } -export function AuthSocial({ label, icon: Icon, strategy }: AuthSocialProps) { +export function AuthSocial({ + label, + icon: Icon, + strategy, + onSignedIn, + onSignedUp, +}: AuthSocialProps) { const { startOAuthFlow } = useOAuth({ strategy }) const onPress = async () => { try { - const { createdSessionId, setActive, signUp } = await startOAuthFlow() + const { createdSessionId, setActive, signUp, signIn } = + await startOAuthFlow() if (createdSessionId) { setActive?.({ session: createdSessionId }) if (signUp?.createdUserId) { - setTimeout( - async () => - await createUser({ - email: signUp.emailAddress!, - name: signUp.firstName ?? '', - }), - 1000, - ) + setTimeout(async () => { + await createUser({ + email: signUp.emailAddress!, + name: signUp.firstName ?? '', + }) + + onSignedUp(strategy, signUp.id) + }, 1000) } } else { // Use signIn or signUp for next steps such as MFA + onSignedIn(strategy, signIn?.id) } // biome-ignore lint/suspicious/noExplicitAny: } catch (err: any) { @@ -53,24 +65,40 @@ export function AuthSocial({ label, icon: Icon, strategy }: AuthSocialProps) { ) } -export function GoogleAuthButton() { +export function GoogleAuthButton({ + onSignedUp, + onSignedIn, +}: { + onSignedUp: (strategy: Strategy, userId?: string) => void + onSignedIn: (strategy: Strategy, userId?: string) => void +}) { const { i18n } = useLingui() return ( ) } -export function AppleAuthButton() { +export function AppleAuthButton({ + onSignedUp, + onSignedIn, +}: { + onSignedUp: (strategy: Strategy, userId?: string) => void + onSignedIn: (strategy: Strategy, userId?: string) => void +}) { const { i18n } = useLingui() return ( ) } diff --git a/apps/mobile/mutations/user.ts b/apps/mobile/mutations/user.ts index c7adbd68..33573734 100644 --- a/apps/mobile/mutations/user.ts +++ b/apps/mobile/mutations/user.ts @@ -7,11 +7,17 @@ export async function createUser(data: CreateUser) { const deviceLanguage = getLocales()[0].languageCode ?? 'vi' const deviceCurrency = getLocales()[0]?.currencyCode ?? 'VND' - await hc.v1.users.$post({ + const res = await hc.v1.users.$post({ json: data, header: { 'x-device-language': deviceLanguage, 'x-device-currency': deviceCurrency, }, }) + + if (!res.ok) { + return null + } + + return res.json() }