diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 040245c0..65b54c42 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,21 +6,17 @@ labels: bug assignees: "" --- -**Describe the bug** -A clear and concise description of what the bug is. +**Describe the bug** A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior: -(we recommend record a screenshot to show the case, like using http://loom.com to record) +**To Reproduce** Steps to reproduce the behavior: (we recommend record a +screenshot to show the case, like using http://loom.com to record) -**Expected behavior** -A clear and concise description of what you expected to happen. +**Expected behavior** A clear and concise description of what you expected to +happen. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Screenshots** If applicable, add screenshots to help explain your problem. -\*\* Node.js version -(The node version on your computer to run the project) +\*\* Node.js version (The node version on your computer to run the project) **Desktop (please complete the following information):** @@ -35,5 +31,4 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. stock browser, safari] - Version [e.g. 22] -**Additional context** -Add any other context about the problem here. +**Additional context** Add any other context about the problem here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fd82b389..4f82edf7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,18 +1,17 @@ -## -### Description +# Description -Please provide a clear and concise description of the changes made, including the purpose and context. +Please provide a clear and concise description of the changes made, including +the purpose and context. **Fixes**: # (issue number) or @@ -20,14 +19,16 @@ or --- -### Changes Made +## Changes Made -- [ ] Changes in **`apps`** folder (specify the app and briefly describe the changes): +- [ ] Changes in **`apps`** folder (specify the app and briefly describe the + changes): - [ ] `Web` - [ ] `Native` -- [ ] Changes in **`packages`** folder (specify the package and briefly describe the changes): +- [ ] Changes in **`packages`** folder (specify the package and briefly describe + the changes): - [ ] `Core` --- @@ -36,12 +37,13 @@ or - [ ] 🐛 **Bug fix** (non-breaking change which fixes an issue) - [ ] ✨ **New feature** (non-breaking change which adds functionality) -- [x] 💥 **Breaking change** (fix or feature that would cause existing functionality to not work as expected) +- [ ] 💥 **Breaking change** (fix or feature that would cause existing + functionality to not work as expected) - [ ] 📝 **Documentation update** (changes) --- -## Screenshots +#### Screenshots | Before | After | | :-----------------: | :----------------: | diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml new file mode 100644 index 00000000..2b340900 --- /dev/null +++ b/.github/workflows/lint-format.yml @@ -0,0 +1,33 @@ +name: Lint & Commit Check + +on: + pull_request: + push: + branches: + - main + +jobs: + lint-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run Prettier Check + run: yarn prettier --check . + + - name: Run Lint-Staged (Prettier & ESLint) + run: yarn lint-staged + + - name: Run Commitlint on Last Commit + run: git log -1 --pretty=format:%s | npx --no -- commitlint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..9e87c054 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +bin \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 9f5494f9..67cdc026 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,5 +5,13 @@ "trailingComma": "all", "useTabs": false, "tabWidth": 2, - "singleQuote": false + "singleQuote": false, + "overrides": [ + { + "files": "*.md", + "options": { + "proseWrap": "always" + } + } + ] } diff --git a/README.md b/README.md index 63fd0f3c..27df3ceb 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,61 @@ # Treetracker Wallet App: Secure and Easy Token Management -**Greenstand** provides a secure and user-friendly platform for managing your digital tokens. Sending and receiving tokens takes just a few taps, making it a breeze to transfer them between users. +**Greenstand** provides a secure and user-friendly platform for managing your +digital tokens. Sending and receiving tokens takes just a few taps, making it a +breeze to transfer them between users. ## **Project Structure:** -Treetracker leverages a monorepo structure, meaning it houses multiple projects in a single repository. This allows for efficient code sharing across different platforms. Here's a breakdown: +Treetracker leverages a monorepo structure, meaning it houses multiple projects +in a single repository. This allows for efficient code sharing across different +platforms. Here's a breakdown: -- **`apps/native`:** This directory contains the React Native code for the mobile app. +- **`apps/native`:** This directory contains the React Native code for the + mobile app. - **`apps/web`:** This directory holds the Next.js code for the web app. -- **`packages/core`:** This shared folder contains the core model layer, accessible by both the mobile and web apps. +- **`packages/core`:** This shared folder contains the core model layer, + accessible by both the mobile and web apps. ## **Getting Started:** Excited to dive in? Here's how to get up and running: -1. **Clone the repository:** Use `git clone https://github.com/Greenstand/treetracker-wallet-app` to grab the code. +1. **Clone the repository:** Use + `git clone https://github.com/Greenstand/treetracker-wallet-app` to grab the + code. -2. **Install dependencies:** Run `yarn` in the main project directory to install all the necessary tools. +2. **Install dependencies:** Run `yarn` in the main project directory to + install all the necessary tools. 3. **Start Development Server (Choose your platform):** -- **Web App:** Navigate to the `web` directory and run `yarn dev`. This launches the Next.js development server, accessible at http://localhost:3000 in your web browser. +- **Web App:** Navigate to the `web` directory and run `yarn dev`. This launches + the Next.js development server, accessible at http://localhost:3000 in your + web browser. -- **Mobile App:** Head to the `native` directory and run `yarn start`. This starts the Expo development server for your mobile app. +- **Mobile App:** Head to the `native` directory and run `yarn start`. This + starts the Expo development server for your mobile app. ## **Changelog** -We use [Conventional Changelog](https://github.com/conventional-changelog/conventional-changelog) to generate our changelog. This means that all changes should be committed using the Conventional Commits format. +We use +[Conventional Changelog](https://github.com/conventional-changelog/conventional-changelog) +to generate our changelog. This means that all changes should be committed using +the Conventional Commits format. -Here are some examples of commit messages and how they would appear in the changelog: +Here are some examples of commit messages and how they would appear in the +changelog: -- **feat:** A new feature - Commit message: `feat: add support for token transfers` +- **feat:** A new feature Commit message: + `feat: add support for token transfers` -- **fix:** A bug fix - Commit message: `fix: prevent token balance from being negative` +- **fix:** A bug fix Commit message: + `fix: prevent token balance from being negative` -- **docs:** An update to documentation - Commit message: `docs: add instructions for contributing` +- **docs:** An update to documentation Commit message: + `docs: add instructions for contributing` ## **Contributing:** @@ -47,13 +63,16 @@ We value your input! Here's how to contribute: - **Found a bug or have an idea?** Open an issue on our GitHub repository. -- **Want to add code?** Fork the repository, make your changes, and submit a pull request. +- **Want to add code?** Fork the repository, make your changes, and submit a + pull request. -- **Testing and Documentation Matter:** Ensure your code is well-tested and adheres to our coding standards before submitting. +- **Testing and Documentation Matter:** Ensure your code is well-tested and + adheres to our coding standards before submitting. **Thank You!** -We appreciate your interest in contributing to Treetracker. Your time and effort are invaluable in making this project even better! +We appreciate your interest in contributing to Treetracker. Your time and effort +are invaluable in making this project even better! **For further details, explore the individual project READMEs:** diff --git a/apps/native/README.md b/apps/native/README.md index 1e3c52ad..856cdb19 100644 --- a/apps/native/README.md +++ b/apps/native/README.md @@ -2,13 +2,15 @@ ========================== -The Treetracker Wallet Mobile App is a user-friendly interface for managing digital tokens. +The Treetracker Wallet Mobile App is a user-friendly interface for managing +digital tokens. ### Overview --- -This app is built using Expo and React Native, and provides a secure and scalable way to manage digital tokens. +This app is built using Expo and React Native, and provides a secure and +scalable way to manage digital tokens. ### Features @@ -37,7 +39,8 @@ cd apps/native yarn start ``` -This will start the app in development mode, and you can access it by scanning the QR code with your Expo Go app. +This will start the app in development mode, and you can access it by scanning +the QR code with your Expo Go app. ### Testing @@ -47,7 +50,8 @@ This will start the app in development mode, and you can access it by scanning t We use Maestro for testing of our app: -- **End-to-End Testing (E2E)**: Testing the entire application flow to ensure it works as expected, from start to finish. +- **End-to-End Testing (E2E)**: Testing the entire application flow to ensure it + works as expected, from start to finish. ```bash yarn maestro test diff --git a/apps/native/app/(auth)/login.tsx b/apps/native/app/(auth)/login.tsx index 00125450..0ce4030d 100644 --- a/apps/native/app/(auth)/login.tsx +++ b/apps/native/app/(auth)/login.tsx @@ -1,3 +1,98 @@ -import LoginScreen from "@/screens/auth/Login.screen"; +import React, { useState } from "react"; +import { + View, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, +} from "react-native"; +import CustomTextInput from "@/components/ui/common/CustomTextInput"; +import CustomTitle from "@/components/ui/common/CustomTitle"; +import CustomSubmitButton from "@/components/ui/common/CustomSubmitButton"; -export default () => ; +const LoginScreen = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const isLoginEnabled = email.length > 0 && password.length > 0; + console.log(isLoginEnabled); + + const handleLogIn = () => {}; + + return ( + + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "white", + }, + keyboardContainer: { + flex: 1, + }, + scrollContainer: { + flex: 1, + justifyContent: "center", + paddingHorizontal: 20, + }, + + buttonContainer: { + paddingVertical: 13, + alignItems: "center", + }, + buttonActive: { + backgroundColor: "#61892F", + paddingVertical: 15, + width: "100%", + alignItems: "center", + }, + buttonDisabled: { + backgroundColor: "gray", + opacity: 0.5, + paddingVertical: 15, + width: "100%", + alignItems: "center", + }, + buttonText: { + fontSize: 18, + fontWeight: "bold", + color: "white", + }, +}); + +export default LoginScreen; diff --git a/apps/native/app/SignUp.tsx b/apps/native/app/SignUp.tsx new file mode 100644 index 00000000..db823139 --- /dev/null +++ b/apps/native/app/SignUp.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { ThemedView } from "@/components/ThemedView"; +import { ThemedText } from "@/components/ThemedText"; +import { Button } from "@/components/Button"; +import { StyleSheet, TextInput, View } from "react-native"; +import { SafeAreaView, SafeAreaProvider } from "react-native-safe-area-context"; +import { Text } from "react-native"; + +const SignUp = () => { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const Submit = () => { + //signup + }; + + const GmailSubmit = () => { + //signup with gmail + }; + + const FacebookSubmit = () => { + //signup with facebook + }; + + const GithubSubmit = () => { + //signup with github + }; + + return ( + + + Sign Up + setName} + placeholder="Name"> + setEmail} + placeholder="Email"> + setPassword} + secureTextEntry={true} + placeholder="Password"> + + + + or + + + + + + + + + + + + + Have an accout? Log in + + By continuing I agree to Greenstand's Privacy Policy and Terms of Use + + + + ); +}; + +const styles = StyleSheet.create({ + input: {}, +}); + +const altSignup = StyleSheet.create({ + input: {}, +}); + +export default SignUp; diff --git a/apps/native/app/_layout.tsx b/apps/native/app/_layout.tsx index ad8ad1d9..f59ff6c3 100644 --- a/apps/native/app/_layout.tsx +++ b/apps/native/app/_layout.tsx @@ -1,32 +1,14 @@ import FontAwesome from "@expo/vector-icons/FontAwesome"; import { useFonts } from "expo-font"; -import { Stack } from "expo-router"; +import { Slot } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; import "react-native-reanimated"; -import { - MD3LightTheme as DefaultTheme, - PaperProvider, -} from "react-native-paper"; - -import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; -import { StyleSheet } from "react-native"; - -export { ErrorBoundary } from "expo-router"; - -const theme = { - ...DefaultTheme, - colors: { - ...DefaultTheme.colors, - primary: "tomato", - secondary: "yellow", - }, -}; SplashScreen.preventAutoHideAsync(); -export default function RootLayout() { - const [loaded, error] = useFonts({ +export default function AppLayout() { + const [areFontsLoaded, fontLoadError] = useFonts({ SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), Roboto: require("../assets/fonts/Roboto-Regular.ttf"), RobotoBold: require("../assets/fonts/Roboto-Bold.ttf"), @@ -35,48 +17,10 @@ export default function RootLayout() { }); useEffect(() => { - if (error) throw error; - }, [error]); - - useEffect(() => { - if (loaded) { + if (areFontsLoaded || fontLoadError) { SplashScreen.hideAsync(); } - }, [loaded]); + }, [areFontsLoaded, fontLoadError]); - if (!loaded) { - return null; - } - - return ( - - - - - - ); + return ; } - -export const options = { - headerShown: false, -}; - -function RootLayoutNav() { - return ( - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); diff --git a/apps/native/app/index.tsx b/apps/native/app/index.tsx index d9024032..df6ae917 100644 --- a/apps/native/app/index.tsx +++ b/apps/native/app/index.tsx @@ -1,2 +1,24 @@ -import OnboardingScreen from "@/screens/onboarding/Onboarding.screen"; -export default () => ; +import React, { useEffect, useState } from "react"; +import { Redirect, useRouter } from "expo-router"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export default function InitialRoute() { + const [shouldShowOnboarding, setShouldShowOnboarding] = useState(false); + const router = useRouter(); + + useEffect(() => { + const verifyAppLaunchStatus = async () => { + const hasCompletedOnboarding = await AsyncStorage.getItem("hasLaunched"); + + if (hasCompletedOnboarding === null) { + setShouldShowOnboarding(true); + } else { + router.replace("/(auth)/login"); + } + }; + + verifyAppLaunchStatus(); + }, [router]); + + return shouldShowOnboarding ? : null; +} diff --git a/apps/native/screens/onboarding/Onboarding.screen.tsx b/apps/native/app/onboarding/index.tsx similarity index 73% rename from apps/native/screens/onboarding/Onboarding.screen.tsx rename to apps/native/app/onboarding/index.tsx index 3ae9fc19..b209dcc7 100644 --- a/apps/native/screens/onboarding/Onboarding.screen.tsx +++ b/apps/native/app/onboarding/index.tsx @@ -1,5 +1,5 @@ -import { router } from "expo-router"; -import React, { useState } from "react"; +import { useRouter } from "expo-router"; +import React, { useState, useCallback } from "react"; import { View, Text, @@ -15,6 +15,8 @@ import Leafs from "@/assets/svg/leafs.svg"; import Wallet from "@/assets/svg/wallet.svg"; import Cloud from "@/assets/svg/cloud.svg"; import { SvgProps } from "react-native-svg"; +import CustomButton from "@/components/ui/common/CustomButton"; +import AsyncStorage from "@react-native-async-storage/async-storage"; const { width, height } = Dimensions.get("window"); @@ -49,9 +51,25 @@ const DATA: OnboardingItem[] = [ const OnboardingScreen = () => { const [currentIndex, setCurrentIndex] = useState(0); - const handleScroll = (event: NativeSyntheticEvent) => { - const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width); - setCurrentIndex(slideIndex); + const router = useRouter(); + let flashListRef = React.useRef>(null); + + const handleScroll = useCallback( + (event: NativeSyntheticEvent) => { + const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width); + setCurrentIndex(slideIndex); + }, + [], + ); + + const handleSignUp = async () => { + await AsyncStorage.setItem("hasLaunched", "true"); + router.push("/(auth)/register"); + }; + + const handleLogIn = async () => { + await AsyncStorage.setItem("hasLaunched", "true"); + router.push("/(auth)/login"); }; const renderItem = ({ item }: { item: OnboardingItem }) => { @@ -68,18 +86,24 @@ const OnboardingScreen = () => { return ( item.id} /> + {DATA.map((_, index) => ( - + flashListRef.current?.scrollToIndex({ index, animated: true }) + } style={[ styles.circleWrapper, currentIndex === index ? styles.activeCircle : null, @@ -90,24 +114,18 @@ const OnboardingScreen = () => { currentIndex === index ? styles.activeDot : styles.inactiveDot, ]} /> - + ))} + - {currentIndex === DATA.length - 1 ? ( - router.push("/(auth)/login")} - style={styles.button}> - GET STARTED - - ) : ( - {}} style={styles.button}> - CONTINUE - - )} - - SKIP THE TOUR - + + + ); @@ -150,11 +168,9 @@ const styles = StyleSheet.create({ justifyContent: "center", width: 20, height: 20, - marginHorizontal: 5, + marginHorizontal: 1, }, activeCircle: { - borderWidth: 2, - borderColor: "#4CAF50", borderRadius: 10, }, dot: { @@ -163,16 +179,17 @@ const styles = StyleSheet.create({ borderRadius: 6, }, activeDot: { - backgroundColor: "#4CAF50", + backgroundColor: "#FF7A00", }, inactiveDot: { - backgroundColor: "transparent", + backgroundColor: "#BDBDBD", borderWidth: 2, borderColor: "#ccc", }, buttonContainer: { alignItems: "center", paddingHorizontal: 20, + gap: 8, marginBottom: 20, }, button: { diff --git a/apps/native/components/ui/common/CustomButton.tsx b/apps/native/components/ui/common/CustomButton.tsx new file mode 100644 index 00000000..46d72d3e --- /dev/null +++ b/apps/native/components/ui/common/CustomButton.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { + TouchableOpacity, + Text, + StyleSheet, + ViewStyle, + TextStyle, +} from "react-native"; + +type ButtonProps = { + title: string; + onPress: () => void; + variant?: "primary" | "secondary"; + style?: ViewStyle; + textStyle?: TextStyle; +}; + +const CustomButton: React.FC = ({ + title, + onPress, + variant = "primary", + style, + textStyle, +}) => { + const isSecondary = variant === "secondary"; + + return ( + + + {title} + + + ); +}; + +const styles = StyleSheet.create({ + button: { + backgroundColor: "#61892F", + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: "center", + justifyContent: "center", + width: "100%", + }, + secondaryButton: { + backgroundColor: "transparent", + }, + buttonText: { + color: "#fff", + fontSize: 16, + fontWeight: "bold", + }, + secondaryText: { + color: "#6B6E70", + }, +}); + +export default CustomButton; diff --git a/apps/native/components/ui/common/CustomSubmitButton.tsx b/apps/native/components/ui/common/CustomSubmitButton.tsx new file mode 100644 index 00000000..2a5abfdb --- /dev/null +++ b/apps/native/components/ui/common/CustomSubmitButton.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Pressable, Text, StyleSheet } from "react-native"; +import { Colors } from "@/constants/Colors"; + +interface CustomSubmitButtonProps { + onPress: () => void; + title: string; + loading?: boolean; + disabled?: boolean; + style?: object; +} + +const CustomSubmitButton: React.FC = ({ + onPress, + title, + loading = false, + disabled = false, + style = {}, +}) => { + const isDisabled = disabled || loading; + + return ( + + + {title} + + + ); +}; + +const styles = StyleSheet.create({ + button: { + backgroundColor: Colors.green, + borderRadius: 5, + paddingVertical: 15, + paddingHorizontal: 30, + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + alignSelf: "stretch", + }, + buttonDisabled: { + backgroundColor: "#ddd", + }, + buttonText: { + color: Colors.white, + fontSize: 16, + fontWeight: "600", + textTransform: "uppercase", + }, + buttonTextDisabled: { + color: "#888", + }, +}); + +export default CustomSubmitButton; diff --git a/apps/native/components/ui/common/CustomTextInput.tsx b/apps/native/components/ui/common/CustomTextInput.tsx new file mode 100644 index 00000000..1e95893f --- /dev/null +++ b/apps/native/components/ui/common/CustomTextInput.tsx @@ -0,0 +1,193 @@ +import { Colors } from "@/constants/Colors"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import React, { useState, useRef } from "react"; +import { + View, + TextInput, + Text, + StyleSheet, + TouchableOpacity, + Animated, +} from "react-native"; + +interface CustomTextInputProps { + label: string; + placeholder?: string; + secureTextEntry?: boolean; + onChangeText: (text: string) => void; + value: string; + keyboardType?: "default" | "email-address" | "numeric" | "phone-pad"; + helperText?: string; + error?: boolean; +} + +const CustomTextInput: React.FC = ({ + label, + placeholder = "", + secureTextEntry = false, + onChangeText, + value, + keyboardType = "default", + helperText, + error = false, +}) => { + const [isSecure, setIsSecure] = useState(secureTextEntry); + const [isFocused, setIsFocused] = useState(false); + const labelAnim = useRef(new Animated.Value(value ? 1 : 0)).current; + + const handleFocus = () => { + setIsFocused(true); + Animated.timing(labelAnim, { + toValue: 1, + duration: 150, + useNativeDriver: false, + }).start(); + }; + + const handleBlur = () => { + setIsFocused(false); + if (!value) { + Animated.timing(labelAnim, { + toValue: 0, + duration: 150, + useNativeDriver: false, + }).start(); + } + }; + + return ( + + + {label} + + + + + {error ? ( + + ) : value.length > 0 && secureTextEntry ? ( + setIsSecure(prev => !prev)} + style={styles.icon}> + + + ) : value.length > 0 ? ( + onChangeText("")} + style={styles.icon}> + + + ) : null} + + + {error && ( + + {helperText} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginVertical: 13, + width: "100%", + position: "relative", + }, + label: { + position: "absolute", + left: 14, + top: 16, + fontSize: 16, + color: "#757575", + backgroundColor: "transparent", + zIndex: 10, + }, + inputContainer: { + flexDirection: "row", + alignItems: "center", + borderRadius: 4, + backgroundColor: Colors.lightGray, + paddingHorizontal: 14, + height: 60, + borderBottomWidth: 2, + borderBottomColor: "#BDBDBD", + }, + input: { + flex: 1, + height: "100%", + fontSize: 16, + color: "#333", + paddingVertical: 8, + textAlignVertical: "bottom", + }, + inputError: { + borderBottomColor: Colors.red, + }, + inputFocused: { + borderBottomColor: Colors.green, + }, + icon: { + marginLeft: 8, + top: 8, + padding: 8, + }, + helperText: { + position: "absolute", + left: 14, + bottom: -18, + fontSize: 12, + zIndex: 5, + }, + helperTextSuccess: { + color: Colors.green, + }, + helperTextError: { + color: Colors.red, + }, +}); + +export default CustomTextInput; diff --git a/apps/native/components/ui/common/CustomTitle.tsx b/apps/native/components/ui/common/CustomTitle.tsx new file mode 100644 index 00000000..ed5d33ed --- /dev/null +++ b/apps/native/components/ui/common/CustomTitle.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Text, View, StyleSheet } from "react-native"; + +interface CustomTitleProps { + title: string; + subtitle?: string; +} + +const CustomTitle: React.FC = ({ title, subtitle }) => { + return ( + + {title} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: 10, + marginBottom: 20, + }, + title: { + fontSize: 30, + color: "#222629DE", + }, +}); + +export default CustomTitle; diff --git a/apps/native/constants/Colors.ts b/apps/native/constants/Colors.ts index c5e8f37e..6ebc181c 100644 --- a/apps/native/constants/Colors.ts +++ b/apps/native/constants/Colors.ts @@ -1,26 +1,13 @@ -/** - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. - * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. - */ - -const tintColorLight = "#0a7ea4"; -const tintColorDark = "#fff"; - export const Colors = { - dark: { - background: "#151718", - icon: "#9BA1A6", - tabIconDefault: "#9BA1A6", - tabIconSelected: tintColorDark, - text: "#ECEDEE", - tint: tintColorDark, - }, - light: { - background: "#fff", - icon: "#687076", - tabIconDefault: "#687076", - tabIconSelected: tintColorLight, - text: "#11181C", - tint: tintColorLight, - }, + green: "#61892F", + white: "#FFFFFF", + red: "#D32F2F", + lightOrange: "#FF7A0080", + lightGreen: "#86C232", + lightGray: "#eeee", + darkGray: "#22262999", + gray: "#E0E0E0", + blackOverlay: "rgba(0, 0, 0, 0.5)", + shadowLight: "rgba(0, 0, 0, 0.1)", + shadowDark: "rgba(0, 0, 0, 0.2)", }; diff --git a/apps/native/screens/auth/Login.screen.tsx b/apps/native/screens/auth/Login.screen.tsx deleted file mode 100644 index 7998d9af..00000000 --- a/apps/native/screens/auth/Login.screen.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { Link } from "expo-router"; -import React from "react"; -import { - View, - Text, - StyleSheet, - Dimensions, - SafeAreaView, - TouchableOpacity, - KeyboardAvoidingView, - Platform, - TextInput, -} from "react-native"; -import TreeTraderLogo from "@/assets/svg/TreeTraderLogo.svg"; -const { height, width } = Dimensions.get("window"); - -const LoginScreen = () => { - const [login, onChangeLogin] = React.useState(""); - const [password, onChangePassword] = React.useState(""); - const loginButtonEnabled = login.length > 0 && password.length > 0; - - return ( - - - {/* TOP */} - - - - - Log In - - We are excited to see you again! - - - - {/* MIDDLE */} - - - - Email - - - - - - - - - - Password - - - - - - - {/* Login Button */} - - {}}> - - LOG IN - - - - - - ); -}; - -const styles = StyleSheet.create({ - // ----------------------------------------------------------- TOP - loginPageContainer: { - // main cointainer - flex: 1, - height: height, - width: width, - backgroundColor: "white", - }, - logoContainer: { - // container - alignContent: "center", - height: "20%", - width: "100%", - alignItems: "center", - justifyContent: "center", - backgroundColor: "white", - }, - textTitlePageContainer: { - // container - height: "15%", - marginTop: 0, - alignItems: "center", - backgroundColor: "white", - }, - textTitleLoginPage: { - width: "90%", - fontSize: 30, - fontWeight: "400", - color: "black", - backgroundColor: "white", - }, - descriptionLogin: { - width: "90%", - marginTop: 5, - fontSize: 15, - fontWeight: "400", - color: "black", - }, - // ------------------------------------------------------------ MIDDLE - inputBoxContainer: { - // container - height: "12%", - marginTop: 20, - justifyContent: "center", - alignItems: "center", - backgroundColor: "white", - }, - inputBox: { - height: "100%", - width: "90%", - backgroundColor: "#f1f3ef", - }, - placeholderInputBox: { - position: "absolute", - top: 16, - left: 5, - fontSize: 20, - color: "#aaa", - }, - placeholderActive: { - fontSize: 12, - top: 5, - color: "#666", - }, - textInputBox: { - height: "100%", - fontSize: 20, - }, - underline: { - position: "absolute", - bottom: 0, // Linha na parte inferior - left: 0, - right: 0, - height: 2, - opacity: 0.42, - backgroundColor: "black", - }, - loginButtonContainer: { - // container - backgroundColor: "white", - height: "15%", - alignItems: "center", - justifyContent: "center", - }, - loginButtonUnavailable: { - alignItems: "center", - justifyContent: "center", - backgroundColor: "gray", - opacity: 0.2, - height: "50%", - width: "91%", - marginTop: 10, - borderRadius: 10, - }, - loginButtonAvailable: { - backgroundColor: "#61892F", - marginTop: 10, - height: "50%", - width: "91%", - borderRadius: 10, - alignItems: "center", - justifyContent: "center", - }, - placeholderLoginButtonAvailable: { - fontSize: 20, - color: "white", - fontWeight: "500", - }, - placeholderLoginButtonUnavailable: { - fontSize: 20, - color: "gray", - fontWeight: "500", - }, - // ---------------------------------------------------------- BOTTOM - forgotPasswordContainer: { - // container - }, - textForgotPassword: {}, - buttonLoginWithExternalAppsContainer: { - // container - }, - placeholderLoginWithExternalApps: {}, - svgExternalApps: {}, - // -------------------------------------------------------------- SignUp - signUpContainer: {}, - textSignUpContainer: {}, -}); - -export default LoginScreen; diff --git a/apps/user/README.md b/apps/user/README.md index 3db76bbd..1d3760ef 100644 --- a/apps/user/README.md +++ b/apps/user/README.md @@ -50,22 +50,21 @@ http://localhost:8080/swagger ## How to access wallet api (Draft) -### Set up Keycloak +### Set up Keycloak -Attach permission by adding new role to this user api client, so the wallet api can auth with this user api client by verifying the role, say: `wallet-operator-microservice` +Attach permission by adding new role to this user api client, so the wallet api +can auth with this user api client by verifying the role, say: +`wallet-operator-microservice` -1. Create a new realm role - 1.1. Go to `Realm Roles` -> `Create Role` - 1.2. Name it `wallet-operator-microservice` - 1.3. Save - -2. Attach the role to the user api client - 2.1. Go to `Clients` -> `wallet-app-user-dev-svc` -> `Client scopes` - 2.2. Find the item: `xxx-dedicated`, here in this case, it is `wallet-app-user-dev-svc-dedicated` - 2.3. Click `Add mapper` -> `by configuration` -> `Hardcoded role` - 2.4. Input `operator`, choose the role created in step 1, `wallet-operator-microservice` - 2.5. Save +1. Create a new realm role 1.1. Go to `Realm Roles` -> `Create Role` 1.2. Name + it `wallet-operator-microservice` 1.3. Save +2. Attach the role to the user api client 2.1. Go to `Clients` -> + `wallet-app-user-dev-svc` -> `Client scopes` 2.2. Find the item: + `xxx-dedicated`, here in this case, it is `wallet-app-user-dev-svc-dedicated` + 2.3. Click `Add mapper` -> `by configuration` -> `Hardcoded role` 2.4. Input + `operator`, choose the role created in step 1, `wallet-operator-microservice` + 2.5. Save ### Get access token @@ -132,9 +131,11 @@ will get a response like: } ``` -In the realm role, `wallet-operator-microservice` is attached to the user api client, `wallet-app-user-dev-svc`, so the access token can be used to access the wallet api. +In the realm role, `wallet-operator-microservice` is attached to the user api +client, `wallet-app-user-dev-svc`, so the access token can be used to access the +wallet api. ### Access wallet api -The wallet api will verify the access token by checking the role `wallet-operator-microservice` in the access token. - +The wallet api will verify the access token by checking the role +`wallet-operator-microservice` in the access token. diff --git a/apps/web/cypress/component/ProfileAvatar.cy.tsx b/apps/web/cypress/component/ProfileAvatar.cy.tsx new file mode 100644 index 00000000..9789b79a --- /dev/null +++ b/apps/web/cypress/component/ProfileAvatar.cy.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import ProfileAvatar from "../../src/app/settings/account/ProfileAvatar"; + +describe("ProfileAvatar Component", () => { + it("renders avatar with initials", () => { + cy.mount(); + cy.get("svg").should("exist"); // Checks if an avatar is rendered + cy.contains("J").should("exist"); + }); + + it("handles different name formats correctly", () => { + const testCases = [ + { input: "John Doe", expected: "JD" }, + { input: "Jane", expected: "J" }, + { input: "John Middle Doe", expected: "JD" }, + ]; + + testCases.forEach(({ input, expected }) => { + cy.mount(); + cy.get(".MuiAvatar-root").should("have.text", expected); + }); + }); + + it("renders avatar with image", () => { + cy.mount(); + cy.get("img").should("have.attr", "src", "dummy-url"); + }); +}); diff --git a/apps/web/cypress/component/ProfileDetails.cy.tsx b/apps/web/cypress/component/ProfileDetails.cy.tsx new file mode 100644 index 00000000..65822e40 --- /dev/null +++ b/apps/web/cypress/component/ProfileDetails.cy.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ProfileDetails from "../../src/app/settings/account/ProfileDetails"; + +describe("ProfileDetails Component", () => { + it("displays user name and email", () => { + cy.mount(); + cy.contains("John Doe").should("exist"); + cy.contains("Emailaddress@gmail.com").should("exist"); + }); + + it("renders the edit profile button", () => { + cy.mount(); + cy.contains("Edit Profile").should("exist").click(); + }); +}); diff --git a/apps/web/package.json b/apps/web/package.json index e6fffc13..0674f40e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,7 +16,7 @@ "cy:open": "cypress open" }, "dependencies": { - "wallet_state":"1.0.0", + "wallet_state": "1.0.0", "@emotion/cache": "11.13.1", "@emotion/react": "11.13.0", "@emotion/styled": "11.13.0", diff --git a/apps/web/src/app/settings/account/ProfileAvatar.tsx b/apps/web/src/app/settings/account/ProfileAvatar.tsx new file mode 100644 index 00000000..24b03239 --- /dev/null +++ b/apps/web/src/app/settings/account/ProfileAvatar.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React from "react"; +import { Avatar } from "@mui/material"; +import QrCodeIcon from "@mui/icons-material/QrCode"; + +interface ProfileAvatarProps { + name: string; + profileImageUrl?: string; +} + +const getInitials = (name: string) => { + const parts = name.trim().split(/\s+/); // Split by whitespace + if (parts.length === 1) return parts[0][0].toUpperCase(); // Single name case + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); // First + Last initials +}; + +export default function ProfileAvatar({ + name, + profileImageUrl, +}: ProfileAvatarProps) { + return ( +
+ + {!profileImageUrl && getInitials(name)} + + +
+ ); +} diff --git a/apps/web/src/app/settings/account/ProfileDetails.tsx b/apps/web/src/app/settings/account/ProfileDetails.tsx new file mode 100644 index 00000000..0d9166a3 --- /dev/null +++ b/apps/web/src/app/settings/account/ProfileDetails.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React from "react"; +import { Typography, Button } from "@mui/material"; + +interface ProfileDetailsProps { + name: string; + email: string; +} + +export default function ProfileDetails({ name, email }: ProfileDetailsProps) { + return ( + <> + + {name} + + + {email} + + + + ); +} diff --git a/apps/web/src/app/settings/account/page.tsx b/apps/web/src/app/settings/account/page.tsx new file mode 100644 index 00000000..6514f21f --- /dev/null +++ b/apps/web/src/app/settings/account/page.tsx @@ -0,0 +1,124 @@ +"use client"; + +import React from "react"; +import { Box, Stack, Button, Typography } from "@mui/material"; +import NotificationsIcon from "@mui/icons-material/NotificationsNoneOutlined"; +import AccountBalanceWalletOutlinedIcon from "@mui/icons-material/AccountBalanceWalletOutlined"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import { useRouter } from "next/navigation"; +import ProfileAvatar from "./ProfileAvatar"; +import ProfileDetails from "./ProfileDetails"; + +export default function Account() { + const router = useRouter(); + + // Mock user data, to be replaced with actual user data later + const user = { + name: "John Doe", + email: "Emailaddress@gmail.com", + profileImageUrl: "", + }; + + return ( + + + + + + + + | + + + + + + + + Member since May 2023 + + + + ); +} diff --git a/lint-staged.config.js b/lint-staged.config.js index ccc8b10b..af7ced71 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,3 +1,3 @@ module.exports = { - "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"], + "*.{js,jsx,ts,tsx,md}": ["prettier --write", "eslint --fix"], }; diff --git a/package.json b/package.json index 618bd104..3e871e40 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,16 @@ "license": "MIT", "private": true, "scripts": { + "web:dev": "yarn workspace web dev", + "web:prod": "yarn workspace web start", + "web:build": "yarn workspace build", + "native:start": "yarn workspace native start", + "native:android": "yarn workspace native android", + "native:ios": "yarn workspace ios", + "user:build": "yarn workspace user build", + "user:dev": "yarn workspace user dev", + "user:debug": "yarn workspace user debug", + "user:prod": "yarn workspace user prod", "pre-commit": "yarn workspaces run lint-staged", "cypress-component-test": "yarn workspace web run cy:component", "commitlint": "commitlint", diff --git a/packages/queue/__tests__/index.spec.js b/packages/queue/__tests__/index.spec.js index ed8621b4..bc31e825 100644 --- a/packages/queue/__tests__/index.spec.js +++ b/packages/queue/__tests__/index.spec.js @@ -22,19 +22,35 @@ describe("tests client subscription", () => { }, }; + clientID1 = uuid.v4(); + clientID2 = uuid.v4(); + // subscribe clients to a channel, return a promise and verify message/payload from resolved promise - Promise.all([ - subscribe({ pgClient, channel: messageObj.channel, clientID: uuid.v4() }), - subscribe({ - pgClient: pgClient2, - channel: messageObj.channel, - clientID: uuid.v4(), - }), - ]).then(values => { - expect(values[0]).toMatchObject(messageObj); // eslint-disable-line - expect(values[1]).toMatchObject(messageObj); // eslint-disable-line - done(); - }); + subscribe({ pgClient: pgClient, channel: messageObj.channel, clientID: clientID1 }).then(emitter1 => { + subscribe({ pgClient: pgClient2, channel: messageObj.channel, clientID: clientID2 }).then(emitter2 => { + + const promise1 = new Promise((resolve) => { + emitter1.on("message", message1 => { + expect(message1).toMatchObject(messageObj); // eslint-disable-line + expect(new Date(message1.ack[clientID1])).toBeInstanceOf(Date); // eslint-disable-line + resolve(message1); + }); + }); + + const promise2 = new Promise((resolve) => { + emitter2.on("message", message2 => { + expect(message2).toMatchObject(messageObj); // eslint-disable-line + expect(new Date(message2.ack[clientID2])).toBeInstanceOf(Date); // eslint-disable-line + resolve(message2); + }); + }); + + Promise.all([promise1, promise2]).then(() => { + done(); + }); + + }); + }) // publish message to a given channel publish({ pgClient, channel: messageObj.channel, data: messageObj.data }); diff --git a/packages/queue/subscribe.js b/packages/queue/subscribe.js index 0479eaf9..7b8d7254 100644 --- a/packages/queue/subscribe.js +++ b/packages/queue/subscribe.js @@ -1,27 +1,32 @@ const ack = require("./ack"); +const EventEmitter = require("events"); // subscribes a client to a channel async function subscribe({ pgClient, clientID, channel }) { - const notiPromise = new Promise(resolve => { - pgClient.on("notification", msg => { - const newRow = JSON.parse(msg.payload); - const date = new Date(); - const dateStr = date.toISOString(); - return ack({ pgClient, id: newRow.id, dateStr, clientID }).then( - response => { - return resolve(response[0]); - }, - ); - }); - }); + const eventEmitter = new EventEmitter(); + // subscribes a client to a channel pgClient.query(`LISTEN ${channel}`, (err, res) => { if (err) throw Error(`subscription error: ${err}`); console.log(`subscription success: ${res}`); }); - return notiPromise; + // define what to do when a message is received + pgClient.on("notification", msg => { + const newRow = JSON.parse(msg.payload); + const date = new Date(); + const dateStr = date.toISOString(); + + // sends message receipt confirmation to DB + ack({ pgClient, id: newRow.id, dateStr, clientID }).then( + response => { + eventEmitter.emit("message", response[0]); + }, + ); + }); + + return eventEmitter; } module.exports = subscribe;