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;