+
{useMemo(() => parseMarkdown(value).content, [value])}
)
diff --git a/apps/mobile/src/contexts/LoginTeamsContext.tsx b/apps/mobile/src/contexts/LoginTeamsContext.tsx
new file mode 100644
index 000000000..f79b2b18b
--- /dev/null
+++ b/apps/mobile/src/contexts/LoginTeamsContext.tsx
@@ -0,0 +1,7 @@
+import { createContext } from "react"
+
+export const LoginTeamsCheckedContext = createContext(__DEV__)
+
+export const LoginTeamsCheckGuardContext = createContext<((callback: () => void) => void) | null>(
+ () => {},
+)
diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx
index 8ae7f5828..f90a55361 100644
--- a/apps/mobile/src/modules/login/email.tsx
+++ b/apps/mobile/src/modules/login/email.tsx
@@ -1,6 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
-import { useEffect } from "react"
+import { router } from "expo-router"
+import { useContext, useEffect } from "react"
import type { Control } from "react-hook-form"
import { useController, useForm } from "react-hook-form"
import type { TextInputProps } from "react-native"
@@ -17,6 +18,7 @@ import { z } from "zod"
import { ReAnimatedPressable } from "@/src/components/common/AnimatedComponents"
import { ThemedText } from "@/src/components/common/ThemedText"
+import { LoginTeamsCheckGuardContext } from "@/src/contexts/LoginTeamsContext"
import { signIn } from "@/src/lib/auth"
import { toast } from "@/src/lib/toast"
import { accentColor, useColor } from "@/src/theme/colors"
@@ -75,7 +77,10 @@ export function EmailLogin() {
mutationFn: onSubmit,
})
- const login = handleSubmit((values) => submitMutation.mutate(values))
+ const teamsCheckGuard = useContext(LoginTeamsCheckGuardContext)
+ const login = handleSubmit((values) => {
+ teamsCheckGuard?.(() => submitMutation.mutate(values))
+ })
const disableColor = useColor("gray3")
diff --git a/apps/mobile/src/modules/login/index.tsx b/apps/mobile/src/modules/login/index.tsx
index c7c087f0b..a31ffcdea 100644
--- a/apps/mobile/src/modules/login/index.tsx
+++ b/apps/mobile/src/modules/login/index.tsx
@@ -1,36 +1,146 @@
+import { noop } from "es-toolkit/compat"
+import { router } from "expo-router"
+import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react"
import { TouchableWithoutFeedback, View } from "react-native"
+import BouncyCheckbox from "react-native-bouncy-checkbox"
import { KeyboardController } from "react-native-keyboard-controller"
+import Animated, {
+ runOnUI,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated"
import { ThemedText } from "@/src/components/common/ThemedText"
+import { ContextMenu } from "@/src/components/ui/context-menu"
import { Logo } from "@/src/components/ui/logo"
+import {
+ LoginTeamsCheckedContext,
+ LoginTeamsCheckGuardContext,
+} from "@/src/contexts/LoginTeamsContext"
+import { isIOS } from "@/src/lib/platform"
+import { toast } from "@/src/lib/toast"
+import { TeamsMarkdown } from "@/src/screens/(headless)/teams"
import { EmailLogin } from "./email"
import { SocialLogin } from "./social"
export function Login() {
+ const [isChecked, setIsChecked] = useState(false)
+
+ const teamsCheckBoxRef = useRef<{ shake: () => void }>(null)
return (
-
- {
- KeyboardController.dismiss()
- }}
- accessible={false}
+
+ void) => {
+ if (isChecked) {
+ callback()
+ } else {
+ toast.info("Please accept the Terms of Service and Privacy Policy")
+
+ teamsCheckBoxRef.current?.shake()
+ }
+ },
+ [isChecked],
+ )}
>
-
-
- Login to Follow
-
+
+ {
+ KeyboardController.dismiss()
+ }}
+ accessible={false}
+ >
+
+
+ Login to Follow
+
+
+
+
+
+
+
+
+ or
+
+
+
+
-
-
-
-
-
- or
-
-
-
-
-
+
+
+ )
+}
+
+const TeamsCheckBox = forwardRef<
+ { shake: () => void },
+ {
+ isChecked: boolean
+ setIsChecked: (isChecked: boolean) => void
+ }
+>(({ isChecked, setIsChecked }, ref) => {
+ const shakeSharedValue = useSharedValue(0)
+ const shakeStyle = useAnimatedStyle(() => ({
+ transform: [{ translateX: shakeSharedValue.value }],
+ }))
+ useImperativeHandle(ref, () => ({
+ shake: () => {
+ const animations = [-10, 10, -8, 8, -6, 6, 0]
+
+ runOnUI(() => {
+ "worklet"
+ shakeSharedValue.value = 0
+
+ const runAnimation = (index: number) => {
+ "worklet"
+ if (index < animations.length) {
+ shakeSharedValue.value = withTiming(animations[index], { duration: 100 }, () => {
+ runAnimation(index + 1)
+ })
+ }
+ }
+
+ runAnimation(0)
+ })()
+ },
+ }))
+ return (
+
+ }
+ onLongPress={() => {
+ if (!isIOS) {
+ router.push("/teams")
+ }
+ }}
+ />
+
+ )
+})
+
+const TeamsText = () => {
+ return (
+
{
+ router.push("/teams")
+ }}
+ renderPreview={() => (
+
+
+
+ )}
+ >
+
+ I agree to the Terms of Service and Privacy Policy
+
+
)
}
diff --git a/apps/mobile/src/modules/login/social.tsx b/apps/mobile/src/modules/login/social.tsx
index babe586af..d1dec8a73 100644
--- a/apps/mobile/src/modules/login/social.tsx
+++ b/apps/mobile/src/modules/login/social.tsx
@@ -1,7 +1,9 @@
import * as AppleAuthentication from "expo-apple-authentication"
import { useColorScheme } from "nativewind"
+import { useContext } from "react"
import { Platform, TouchableOpacity, View } from "react-native"
+import { LoginTeamsCheckGuardContext } from "@/src/contexts/LoginTeamsContext"
import { AppleCuteFiIcon } from "@/src/icons/apple_cute_fi"
import { GithubCuteFiIcon } from "@/src/icons/github_cute_fi"
import { GoogleCuteFiIcon } from "@/src/icons/google_cute_fi"
@@ -38,6 +40,7 @@ const provider: Record<
export function SocialLogin() {
const { data } = useAuthProviders()
+ const teamsCheckGuard = useContext(LoginTeamsCheckGuardContext)
const { colorScheme } = useColorScheme()
return (
@@ -50,40 +53,42 @@ export function SocialLogin() {
{
- if (!data?.[providerInfo.id]) return
+ onPress={() =>
+ teamsCheckGuard?.(async () => {
+ if (!data?.[providerInfo.id]) return
- if (providerInfo.id === "apple") {
- try {
- const credential = await AppleAuthentication.signInAsync({
- requestedScopes: [
- AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
- AppleAuthentication.AppleAuthenticationScope.EMAIL,
- ],
- })
-
- if (credential.identityToken) {
- await signIn.social({
- provider: "apple",
- idToken: {
- token: credential.identityToken,
- },
+ if (providerInfo.id === "apple") {
+ try {
+ const credential = await AppleAuthentication.signInAsync({
+ requestedScopes: [
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
+ ],
})
- } else {
- throw new Error("No identityToken.")
- }
- } catch (e) {
- console.error(e)
- // handle errors
- }
- return
- }
- signIn.social({
- provider: providerInfo.id as any,
- callbackURL: "/",
+ if (credential.identityToken) {
+ await signIn.social({
+ provider: "apple",
+ idToken: {
+ token: credential.identityToken,
+ },
+ })
+ } else {
+ throw new Error("No identityToken.")
+ }
+ } catch (e) {
+ console.error(e)
+ // handle errors
+ }
+ return
+ }
+
+ signIn.social({
+ provider: providerInfo.id as any,
+ callbackURL: "/",
+ })
})
- }}
+ }
disabled={!data?.[providerInfo.id]}
>
{
+ return (
+
+ )
+}
+
+export default function Teams() {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 050baabd3..2a5fb1618 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -562,6 +562,9 @@ importers:
react-native:
specifier: 0.76.5
version: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)
+ react-native-bouncy-checkbox:
+ specifier: 4.1.2
+ version: 4.1.2
react-native-context-menu-view:
specifier: 1.16.0
version: 1.16.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
@@ -3744,6 +3747,9 @@ packages:
'@fontsource/sn-pro@5.1.0':
resolution: {integrity: sha512-k7cdU1hftD/pyrnrmQg+egKAssbuPORbtgQM/9JG5b76EchEKZdl/juO8xBjN3O/H5BQ16iu8/9vNPgzyExZeg==}
+ '@freakycoder/react-native-bounceable@1.0.3':
+ resolution: {integrity: sha512-+iMq2tnqxCwFjitbPUz9nZ+VfJ8OU9waIlDJAAsoq1229QEwCmERCNy5zVtDsz75q3i4FLXX/n7fimdMzmP21A==}
+
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -12688,6 +12694,9 @@ packages:
react-merge-refs@1.1.0:
resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==}
+ react-native-bouncy-checkbox@4.1.2:
+ resolution: {integrity: sha512-hB7YwCGTNoMpTPOPiP+RWyQH35S6vxUbc7IGEW/Rqyp7GonEyhtqtthmxiphneRXnywMh8CZwND7OnvppJZscg==}
+
react-native-context-menu-view@1.16.0:
resolution: {integrity: sha512-zqeOAizM7MVV9o6h/quS0REQikBq3J4BkIRLFygY6RiCjr6rwuzSGkif7JRCHpAQQumSKlLqYl4N2h3AdoIHVg==}
peerDependencies:
@@ -18229,6 +18238,8 @@ snapshots:
'@fontsource/sn-pro@5.1.0': {}
+ '@freakycoder/react-native-bounceable@1.0.3': {}
+
'@gar/promisify@1.1.3': {}
'@gorhom/portal@1.0.14(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)':
@@ -29273,6 +29284,10 @@ snapshots:
react-merge-refs@1.1.0: {}
+ react-native-bouncy-checkbox@4.1.2:
+ dependencies:
+ '@freakycoder/react-native-bounceable': 1.0.3
+
react-native-context-menu-view@1.16.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1