feat(rn): add login teams

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-01-17 21:10:51 +08:00
parent 8a1ff735bd
commit aef9406def
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
11 changed files with 315 additions and 56 deletions

View File

@ -69,6 +69,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "7.54.0",
"react-native": "0.76.5",
"react-native-bouncy-checkbox": "4.1.2",
"react-native-context-menu-view": "1.16.0",
"react-native-gesture-handler": "~2.20.2",
"react-native-image-colors": "2.4.0",

View File

@ -1,6 +1,7 @@
import { cn } from "@follow/utils"
import type { TextProps } from "react-native"
import { Text } from "react-native"
export function ThemedText(props: TextProps) {
return <Text {...props} className={`font-sn text-text ${props.className}`} />
return <Text {...props} className={cn("font-sn text-label", props.className)} />
}

View File

@ -15,6 +15,7 @@ export const ContextMenu: FC<ContextMenuProps & PropsWithChildren> = ({
onPressMenuItem,
children,
renderPreview,
onPressPreview,
...props
}) => {
const [actionKeyMap] = useState(() => new Map<string, IContextMenuItemConfig>())
@ -80,6 +81,7 @@ export const ContextMenu: FC<ContextMenuProps & PropsWithChildren> = ({
}
renderPreview={renderPreview}
menuConfig={menuViewConfig}
onPressMenuPreview={onPressPreview}
onPressMenuItem={(e) => {
onPressMenuItem(actionKeyMap.get(e.nativeEvent.actionKey)!)
}}

View File

@ -34,4 +34,8 @@ export interface ContextMenuProps extends ViewProps {
* @note only available on iOS
*/
renderPreview?: RenderItem
/**
* @note only available on iOS
*/
onPressPreview?: () => void
}

View File

@ -8,12 +8,18 @@ import { useDarkMode } from "usehooks-ts"
import { useCSSInjection } from "@/src/theme/web"
const MarkdownWeb: WebComponent<{ value: string }> = ({ value }) => {
const MarkdownWeb: WebComponent<{ value: string; style?: React.CSSProperties }> = ({
value,
style,
}) => {
useCSSInjection()
const { isDarkMode } = useDarkMode()
return (
<div className={cn("text-text prose min-w-0", isDarkMode ? "prose-invert" : "prose")}>
<div
className={cn("text-text prose min-w-0", isDarkMode ? "prose-invert" : "prose")}
style={style}
>
{useMemo(() => parseMarkdown(value).content, [value])}
</div>
)

View File

@ -0,0 +1,7 @@
import { createContext } from "react"
export const LoginTeamsCheckedContext = createContext(__DEV__)
export const LoginTeamsCheckGuardContext = createContext<((callback: () => void) => void) | null>(
() => {},
)

View File

@ -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")

View File

@ -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 (
<View className="flex-1 gap-10 p-safe">
<TouchableWithoutFeedback
onPress={() => {
KeyboardController.dismiss()
}}
accessible={false}
<LoginTeamsCheckedContext.Provider value={isChecked}>
<LoginTeamsCheckGuardContext.Provider
value={useCallback(
(callback: () => void) => {
if (isChecked) {
callback()
} else {
toast.info("Please accept the Terms of Service and Privacy Policy")
teamsCheckBoxRef.current?.shake()
}
},
[isChecked],
)}
>
<View className="flex-1 items-center gap-8 pt-20">
<Logo style={{ width: 80, height: 80 }} />
<ThemedText className="text-2xl font-bold">Login to Follow</ThemedText>
<EmailLogin />
<View className="flex-1 p-safe">
<TouchableWithoutFeedback
onPress={() => {
KeyboardController.dismiss()
}}
accessible={false}
>
<View className="flex-1 items-center gap-8 pt-20">
<Logo style={{ width: 80, height: 80 }} />
<ThemedText className="text-2xl font-bold">Login to Follow</ThemedText>
<EmailLogin />
</View>
</TouchableWithoutFeedback>
<TeamsCheckBox ref={teamsCheckBoxRef} isChecked={isChecked} setIsChecked={setIsChecked} />
<View className="border-t-opaque-separator border-t-hairline mx-28" />
<View className="mt-2 items-center">
<View className="mb-4 flex w-full max-w-sm flex-row items-center gap-4">
<View className="bg-separator my-4 h-[0.5px] flex-1" />
<ThemedText className="text-secondary-label text-lg">or</ThemedText>
<View className="bg-separator my-4 h-[0.5px] flex-1" />
</View>
<SocialLogin />
</View>
</View>
</TouchableWithoutFeedback>
<View className="border-t-opaque-separator border-t-hairline mx-28" />
<View className="items-center">
<View className="mb-4 flex w-full max-w-sm flex-row items-center gap-4">
<View className="bg-separator my-4 h-[0.5px] flex-1" />
<ThemedText className="text-xl">or</ThemedText>
<View className="bg-separator my-4 h-[0.5px] flex-1" />
</View>
<SocialLogin />
</View>
</View>
</LoginTeamsCheckGuardContext.Provider>
</LoginTeamsCheckedContext.Provider>
)
}
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 (
<Animated.View className="mb-4 flex-row items-center gap-2 px-8" style={shakeStyle}>
<BouncyCheckbox
isChecked={isChecked}
onPress={setIsChecked}
size={14}
textComponent={<TeamsText />}
onLongPress={() => {
if (!isIOS) {
router.push("/teams")
}
}}
/>
</Animated.View>
)
})
const TeamsText = () => {
return (
<ContextMenu
className="overflow-hidden rounded-full px-2"
config={{ items: [] }}
onPressMenuItem={noop}
onPressPreview={() => {
router.push("/teams")
}}
renderPreview={() => (
<View className="flex-1">
<TeamsMarkdown />
</View>
)}
>
<ThemedText className="text-secondary-label text-sm">
I agree to the Terms of Service and Privacy Policy
</ThemedText>
</ContextMenu>
)
}

View File

@ -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() {
<TouchableOpacity
key={key}
className="border-opaque-separator border-hairline rounded-full p-2"
onPress={async () => {
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]}
>
<providerInfo.icon

View File

@ -0,0 +1,103 @@
import { Stack } from "expo-router"
import { View } from "react-native"
import MarkdownWeb from "@/src/components/ui/typography/MarkdownWeb"
const txt = `# Terms of Service
**Effective Date:** 2025-01-17
Welcome to Follow, your personalized RSS reader and content hub. By using our application, you agree to these Terms of Service ("Terms"). Please read them carefully as they govern your use of the Service and the rights and obligations that come with it.
Follow is designed to give you an intuitive, efficient, and user-friendly experience in managing your RSS feeds. We aim to provide a seamless and secure environment, but its important for you to understand how your rights are protected and the scope of your responsibilities while using the Service.
## 1. Acceptance of Terms
By accessing or using Follow ("the Service"), you agree to comply with and be bound by these Terms and our Privacy Policy. If you do not agree to these Terms, you may not use the Service. These Terms are a legally binding contract between you and Natural Selection Limited, which owns and operates Follow. By using the Service, you acknowledge that you are responsible for your actions and for ensuring that your usage of Follow is consistent with these Terms.
## 2. Eligibility
You must be at least 13 years old to use Follow. By using the Service, you represent and warrant that you meet this eligibility requirement. If you are under the age of 13, you are prohibited from using the Service. Natural Selection Limited reserves the right to suspend or terminate the access of any user who violates these eligibility requirements. Additionally, if you are using Follow on behalf of a company, you confirm that you have the authority to bind the company to these Terms.
## 3. User Account
To access certain features of the Service, you may be required to create an account. You are responsible for maintaining the confidentiality of your account credentials, such as your username and password, and for all activities that occur under your account. If you suspect any unauthorized access or use of your account, you must notify us immediately to avoid any potential security breaches. You agree to provide accurate, up-to-date information when creating or maintaining your account and understand that failure to do so may result in limitations to your access or functionality of the Service.
## 4. Permitted Use
You agree to use Follow solely for lawful purposes and in a manner that does not violate the rights of others. You shall not use the Service for any unlawful, harmful, or malicious activities. You are prohibited from transmitting harmful content such as malware, viruses, or phishing attempts, and from interfering with the operation or security features of the Service. Unauthorized attempts to gain access to the Service through hacking, password mining, or any other unlawful means are strictly prohibited and may result in immediate termination of your account.
You also agree not to exploit any part of the Service, including features, tools, or content, for commercial purposes unless explicitly authorized by Natural Selection Limited.
## 5. Content and Intellectual Property
### 5.1 User Content
Follow enables you to import, subscribe to, and read content via RSS feeds. You retain full ownership of any content you post, upload, or submit to the Service. By submitting or sharing content, you grant us a worldwide, royalty-free, and non-exclusive license to host, display, modify, and distribute your content as necessary to operate, improve, and provide the Service. You are solely responsible for ensuring that the content you share does not infringe on the intellectual property rights of any third party. You also agree to respect the rights of content creators and copyright holders.
### 5.2 Intellectual Property Rights
The Service and its underlying technology, including software, designs, and content, are owned by Natural Selection Limited or its licensors. You are granted a limited, non-exclusive, non-transferable right to access and use the Service solely for personal, non-commercial purposes. You may not copy, modify, reverse-engineer, distribute, or otherwise exploit any part of the Service without explicit permission from us. All trademarks, logos, and service marks displayed on the Service are the property of Natural Selection Limited or their respective owners. Unauthorized use of any intellectual property displayed on the Service is strictly prohibited.
### 5.3 AI Features and Usage
Follow incorporates AI-powered features that assist in content translation, summarization, intelligent recommendations, and more. While these features are designed to enhance your user experience, you acknowledge that the accuracy and usefulness of AI-generated content may vary. We do not guarantee the correctness, completeness, or reliability of AI outputs and disclaim all responsibility for any adverse effects resulting from their use. Use of these features is entirely at your own risk, and you agree to hold Natural Selection Limited harmless for any errors, misunderstandings, or unintended outcomes arising from the use of AI functionality.
### 5.4 $POWER Economy
Follow introduces the $POWER system, a way for users to support content creators and contributors by tipping or rewarding them with $POWER. You can acquire $POWER by participating in the community, completing designated tasks, or purchasing it through the app. You agree to abide by the rules governing the $POWER system, including: (i) not engaging in fraudulent activities or exploiting the system for illegal or malicious purposes; (ii) acknowledging that $POWER cannot be exchanged for real-world currency or transferred outside of the Service; (iii) accepting that $POWER transactions are final and non-refundable.
Follow reserves the right to modify, suspend, or terminate the $POWER system at any time without prior notice.
## 6. Prohibited Activities
You agree not to engage in any of the following activities while using the Service:
- Engage in any illegal or harmful activities, including distributing malicious software or engaging in data breaches.
- Attempt to gain unauthorized access to any part of the Service or its security features.
- Misuse the $POWER system by gaming the economy or making fraudulent transactions.
- Engage in any behavior that disrupts the normal functionality of the Service or harms other users experiences.
Violations of these activities may result in the immediate suspension or termination of your account. In some cases, legal action may be taken if necessary to protect our rights or the rights of others.
## 7. Disclaimer of Warranties
The Service is provided on an "AS IS" and "AS AVAILABLE" basis. We do not make any representations or warranties regarding the availability, functionality, or reliability of the Service. We disclaim all express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. We do not guarantee that the Service will be uninterrupted, error-free, or free from security vulnerabilities.
## 8. Limitation of Liability
To the fullest extent permitted by law, Natural Selection Limited shall not be liable for any indirect, incidental, special, or consequential damages arising from your use of the Service, including but not limited to loss of profits, data, or goodwill. In no event shall our liability exceed the total amount you have paid to access the Service during the past 12 months. You agree that your use of the Service is at your own risk.
## 9. Modifications to the Terms
We reserve the right to modify these Terms at any time. Any changes will be effective immediately upon posting on the Service. We will notify you of any significant changes, but it is your responsibility to review the Terms periodically. Your continued use of the Service after any modifications will constitute your acceptance of the updated Terms. If you do not agree to the changes, you must cease using the Service and delete your account.
## 10. Termination
We may suspend, disable, or terminate your access to the Service at any time, for any reason, including but not limited to violations of these Terms, fraudulent behavior, or other actions that disrupt the normal operation of the Service. Upon termination, your account will be deactivated, and you may lose access to your content and any other account-related data. If you wish to terminate your account, you can do so by contacting us or using the account settings feature within the app.
## 11. Governing Law
These Terms are governed by and construed in accordance with the laws of [Insert Jurisdiction]. Any disputes arising out of or in connection with these Terms will be subject to the exclusive jurisdiction of the courts of [Insert Jurisdiction].
## 12. Contact Us
If you have any questions, concerns, or inquiries about these Terms, please contact us at:
- Email: follow@rss3.io
By using Follow, you acknowledge that you have read, understood, and agree to these Terms of Service, as well as our Privacy Policy.
## 13. Community Participation and Contribution
Follow is an open-source project, and we welcome contributions from users and developers. If you are eligible to use Follow, you may participate in the development of the Service by submitting bug reports, feature requests, and improvements. All contributions must adhere to our [code of conduct](https://github.com/RSSNext/Follow/blob/main/CODE_OF_CONDUCT.md).
Before contributing, ensure that you have read and understood our contributing guidelines and the [Corepack](https://nodejs.org/api/corepack.html) setup instructions. By contributing, you agree that your submissions will be licensed under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html) version 3.
## 14. Privacy and Data Use
Follow takes your privacy seriously. As a user, you acknowledge that we may collect, store, and process your personal information, including your usage patterns and interactions with content. We are committed to ensuring that your data is handled securely and transparently. Please refer to our [Privacy Policy](#) for more information on how we collect, process, and protect your data.
`
export const TeamsMarkdown = () => {
return (
<MarkdownWeb
value={txt}
dom={{ matchContents: true, scrollEnabled: false }}
style={{ padding: 16 }}
/>
)
}
export default function Teams() {
return (
<View className="flex-1">
<Stack.Screen
options={{ headerBackTitle: "Login", headerTitle: "Terms of Service", headerShown: true }}
/>
<TeamsMarkdown />
</View>
)
}

View File

@ -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