diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9d5b612..519cb58 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,5 @@
{
"postman.settings.dotenv-detection-notification-visibility": false,
- "java.compile.nullAnalysis.mode": "automatic"
+ "java.compile.nullAnalysis.mode": "automatic",
+ "java.configuration.updateBuildConfiguration": "interactive"
}
\ No newline at end of file
diff --git a/App.tsx b/App.tsx
index 5676491..dfb5517 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
-import { StyleSheet, useColorScheme } from 'react-native';
+import { StyleSheet } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PaperProvider } from 'react-native-paper';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AppNavigator from './src/navigation/AppNavigator';
import notifee from '@notifee/react-native';
@@ -15,8 +16,16 @@ import {
} from './src/utils/firebase/firebaseServices';
-import { dark, light } from './src/theme/theme';
-import { ThemeProvider} from './src/theme/ThemeProvider';
+import { ThemeProvider } from './src/theme/ThemeProvider';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 2,
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ },
+ },
+});
function App() {
@@ -46,11 +55,13 @@ function App() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..e479558
--- /dev/null
+++ b/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=false
+show.console.view=false
+show.executions.view=false
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c741a4f..8230a48 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -117,4 +117,5 @@ dependencies {
implementation jscFlavor
}
}
-apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
+apply plugin: 'com.google.gms.google-services'
+apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
\ No newline at end of file
diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk
deleted file mode 100644
index 19b281a..0000000
Binary files a/android/app/release/app-release.apk and /dev/null differ
diff --git a/android/app/release/baselineProfiles/0/app-release.dm b/android/app/release/baselineProfiles/0/app-release.dm
deleted file mode 100644
index 17e7a8d..0000000
Binary files a/android/app/release/baselineProfiles/0/app-release.dm and /dev/null differ
diff --git a/android/app/release/baselineProfiles/1/app-release.dm b/android/app/release/baselineProfiles/1/app-release.dm
deleted file mode 100644
index eb636ce..0000000
Binary files a/android/app/release/baselineProfiles/1/app-release.dm and /dev/null differ
diff --git a/android/app/release/output-metadata.json b/android/app/release/output-metadata.json
deleted file mode 100644
index 08b0ec4..0000000
--- a/android/app/release/output-metadata.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "version": 3,
- "artifactType": {
- "type": "APK",
- "kind": "Directory"
- },
- "applicationId": "com.circlecollect",
- "variantName": "release",
- "elements": [
- {
- "type": "SINGLE",
- "filters": [],
- "attributes": [],
- "versionCode": 1,
- "versionName": "1.0",
- "outputFile": "app-release.apk"
- }
- ],
- "elementType": "File",
- "baselineProfiles": [
- {
- "minApi": 28,
- "maxApi": 30,
- "baselineProfiles": [
- "baselineProfiles/1/app-release.dm"
- ]
- },
- {
- "minApi": 31,
- "maxApi": 2147483647,
- "baselineProfiles": [
- "baselineProfiles/0/app-release.dm"
- ]
- }
- ],
- "minSdkVersionForDexing": 24
-}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0a11ef5..d0d9fbe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,9 +26,11 @@
"@react-navigation/native-stack": "^7.3.26",
"@react-navigation/stack": "^7.4.8",
"@reduxjs/toolkit": "^2.9.0",
+ "@tanstack/react-query": "^5.90.21",
"@types/react-native-material-ripple": "^0.9.6",
"axios": "^1.12.2",
"install": "^0.13.0",
+ "lucide-react-native": "^0.563.0",
"npm": "^11.6.0",
"react": "19.1.0",
"react-native": "0.81.1",
@@ -59,9 +61,9 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.0.0",
- "@react-native-community/cli-platform-android": "20.0.0",
- "@react-native-community/cli-platform-ios": "20.0.0",
+ "@react-native-community/cli": "^14.0.1",
+ "@react-native-community/cli-platform-android": "^15.1.0",
+ "@react-native-community/cli-platform-ios": "^14.0.1",
"@react-native/babel-preset": "0.81.1",
"@react-native/eslint-config": "0.81.1",
"@react-native/metro-config": "0.81.1",
@@ -2713,11 +2715,15 @@
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
+ "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+ "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3342,16 +3348,19 @@
}
},
"node_modules/@react-native-community/cli": {
- "version": "20.0.0",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-14.0.1.tgz",
+ "integrity": "sha512-QxBbQmZhhDZKOGTIjPr0cDrFcVrxlzFG1BmOuhok3x4xUk09B3zyIl8xnaqQ53YIVhXQHS0BX0f0evuKBcbcqQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-clean": "20.0.0",
- "@react-native-community/cli-config": "20.0.0",
- "@react-native-community/cli-doctor": "20.0.0",
- "@react-native-community/cli-server-api": "20.0.0",
- "@react-native-community/cli-tools": "20.0.0",
- "@react-native-community/cli-types": "20.0.0",
+ "@react-native-community/cli-clean": "14.0.1",
+ "@react-native-community/cli-config": "14.0.1",
+ "@react-native-community/cli-debugger-ui": "14.0.1",
+ "@react-native-community/cli-doctor": "14.0.1",
+ "@react-native-community/cli-server-api": "14.0.1",
+ "@react-native-community/cli-tools": "14.0.1",
+ "@react-native-community/cli-types": "14.0.1",
"chalk": "^4.1.2",
"commander": "^9.4.1",
"deepmerge": "^4.3.0",
@@ -3370,22 +3379,26 @@
}
},
"node_modules/@react-native-community/cli-clean": {
- "version": "20.0.0",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-14.0.1.tgz",
+ "integrity": "sha512-vf6dEwq0WmsQu2BViSZI8ascTdk7J4o82FRqpHiYSZkQau6eRmYFrgLoC5hg985Hp/HD+3wRSDW+lB/2+fz3yA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.0.0",
+ "@react-native-community/cli-tools": "14.0.1",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2"
}
},
"node_modules/@react-native-community/cli-config": {
- "version": "20.0.0",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-14.0.1.tgz",
+ "integrity": "sha512-yiyBdrsqZmaTtb5XFcfCwFavPagw7UxQVKnRBNVdwQNQHsRqhhPWZ0c7W+sESTMC/L9T0zvr7k4dxZ6TJEqqKQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.0.0",
+ "@react-native-community/cli-tools": "14.0.1",
"chalk": "^4.1.2",
"cosmiconfig": "^9.0.0",
"deepmerge": "^4.3.0",
@@ -3393,38 +3406,28 @@
"joi": "^17.2.1"
}
},
- "node_modules/@react-native-community/cli-config-android": {
- "version": "20.0.0",
+ "node_modules/@react-native-community/cli-debugger-ui": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-14.0.1.tgz",
+ "integrity": "sha512-8eQ1U21Uwwm71jmIi9iaiou2VxqJQTm9k7Ch5Nj1ZEErO+nKFc9qExPTGtzsE80mHncLq8yNkQfV5waJvvhclg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-tools": "20.0.0",
- "chalk": "^4.1.2",
- "fast-glob": "^3.3.2",
- "fast-xml-parser": "^4.4.1"
- }
- },
- "node_modules/@react-native-community/cli-config-apple": {
- "version": "20.0.0",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@react-native-community/cli-tools": "20.0.0",
- "chalk": "^4.1.2",
- "execa": "^5.0.0",
- "fast-glob": "^3.3.2"
+ "serve-static": "^1.13.1"
}
},
"node_modules/@react-native-community/cli-doctor": {
- "version": "20.0.0",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-14.0.1.tgz",
+ "integrity": "sha512-wXLa4cPgNuFJE6K2dv/0JRq62iJUiSVLN0Wa5TxugX+ojXBle/oe1W3nibOJFYdyRIEUGk3u7A6gdoznNtZEJg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config": "20.0.0",
- "@react-native-community/cli-platform-android": "20.0.0",
- "@react-native-community/cli-platform-apple": "20.0.0",
- "@react-native-community/cli-platform-ios": "20.0.0",
- "@react-native-community/cli-tools": "20.0.0",
+ "@react-native-community/cli-config": "14.0.1",
+ "@react-native-community/cli-platform-android": "14.0.1",
+ "@react-native-community/cli-platform-apple": "14.0.1",
+ "@react-native-community/cli-platform-ios": "14.0.1",
+ "@react-native-community/cli-tools": "14.0.1",
"chalk": "^4.1.2",
"command-exists": "^1.2.8",
"deepmerge": "^4.3.0",
@@ -3433,12 +3436,40 @@
"node-stream-zip": "^1.9.1",
"ora": "^5.4.1",
"semver": "^7.5.2",
+ "strip-ansi": "^5.2.0",
"wcwidth": "^1.0.1",
"yaml": "^2.2.1"
}
},
+ "node_modules/@react-native-community/cli-doctor/node_modules/@react-native-community/cli-platform-android": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-14.0.1.tgz",
+ "integrity": "sha512-HNLIuD3ie8p1cLhsvfH19XPhjZjqZGX+zYlm4lpOKxPTSEzkaiAdOIMj5dVdN79zO1tmEawyyrwT4Jx5ddeUsQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-community/cli-tools": "14.0.1",
+ "chalk": "^4.1.2",
+ "execa": "^5.0.0",
+ "fast-glob": "^3.3.2",
+ "fast-xml-parser": "^4.2.4",
+ "logkitty": "^0.7.1"
+ }
+ },
+ "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@react-native-community/cli-doctor/node_modules/semver": {
- "version": "7.7.2",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"license": "ISC",
"bin": {
@@ -3448,74 +3479,183 @@
"node": ">=10"
}
},
- "node_modules/@react-native-community/cli-platform-android": {
- "version": "20.0.0",
+ "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config-android": "20.0.0",
- "@react-native-community/cli-tools": "20.0.0",
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@react-native-community/cli-platform-android": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.1.0.tgz",
+ "integrity": "sha512-7gKM/3RK0BVyWEzUTQMvAMUvdvX/IQdKjaOjEgi94lVmRrQiCMhcP+eM3nl1ds7GpoXAGZjHC9j7z1yfDYSMmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-community/cli-tools": "15.1.0",
"chalk": "^4.1.2",
"execa": "^5.0.0",
+ "fast-glob": "^3.3.2",
+ "fast-xml-parser": "^4.4.1",
"logkitty": "^0.7.1"
}
},
- "node_modules/@react-native-community/cli-platform-apple": {
- "version": "20.0.0",
- "devOptional": true,
+ "node_modules/@react-native-community/cli-platform-android/node_modules/@react-native-community/cli-tools": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.1.0.tgz",
+ "integrity": "sha512-WucpEF33q3sjlhRTww8awrmq6k5AqwFOVQtk/iY4sWMwiJDvhRh0+zzh8rTt9LN/cLh+Lvyq+SOuSWFW7emjug==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@react-native-community/cli-config-apple": "20.0.0",
- "@react-native-community/cli-tools": "20.0.0",
- "chalk": "^4.1.2",
- "execa": "^5.0.0",
- "fast-xml-parser": "^4.4.1"
- }
- },
- "node_modules/@react-native-community/cli-platform-ios": {
- "version": "20.0.0",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@react-native-community/cli-platform-apple": "20.0.0"
- }
- },
- "node_modules/@react-native-community/cli-server-api": {
- "version": "20.0.0",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@react-native-community/cli-tools": "20.0.0",
- "body-parser": "^1.20.3",
- "compression": "^1.7.1",
- "connect": "^3.6.5",
- "errorhandler": "^1.5.1",
- "nocache": "^3.0.1",
- "open": "^6.2.0",
- "pretty-format": "^29.7.0",
- "serve-static": "^1.13.1",
- "ws": "^6.2.3"
- }
- },
- "node_modules/@react-native-community/cli-tools": {
- "version": "20.0.0",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@vscode/sudo-prompt": "^9.0.0",
"appdirsjs": "^1.2.4",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"find-up": "^5.0.0",
- "launch-editor": "^2.9.1",
"mime": "^2.4.1",
+ "open": "^6.2.0",
"ora": "^5.4.1",
"prompts": "^2.4.2",
- "semver": "^7.5.2"
+ "semver": "^7.5.2",
+ "shell-quote": "^1.7.3",
+ "sudo-prompt": "^9.0.0"
+ }
+ },
+ "node_modules/@react-native-community/cli-platform-android/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@react-native-community/cli-platform-apple": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-14.0.1.tgz",
+ "integrity": "sha512-KY7zlDpzKfVzBuHWde7EXKgdkFFYl5Lp1o1ro8ie83sDzwSVTSyd5Pmb351SJABJuNFSuTzbyyMiuG+1BQ4hyg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-community/cli-tools": "14.0.1",
+ "chalk": "^4.1.2",
+ "execa": "^5.0.0",
+ "fast-glob": "^3.3.2",
+ "fast-xml-parser": "^4.2.4",
+ "ora": "^5.4.1"
+ }
+ },
+ "node_modules/@react-native-community/cli-platform-ios": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-14.0.1.tgz",
+ "integrity": "sha512-9DKbMHf53A4ae2sJ0rZFqi2m7Z1NoaWZYhkWufO8tEXCKckBat4ddio/Ei6g0bQk8IKdLzHogQFMuU3jbT3LYA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-community/cli-platform-apple": "14.0.1"
+ }
+ },
+ "node_modules/@react-native-community/cli-server-api": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-14.0.1.tgz",
+ "integrity": "sha512-8oKMcDH72vuX2pWv+N07NcBWoMOykK7cUKshCjSqupWNr1wGxHnJC4uFvLLAwpoewYlOQXMnU9Jn/9IWA6Sm2w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@react-native-community/cli-debugger-ui": "14.0.1",
+ "@react-native-community/cli-tools": "14.0.1",
+ "compression": "^1.7.1",
+ "connect": "^3.6.5",
+ "errorhandler": "^1.5.1",
+ "nocache": "^3.0.1",
+ "pretty-format": "^26.6.2",
+ "serve-static": "^1.13.1",
+ "ws": "^6.2.3"
+ }
+ },
+ "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": {
+ "version": "26.6.2",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz",
+ "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^15.0.0",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10.14.2"
+ }
+ },
+ "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": {
+ "version": "15.0.20",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz",
+ "integrity": "sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": {
+ "version": "26.6.2",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
+ "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^26.6.2",
+ "ansi-regex": "^5.0.0",
+ "ansi-styles": "^4.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@react-native-community/cli-server-api/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@react-native-community/cli-tools": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-14.0.1.tgz",
+ "integrity": "sha512-4tksgruPliZlQmO6kfX1gUBNvDChoOcZgGt9r4KvvyFelCR3VUWEoJkvhKlDlSoaEtzBO2L426CTnFK4ED/yBQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "appdirsjs": "^1.2.4",
+ "chalk": "^4.1.2",
+ "execa": "^5.0.0",
+ "find-up": "^5.0.0",
+ "mime": "^2.4.1",
+ "open": "^6.2.0",
+ "ora": "^5.4.1",
+ "semver": "^7.5.2",
+ "shell-quote": "^1.7.3",
+ "sudo-prompt": "^9.0.0"
}
},
"node_modules/@react-native-community/cli-tools/node_modules/semver": {
- "version": "7.7.2",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"license": "ISC",
"bin": {
@@ -3526,7 +3666,9 @@
}
},
"node_modules/@react-native-community/cli-types": {
- "version": "20.0.0",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-14.0.1.tgz",
+ "integrity": "sha512-Yk6TLdkSQHaSG9NlcMsHtX7ZYuHaOrZ7K/3zBbpabWUm006blND5NKw8YQuk9guqK7i/PMSUKJUq1NbF7bGjDA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -4195,6 +4337,8 @@
},
"node_modules/@sideway/address": {
"version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
+ "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4203,11 +4347,15 @@
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"devOptional": true,
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"devOptional": true,
"license": "BSD-3-Clause"
},
@@ -4237,6 +4385,32 @@
"version": "0.3.0",
"license": "MIT"
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.21",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
+ "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"license": "MIT",
@@ -4589,11 +4763,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/@vscode/sudo-prompt": {
- "version": "9.3.1",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/@webgpu/types": {
"version": "0.1.21",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz",
@@ -4707,6 +4876,8 @@
},
"node_modules/ansi-fragments": {
"version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz",
+ "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -4717,6 +4888,8 @@
},
"node_modules/ansi-fragments/node_modules/ansi-regex": {
"version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -4725,6 +4898,8 @@
},
"node_modules/ansi-fragments/node_modules/strip-ansi": {
"version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -4767,6 +4942,8 @@
},
"node_modules/appdirsjs": {
"version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz",
+ "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==",
"devOptional": true,
"license": "MIT"
},
@@ -4913,6 +5090,8 @@
},
"node_modules/astral-regex": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -4933,6 +5112,8 @@
},
"node_modules/asynckit": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
@@ -4950,11 +5131,13 @@
}
},
"node_modules/axios": {
- "version": "1.12.2",
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -5138,6 +5321,8 @@
},
"node_modules/bl": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5146,42 +5331,6 @@
"readable-stream": "^3.4.0"
}
},
- "node_modules/body-parser": {
- "version": "1.20.3",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.13.0",
- "raw-body": "2.5.2",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/body-parser/node_modules/debug": {
- "version": "2.6.9",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/body-parser/node_modules/ms": {
- "version": "2.0.0",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/boolbase": {
"version": "1.0.0",
"license": "ISC"
@@ -5244,6 +5393,8 @@
},
"node_modules/buffer": {
"version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"devOptional": true,
"funding": [
{
@@ -5271,6 +5422,8 @@
},
"node_modules/bytes": {
"version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -5307,7 +5460,7 @@
},
"node_modules/call-bound": {
"version": "1.0.4",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -5453,6 +5606,8 @@
},
"node_modules/cli-cursor": {
"version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5464,6 +5619,8 @@
},
"node_modules/cli-spinners": {
"version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -5487,6 +5644,8 @@
},
"node_modules/clone": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -5542,11 +5701,15 @@
},
"node_modules/colorette": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"devOptional": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -5557,6 +5720,8 @@
},
"node_modules/command-exists": {
"version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
+ "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==",
"devOptional": true,
"license": "MIT"
},
@@ -5570,6 +5735,8 @@
},
"node_modules/compressible": {
"version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5581,6 +5748,8 @@
},
"node_modules/compression": {
"version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5598,6 +5767,8 @@
},
"node_modules/compression/node_modules/debug": {
"version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5606,6 +5777,8 @@
},
"node_modules/compression/node_modules/ms": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"devOptional": true,
"license": "MIT"
},
@@ -5637,14 +5810,6 @@
"version": "2.0.0",
"license": "MIT"
},
- "node_modules/content-type": {
- "version": "1.0.5",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/convert-source-map": {
"version": "2.0.0",
"license": "MIT"
@@ -5663,6 +5828,8 @@
},
"node_modules/cosmiconfig": {
"version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5807,7 +5974,9 @@
}
},
"node_modules/dayjs": {
- "version": "1.11.18",
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"devOptional": true,
"license": "MIT"
},
@@ -5828,6 +5997,8 @@
},
"node_modules/decamelize": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -5868,6 +6039,8 @@
},
"node_modules/defaults": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5911,6 +6084,8 @@
},
"node_modules/delayed-stream": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -6070,6 +6245,8 @@
},
"node_modules/env-paths": {
"version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -6077,7 +6254,9 @@
}
},
"node_modules/envinfo": {
- "version": "7.14.0",
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+ "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
"devOptional": true,
"license": "MIT",
"bin": {
@@ -6103,15 +6282,21 @@
}
},
"node_modules/errorhandler": {
- "version": "1.5.1",
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz",
+ "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "accepts": "~1.3.7",
+ "accepts": "~1.3.8",
"escape-html": "~1.0.3"
},
"engines": {
"node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/es-abstract": {
@@ -6898,6 +7083,8 @@
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
+ "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"devOptional": true,
"funding": [
{
@@ -7111,7 +7298,9 @@
}
},
"node_modules/form-data": {
- "version": "4.0.4",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -7540,17 +7729,6 @@
"node": ">=10.17.0"
}
},
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
@@ -7559,6 +7737,8 @@
},
"node_modules/ieee754": {
"version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"devOptional": true,
"funding": [
{
@@ -7845,6 +8025,8 @@
},
"node_modules/is-fullwidth-code-point": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -7889,6 +8071,8 @@
},
"node_modules/is-interactive": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -8056,6 +8240,8 @@
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -8107,6 +8293,8 @@
},
"node_modules/is-wsl": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -8739,6 +8927,8 @@
},
"node_modules/joi": {
"version": "17.13.3",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
+ "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -8848,15 +9038,6 @@
"node": ">=6"
}
},
- "node_modules/launch-editor": {
- "version": "2.11.1",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "picocolors": "^1.1.1",
- "shell-quote": "^1.8.3"
- }
- },
"node_modules/leven": {
"version": "3.1.0",
"license": "MIT",
@@ -8915,7 +9096,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.21",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -8943,6 +9126,8 @@
},
"node_modules/log-symbols": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -8958,6 +9143,8 @@
},
"node_modules/logkitty": {
"version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz",
+ "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -8971,6 +9158,8 @@
},
"node_modules/logkitty/node_modules/cliui": {
"version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"devOptional": true,
"license": "ISC",
"dependencies": {
@@ -8981,6 +9170,8 @@
},
"node_modules/logkitty/node_modules/find-up": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -8993,6 +9184,8 @@
},
"node_modules/logkitty/node_modules/locate-path": {
"version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9004,6 +9197,8 @@
},
"node_modules/logkitty/node_modules/p-limit": {
"version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9018,6 +9213,8 @@
},
"node_modules/logkitty/node_modules/p-locate": {
"version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9029,6 +9226,8 @@
},
"node_modules/logkitty/node_modules/wrap-ansi": {
"version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9042,11 +9241,15 @@
},
"node_modules/logkitty/node_modules/y18n": {
"version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"devOptional": true,
"license": "ISC"
},
"node_modules/logkitty/node_modules/yargs": {
"version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -9068,6 +9271,8 @@
},
"node_modules/logkitty/node_modules/yargs-parser": {
"version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"devOptional": true,
"license": "ISC",
"dependencies": {
@@ -9101,6 +9306,17 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react-native": {
+ "version": "0.563.0",
+ "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.563.0.tgz",
+ "integrity": "sha512-q4tYoAMorTqv+UXRYc0MyiEAOF+4Bu73zxD63EDrnGCFL+xuj+imBm3E2rIKRmME0heVHlK+98fsi8wbL92LNQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-native": "*",
+ "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
+ }
+ },
"node_modules/make-dir": {
"version": "4.0.0",
"dev": true,
@@ -9148,14 +9364,6 @@
"version": "2.0.14",
"license": "CC0-1.0"
},
- "node_modules/media-typer": {
- "version": "0.3.0",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/memoize-one": {
"version": "5.2.1",
"license": "MIT"
@@ -9504,6 +9712,8 @@
},
"node_modules/mime": {
"version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"devOptional": true,
"license": "MIT",
"bin": {
@@ -9515,6 +9725,8 @@
},
"node_modules/mime-db": {
"version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -9605,6 +9817,8 @@
},
"node_modules/negotiator": {
"version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -9613,6 +9827,8 @@
},
"node_modules/nocache": {
"version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz",
+ "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -9629,6 +9845,8 @@
},
"node_modules/node-stream-zip": {
"version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
+ "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -9647,9 +9865,9 @@
}
},
"node_modules/npm": {
- "version": "11.6.4",
- "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.4.tgz",
- "integrity": "sha512-ERjKtGoFpQrua/9bG0+h3xiv/4nVdGViCjUYA1AmlV24fFvfnSB7B7dIfZnySQ1FDLd0ZVrWPsLLp78dCtJdRQ==",
+ "version": "11.9.0",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-11.9.0.tgz",
+ "integrity": "sha512-BBZoU926FCypj4b7V7ElinxsWcy4Kss88UG3ejFYmKyq7Uc5XnT34Me2nEhgCOaL5qY4HvGu5aI92C4OYd7NaA==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -9728,8 +9946,8 @@
],
"dependencies": {
"@isaacs/string-locale-compare": "^1.1.0",
- "@npmcli/arborist": "^9.1.8",
- "@npmcli/config": "^10.4.4",
+ "@npmcli/arborist": "^9.2.0",
+ "@npmcli/config": "^10.6.0",
"@npmcli/fs": "^5.0.0",
"@npmcli/map-workspaces": "^5.0.3",
"@npmcli/metavuln-calculator": "^9.0.3",
@@ -9737,12 +9955,12 @@
"@npmcli/promise-spawn": "^9.0.1",
"@npmcli/redact": "^4.0.0",
"@npmcli/run-script": "^10.0.3",
- "@sigstore/tuf": "^4.0.0",
+ "@sigstore/tuf": "^4.0.1",
"abbrev": "^4.0.0",
"archy": "~1.0.0",
"cacache": "^20.0.3",
"chalk": "^5.6.2",
- "ci-info": "^4.3.1",
+ "ci-info": "^4.4.0",
"cli-columns": "^4.0.0",
"fastest-levenshtein": "^1.0.16",
"fs-minipass": "^3.0.3",
@@ -9754,11 +9972,11 @@
"is-cidr": "^6.0.1",
"json-parse-even-better-errors": "^5.0.0",
"libnpmaccess": "^10.0.3",
- "libnpmdiff": "^8.0.11",
- "libnpmexec": "^10.1.10",
- "libnpmfund": "^7.0.11",
+ "libnpmdiff": "^8.1.0",
+ "libnpmexec": "^10.2.0",
+ "libnpmfund": "^7.0.14",
"libnpmorg": "^8.0.1",
- "libnpmpack": "^9.0.11",
+ "libnpmpack": "^9.1.0",
"libnpmpublish": "^11.1.3",
"libnpmsearch": "^9.0.1",
"libnpmteam": "^8.0.2",
@@ -9768,7 +9986,7 @@
"minipass": "^7.1.1",
"minipass-pipeline": "^1.2.4",
"ms": "^2.1.2",
- "node-gyp": "^12.1.0",
+ "node-gyp": "^12.2.0",
"nopt": "^9.0.0",
"npm-audit-report": "^7.0.0",
"npm-install-checks": "^8.0.0",
@@ -9778,7 +9996,7 @@
"npm-registry-fetch": "^19.1.1",
"npm-user-validate": "^4.0.0",
"p-map": "^7.0.4",
- "pacote": "^21.0.4",
+ "pacote": "^21.1.0",
"parse-conflict-json": "^5.0.1",
"proc-log": "^6.1.0",
"qrcode-terminal": "^0.12.0",
@@ -9787,11 +10005,11 @@
"spdx-expression-parse": "^4.0.0",
"ssri": "^13.0.0",
"supports-color": "^10.2.2",
- "tar": "^7.5.2",
+ "tar": "^7.5.7",
"text-table": "~0.2.0",
"tiny-relative-date": "^2.0.2",
"treeverse": "^3.0.0",
- "validate-npm-package-name": "^7.0.0",
+ "validate-npm-package-name": "^7.0.2",
"which": "^6.0.0"
},
"bin": {
@@ -9822,7 +10040,7 @@
}
},
"node_modules/npm/node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
+ "version": "5.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -9864,7 +10082,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/arborist": {
- "version": "9.1.8",
+ "version": "9.2.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -9881,7 +10099,7 @@
"@npmcli/run-script": "^10.0.0",
"bin-links": "^6.0.0",
"cacache": "^20.0.1",
- "common-ancestor-path": "^1.0.1",
+ "common-ancestor-path": "^2.0.0",
"hosted-git-info": "^9.0.0",
"json-stringify-nice": "^1.1.4",
"lru-cache": "^11.2.1",
@@ -9910,7 +10128,7 @@
}
},
"node_modules/npm/node_modules/@npmcli/config": {
- "version": "10.4.4",
+ "version": "10.6.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -10091,7 +10309,7 @@
}
},
"node_modules/npm/node_modules/@sigstore/core": {
- "version": "3.0.0",
+ "version": "3.1.0",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
@@ -10107,48 +10325,40 @@
}
},
"node_modules/npm/node_modules/@sigstore/sign": {
- "version": "4.0.1",
+ "version": "4.1.0",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.0.0",
+ "@sigstore/core": "^3.1.0",
"@sigstore/protobuf-specs": "^0.5.0",
- "make-fetch-happen": "^15.0.2",
- "proc-log": "^5.0.0",
+ "make-fetch-happen": "^15.0.3",
+ "proc-log": "^6.1.0",
"promise-retry": "^2.0.1"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": {
- "version": "5.0.0",
- "inBundle": true,
- "license": "ISC",
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/npm/node_modules/@sigstore/tuf": {
- "version": "4.0.0",
+ "version": "4.0.1",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"@sigstore/protobuf-specs": "^0.5.0",
- "tuf-js": "^4.0.0"
+ "tuf-js": "^4.1.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/npm/node_modules/@sigstore/verify": {
- "version": "3.0.0",
+ "version": "3.1.0",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.0.0",
+ "@sigstore/core": "^3.1.0",
"@sigstore/protobuf-specs": "^0.5.0"
},
"engines": {
@@ -10164,31 +10374,17 @@
}
},
"node_modules/npm/node_modules/@tufjs/models": {
- "version": "4.0.0",
+ "version": "4.1.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"@tufjs/canonical-json": "2.0.0",
- "minimatch": "^9.0.5"
+ "minimatch": "^10.1.1"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": {
- "version": "9.0.5",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/npm/node_modules/abbrev": {
"version": "4.0.0",
"inBundle": true,
@@ -10223,11 +10419,6 @@
"inBundle": true,
"license": "MIT"
},
- "node_modules/npm/node_modules/balanced-match": {
- "version": "1.0.2",
- "inBundle": true,
- "license": "MIT"
- },
"node_modules/npm/node_modules/bin-links": {
"version": "6.0.0",
"inBundle": true,
@@ -10254,14 +10445,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/npm/node_modules/brace-expansion": {
- "version": "2.0.2",
- "inBundle": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
"node_modules/npm/node_modules/cacache": {
"version": "20.0.3",
"inBundle": true,
@@ -10303,7 +10486,7 @@
}
},
"node_modules/npm/node_modules/ci-info": {
- "version": "4.3.1",
+ "version": "4.4.0",
"funding": [
{
"type": "github",
@@ -10348,9 +10531,12 @@
}
},
"node_modules/npm/node_modules/common-ancestor-path": {
- "version": "1.0.1",
+ "version": "2.0.0",
"inBundle": true,
- "license": "ISC"
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">= 18"
+ }
},
"node_modules/npm/node_modules/cssesc": {
"version": "3.0.0",
@@ -10380,7 +10566,7 @@
}
},
"node_modules/npm/node_modules/diff": {
- "version": "8.0.2",
+ "version": "8.0.3",
"inBundle": true,
"license": "BSD-3-Clause",
"engines": {
@@ -10439,11 +10625,11 @@
}
},
"node_modules/npm/node_modules/glob": {
- "version": "13.0.0",
+ "version": "13.0.1",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "minimatch": "^10.1.1",
+ "minimatch": "^10.1.2",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
@@ -10556,7 +10742,7 @@
}
},
"node_modules/npm/node_modules/ip-address": {
- "version": "10.0.1",
+ "version": "10.1.0",
"inBundle": true,
"license": "MIT",
"engines": {
@@ -10575,11 +10761,11 @@
}
},
"node_modules/npm/node_modules/is-cidr": {
- "version": "6.0.1",
+ "version": "6.0.2",
"inBundle": true,
"license": "BSD-2-Clause",
"dependencies": {
- "cidr-regex": "5.0.1"
+ "cidr-regex": "^5.0.1"
},
"engines": {
"node": ">=20"
@@ -10648,11 +10834,11 @@
}
},
"node_modules/npm/node_modules/libnpmdiff": {
- "version": "8.0.11",
+ "version": "8.1.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.1.8",
+ "@npmcli/arborist": "^9.2.0",
"@npmcli/installed-package-contents": "^4.0.0",
"binary-extensions": "^3.0.0",
"diff": "^8.0.2",
@@ -10666,11 +10852,11 @@
}
},
"node_modules/npm/node_modules/libnpmexec": {
- "version": "10.1.10",
+ "version": "10.2.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.1.8",
+ "@npmcli/arborist": "^9.2.0",
"@npmcli/package-json": "^7.0.0",
"@npmcli/run-script": "^10.0.0",
"ci-info": "^4.0.0",
@@ -10688,11 +10874,11 @@
}
},
"node_modules/npm/node_modules/libnpmfund": {
- "version": "7.0.11",
+ "version": "7.0.14",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.1.8"
+ "@npmcli/arborist": "^9.2.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -10711,11 +10897,11 @@
}
},
"node_modules/npm/node_modules/libnpmpack": {
- "version": "9.0.11",
+ "version": "9.1.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "@npmcli/arborist": "^9.1.8",
+ "@npmcli/arborist": "^9.2.0",
"@npmcli/run-script": "^10.0.0",
"npm-package-arg": "^13.0.0",
"pacote": "^21.0.2"
@@ -10781,9 +10967,9 @@
}
},
"node_modules/npm/node_modules/lru-cache": {
- "version": "11.2.2",
+ "version": "11.2.5",
"inBundle": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
@@ -10810,11 +10996,11 @@
}
},
"node_modules/npm/node_modules/minimatch": {
- "version": "10.1.1",
+ "version": "10.1.2",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "@isaacs/brace-expansion": "^5.0.0"
+ "@isaacs/brace-expansion": "^5.0.1"
},
"engines": {
"node": "20 || >=22"
@@ -10843,12 +11029,12 @@
}
},
"node_modules/npm/node_modules/minipass-fetch": {
- "version": "5.0.0",
+ "version": "5.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"minipass": "^7.0.3",
- "minipass-sized": "^1.0.3",
+ "minipass-sized": "^2.0.0",
"minizlib": "^3.0.1"
},
"engines": {
@@ -10903,22 +11089,11 @@
}
},
"node_modules/npm/node_modules/minipass-sized": {
- "version": "1.0.3",
+ "version": "2.0.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": {
- "version": "3.3.6",
- "inBundle": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
+ "minipass": "^7.1.2"
},
"engines": {
"node": ">=8"
@@ -10957,7 +11132,7 @@
}
},
"node_modules/npm/node_modules/node-gyp": {
- "version": "12.1.0",
+ "version": "12.2.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -10968,7 +11143,7 @@
"nopt": "^9.0.0",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
- "tar": "^7.5.2",
+ "tar": "^7.5.4",
"tinyglobby": "^0.2.12",
"which": "^6.0.0"
},
@@ -11121,7 +11296,7 @@
}
},
"node_modules/npm/node_modules/pacote": {
- "version": "21.0.4",
+ "version": "21.1.0",
"inBundle": true,
"license": "ISC",
"dependencies": {
@@ -11164,7 +11339,7 @@
}
},
"node_modules/npm/node_modules/path-scurry": {
- "version": "2.0.0",
+ "version": "2.0.1",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -11179,7 +11354,7 @@
}
},
"node_modules/npm/node_modules/postcss-selector-parser": {
- "version": "7.1.0",
+ "version": "7.1.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -11308,16 +11483,16 @@
}
},
"node_modules/npm/node_modules/sigstore": {
- "version": "4.0.0",
+ "version": "4.1.0",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.0.0",
+ "@sigstore/core": "^3.1.0",
"@sigstore/protobuf-specs": "^0.5.0",
- "@sigstore/sign": "^4.0.0",
- "@sigstore/tuf": "^4.0.0",
- "@sigstore/verify": "^3.0.0"
+ "@sigstore/sign": "^4.1.0",
+ "@sigstore/tuf": "^4.0.1",
+ "@sigstore/verify": "^3.1.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -11442,7 +11617,7 @@
}
},
"node_modules/npm/node_modules/tar": {
- "version": "7.5.2",
+ "version": "7.5.7",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -11525,13 +11700,13 @@
}
},
"node_modules/npm/node_modules/tuf-js": {
- "version": "4.0.0",
+ "version": "4.1.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "@tufjs/models": "4.0.0",
- "debug": "^4.4.1",
- "make-fetch-happen": "^15.0.0"
+ "@tufjs/models": "4.1.0",
+ "debug": "^4.4.3",
+ "make-fetch-happen": "^15.0.1"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -11583,7 +11758,7 @@
}
},
"node_modules/npm/node_modules/validate-npm-package-name": {
- "version": "7.0.0",
+ "version": "7.0.2",
"inBundle": true,
"license": "ISC",
"engines": {
@@ -11662,7 +11837,7 @@
},
"node_modules/object-inspect": {
"version": "1.13.4",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11758,6 +11933,8 @@
},
"node_modules/on-headers": {
"version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -11787,6 +11964,8 @@
},
"node_modules/open": {
"version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz",
+ "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -11821,6 +12000,8 @@
},
"node_modules/ora": {
"version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -12193,20 +12374,6 @@
],
"license": "MIT"
},
- "node_modules/qs": {
- "version": "6.13.0",
- "devOptional": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/query-string": {
"version": "7.1.3",
"license": "MIT",
@@ -12256,20 +12423,6 @@
"node": ">= 0.6"
}
},
- "node_modules/raw-body": {
- "version": "2.5.2",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/react": {
"version": "19.1.0",
"license": "MIT",
@@ -12857,6 +13010,8 @@
},
"node_modules/readable-stream": {
"version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -12997,6 +13152,8 @@
},
"node_modules/require-main-filename": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"devOptional": true,
"license": "ISC"
},
@@ -13060,6 +13217,8 @@
},
"node_modules/restore-cursor": {
"version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -13181,11 +13340,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "devOptional": true,
- "license": "MIT"
- },
"node_modules/scheduler": {
"version": "0.26.0",
"license": "MIT"
@@ -13276,6 +13430,8 @@
},
"node_modules/set-blocking": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"devOptional": true,
"license": "ISC"
},
@@ -13357,7 +13513,7 @@
},
"node_modules/side-channel": {
"version": "1.1.0",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -13375,7 +13531,7 @@
},
"node_modules/side-channel-list": {
"version": "1.0.0",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -13390,7 +13546,7 @@
},
"node_modules/side-channel-map": {
"version": "1.0.1",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -13407,7 +13563,7 @@
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -13452,6 +13608,8 @@
},
"node_modules/slice-ansi": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -13465,6 +13623,8 @@
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -13476,6 +13636,8 @@
},
"node_modules/slice-ansi/node_modules/color-convert": {
"version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -13484,6 +13646,8 @@
},
"node_modules/slice-ansi/node_modules/color-name": {
"version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"devOptional": true,
"license": "MIT"
},
@@ -13580,6 +13744,8 @@
},
"node_modules/string_decoder": {
"version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -13749,6 +13915,8 @@
},
"node_modules/strnum": {
"version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
+ "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"devOptional": true,
"funding": [
{
@@ -13758,6 +13926,14 @@
],
"license": "MIT"
},
+ "node_modules/sudo-prompt": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz",
+ "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"license": "MIT",
@@ -13930,18 +14106,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/type-is": {
- "version": "1.6.18",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"dev": true,
@@ -14014,7 +14178,7 @@
},
"node_modules/typescript": {
"version": "5.9.2",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -14154,6 +14318,8 @@
},
"node_modules/util-deprecate": {
"version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"devOptional": true,
"license": "MIT"
},
@@ -14179,6 +14345,8 @@
},
"node_modules/vary": {
"version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -14202,6 +14370,8 @@
},
"node_modules/wcwidth": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -14318,6 +14488,8 @@
},
"node_modules/which-module": {
"version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"devOptional": true,
"license": "ISC"
},
diff --git a/package.json b/package.json
index e69be6f..78755c5 100644
--- a/package.json
+++ b/package.json
@@ -28,9 +28,11 @@
"@react-navigation/native-stack": "^7.3.26",
"@react-navigation/stack": "^7.4.8",
"@reduxjs/toolkit": "^2.9.0",
+ "@tanstack/react-query": "^5.90.21",
"@types/react-native-material-ripple": "^0.9.6",
"axios": "^1.12.2",
"install": "^0.13.0",
+ "lucide-react-native": "^0.563.0",
"npm": "^11.6.0",
"react": "19.1.0",
"react-native": "0.81.1",
@@ -61,9 +63,9 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
- "@react-native-community/cli": "20.0.0",
- "@react-native-community/cli-platform-android": "20.0.0",
- "@react-native-community/cli-platform-ios": "20.0.0",
+ "@react-native-community/cli": "^14.0.1",
+ "@react-native-community/cli-platform-android": "^15.1.0",
+ "@react-native-community/cli-platform-ios": "^14.0.1",
"@react-native/babel-preset": "0.81.1",
"@react-native/eslint-config": "0.81.1",
"@react-native/metro-config": "0.81.1",
diff --git a/src/api/client.ts b/src/api/client.ts
new file mode 100644
index 0000000..902d806
--- /dev/null
+++ b/src/api/client.ts
@@ -0,0 +1,129 @@
+import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
+import * as Keychain from 'react-native-keychain';
+import { BASE_URL } from '../utils/ApiUrls';
+
+const apiClient = axios.create({
+ baseURL: BASE_URL,
+ headers: {
+ Accept: 'application/json',
+ },
+});
+
+const buildRequestUrl = (config: InternalAxiosRequestConfig) => {
+ const requestUrl = config.url ?? '';
+ if (/^https?:\/\//i.test(requestUrl)) return requestUrl;
+
+ const base = config.baseURL ?? '';
+ if (!base) return requestUrl;
+
+ return `${base.replace(/\/+$/, '')}/${requestUrl.replace(/^\/+/, '')}`;
+};
+
+const getPayloadByMethod = (config: InternalAxiosRequestConfig) => {
+ const method = (config.method || 'GET').toUpperCase();
+ if (method === 'GET' || method === 'DELETE') {
+ return config.params ?? null;
+ }
+ return config.data ?? null;
+};
+
+const isFormDataPayload = (payload: unknown): payload is FormData => {
+ return typeof FormData !== 'undefined' && payload instanceof FormData;
+};
+
+const getHeaderValue = (headers: InternalAxiosRequestConfig['headers'], name: string) => {
+ const normalizedName = name.toLowerCase();
+ const axiosHeaders = headers as unknown as {
+ get?: (key: string) => string | undefined;
+ [key: string]: unknown;
+ };
+
+ if (axiosHeaders?.get) {
+ return axiosHeaders.get(name) || axiosHeaders.get(normalizedName);
+ }
+
+ const plainHeaders = (headers ?? {}) as Record;
+ return plainHeaders[name] || plainHeaders[normalizedName];
+};
+
+const setHeaderValue = (config: InternalAxiosRequestConfig, name: string, value: string) => {
+ const axiosHeaders = config.headers as unknown as {
+ set?: (key: string, value: string) => void;
+ };
+
+ if (axiosHeaders?.set) {
+ axiosHeaders.set(name, value);
+ return;
+ }
+
+ config.headers = {
+ ...(config.headers as Record),
+ [name]: value,
+ };
+};
+
+const applyRequestContentType = (config: InternalAxiosRequestConfig) => {
+ const method = (config.method || 'GET').toUpperCase();
+ const hasBody = !['GET', 'DELETE', 'HEAD'].includes(method);
+ const currentContentType = getHeaderValue(config.headers, 'Content-Type');
+
+ if (isFormDataPayload(config.data)) {
+ setHeaderValue(config, 'Content-Type', 'multipart/form-data');
+ return;
+ }
+
+ if (hasBody && !currentContentType) {
+ setHeaderValue(config, 'Content-Type', 'application/json');
+ }
+};
+
+apiClient.interceptors.request.use(
+ async (config) => {
+ try {
+ const credentials = await Keychain.getGenericPassword();
+ if (credentials) {
+ config.headers.Authorization = `Bearer ${credentials.password}`;
+ }
+ } catch (error) {
+ console.error('Error fetching credentials', error);
+ }
+
+ applyRequestContentType(config);
+
+ const method = (config.method || 'GET').toUpperCase();
+ const url = buildRequestUrl(config);
+ console.log(`[HOOK_API][${method}] URL:`, url);
+ console.log(`[HOOK_API][${method}] PAYLOAD:`, getPayloadByMethod(config));
+
+ return config;
+ },
+ (error) => Promise.reject(error)
+);
+
+apiClient.interceptors.response.use(
+ (response) => {
+ const method = (response.config.method || 'GET').toUpperCase();
+ const url = buildRequestUrl(response.config);
+
+ console.log(`[HOOK_API][${method}] SUCCESS URL:`, url);
+ console.log(`[HOOK_API][${method}] SUCCESS META:`, {
+ statusCode: response.status ?? null,
+ status: response.statusText ?? null,
+ message: (response.data as { message?: string })?.message ?? null,
+ });
+ console.log(`[HOOK_API][${method}] SUCCESS RESPONSE:`, response.data);
+
+ return response;
+ },
+ (error: AxiosError) => {
+ const method = (error.config?.method || 'GET').toUpperCase();
+ const url = error.config ? buildRequestUrl(error.config as InternalAxiosRequestConfig) : '';
+
+ console.error(`[HOOK_API][${method}] ERROR URL:`, url || null);
+ console.error(`[HOOK_API][${method}] ERROR RESPONSE:`, error.response?.data ?? error.message);
+
+ return Promise.reject(error);
+ }
+);
+
+export default apiClient;
diff --git a/src/components/AppView.tsx b/src/components/AppView.tsx
index 7f95793..393f12e 100644
--- a/src/components/AppView.tsx
+++ b/src/components/AppView.tsx
@@ -1,40 +1,97 @@
-import React from "react";
-import { StatusBar, StyleSheet, View, ViewStyle } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
+import React from 'react';
+import {
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ TouchableWithoutFeedback,
+ View,
+ ViewStyle,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useTheme } from '../hooks/ui/useTheme';
type AppViewProps = {
children: React.ReactNode;
backgroundColor?: string;
- barStyle?: "default" | "light-content" | "dark-content";
+ barStyle?: 'default' | 'light-content' | 'dark-content';
paddingHorizontal?: number;
paddingTop?: number;
style?: ViewStyle;
+ keyboardSafe?: boolean;
+ keyboardVerticalOffset?: number;
+ dismissKeyboardOnTap?: boolean;
};
export default function AppView({
children,
- backgroundColor = "#f5f5f5",
- barStyle = "dark-content",
- paddingHorizontal = 12, // default
- paddingTop = 8, // default
+ backgroundColor,
+ barStyle,
+ paddingHorizontal,
+ paddingTop,
style,
+ keyboardSafe = true,
+ keyboardVerticalOffset = 0,
+ dismissKeyboardOnTap = true,
}: AppViewProps) {
+ const theme = useTheme();
+
+ const finalBackgroundColor = backgroundColor || theme.colors.background;
+ const finalBarStyle = barStyle || (theme.mode === 'dark' ? 'light-content' : 'dark-content');
+ const finalPaddingHorizontal = paddingHorizontal !== undefined ? paddingHorizontal : theme.spacing.sm;
+ const finalPaddingTop = paddingTop !== undefined ? paddingTop : 0;
+
return (
-
+
-
- {children}
-
+ {keyboardSafe ? (
+
+ {dismissKeyboardOnTap ? (
+
+
+
+ {children}
+
+
+
+ ) : (
+
+ {children}
+
+ )}
+
+ ) : (
+
+ {children}
+
+ )}
);
}
@@ -43,6 +100,9 @@ const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
+ keyboardContainer: {
+ flex: 1,
+ },
container: {
flex: 1,
},
diff --git a/src/components/CostomAlart.tsx b/src/components/CostomAlart.tsx
index 4dbc7c2..ea6fd5d 100644
--- a/src/components/CostomAlart.tsx
+++ b/src/components/CostomAlart.tsx
@@ -8,7 +8,7 @@ import {
Platform,
} from "react-native";
import { BlurView } from "@react-native-community/blur";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
+import LucideIcon from './common/LucideIcon';
import colors from "./colors";
import { responsiveFontSize } from "react-native-responsive-dimensions";
import { useTheme } from "../theme/ThemeProvider";
@@ -121,7 +121,7 @@ const CustomAlert: React.FC = ({
-
+
{title}
diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx
index b1332bb..2041022 100644
--- a/src/components/JoinedGroupCard.tsx
+++ b/src/components/JoinedGroupCard.tsx
@@ -1,10 +1,12 @@
import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ToastAndroid } from 'react-native';
-import colors from './colors';
import { useNavigation } from '@react-navigation/native';
-import { BASE_URL, CONTRIBUTE } from '../utils/ApiUrls';
-import { apiCall } from '../utils/ApiHelper';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useFetch } from '../hooks/core/useFetch';
+import { useCreate } from '../hooks/core/useCreate';
+import { CONTRIBUTE } from '../utils/ApiUrls';
+import FontsFamily from '../constents/FontsFamily';
interface JoinedGroup {
id: string;
@@ -13,8 +15,8 @@ interface JoinedGroup {
visibility: 'PUBLIC' | 'PRIVATE';
createdByUserId: number;
createdByName: string;
- startDate: [number, number, number] | null;
- endDate: [number, number, number] | null;
+ startDate: string | null;
+ endDate: string | null;
minParticipants: number;
maxParticipants: number;
currentParticipants: number;
@@ -24,138 +26,141 @@ interface JoinedGroup {
}
interface JoinedGroupCardProps {
- item: JoinedGroup | any;
- onViewPress: () => void;
- onPayPress: () => void; // function for Pay button
- hasPaid?: boolean; // whether user has already paid
+ item: JoinedGroup;
+ hasPaid?: boolean;
}
const formatDate = (dateStr: string | null): string => {
if (!dateStr) return 'N/A';
-
const [year, month, day] = dateStr.split('-');
- return `${day}-${month}-${year}`;
+ const months = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
+ ];
+ const monthIndex = parseInt(month, 10) - 1;
+ const dayNum = parseInt(day, 10);
+ return `${dayNum} ${months[monthIndex]} ${year}`;
};
-
-
-const getStatusStyle = (status: JoinedGroup['status']) => {
- switch (status) {
- case 'ACTIVE':
- return {
- backgroundColor: '#D1FAE5',
- textColor: '#065F46',
- };
- case 'PENDING':
- return {
- backgroundColor: '#FEF3C7',
- textColor: '#92400E',
- };
- case 'COMPLETED':
- return {
- backgroundColor: '#E5E7EB',
- textColor: '#4B5563',
- };
- default:
- return {
- backgroundColor: '#E5E7EB',
- textColor: '#4B5563',
- };
- }
-};
-
-export const JoinedGroupCard: FC = ({ item, onViewPress, hasPaid }) => {
- const progress = (item.currentParticipants / item.maxParticipants) * 100;
- const statusStyle = getStatusStyle(item.status);
+export const JoinedGroupCard: FC = ({ item }) => {
+ const theme = useTheme();
const navigation = useNavigation();
- const [loading, setLoading] = React.useState(false);
- const [payable, setPayable] = React.useState();
+ const { data: payableRes } = useFetch(`${CONTRIBUTE}${item.id}/calculate-contribution`);
+ const payable = payableRes?.data;
+ const { mutate: contribute, isPending: loading } = useCreate(
+ `payments/contribute/${item?.id}?amount=${payable?.totalPayable}`,
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Payment successful!', ToastAndroid.SHORT);
+ },
+ onError: () => {
+ ToastAndroid.show('Payment failed. Please try again.', ToastAndroid.SHORT);
+ },
+ }
+ );
-
- const onPayPress = async () => {
- // Navigate to payment screen or handle payment logic
- // navigation.navigate('PaymentScreen', { groupId: item.id, amount: item.contributionAmount });
- setLoading(true);
- try {
- const res = await apiCall(`${BASE_URL}payments/contribute/${item?.id}?amount=${payable?.totalPayable}`, 'POST') as any;
- console.log('payment res ', res)
- if (res.status === 200 || res.status === 201 || res.statusCode === 200) {
- ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
- setLoading(false);
- }
- } catch (error) {
- ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT);
- } finally {
- setLoading(false);
+ const getStatusColors = (status: JoinedGroup['status']) => {
+ switch (status) {
+ case 'ACTIVE':
+ return { bg: theme.palette.greyExtraLight, text: theme.palette.black };
+ case 'COMPLETED':
+ return { bg: theme.palette.greyLight, text: theme.palette.greyDark };
+ default:
+ return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark };
}
};
- const handleContribution = async () => {
- try {
- const res = await apiCall(`${CONTRIBUTE}${item.id}/calculate-contribution`, 'GET') as any;
- setPayable(res.data);
- } catch (error) {
- console.log("Error in handleContribution: ", error);
- }
- };
- React.useEffect(() => {
- handleContribution();
- }, []);
+ const statusColors = getStatusColors(item.status);
+
return (
- navigation.navigate("GroupDetails", { item, color: colors.black })}>
- {/* Header */}
+ navigation.navigate('GroupDetails', { item })}
+ activeOpacity={0.7}
+ >
- {item.groupName}
-
- {item.status}
+
+ {item.groupName}
+
+ by {item.createdByName}
+
+
+
+ {item.status}
- Created by {item.createdByName}
- {/* Financial Info */}
-
+
+ {/* Contribution Amount — always visible */}
+
- Contribution
- ₹{item.contributionAmount.toLocaleString('en-IN')}
+ Contribution
+
+ UGX {item.contributionAmount.toLocaleString()}
+
- Cycle
- {item.cycleType}
+ Type
+ {item.cycleType}
- {payable?.effectiveLateFee && (
- <>
- Total Payable: ₹{payable?.totalPayable.toLocaleString('en-IN')}
- Effective Late Fee : ₹{payable?.effectiveLateFee.toLocaleString('en-IN')}
- >
+
+ {/* Payment Details — shown when NOT paid and there's an amount due */}
+ {payable && !payable.hasPaid && payable.totalPayable > 0 && (
+
+
+ Total Payable
+
+ UGX {payable.totalPayable.toLocaleString()}
+
+
+ {payable.effectiveLateFee > 0 && (
+
+ Late Fee
+
+ + UGX {payable.effectiveLateFee.toLocaleString()}
+
+
+ )}
+ {payable.alreadyPaid > 0 && (
+
+ Already Paid
+
+ UGX {payable.alreadyPaid.toLocaleString()}
+
+
+ )}
+
)}
- {/* Footer with Dates & Buttons */}
+
-
- {formatDate(item.startDate)} to {formatDate(item.endDate)}
+
+ {formatDate(item.startDate)} — {formatDate(item.endDate)}
- {/* Add Pay / Paid button here */}
- {/* {hasPaid ? (
-
- Paid
+
+ {payable?.hasPaid ? (
+
+ ✓ PAID
) : (
-
-
- {loading ? 'Loading...' : ' Pay'}
-
- )} */}
- {
- hasPaid &&
- Paid
-
- }
- {
- item.status !== 'COMPLETED' &&
- {loading ? 'Loading...' : ' Pay'}
-
- }
+ item.status !== 'COMPLETED' && (
+ {
+ navigation.navigate('PaymentMode' as any, {
+ groupId: item.id,
+ routeFrom: 'GroupDetails'
+ })
+ }}
+ disabled={loading}
+ >
+
+ {loading ? '...' : 'Pay'}
+
+
+ )
+ )}
);
@@ -163,144 +168,109 @@ export const JoinedGroupCard: FC = ({ item, onViewPress, h
const styles = StyleSheet.create({
card: {
- backgroundColor: colors.pastelOrange,
borderRadius: 16,
- padding: 20,
- marginBottom: 16,
- shadowColor: '#374151',
- shadowOpacity: 0.08,
- shadowRadius: 10,
- shadowOffset: { width: 0, height: 4 },
- elevation: 5,
- marginHorizontal: 12
+ padding: 14,
+ marginBottom: 12,
+ marginHorizontal: 12,
+ borderWidth: 1,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 2,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
- marginBottom: 4,
+ marginBottom: 10,
+ },
+ titleContainer: {
+ flex: 1,
},
groupName: {
fontSize: responsiveFontSize(2),
- fontWeight: 'bold',
- color: '#1F2937',
- flexShrink: 1,
+ fontFamily: FontsFamily.nunitoExtraBold,
+ marginBottom: 2,
},
createdBy: {
fontSize: responsiveFontSize(1.4),
- color: '#6B7280',
- marginBottom: 16,
+ fontFamily: FontsFamily.nunitoRegular,
},
statusBadge: {
- paddingHorizontal: 10,
- paddingVertical: 5,
- borderRadius: 12,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 6,
},
statusText: {
fontSize: responsiveFontSize(1.2),
- fontWeight: '600',
- textTransform: 'uppercase',
+ fontFamily: FontsFamily.nunitoBold,
},
infoRow: {
flexDirection: 'row',
- justifyContent: 'space-between',
- paddingVertical: 16,
+ paddingVertical: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
- borderColor: colors.pastelOrange,
- marginBottom: 16,
- backgroundColor: colors.white,
- borderRadius: 15,
+ marginBottom: 10,
},
infoItem: {
- padding: 5,
- paddingHorizontal: 10,
- backgroundColor: colors.white,
- alignItems: 'flex-start',
+ flex: 1,
},
infoLabel: {
- fontSize: responsiveFontSize(1.4),
- color: '#6B7280',
- marginBottom: 4,
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 2,
},
infoValue: {
fontSize: responsiveFontSize(1.6),
- fontWeight: '600',
- color: '#111827',
- textTransform: 'capitalize',
+ fontFamily: FontsFamily.nunitoBold,
},
- progressContainer: {
- marginBottom: 16,
+ payableSection: {
+ marginBottom: 12,
+ gap: 6,
},
- progressLabels: {
+ payableRow: {
flexDirection: 'row',
justifyContent: 'space-between',
- marginBottom: 8,
+ alignItems: 'center',
},
- participantText: {
- fontSize: responsiveFontSize(1.4),
- color: '#6B7280',
+ payableLabel: {
+ fontSize: responsiveFontSize(1.35),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
- participantCount: {
- fontSize: responsiveFontSize(1.4),
- fontWeight: '600',
- color: '#111827',
- },
- progressBarBackground: {
- height: 8,
- backgroundColor: '#E5E7EB',
- borderRadius: 4,
- overflow: 'hidden',
- },
- progressBarFill: {
- height: '100%',
- backgroundColor: colors.pastelOrange,
- borderRadius: 4,
+ payableValue: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
+ marginTop: 4,
},
dateText: {
- fontSize: responsiveFontSize(1.4),
- color: '#4B5563',
- fontWeight: '500',
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoMedium,
},
- // Pay button
payButton: {
- elevation: 5,
- backgroundColor: colors.white,
- paddingHorizontal: 25,
- paddingVertical: 10,
- borderRadius: 10,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 8,
+ minWidth: 80,
+ alignItems: 'center',
},
payButtonText: {
- color: colors.black,
- fontSize: responsiveFontSize(1.4),
- fontWeight: '600',
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
},
- // Paid button
- paidButton: {
- backgroundColor: '#D1FAE5',
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 10,
+ paidBadge: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 6,
},
- paidButtonText: {
- color: '#065F46',
- fontSize: responsiveFontSize(1.4),
- fontWeight: '600',
- },
- viewButton: {
- backgroundColor: colors.pastelOrange,
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 10,
- },
- viewButtonText: {
- color: '#000000ff',
- fontSize: responsiveFontSize(1.4),
- fontWeight: '600',
+ paidText: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoBold,
+ letterSpacing: 0.5,
},
});
diff --git a/src/components/MyGroupCard.tsx b/src/components/MyGroupCard.tsx
index 8058864..5474389 100644
--- a/src/components/MyGroupCard.tsx
+++ b/src/components/MyGroupCard.tsx
@@ -1,26 +1,21 @@
-// components/MyGroupCard.tsx
-import React from "react";
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
-} from "react-native";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from "./colors";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import LucideIcon from './common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import FontsFamily from '../constents/FontsFamily';
interface MyGroup {
id: string;
groupName: string;
description: string;
- visibility: "PUBLIC" | "PRIVATE";
+ visibility: 'PUBLIC' | 'PRIVATE';
createdByName: string;
currentParticipants: number;
maxParticipants: number;
contributionAmount: number;
- cycleType: "MONTHLY" | "QUARTERLY" | "YEARLY";
- status: "PENDING" | "ACTIVE" | "COMPLETED";
+ cycleType: 'CUSTOM' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY';
+ status: 'PENDING' | 'APPROVED' | 'ACTIVE' | 'DELAY' | 'COMPLETED' | 'REJECTED' | 'CANCELLED';
}
interface Props {
@@ -29,55 +24,74 @@ interface Props {
}
export const MyGroupCard: React.FC = ({ item, onManage }) => {
- const getStatusColor = () => {
+ const theme = useTheme();
+
+ const getStatusColors = () => {
switch (item.status) {
- case "ACTIVE":
- return colors.pastelGreen;
- case "PENDING":
- return colors.pastelOrange; // amber
- case "COMPLETED":
- return colors.pastelBlue;
+ case 'ACTIVE':
+ return { bg: theme.palette.greyExtraLight, text: theme.palette.black };
+ case 'APPROVED':
+ return { bg: theme.colors.success + '22', text: theme.colors.success };
+ case 'DELAY':
+ return { bg: theme.colors.warning + '22', text: theme.colors.warning };
+ case 'PENDING':
+ return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark };
+ case 'COMPLETED':
+ return { bg: theme.palette.greyLight, text: theme.palette.greyDark };
+ case 'REJECTED':
+ case 'CANCELLED':
+ return { bg: theme.colors.error + '22', text: theme.colors.error };
default:
- return "#adb5bd";
+ return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark };
}
};
+ const statusColors = getStatusColors();
+
return (
-
- {/* Left Content */}
+
-
+
{item.groupName}
-
- {item.status}
+
+ {item.status}
-
+
{item.description}
-
- {item.currentParticipants}/{item.maxParticipants}
-
-
- ₹{item.contributionAmount}
-
-
- {item.cycleType}
-
+
+
+
+ {item.currentParticipants}/{item.maxParticipants}
+
+
+
+
+
+ ₹{item.contributionAmount}
+
+
+
+
+
+ {item.cycleType}
+
+
-
- Created by {item.createdByName}
- {/* Manage Button */}
- onManage(item)}>
-
- Manage
+ onManage(item)}
+ >
+
+ Manage
);
@@ -85,80 +99,72 @@ export const MyGroupCard: React.FC = ({ item, onManage }) => {
const styles = StyleSheet.create({
card: {
- backgroundColor: "#fff",
borderRadius: 12,
- padding: 16,
- marginBottom: 12,
- flexDirection: "row",
- alignItems: "center",
- elevation: 2,
- shadowColor: "#000",
+ padding: 14,
+ marginBottom: 10,
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderWidth: 1,
+ shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 4,
- marginHorizontal:12
+ marginHorizontal: 12,
+ elevation: 2,
},
cardHeader: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- marginBottom: 6,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 8,
},
cardTitle: {
fontSize: responsiveFontSize(1.8),
- fontWeight: "bold",
- color: "#343a40",
+ fontFamily: FontsFamily.nunitoExtraBold,
flex: 1,
marginRight: 10,
},
cardSubtitle: {
fontSize: responsiveFontSize(1.4),
- color: "#6c757d",
- marginBottom: 8,
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 10,
},
cardMetaContainer: {
- flexDirection: "row",
- alignItems: "center",
- flexWrap: "wrap",
- marginBottom: 6,
+ flexDirection: 'row',
+ alignItems: 'center',
+ flexWrap: 'wrap',
},
- manageButtonText: {
- fontSize: responsiveFontSize(1.2),
- color: colors.textDark,
+ metaItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 16,
+ marginBottom: 4,
},
cardMetaText: {
fontSize: responsiveFontSize(1.3),
- color: "#495057",
- fontWeight: "500",
- marginRight: 16,
- flexDirection: "row",
- alignItems: "center",
+ fontFamily: FontsFamily.nunitoSemiBold,
+ marginLeft: 4,
},
statusBadge: {
- paddingHorizontal: 10,
+ paddingHorizontal: 8,
paddingVertical: 4,
- borderRadius: 5,
- elevation: 1,
+ borderRadius: 4,
},
statusBadgeText: {
- color: colors.textDark,
fontSize: responsiveFontSize(1),
- fontWeight: "bold",
- textTransform: "uppercase",
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
},
manageButton: {
- width: 100,
- borderRadius:10,
- flexDirection:"row",
- alignItems: "center",
- justifyContent: "space-around",
- padding: 6,
+ width: 84,
+ borderRadius: 8,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 10,
marginLeft: 10,
- elevation:2,
- backgroundColor: colors.pastelOrange,
},
- createdBy: {
+ manageButtonText: {
fontSize: responsiveFontSize(1.2),
- color: "#868e96",
+ fontFamily: FontsFamily.nunitoBold,
marginTop: 4,
},
});
diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx
index 9c070a9..d1f3a7a 100644
--- a/src/components/RequestCard.tsx
+++ b/src/components/RequestCard.tsx
@@ -1,11 +1,10 @@
-// src/components/RequestCard.tsx
-
import React, { FC } from 'react';
import { View, Text, Image, TouchableOpacity, StyleSheet, Platform } from 'react-native';
-import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming Expo for icons
-import colors from './colors'; // Your color palette
-import { JoinRequest } from '../types'; // Your type definition
+import LucideIcon from './common/LucideIcon';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import { JoinRequest } from '../types';
+import FontsFamily from '../constents/FontsFamily';
interface RequestCardProps {
item: JoinRequest;
@@ -14,46 +13,57 @@ interface RequestCardProps {
}
export const RequestCard: FC = ({ item, onApprove, onDecline }) => {
+ const theme = useTheme();
+
return (
-
- {/* User Information Section */}
+
-
+
- {item.userName}
+
+ {item.userName}
+
-
- {item.mobile}
+
+ {item.mobile}
- {/* Group Information Section */}
-
- Wants to join group
- {item.groupName}
+
+
+ Wants to join group
+
+
+ {item.groupName}
+
- {/* Action Buttons Section */}
- onDecline(item)}
activeOpacity={0.7}
>
-
- Decline
+
+ Decline
- onApprove(item)}
activeOpacity={0.8}
>
-
- Approve
+
+ Approve
@@ -62,110 +72,82 @@ export const RequestCard: FC = ({ item, onApprove, onDecline }
const styles = StyleSheet.create({
card: {
- marginHorizontal: 16,
- marginVertical: 12,
- backgroundColor: colors.white,
- borderRadius: 24,
- padding: 20,
+ marginHorizontal: 12,
+ marginVertical: 8,
+ padding: 16,
+ borderWidth: 1,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.08,
- shadowRadius: 12,
+ shadowOpacity: 0.05,
+ shadowRadius: 10,
},
android: {
- elevation: 8,
- shadowColor: '#00000020', // Softer shadow color for Android
+ elevation: 3,
},
}),
- borderWidth: 1,
- borderColor: '#f0f0f0',
},
userInfoSection: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
- width: 60,
- height: 60,
- borderRadius: 30,
- marginRight: 16,
- borderWidth: 2,
- borderColor: colors.pastelOrange,
+ width: 52,
+ height: 52,
+ borderRadius: 26,
+ marginRight: 14,
+ backgroundColor: '#E6E6E6',
},
userInfo: {
flex: 1,
},
userName: {
- fontSize: responsiveFontSize(1.9),
- fontWeight: 'bold',
- color: colors.textDark,
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoExtraBold,
},
userMetaContainer: {
flexDirection: 'row',
alignItems: 'center',
- marginTop: 5,
+ marginTop: 4,
},
userMeta: {
fontSize: responsiveFontSize(1.4),
- color: colors.textLight,
- marginLeft: 8,
+ fontFamily: FontsFamily.nunitoRegular,
+ marginLeft: 6,
},
groupInfoSection: {
- marginTop: 18,
- paddingVertical: 16,
- paddingHorizontal: 16,
- backgroundColor: '#F7F8FA',
- borderRadius: 16,
+ marginTop: 14,
+ padding: 14,
+ borderRadius: 12,
alignItems: 'center',
},
requestText: {
- fontSize: responsiveFontSize(1.4),
- color: colors.textLight,
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
},
groupName: {
- fontSize: responsiveFontSize(1.7),
- fontWeight: '600',
- color: colors.textDark,
- marginTop: 6,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 4,
textAlign: 'center',
},
actionsSection: {
flexDirection: 'row',
- marginTop: 20,
- gap: 12,
+ marginTop: 16,
+ gap: 10,
},
actionButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
- paddingVertical: 16,
- borderRadius: 16,
- },
- declineButton: {
- backgroundColor: '#F1F3F6',
- },
- approveButton: {
- backgroundColor: colors.pastelOrange,
- ...Platform.select({
- ios: {
- shadowColor: colors.pastelOrange,
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- },
- android: {
- elevation: 6,
- shadowColor: colors.pastelOrange,
- },
- }),
+ paddingVertical: 12,
+ borderRadius: 12,
},
actionButtonText: {
- fontSize: responsiveFontSize(1.6),
- fontWeight: 'bold',
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
marginLeft: 8,
- color: colors.textDark,
},
});
diff --git a/src/components/UserRequested.tsx b/src/components/UserRequested.tsx
index a6f6999..cf14145 100644
--- a/src/components/UserRequested.tsx
+++ b/src/components/UserRequested.tsx
@@ -1,42 +1,43 @@
import React, { FC } from "react";
import { View, Text, StyleSheet } from "react-native";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from "./colors";
+import LucideIcon from './common/LucideIcon';
import { UserJoinRequest } from "../types";
import { formatDate } from "../utils/HelperFunction";
import { responsiveFontSize } from "react-native-responsive-dimensions";
+import { useTheme } from "../hooks/ui/useTheme";
+import FontsFamily from "../constents/FontsFamily";
interface UserRequestedProps {
item: UserJoinRequest;
}
-const getStatusStyle = (status: UserJoinRequest["status"]) => {
+const getStatusStyle = (status: UserJoinRequest["status"], theme: ReturnType) => {
switch (status) {
case "APPROVED":
return {
- backgroundColor: "#D1FAE5",
- textColor: "#065F46",
+ backgroundColor: theme.colors.success + "22",
+ textColor: theme.colors.success,
label: "Approved",
icon: "checkmark-circle-outline",
};
case "PENDING":
return {
- backgroundColor: "#FEF3C7",
- textColor: "#92400E",
+ backgroundColor: theme.colors.warning + "22",
+ textColor: theme.colors.warning,
label: "Pending",
icon: "time-outline",
};
case "REJECT":
return {
- backgroundColor: "#FEE2E2",
- textColor: "#B91C1C",
+ backgroundColor: theme.colors.error + "22",
+ textColor: theme.colors.error,
label: "Rejected",
icon: "close-circle-outline",
};
default:
return {
- backgroundColor: "#E5E7EB",
- textColor: "#4B5563",
+ backgroundColor: theme.colors.borderLight,
+ textColor: theme.colors.textSecondary,
label: "Unknown",
icon: "help-outline",
};
@@ -44,41 +45,40 @@ const getStatusStyle = (status: UserJoinRequest["status"]) => {
};
export const UserRequested: FC = ({ item }) => {
- const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status);
-
- // Optionally, initials avatar (remove/comment if you want):
- const initials = item.createdBy?.trim().split(" ").map((w) => w[0])?.join("").toUpperCase();
+ const theme = useTheme();
+ const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status, theme);
+ const initials = item.createdBy?.trim().split(" ").map((w) => w[0])?.join("").toUpperCase() || "?";
return (
-
-
+
+
-
- {initials}
+
+ {initials}
- {item.groupName}
-
- {" "}
+ {item.groupName}
+
+ {" "}
{formatDate(item.requestDate)}
-
+
{label}
-
-
+
+
{" "}Group Created By:{" "}
- {item.createdBy}
+ {item.createdBy}
{item.reason ? (
-
- {item.reason}
+
+ {item.reason}
) : null}
@@ -88,79 +88,73 @@ export const UserRequested: FC = ({ item }) => {
const styles = StyleSheet.create({
card: {
- backgroundColor: colors.white,
- borderRadius: 16,
- marginVertical: 14, // More space
- marginHorizontal: 10,
- elevation: 4,
+ borderRadius: 14,
+ marginVertical: 8,
+ marginHorizontal: 12,
+ elevation: 2,
+ borderWidth: 1,
shadowColor: "#000",
- shadowOpacity: 0.12,
- shadowRadius: 8,
+ shadowOpacity: 0.08,
+ shadowRadius: 6,
shadowOffset: { width: 0, height: 2 },
- padding: 16,
+ padding: 14,
},
topRow: {
flexDirection: "row",
alignItems: "flex-start",
borderBottomWidth: 1,
- borderBottomColor: "#e5e7eb",
- paddingBottom: 10,
- marginBottom: 6
+ paddingBottom: 8,
+ marginBottom: 6,
},
avatarContainer: {
- marginRight: 10,
+ marginRight: 8,
marginTop: 2,
},
avatarCircle: {
width: 36,
height: 36,
borderRadius: 18,
- backgroundColor: colors.pastelBlue,
justifyContent: "center",
alignItems: "center"
},
avatarText: {
- fontWeight: "700",
+ fontFamily: FontsFamily.nunitoBold,
fontSize: responsiveFontSize(1.6),
- color: colors.white,
},
userInfoSection: {
flex: 1,
- paddingRight: 8,
+ paddingRight: 6,
},
userName: {
- fontWeight: "bold",
- color: colors.textDark,
+ fontFamily: FontsFamily.nunitoBold,
fontSize: responsiveFontSize(1.5),
},
userMeta: {
fontSize: responsiveFontSize(1.2),
- color: colors.textLight,
+ fontFamily: FontsFamily.nunitoRegular,
marginTop: 3,
marginBottom: -3,
},
groupInfoSection: {
- paddingTop: 10,
- paddingBottom: 6,
+ paddingTop: 8,
+ paddingBottom: 2,
},
requestText: {
- color: colors.textDark,
fontSize: responsiveFontSize(1.5),
marginBottom: 2,
- lineHeight: 22,
+ lineHeight: 20,
+ fontFamily: FontsFamily.nunitoRegular,
},
groupName: {
- fontWeight: "600",
- color: colors.Settings,
+ fontFamily: FontsFamily.nunitoSemiBold,
fontSize: responsiveFontSize(1.8),
marginBottom: 2,
},
reasonText: {
fontSize: responsiveFontSize(1.2),
- color: colors.textLight,
marginTop: 6,
paddingLeft: 1,
- lineHeight: 18,
+ lineHeight: 16,
fontStyle: "italic",
},
statusBadge: {
@@ -170,15 +164,15 @@ const styles = StyleSheet.create({
paddingHorizontal: 10,
paddingVertical: 5,
marginLeft: 4,
- minWidth: 90,
+ minWidth: 84,
justifyContent: "center",
alignSelf: "flex-start",
marginTop: 2
},
statusText: {
fontSize: responsiveFontSize(1.4),
- fontWeight: "700",
+ fontFamily: FontsFamily.nunitoBold,
marginLeft: 6,
- letterSpacing: 0.5
+ letterSpacing: 0.3
},
});
diff --git a/src/components/common/AnimatedCard.tsx b/src/components/common/AnimatedCard.tsx
index 5243710..b0acd04 100644
--- a/src/components/common/AnimatedCard.tsx
+++ b/src/components/common/AnimatedCard.tsx
@@ -1,44 +1,62 @@
-import React, { use, useRef } from "react";
-import { View, Text, StyleSheet, Animated, TouchableWithoutFeedback } from "react-native";
-import LinearGradient from "react-native-linear-gradient";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from "../colors";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
-import { useTheme } from "../../theme/ThemeProvider";
-import FontsFamily from "../../constents/FontsFamily";
+import React, { useRef } from 'react';
+import { View, Text, StyleSheet, Animated, TouchableWithoutFeedback } from 'react-native';
+import LucideIcon from './LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import FontsFamily from '../../constents/FontsFamily';
type CardProps = {
title: string;
subtitle?: string;
iconName?: string;
onPress?: () => void;
- backOne?: string;
- backTwo?: string;
};
const AnimatedCard: React.FC = ({
title,
subtitle,
- iconName = "ellipse-outline",
+ iconName = 'ellipse-outline',
onPress,
- backOne = colors.primary,
- backTwo = colors.pastelPink,
}) => {
+ const theme = useTheme();
const scale = useRef(new Animated.Value(1)).current;
- const{ theme } = useTheme()
- const handlePress = () => {
- Animated.sequence([
- Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }),
- ]).start(() => onPress && onPress());
+
+ const handlePressIn = () => {
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true }).start();
+ };
+
+ const handlePressOut = () => {
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start();
};
return (
-
-
-
-
- {title}
- {subtitle && {subtitle}}
+
+
+
+
+ {title}
+ {subtitle && (
+ {subtitle}
+ )}
@@ -48,20 +66,33 @@ const AnimatedCard: React.FC = ({
const styles = StyleSheet.create({
card: {
flex: 1,
- height: 160,
- margin: 8,
- borderRadius: 20,
- overflow: "hidden",
- elevation: 5,
+ height: 150,
+ margin: 6,
+ borderWidth: 1,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.08,
+ shadowRadius: 4,
},
- gradient: {
+ content: {
flex: 1,
- justifyContent: "center",
- alignItems: "center",
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ },
+ icon: { marginBottom: 12 },
+ title: {
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ textAlign: 'center',
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ marginTop: 4,
},
- icon: { marginBottom: 10 },
- title: { fontSize: responsiveFontSize(2.2), fontFamily: FontsFamily.nunitoExtraBold, color: "#fff" },
- subtitle: { fontSize: responsiveFontSize(1.5), fontFamily: FontsFamily.nunitoRegular, opacity: 0.9 },
});
export default AnimatedCard;
diff --git a/src/components/common/AnimatedText.tsx b/src/components/common/AnimatedText.tsx
index a1418ba..cf06f9c 100644
--- a/src/components/common/AnimatedText.tsx
+++ b/src/components/common/AnimatedText.tsx
@@ -62,4 +62,4 @@ const styles = StyleSheet.create({
text: {
textAlign: 'center',
},
-});
\ No newline at end of file
+});
diff --git a/src/components/common/Custom.tsx b/src/components/common/Custom.tsx
index 1fc1d86..cbc82e7 100644
--- a/src/components/common/Custom.tsx
+++ b/src/components/common/Custom.tsx
@@ -1,46 +1,76 @@
-import React from "react";
-import {
- StyleSheet,
- View,
- Text,
- TextInput,
- TouchableOpacity,
- TextInputProps,
-} from "react-native";
-import {Ionicons, IoniconsIconName} from "@react-native-vector-icons/ionicons";
-import colors from "../colors";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+import React from 'react';
+import { StyleSheet, View, Text, TextInput, TouchableOpacity, TextInputProps } from 'react-native';
+import LucideIcon, { LucideIconName } from './LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
// --- Custom Input ---
type CustomInputProps = TextInputProps & {
label?: string;
- icon?: IoniconsIconName;
- error?:string
+ icon?: string;
+ error?: string;
value?: string | number;
};
-const CustomInput: React.FC = ({
- label,
- value,
- icon,
- style,
- error,
- ...rest
-}) => {
+const withOpacity = (hex: string, alpha: number) => {
+ const normalized = hex.replace('#', '');
+ if (normalized.length !== 6) return hex;
+
+ const r = parseInt(normalized.slice(0, 2), 16);
+ const g = parseInt(normalized.slice(2, 4), 16);
+ const b = parseInt(normalized.slice(4, 6), 16);
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+};
+
+const CustomInput: React.FC = ({ label, value, icon, style, error, placeholder, ...rest }) => {
+ const theme = useTheme();
+ const computedPlaceholder = placeholder ?? (label ? `Enter ${label.toLowerCase()}` : undefined);
+ const hasError = Boolean(error && String(error).trim());
+
return (
- {label ? {label} : null}
+ {label ? {label} : null}
- {icon && }
+ {icon && (
+
+ )}
+ {hasError ? (
+
+
+ {error}
+
+ ) : null}
);
};
@@ -49,32 +79,72 @@ const CustomInput: React.FC = ({
type CustomButtonProps = {
title: string;
onPress: () => void;
- type?: "primary" | "secondary" | "danger";
- icon?: IoniconsIconName;
+ type?: 'primary' | 'secondary' | 'outline';
+ icon?: string;
style?: object;
buttonBackground?: string;
+ textColor?: string;
+ loading?: boolean;
+ disabled?: boolean;
};
const CustomButton: React.FC = ({
title,
onPress,
- type = "primary",
+ type = 'primary',
icon,
style = {},
- buttonBackground,
+ buttonBackground,
+ textColor: customTextColor,
+ loading = false,
+ disabled = false,
}) => {
+ const theme = useTheme();
- const textColor = type === "secondary" ? colors.textDark : '#000';
+ const getBackgroundColor = () => {
+ if (disabled) return theme.palette.greyLight;
+ if (buttonBackground) return buttonBackground;
+ if (type === 'primary') return theme.colors.primary;
+ if (type === 'secondary') return theme.palette.greyMedium;
+ return 'transparent';
+ };
+
+ const getTextColor = () => {
+ if (disabled) return theme.colors.textMuted;
+ if (customTextColor) return customTextColor;
+ if (type === 'primary') return theme.colors.onPrimary;
+ if (type === 'secondary') return theme.colors.textPrimary;
+ if (type === 'outline') return theme.colors.primary;
+ return theme.colors.onPrimary;
+ };
+
+ const backgroundColor = getBackgroundColor();
+ const textColor = getTextColor();
return (
- {icon && }
- {title}
+ {icon && !loading && (
+
+ )}
+
+ {loading ? 'Processing...' : title}
+
);
@@ -82,42 +152,72 @@ const CustomButton: React.FC = ({
// --- Styles ---
const styles = StyleSheet.create({
- inputContainer: { marginBottom: 12,elevation:5 },
- inputLabel: { fontSize: responsiveFontSize(1.5), fontWeight: "500", color: colors.textDark, marginBottom: 6 },
+ inputContainer: {
+ marginBottom: 14,
+ },
+ inputLabel: {
+ fontSize: responsiveFontSize(1.6),
+ fontWeight: '600',
+ marginBottom: 8,
+ },
inputWrapper: {
- flexDirection: "row",
- alignItems: "center",
- position: "relative",
+ flexDirection: 'row',
+ alignItems: 'center',
+ position: 'relative',
},
inputIcon: {
- position: "absolute",
- left: 10,
+ position: 'absolute',
+ left: 12,
zIndex: 1,
},
input: {
flex: 1,
- borderRadius: 15,
- padding: 12,
- paddingLeft: 12,
- backgroundColor: colors.white,
- color: colors.textDark,
- fontSize: responsiveFontSize(1.5),
+ borderRadius: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ fontSize: responsiveFontSize(1.6),
+ },
+ errorContainer: {
+ marginTop: 6,
+ borderWidth: 1,
+ borderRadius: 10,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ errorText: {
+ fontSize: responsiveFontSize(1.25),
+ fontWeight: '600',
+ flex: 1,
},
button: {
- elevation:5,
- paddingVertical: 12,
+ paddingVertical: 14,
borderRadius: 12,
- alignItems: "center",
+ alignItems: 'center',
marginVertical: 6,
+ // Soft shadow for depth
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.08,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ outlineButton: {
+ shadowOpacity: 0,
+ shadowRadius: 0,
+ shadowOffset: { width: 0, height: 0 },
+ elevation: 0,
},
buttonContent: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
},
buttonText: {
- fontSize: responsiveFontSize(1.6),
- fontWeight: "600",
+ fontSize: responsiveFontSize(1.8),
+ fontWeight: '700',
},
});
diff --git a/src/components/common/CustomModal.tsx b/src/components/common/CustomModal.tsx
index dba0e72..c9d32e5 100644
--- a/src/components/common/CustomModal.tsx
+++ b/src/components/common/CustomModal.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useEffect } from "react";
+import React, { useRef, useEffect } from 'react';
import {
View,
Text,
@@ -7,11 +7,16 @@ import {
TouchableOpacity,
Animated,
Dimensions,
-} from "react-native";
-import colors from "../colors";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+ KeyboardAvoidingView,
+ Platform,
+ TouchableWithoutFeedback,
+ Keyboard,
+} from 'react-native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import FontsFamily from '../../constents/FontsFamily';
-const { height } = Dimensions.get("window");
+const { height } = Dimensions.get('window');
type CustomModalProps = {
visible: boolean;
@@ -21,9 +26,9 @@ type CustomModalProps = {
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
- confirmButton?:string
- confirmTextColor?:string
- elevation?:number
+ confirmButton?: string;
+ confirmTextColor?: string;
+ elevation?: number;
};
const CustomModal: React.FC = ({
@@ -31,14 +36,15 @@ const CustomModal: React.FC = ({
onClose,
title,
children,
- confirmText = "Confirm",
- cancelText = "Cancel",
+ confirmText = 'Confirm',
+ cancelText = 'Cancel',
onConfirm,
confirmTextColor,
- elevation,
- confirmButton = colors.primary
+ elevation = 5,
+ confirmButton,
}) => {
- const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen
+ const theme = useTheme();
+ const slideAnim = useRef(new Animated.Value(height)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
@@ -69,45 +75,65 @@ const CustomModal: React.FC = ({
}),
]).start();
}
- }, [visible]);
+ }, [fadeAnim, slideAnim, visible]);
return (
- {/* Overlay */}
- {/* Modal Box */}
-
- {/* Header */}
- {
- title &&
+
+
+
+ {title && (
+ {title}
+ )}
+ {children}
- {title}
- }
- {/* Body */}
- {children}
+
+
+ {cancelText}
+
- {/* Footer Actions */}
-
-
- {cancelText}
-
-
-
- {confirmText}
-
-
-
+
+
+ {confirmText}
+
+
+
+
+
+
+
);
@@ -118,46 +144,41 @@ export default CustomModal;
const styles = StyleSheet.create({
overlay: {
flex: 1,
- backgroundColor: "rgba(0,0,0,0.4)",
- justifyContent: "flex-end",
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ justifyContent: 'flex-end',
+ },
+ keyboardContainer: {
+ flex: 1,
+ justifyContent: 'flex-end',
},
modalBox: {
- backgroundColor: "#fff",
- borderTopLeftRadius: 20,
- borderTopRightRadius: 20,
- padding: 20,
+ padding: 24,
elevation: 10,
},
title: {
- fontSize: responsiveFontSize(1.8),
- fontWeight: "700",
- color: "#1f2937",
- marginBottom: 12,
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 16,
},
content: {
- marginBottom: 20,
+ marginBottom: 24,
},
actions: {
- flexDirection: "row",
- justifyContent: "flex-end",
- marginTop: 10,
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ gap: 12,
},
button: {
- paddingVertical: 10,
- paddingHorizontal: 18,
- borderRadius: 10,
- marginLeft: 10,
+ paddingVertical: 12,
+ paddingHorizontal: 24,
+ borderRadius: 12,
},
- cancelButton: {
- backgroundColor: "#e5e7eb",
- },
-
cancelText: {
- color: "#374151",
- fontWeight: "500",
+ fontFamily: FontsFamily.nunitoSemiBold,
+ fontSize: responsiveFontSize(1.6),
},
confirmText: {
- color: "#fff",
- fontWeight: "600",
+ fontFamily: FontsFamily.nunitoBold,
+ fontSize: responsiveFontSize(1.6),
},
});
diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index a2600ea..91685e1 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -1,9 +1,10 @@
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { useNavigation } from "@react-navigation/native";
-import {Ionicons} from "@react-native-vector-icons/ionicons";
-import colors from "../colors";
+import LucideIcon from './LucideIcon';
import { responsiveFontSize } from "react-native-responsive-dimensions";
+import { useTheme } from "../../hooks/ui/useTheme";
+import FontsFamily from "../../constents/FontsFamily";
type HeaderProps = {
title: string;
@@ -19,10 +20,14 @@ const Header: React.FC = ({
showBack = true,
onBackPress,
rightAction,
- titleColor = '#000',
- arrowColor = '#000'
+ titleColor,
+ arrowColor
}) => {
const navigation = useNavigation();
+ const theme = useTheme();
+
+ const resolvedTitleColor = titleColor || theme.colors.textPrimary;
+ const resolvedArrowColor = arrowColor || theme.colors.textPrimary;
const handleBack = () => {
if (onBackPress) {
@@ -35,12 +40,15 @@ const Header: React.FC = ({
return (
{showBack && (
-
-
+
+
- )}
- {title}
- {rightAction}
+ )}
+ {!showBack && }
+
+ {title}
+
+ {rightAction}
);
};
@@ -50,15 +58,21 @@ export default Header;
const styles = StyleSheet.create({
header: {
flexDirection: "row",
- // alignItems: "center",
- // justifyContent: "space-between",
- paddingVertical: 12,
- paddingHorizontal:12
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ },
+ sideAction: {
+ width: 36,
+ height: 36,
+ alignItems: "center",
+ justifyContent: "center",
},
headerTitle: {
- fontSize: responsiveFontSize(1.8),
- fontWeight: "600",
- // textTransform: "capitalize",
+ flex: 1,
+ textAlign: "center",
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
},
-
});
diff --git a/src/components/common/LucideIcon.tsx b/src/components/common/LucideIcon.tsx
new file mode 100644
index 0000000..4591a3e
--- /dev/null
+++ b/src/components/common/LucideIcon.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import {
+ Home, User, Users, UsersRound, Settings, Bell, BellOff,
+ ArrowLeft, ArrowRight, ChevronRight, ChevronDown, X, XCircle,
+ Search, Wallet, Banknote, Trophy, Receipt, PlusCircle,
+ MailOpen, Mail, Pencil, Camera, CloudUpload, LogOut,
+ AlertCircle, Info, CheckCircle, CheckSquare, Square,
+ Phone, Trash2, MoreVertical, Power, Repeat, Ticket,
+ PieChart, BookOpen, Globe, FileText, UserPlus, Sparkles,
+ Cpu, Circle, Copy, Star, Calendar, Clock, ArrowUpDown,
+ Link, ShieldCheck, ChevronUp, Eye, EyeOff, Building2,
+ CreditCard, Hash, Check, HelpCircle, MessageCircle, CheckCircle2,
+ RefreshCw, ArrowDownCircle, CircleUserRound,
+ AtSign, MapPin, Barcode, Flag, Send, Tag, Code,
+ type LucideProps,
+} from 'lucide-react-native';
+
+// Map of Ionicons names to Lucide components
+const iconMap: Record> = {
+ // Home
+ 'home': Home,
+ 'home-outline': Home,
+ // User / Person
+ 'person': User,
+ 'person-outline': User,
+ 'people': Users,
+ 'people-outline': Users,
+ 'people-circle-outline': UsersRound,
+ 'person-add-outline': UserPlus,
+ 'person-circle-outline': CircleUserRound,
+ // Settings
+ 'settings': Settings,
+ 'settings-outline': Settings,
+ // Notifications
+ 'notifications-outline': Bell,
+ 'notifications-off-outline': BellOff,
+ // Navigation
+ 'arrow-back': ArrowLeft,
+ 'arrow-forward': ArrowRight,
+ 'arrow-down-circle-outline': ArrowDownCircle,
+ 'chevron-forward': ChevronRight,
+ 'chevron-down': ChevronDown,
+ 'chevron-up': ChevronUp,
+ 'swap-vertical': ArrowUpDown,
+ // Close
+ 'close': X,
+ 'close-circle': XCircle,
+ // Search
+ 'search': Search,
+ // Finance
+ 'wallet-outline': Wallet,
+ 'cash': Banknote,
+ 'cash-outline': Banknote,
+ 'card-outline': CreditCard,
+ // Trophy
+ 'trophy': Trophy,
+ // Receipt
+ 'receipt-outline': Receipt,
+ // Add
+ 'add-circle-outline': PlusCircle,
+ 'add': PlusCircle,
+ // Mail
+ 'mail-open-outline': MailOpen,
+ 'mail-unread-outline': Mail,
+ // Edit
+ 'create-outline': Pencil,
+ // Camera
+ 'camera': Camera,
+ // Upload
+ 'cloud-upload': CloudUpload,
+ // Auth
+ 'log-out-outline': LogOut,
+ // Alert / Info
+ 'alert-circle': AlertCircle,
+ 'alert-circle-outline': AlertCircle,
+ 'information-circle-outline': Info,
+ // Check
+ 'checkmark-circle': CheckCircle,
+ 'checkbox': CheckSquare,
+ 'square-outline': Square,
+ // Phone
+ 'call-outline': Phone,
+ // Delete
+ 'trash-outline': Trash2,
+ // More
+ 'ellipsis-vertical': MoreVertical,
+ // Power
+ 'power': Power,
+ // Repeat
+ 'repeat-outline': Repeat,
+ // Ticket
+ 'ticket-outline': Ticket,
+ // Charts
+ 'pie-chart-outline': PieChart,
+ // Book
+ 'book-outline': BookOpen,
+ // Globe
+ 'planet-outline': Globe,
+ // Documents
+ 'documents-outline': FileText,
+ // Sparkles
+ 'sparkles-outline': Sparkles,
+ // Hardware
+ 'hardware-chip-outline': Cpu,
+ // Shapes
+ 'ellipse-outline': Circle,
+ // Copy
+ 'copy-outline': Copy,
+ // Star
+ 'star': Star,
+ 'star-outline': Star,
+ // Calendar
+ 'calendar-outline': Calendar,
+ // Time
+ 'time-outline': Clock,
+ // Link
+ 'link-outline': Link,
+ // Shield
+ 'shield-checkmark-outline': ShieldCheck,
+ // Eye
+ 'eye-outline': Eye,
+ 'eye-off-outline': EyeOff,
+ // Building
+ 'business-outline': Building2,
+ // Hash
+ 'keypad-outline': Hash,
+ // Check
+ 'checkmark': Check,
+ 'checkmark-circle-outline': CheckCircle,
+ // Close circle outline
+ 'close-circle-outline': XCircle,
+ // Help
+ 'help-outline': HelpCircle,
+ // Chat
+ 'chatbubbles-outline': MessageCircle,
+ // Information circle (without -outline)
+ 'information-circle': Info,
+ // Sync / Refresh
+ 'sync-outline': RefreshCw,
+ 'sync': RefreshCw,
+ // Chat bubble
+ 'chatbubble-ellipses-outline': MessageCircle,
+ 'chatbubble-outline': MessageCircle,
+ // Card (without -outline)
+ 'card': CreditCard,
+ // Mail (without -outline)
+ 'mail-outline': Mail,
+ // At / Username
+ 'at-outline': AtSign,
+ // Location
+ 'location-outline': MapPin,
+ // Gender
+ 'male-female-outline': Users,
+ 'man-outline': User,
+ 'woman-outline': User,
+ // Barcode
+ 'barcode-outline': Barcode,
+ // Close (outline variant)
+ 'close-outline': X,
+ // Flag
+ 'flag-outline': Flag,
+ // Arrow forward outline
+ 'arrow-forward-outline': ArrowRight,
+ // Code
+ 'code-working-outline': Code,
+ // Document text
+ 'document-text-outline': FileText,
+ // Tag / Pricetag
+ 'pricetag-outline': Tag,
+ // Search outline
+ 'search-outline': Search,
+ // Send
+ 'send-outline': Send,
+};
+
+export type LucideIconName = keyof typeof iconMap;
+
+type LucideIconProps = {
+ name: string;
+ size?: number;
+ color?: string;
+ style?: any;
+ strokeWidth?: number;
+};
+
+const LucideIcon: React.FC = ({
+ name,
+ size = 24,
+ color = '#000',
+ style,
+ strokeWidth = 2,
+}) => {
+ const IconComponent = iconMap[name] || Circle;
+ return ;
+};
+
+export default LucideIcon;
diff --git a/src/components/common/SkeletonLoader.tsx b/src/components/common/SkeletonLoader.tsx
new file mode 100644
index 0000000..c4e438b
--- /dev/null
+++ b/src/components/common/SkeletonLoader.tsx
@@ -0,0 +1,65 @@
+import React, { useEffect, useRef } from 'react';
+import { Animated, StyleSheet, ViewStyle, DimensionValue } from 'react-native';
+import { useTheme } from '../../hooks/ui/useTheme';
+
+interface SkeletonProps {
+ width?: DimensionValue;
+ height?: DimensionValue;
+ borderRadius?: number;
+ style?: ViewStyle;
+}
+
+const SkeletonLoader: React.FC = ({
+ width = '100%',
+ height = 20,
+ borderRadius = 4,
+ style,
+}) => {
+ const theme = useTheme();
+ const opacity = useRef(new Animated.Value(0.3)).current;
+
+ useEffect(() => {
+ const animation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(opacity, {
+ toValue: 0.7,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacity, {
+ toValue: 0.3,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+
+ animation.start();
+
+ return () => animation.stop();
+ }, [opacity]);
+
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ skeleton: {
+ overflow: 'hidden',
+ },
+});
+
+export default SkeletonLoader;
diff --git a/src/hooks/core/apiDebug.ts b/src/hooks/core/apiDebug.ts
new file mode 100644
index 0000000..405b988
--- /dev/null
+++ b/src/hooks/core/apiDebug.ts
@@ -0,0 +1,69 @@
+import { AxiosError } from 'axios';
+
+type ApiErrorPayload = {
+ message?: string;
+ error?: string;
+ status?: string;
+ statusCode?: number;
+ data?: unknown;
+};
+
+export const logGroupRequest = (method: string, url: string, payload?: unknown) => {
+ console.log(`[HOOK_API][${method}] URL:`, url);
+ console.log(`[HOOK_API][${method}] PAYLOAD:`, payload ?? null);
+};
+
+export const logGroupResponse = (method: string, url: string, response: unknown) => {
+ const meta = response as { statusCode?: number; status?: string; message?: string };
+ console.log(`[HOOK_API][${method}] SUCCESS URL:`, url);
+ console.log(`[HOOK_API][${method}] SUCCESS META:`, {
+ statusCode: meta?.statusCode ?? null,
+ status: meta?.status ?? null,
+ message: meta?.message ?? null,
+ });
+ console.log(`[HOOK_API][${method}] SUCCESS RESPONSE:`, response);
+};
+
+export const logGroupError = (method: string, url: string, error: unknown) => {
+ const axiosError = error as AxiosError;
+ console.error(
+ `[HOOK_API][${method}] ERROR RESPONSE:`,
+ axiosError?.response?.data ?? axiosError?.message ?? error
+ );
+};
+
+export const getApiErrorMessage = (error: unknown) => {
+ const axiosError = error as AxiosError;
+ const apiMessage =
+ axiosError?.response?.data?.message ||
+ axiosError?.response?.data?.error ||
+ axiosError?.message;
+
+ return apiMessage || 'An unexpected error occurred';
+};
+
+export const normalizeApiError = (error: unknown) => {
+ const normalized = new Error(getApiErrorMessage(error));
+ (normalized as Error & { originalError?: unknown }).originalError = error;
+ return normalized;
+};
+
+export const resolveMutationRequest = (resource: string, payload: T) => {
+ const rawPayload = payload as unknown as { resource?: string; payload?: unknown };
+ const canUseDynamicResource =
+ (!resource || resource.trim() === '') &&
+ typeof rawPayload?.resource === 'string' &&
+ rawPayload.resource.trim() !== '';
+
+ if (canUseDynamicResource) {
+ return {
+ url: rawPayload.resource as string,
+ body: rawPayload.payload ?? payload,
+ };
+ }
+
+ return {
+ url: resource,
+ body: payload,
+ };
+};
diff --git a/src/hooks/core/useCreate.ts b/src/hooks/core/useCreate.ts
new file mode 100644
index 0000000..2422ba3
--- /dev/null
+++ b/src/hooks/core/useCreate.ts
@@ -0,0 +1,38 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
+import apiClient from '../../api/client';
+import {
+ normalizeApiError,
+ resolveMutationRequest,
+} from './apiDebug';
+
+export const useCreate = (
+ resource: string,
+ options?: UseMutationOptions
+) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ ...options,
+ mutationFn: async (payload: T) => {
+ const { url, body } = resolveMutationRequest(resource, payload);
+
+ try {
+ const { data } = await apiClient.post(url, body);
+ return data;
+ } catch (error) {
+ throw normalizeApiError(error);
+ }
+ },
+ onSuccess: (data, variables, onMutateResult, context) => {
+ const { url } = resolveMutationRequest(resource, variables);
+ queryClient.invalidateQueries({ queryKey: [url] });
+ if (resource && resource !== url) {
+ queryClient.invalidateQueries({ queryKey: [resource] });
+ }
+ options?.onSuccess?.(data, variables, onMutateResult, context);
+ },
+ onError: (error, variables, onMutateResult, context) => {
+ options?.onError?.(error, variables, onMutateResult, context);
+ },
+ });
+};
diff --git a/src/hooks/core/useDelete.ts b/src/hooks/core/useDelete.ts
new file mode 100644
index 0000000..33089bd
--- /dev/null
+++ b/src/hooks/core/useDelete.ts
@@ -0,0 +1,31 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
+import apiClient from '../../api/client';
+import { normalizeApiError } from './apiDebug';
+
+export const useDelete = (
+ resource: string,
+ options?: UseMutationOptions
+) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ ...options,
+ mutationFn: async (id: string | number) => {
+ const url = `${resource}/${id}`;
+
+ try {
+ const { data } = await apiClient.delete(url);
+ return data;
+ } catch (error) {
+ throw normalizeApiError(error);
+ }
+ },
+ onSuccess: (data, variables, onMutateResult, context) => {
+ queryClient.invalidateQueries({ queryKey: [resource] });
+ options?.onSuccess?.(data, variables, onMutateResult, context);
+ },
+ onError: (error, variables, onMutateResult, context) => {
+ options?.onError?.(error, variables, onMutateResult, context);
+ },
+ });
+};
diff --git a/src/hooks/core/useFetch.ts b/src/hooks/core/useFetch.ts
new file mode 100644
index 0000000..ba313af
--- /dev/null
+++ b/src/hooks/core/useFetch.ts
@@ -0,0 +1,22 @@
+import { useQuery, UseQueryOptions } from '@tanstack/react-query';
+import apiClient from '../../api/client';
+import { normalizeApiError } from './apiDebug';
+
+export const useFetch = (
+ resource: string,
+ params?: object,
+ options?: Omit, 'queryKey' | 'queryFn'>
+) => {
+ return useQuery({
+ queryKey: [resource, params],
+ queryFn: async () => {
+ try {
+ const { data } = await apiClient.get(resource, { params });
+ return data;
+ } catch (error) {
+ throw normalizeApiError(error);
+ }
+ },
+ ...options,
+ });
+};
diff --git a/src/hooks/core/useInvite.ts b/src/hooks/core/useInvite.ts
new file mode 100644
index 0000000..ad27cfc
--- /dev/null
+++ b/src/hooks/core/useInvite.ts
@@ -0,0 +1,21 @@
+import { useMutation, UseMutationOptions } from '@tanstack/react-query';
+import apiClient from '../../api/client';
+import { normalizeApiError } from './apiDebug';
+
+/**
+ * Hook to generate a group invite link.
+ * Returns the mutation result which includes the API response data.
+ */
+export const useInvite = (options?: UseMutationOptions) => {
+ return useMutation({
+ ...options,
+ mutationFn: async (groupId: string | number) => {
+ try {
+ const { data } = await apiClient.get(`invite/generate?groupId=${groupId}`);
+ return data;
+ } catch (error) {
+ throw normalizeApiError(error);
+ }
+ },
+ });
+};
diff --git a/src/hooks/core/useUpdate.ts b/src/hooks/core/useUpdate.ts
new file mode 100644
index 0000000..7b4e403
--- /dev/null
+++ b/src/hooks/core/useUpdate.ts
@@ -0,0 +1,38 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
+import apiClient from '../../api/client';
+import {
+ normalizeApiError,
+ resolveMutationRequest,
+} from './apiDebug';
+
+export const useUpdate = (
+ resource: string,
+ options?: UseMutationOptions
+) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ ...options,
+ mutationFn: async (payload: T) => {
+ const { url, body } = resolveMutationRequest(resource, payload);
+
+ try {
+ const { data } = await apiClient.put(url, body);
+ return data;
+ } catch (error) {
+ throw normalizeApiError(error);
+ }
+ },
+ onSuccess: (data, variables, onMutateResult, context) => {
+ const { url } = resolveMutationRequest(resource, variables);
+ queryClient.invalidateQueries({ queryKey: [url] });
+ if (resource && resource !== url) {
+ queryClient.invalidateQueries({ queryKey: [resource] });
+ }
+ options?.onSuccess?.(data, variables, onMutateResult, context);
+ },
+ onError: (error, variables, onMutateResult, context) => {
+ options?.onError?.(error, variables, onMutateResult, context);
+ },
+ });
+};
diff --git a/src/hooks/ui/useTheme.ts b/src/hooks/ui/useTheme.ts
new file mode 100644
index 0000000..bc28a61
--- /dev/null
+++ b/src/hooks/ui/useTheme.ts
@@ -0,0 +1,7 @@
+import { useTheme as useThemeContext } from '../../theme/ThemeProvider';
+import { Theme } from '../../theme/theme';
+
+export const useTheme = (): Theme => {
+ const { theme } = useThemeContext();
+ return theme;
+};
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index 13ea7cd..de11043 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -24,6 +24,7 @@ import Notification from "../screens/notification/Notification";
import ManagementScreen from "../screens/management/ManagementScreen";
import { navigationRef } from "../utils/navigationService";
import NotificationRedirection from "../screens/NotificationRedirection";
+import PaymentModeScreen from "../screens/payment/PaymentModeScreen";
// import screen from "../components/test";
export type RootStackParamList = {
@@ -40,13 +41,14 @@ export type RootStackParamList = {
BecomeOrg: undefined;
YourContacts: undefined;
Transaction: undefined;
- EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null; onUpdate:()=>void };
+ EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null; onUpdate: () => void };
ManageJoinedGroup: { groupId: string | number };
Invite: { groupId: string | number; code: string | number };
ManagementTabs: { groupId: string | number; }
TransactionList: undefined
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
Notification: undefined;
+ PaymentMode: { groupId: string | number; amount: number; payableData?: any };
NotificationRedirection: { data: any };
Management:
| { actionTab: "My Groups" }
@@ -119,8 +121,9 @@ export default function AppNavigator() {
+
);
}
-;
\ No newline at end of file
+;
diff --git a/src/navigation/BottomTabs.tsx b/src/navigation/BottomTabs.tsx
index 8f4e9e5..288cb3c 100644
--- a/src/navigation/BottomTabs.tsx
+++ b/src/navigation/BottomTabs.tsx
@@ -1,50 +1,22 @@
-import React, { useState, useEffect } from "react";
+import React from "react";
import {
BottomTabNavigationProp,
createBottomTabNavigator,
} from "@react-navigation/bottom-tabs";
import {
View,
- Text,
StyleSheet,
- Platform,
- AccessibilityInfo,
} from "react-native";
-import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
-import GroupScreen from "../screens/GroupScreen";
-import Profile from "../screens/profile/Profile";
-import Home from "../screens/home/Home";
-import colors from "../components/colors";
-import Setting from "../screens/setting/Setting";
-import ManagementScreen from "../screens/management/ManagementScreen";
-import {
- responsiveFontSize,
- responsiveHeight,
-} from "react-native-responsive-dimensions";
-import { useTheme } from "../theme/ThemeProvider";
+import { Home, User, Settings, Users } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { responsiveFontSize } from "react-native-responsive-dimensions";
-const TAB_CONFIG = {
- activeColor: "#000",
- inactiveColor: "#8e8e93",
- backgroundColor: "#fff",
- height: 70,
- iconSize: 28,
-};
-
-const activeTabColors: { [key: string]: string } = {
- Home: colors.pastelPink,
- Profile: colors.pastelBlue,
- Group: colors.pastelGreen,
- Management: colors.pastelOrange,
-};
-
-const activeIconColors: { [key: string]: string } = {
- Home: "#333",
- Profile: "#333",
- Group: "#333",
- Management: "#333",
-};
+import HomeScreen from "../screens/home/Home";
+import Profile from "../screens/profile/Profile";
+import GroupScreen from "../screens/GroupScreen";
+import ManagementScreen from "../screens/management/ManagementScreen";
+import { useTheme } from "../hooks/ui/useTheme";
+import FontsFamily from "../constents/FontsFamily";
export type RootTabParamList = {
Home: undefined;
@@ -59,87 +31,72 @@ export type TabNavigationProp = BottomTabNavigationProp;
const Tab = createBottomTabNavigator();
const BottomTabs: React.FC = () => {
- const { theme } = useTheme();
+ const theme = useTheme();
const insets = useSafeAreaInsets();
- const [isReduceMotionEnabled, setIsReduceMotionEnabled] =
- useState(false);
- const [isScreenReaderEnabled, setIsScreenReaderEnabled] =
- useState(false);
-
- useEffect(() => {
- AccessibilityInfo.isScreenReaderEnabled().then(setIsScreenReaderEnabled);
- AccessibilityInfo.isReduceMotionEnabled().then(setIsReduceMotionEnabled);
-
- const screenReaderSubscription = AccessibilityInfo.addEventListener(
- "screenReaderChanged",
- setIsScreenReaderEnabled
- );
-
- return () => screenReaderSubscription.remove();
- }, []);
-
return (
({
headerShown: false,
- tabBarStyle: [
- [styles.tabBar,{ backgroundColor: theme.colors.reverseBackground }],
- {
- bottom: insets.bottom + 10
- },
- ],
-
- // Make each tab item occupy the full bar height and center its content
+ tabBarShowLabel: true,
+ tabBarStyle: {
+ position: "absolute",
+ bottom: Math.max(insets.bottom, 8),
+ left: 12,
+ right: 12,
+ height: 64,
+ borderRadius: 18,
+ backgroundColor: theme.colors.surface,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ borderTopWidth: 0,
+ elevation: 8,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.12,
+ shadowRadius: 8,
+ paddingBottom: 0,
+ },
tabBarItemStyle: {
- height: "100%",
+ height: 64,
justifyContent: "center",
alignItems: "center",
- padding: 0,
- margin: 0,
+ paddingVertical: 6,
},
+ tabBarActiveTintColor: theme.colors.primary,
+ tabBarInactiveTintColor: theme.colors.textMuted,
+ tabBarLabelStyle: {
+ fontFamily: FontsFamily.nunitoBold,
+ fontSize: responsiveFontSize(1.2),
+ marginTop: -2,
+ marginBottom: 4,
+ },
+ tabBarIcon: ({ focused, color }) => {
+ const iconColor = focused ? theme.colors.primary : color;
+ const iconSize = 22;
+ let IconComponent: React.FC = Home;
- tabBarLabel: ({ focused, color }) =>
- !focused ? (
- {route.name}
- ) : null,
-
- tabBarIcon: ({ focused }) => {
- let iconName: IoniconsIconName = "home-outline";
-
- if (route.name === "Home")
- iconName = focused ? "home" : "home-outline";
- else if (route.name === "Profile")
- iconName = focused ? "person" : "person-outline";
- else if (route.name === "Management")
- iconName = focused ? "settings" : "settings-outline";
- else if (route.name === "Group")
- iconName = focused ? "people" : "people-outline";
-
- const activeBackgroundColor = theme.colors.background;
- const activeIconColor = activeIconColors[route.name];
+ if (route.name === "Home") IconComponent = Home;
+ else if (route.name === "Profile") IconComponent = User;
+ else if (route.name === "Management") IconComponent = Settings;
+ else if (route.name === "Group") IconComponent = Users;
return (
-
-
+
);
},
})}
>
-
+
@@ -148,47 +105,13 @@ const BottomTabs: React.FC = () => {
};
const styles = StyleSheet.create({
- screen: {
- flex: 1,
+ iconWrapper: {
+ width: 40,
+ height: 28,
justifyContent: "center",
alignItems: "center",
- backgroundColor: "#f5f7fa",
- },
- text: {
- fontSize: responsiveFontSize(1.8),
- fontWeight: "600",
- color: "#111",
- },
- tabBar: {
- position: "absolute",
- left: 20,
- right: 20,
- height: TAB_CONFIG.height,
- borderRadius: 20,
- shadowColor: "#000",
- shadowOpacity: 0.1,
- shadowOffset: { width: 0, height: 5 },
- shadowRadius: 8,
- elevation: 10,
- paddingTop: 0,
- marginHorizontal: 10,
- },
- label: {
- paddingVertical: 5,
- fontSize: responsiveFontSize(1.2),
- },
- iconContainer: {
- width: 60,
- height: 50,
- justifyContent: "center",
- alignItems: "center",
- borderRadius: 15,
- },
- activeIconContainer: {
- elevation: 2,
- shadowColor: "#000",
- shadowOpacity: 0.1,
- shadowRadius: 5,
+ borderRadius: 10,
+ marginBottom: 2,
},
});
diff --git a/src/payment/webView.tsx b/src/payment/webView.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/screens/ContributionScreen.tsx b/src/screens/ContributionScreen.tsx
index b35630a..542e8a5 100644
--- a/src/screens/ContributionScreen.tsx
+++ b/src/screens/ContributionScreen.tsx
@@ -1,15 +1,16 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
Easing,
-} from "react-native";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from "../components/colors";
-import CustomModal from "../components/common/CustomModal";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+} from 'react-native';
+import LucideIcon from '../components/common/LucideIcon';
+import CustomModal from '../components/common/CustomModal';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import FontsFamily from '../constents/FontsFamily';
interface ContributionProps {
data: any;
@@ -24,137 +25,127 @@ const ContributionDetails: React.FC = ({
onClose,
onConfirm,
}) => {
+ const theme = useTheme();
+
// Animations
const fadeAnim = useRef(new Animated.Value(0)).current;
- const slideAnim = useRef(new Animated.Value(50)).current;
- const scaleAnim = useRef(new Animated.Value(0.9)).current;
+ const slideAnim = useRef(new Animated.Value(20)).current;
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
- duration: 600,
+ duration: 400,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
- duration: 600,
- easing: Easing.out(Easing.exp),
- useNativeDriver: true,
- }),
- Animated.spring(scaleAnim, {
- toValue: 1,
- friction: 6,
+ duration: 400,
+ easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start();
} else {
fadeAnim.setValue(0);
- slideAnim.setValue(50);
- scaleAnim.setValue(0.9);
+ slideAnim.setValue(20);
}
}, [visible]);
return (
-
- {/* Header */}
-
-
- Contribution Summary
-
+
+
+ {/* Info Card */}
+
+
+
+ ₹{data?.totalPayable || data?.baseAmount}
+
- {/* Contribution Info Card */}
-
- Base Amount
- ₹{data?.baseAmount}
- {
- data?.alreadyPaid ? Already Paid Amount :
-
- Total Payable
- ₹{data?.totalPayable}
- }
+
+
+
+
+
+
+
-
- Due Date
- {data?.dueDate}
-
- Cycle End
- {data?.cycleEnd}
-
-
- {/* Extra Details */}
-
- Cycle Number
- {data?.cycleNumber}
-
- Late Fee Mode
- {data?.lateFeeMode}
-
- Late Fee Value
- ₹{data?.lateFeeValue}
-
- Effective Late Fee
- ₹{data?.effectiveLateFee}
+ {/* Status Card */}
+
+ Cycle {data?.cycleNumber}
+ {data?.alreadyPaid && (
+ Payment Already Completed
+ )}
+
);
};
+const InfoItem = ({ label, value }: { label: string; value: string | number }) => {
+ const theme = useTheme();
+ return (
+
+ {label}
+ {value}
+
+ );
+};
+
export default ContributionDetails;
const styles = StyleSheet.create({
- header: {
- flexDirection: "row",
- alignItems: "center",
- marginBottom: 24,
- },
- headerTitle: {
- fontSize: responsiveFontSize(2.2),
- fontWeight: "700",
- marginLeft: 10,
- },
card: {
- backgroundColor: "#fff",
- padding: 16,
+ padding: 20,
borderRadius: 16,
- marginBottom: 16,
- shadowColor: "#000",
- shadowOpacity: 0.08,
- shadowOffset: { width: 0, height: 3 },
- shadowRadius: 6,
- elevation: 3,
+ borderWidth: 1,
+ marginBottom: 12,
+ },
+ headerRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ marginBottom: 20,
+ },
+ amount: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ infoGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 16,
+ },
+ infoItem: {
+ width: '45%',
+ },
+ miniCard: {
+ padding: 16,
+ borderRadius: 12,
+ borderWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
},
label: {
- fontSize: responsiveFontSize(1.3),
- color: "#6b7280",
- marginTop: 6,
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoMedium,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
},
value: {
- fontSize: responsiveFontSize(1.5),
- fontWeight: "500",
- color: "#374151",
- marginBottom: 6,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 2,
},
- valueHighlight: {
- fontSize: responsiveFontSize(1.8),
- fontWeight: "700",
- color: colors.black,
- marginBottom: 10,
+ statusText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoBold,
},
});
diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx
index 229aeac..9603a48 100644
--- a/src/screens/GroupCreation.tsx
+++ b/src/screens/GroupCreation.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState } from 'react';
import {
View,
Text,
@@ -7,37 +7,64 @@ import {
KeyboardAvoidingView,
Platform,
TouchableOpacity,
- Alert,
- Image,
ToastAndroid,
-} from "react-native";
-import { useNavigation } from "@react-navigation/native";
-import { CustomInput } from "../components/common/Custom";
-import CustomButton from "../components/CustomButton";
-import colors from "../components/colors";
-import { launchImageLibrary } from "react-native-image-picker";
-import Ionicons from "@react-native-vector-icons/ionicons";
-import { UPLOAD_GROUP_IMAGE } from "../utils/ApiUrls";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { CustomInput, CustomButton } from '../components/common/Custom';
+import { useTheme } from '../hooks/ui/useTheme';
+import FontsFamily from '../constents/FontsFamily';
+import AppView from '../components/AppView';
+
+type GroupCreationPayload = {
+ groupName: string;
+ description: string;
+ minParticipants: number;
+ maxParticipants: number;
+ contributionAmount: number;
+ cycleType: string;
+ customCycleDays: number;
+ visibility: 'PUBLIC' | 'PRIVATE';
+ withdrawalMethod: 'SEQUENTIAL' | 'RANDOM';
+ lateFeeMode: 'DAILY' | 'MONTHLY';
+ lateFeeValue: number;
+ entryFee: number;
+};
+
+type GroupCreationFormState = {
+ groupName: string;
+ description: string;
+ minParticipants: string;
+ maxParticipants: string;
+ contributionAmount: string;
+ cycleType: string;
+ customCycleDays: string;
+ visibility: 'PUBLIC' | 'PRIVATE';
+ withdrawalMethod: 'SEQUENTIAL' | 'RANDOM';
+ lateFeeMode: 'DAILY' | 'MONTHLY';
+ lateFeeValue: string;
+ entryFee: string;
+};
export default function GroupCreationForm() {
const navigation = useNavigation();
- const [form, setForm] = useState({
- groupName: "",
- description: "",
- visibility: "PRIVATE",
- withdrawalMethod: "RANDOM",
- minParticipants: "",
- maxParticipants: "",
- contributionAmount: "",
- cycleType: "MONTHLY",
- customCycleDays: "",
- lateFeeMode: "DAILY",
- lateFeeValue: "",
+ const theme = useTheme();
+ const [form, setForm] = useState({
+ groupName: '',
+ description: '',
+ visibility: 'PUBLIC',
+ withdrawalMethod: 'SEQUENTIAL',
+ minParticipants: '',
+ maxParticipants: '',
+ contributionAmount: '',
+ cycleType: 'MONTHLY',
+ customCycleDays: '',
+ lateFeeMode: 'DAILY',
+ lateFeeValue: '',
+ entryFee: '0',
});
const [errors, setErrors] = useState({});
- const [selectedImage, setSelectedImage] = useState();
const handleChange = (key: string, value: string) => {
setForm({ ...form, [key]: value });
@@ -46,277 +73,351 @@ export default function GroupCreationForm() {
}
};
- // ---- Custom logic for Payment Cycle ----
- const cycleTypes = [
- "CUSTOM",
- "DAILY",
- "WEEKLY",
- "MONTHLY",
- "QUARTERLY",
- "HALF_YEARLY",
- "YEARLY",
- ];
- const distributionTypes = ["SEQUENTIAL", "RANDOM"];
- const lateFeeModes = ["DAILY", "MONTHLY"];
+ const distributionTypes = ['SEQUENTIAL', 'RANDOM'];
+ const cycleTypes = ['CUSTOM', 'DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'HALF_YEARLY', 'YEARLY'];
+ const lateFeeModes = ['DAILY', 'MONTHLY'];
- // The core: handle payment cycle options
const handleCycleChange = (val: string) => {
let newForm = { ...form, cycleType: val };
- if (val === "DAILY") {
- newForm = { ...newForm, cycleType: "CUSTOM", customCycleDays: "1" };
- } else if (val === "WEEKLY") {
- newForm = { ...newForm, cycleType: "CUSTOM", customCycleDays: "7" };
- } else if (
- val !== "CUSTOM"
- ) {
- newForm = { ...newForm, customCycleDays: "" }; // Clear custom days
+ if (val === 'DAILY') {
+ newForm = { ...newForm, cycleType: 'CUSTOM', customCycleDays: '1' };
+ } else if (val === 'WEEKLY') {
+ newForm = { ...newForm, cycleType: 'CUSTOM', customCycleDays: '7' };
+ } else if (val !== 'CUSTOM') {
+ newForm = { ...newForm, customCycleDays: '' };
}
setForm(newForm);
- if (errors["customCycleDays"]) {
+ if (errors.customCycleDays) {
setErrors({ ...errors, customCycleDays: null });
}
};
const validateForm = () => {
const newErrors: any = {};
- if (!form.groupName.trim()) newErrors.groupName = "Group name is required.";
- if (!form.description.trim()) newErrors.description = "Description is required.";
- if (!form.minParticipants || Number(form.minParticipants) <= 0) newErrors.minParticipants = "Min participants must be at least 1.";
- if (!form.maxParticipants || Number(form.maxParticipants) < Number(form.minParticipants)) newErrors.maxParticipants = "Max must be greater than or equal to min.";
- if (!form.contributionAmount || Number(form.contributionAmount) <= 0) newErrors.contributionAmount = "Amount must be greater than 0.";
- if (form.cycleType === "CUSTOM" && (!form.customCycleDays || Number(form.customCycleDays) <= 0)) newErrors.customCycleDays = "Custom days must be greater than 0.";
- if (!form.lateFeeMode && (!form.lateFeeValue || Number(form.lateFeeValue) <= 0)) newErrors.lateFeeValue = "Late fee value is required.";
+ const entryFeeValue = form.entryFee.trim() === '' ? 0 : Number(form.entryFee);
+ if (!form.groupName.trim()) newErrors.groupName = 'Group name is required.';
+ if (!form.description.trim()) newErrors.description = 'Description is required.';
+ if (!form.minParticipants || Number(form.minParticipants) <= 0)
+ newErrors.minParticipants = 'Min participants must be at least 1.';
+ if (!form.maxParticipants || Number(form.maxParticipants) < Number(form.minParticipants))
+ newErrors.maxParticipants = 'Max must be >= min.';
+ if (!form.contributionAmount || Number(form.contributionAmount) <= 0)
+ newErrors.contributionAmount = 'Amount must be > 0.';
+ if (Number.isNaN(entryFeeValue) || entryFeeValue < 0)
+ newErrors.entryFee = 'Entry fee cannot be negative.';
+ if (form.cycleType === 'CUSTOM' && (!form.customCycleDays || Number(form.customCycleDays) <= 0))
+ newErrors.customCycleDays = 'Custom days must be > 0.';
+ if (!form.lateFeeValue || Number(form.lateFeeValue) <= 0)
+ newErrors.lateFeeValue = 'Late fee is required.';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleReview = () => {
if (validateForm()) {
- const payload = {
- ...form,
- contributionAmount: Number(form.contributionAmount),
- customCycleDays: Number(form.customCycleDays) || 0,
- lateFeeValue: Number(form.lateFeeValue) || 0,
- maxParticipants: Number(form.maxParticipants),
+ const entryFeeValue = form.entryFee.trim() === '' ? 0 : Number(form.entryFee);
+ const payload: GroupCreationPayload = {
+ groupName: form.groupName.trim(),
+ description: form.description.trim(),
minParticipants: Number(form.minParticipants),
+ maxParticipants: Number(form.maxParticipants),
+ contributionAmount: Number(form.contributionAmount),
+ cycleType: form.cycleType,
+ customCycleDays: form.cycleType === 'CUSTOM' ? Number(form.customCycleDays) || 0 : 0,
+ visibility: form.visibility,
+ withdrawalMethod: form.withdrawalMethod,
+ lateFeeMode: form.lateFeeMode,
+ lateFeeValue: Number(form.lateFeeValue) || 0,
+ entryFee: Number.isNaN(entryFeeValue) ? 0 : entryFeeValue,
};
- console.log('review payload',payload)
- navigation.navigate("GroupReview", { form: payload });
+ navigation.navigate('GroupReview', { form: payload });
} else {
- ToastAndroid.show('Please fill out all required fields correctly.', ToastAndroid.SHORT);
+ ToastAndroid.show('Please fill required fields correctly.', ToastAndroid.SHORT);
}
};
return (
-
-
-
- Create New Group
-
- handleChange("groupName", val)}
- icon="people"
- error={errors.groupName}
- />
- handleChange("description", val)}
- multiline
- icon="document-text"
- error={errors.description}
- />
-
-
- handleChange("visibility", val)}
- />
-
-
- handleChange("withdrawalMethod", val)}
- />
-
-
-
-
-
- handleChange("minParticipants", val)}
- keyboardType="numeric"
- icon="person-add"
- error={errors.minParticipants}
- />
-
-
- handleChange("maxParticipants", val)}
- keyboardType="numeric"
- icon="people-circle"
- error={errors.maxParticipants}
- />
-
+
+
+
+
+ Create Circle
+
+ Define your saving group and its rules.
+
-
-
- handleChange("contributionAmount", val)}
- keyboardType="numeric"
- icon="cash"
- error={errors.contributionAmount}
- />
-
-
+ handleChange('groupName', val)}
+ icon="people-outline"
+ error={errors.groupName}
/>
-
- {form.cycleType === "CUSTOM" &&
- form.customCycleDays !== "1" &&
- form.customCycleDays !== "7" && (
- handleChange("customCycleDays", val)}
- keyboardType="numeric"
- icon="calendar"
- error={errors.customCycleDays}
+ handleChange('description', val)}
+ multiline
+ style={styles.textArea}
+ icon="document-text-outline"
+ error={errors.description}
+ />
+
+
+
+
+ handleChange('visibility', val)}
/>
- )}
-
-
-
- handleChange("lateFeeMode", val)}
+
+
+ handleChange('withdrawalMethod', val)}
+ />
+
+
+
+
+
+
+ handleChange('minParticipants', val)}
+ keyboardType="numeric"
+ icon="person-outline"
+ error={errors.minParticipants}
+ />
+
+
+ handleChange('maxParticipants', val)}
+ keyboardType="numeric"
+ icon="people-outline"
+ error={errors.maxParticipants}
+ />
+
+
+
+
+
+ handleChange('contributionAmount', val)}
+ keyboardType="numeric"
+ icon="cash-outline"
+ error={errors.contributionAmount}
/>
-
- handleChange("lateFeeValue", val)}
- keyboardType="numeric"
- icon="alert-circle"
- error={errors.lateFeeValue}
+ handleChange('entryFee', val)}
+ keyboardType="numeric"
+ icon="pricetag-outline"
+ error={errors.entryFee}
+ />
+
+
+
+ {form.cycleType === 'CUSTOM' &&
+ form.customCycleDays !== '1' &&
+ form.customCycleDays !== '7' && (
+ handleChange('customCycleDays', val)}
+ keyboardType="numeric"
+ icon="calendar-outline"
+ error={errors.customCycleDays}
+ />
+ )}
+
+
+
+
+ handleChange('lateFeeMode', val)}
+ />
+
+ handleChange('lateFeeValue', val)}
+ keyboardType="numeric"
+ icon="alert-circle-outline"
+ error={errors.lateFeeValue}
+ />
+
+
+
-
-
-
-
+
+
+
);
}
-// --- Helper Components for a cleaner layout ---
-const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
-
- {title}
- {children}
-
-);
+const Section = ({ title, children }: { title: string; children: React.ReactNode }) => {
+ const theme = useTheme();
+ return (
+
+ {title}
+ {children}
+
+ );
+};
-const SettingRow: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
-
- {label}
- {children}
-
-);
+const SettingRow = ({ label, children }: { label: string; children: React.ReactNode }) => {
+ const theme = useTheme();
+ return (
+
+ {label}
+ {children}
+
+ );
+};
-const ToggleButtons: React.FC<{
+const ToggleButtons = ({
+ options,
+ selectedValue,
+ onSelect,
+ isWrap = false,
+}: {
options: string[];
selectedValue: string;
onSelect: (value: string) => void;
isWrap?: boolean;
-}> = ({ options, selectedValue, onSelect, isWrap = false }) => (
-
- {options.map((option) => (
- onSelect(option)}
- style={[
- styles.toggleButton,
- selectedValue === option && styles.selectedButton,
- isWrap && { flex: 0, paddingHorizontal: 15 },
- ]}
- >
-
- {option.charAt(0) + option.slice(1).toLowerCase()}
-
-
- ))}
-
-);
+}) => {
+ const theme = useTheme();
+ return (
+
+ {options.map((option) => (
+ onSelect(option)}
+ activeOpacity={0.7}
+ style={[
+ styles.toggleButton,
+ { borderColor: theme.colors.border },
+ selectedValue === option && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary },
+ isWrap && { flex: 0, paddingHorizontal: 16 },
+ ]}
+ >
+
+ {option}
+
+
+ ))}
+
+ );
+};
const styles = StyleSheet.create({
- container: { flexGrow: 1, padding: 16, backgroundColor: "#f5f6fa" },
- titleContainer: { marginBottom: 20, paddingHorizontal: 4 },
- title: { fontSize: responsiveFontSize(2.8), fontWeight: "bold", color: "#1F2937" },
+ scrollContent: {
+ paddingBottom: 40,
+ },
+ header: {
+ marginBottom: 24,
+ },
+ title: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 4,
+ },
section: {
- backgroundColor: "#fff",
- borderRadius: 12,
+ borderRadius: 16,
+ borderWidth: 1,
padding: 16,
marginBottom: 16,
- elevation: 1,
- shadowColor: "#4A5568",
- shadowOpacity: 0.06,
- shadowRadius: 10,
},
sectionTitle: {
fontSize: responsiveFontSize(1.8),
- fontWeight: "600",
- color: "#374151",
+ fontFamily: FontsFamily.nunitoBold,
marginBottom: 16,
- borderBottomWidth: 1,
- borderBottomColor: "#F3F4F6",
- paddingBottom: 10,
},
settingRow: {
marginBottom: 16,
},
- label: { fontSize: responsiveFontSize(1.4), fontWeight: "600", marginBottom: 8, color: "#4B5563" },
- toggleContainer: { flexDirection: "row", gap: 10 },
+ label: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ marginBottom: 8,
+ },
+ toggleContainer: {
+ flexDirection: 'row',
+ gap: 8,
+ },
toggleButton: {
flex: 1,
- paddingVertical: 12,
+ paddingVertical: 10,
borderWidth: 1,
- borderColor: "#D1D5DB",
- borderRadius: 8,
- alignItems: "center",
- backgroundColor: "#F9FAFB",
+ borderRadius: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ toggleText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoMedium,
+ textTransform: 'capitalize',
+ },
+ row: {
+ flexDirection: 'row',
+ gap: 16,
+ },
+ flex1: {
+ flex: 1,
+ },
+ textArea: {
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ submitBtn: {
+ marginTop: 8,
},
- selectedButton: { backgroundColor: colors.pastelGreen, borderColor: "#62b59a" },
- toggleButtonText: { color: "#374151", fontWeight: "600", textTransform: "capitalize" },
- selectedButtonText: { color: "#1F2937" },
- participantsRow: { flexDirection: "row", gap: 15 },
- participantInput: { flex: 1 },
});
diff --git a/src/screens/GroupReview.tsx b/src/screens/GroupReview.tsx
index a5edce7..6e08035 100644
--- a/src/screens/GroupReview.tsx
+++ b/src/screens/GroupReview.tsx
@@ -1,20 +1,37 @@
-import React, { useState } from "react";
-import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, ActivityIndicator } from "react-native";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import AppView from "../components/AppView";
-import { apiCall } from "../utils/ApiHelper";
-import { GROUP_CREATION_URL } from "../utils/ApiUrls";
-import colors from "../components/colors";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, ScrollView, Modal } from 'react-native';
+import LucideIcon from '../components/common/LucideIcon';
+import AppView from '../components/AppView';
+import { GROUP_CREATION_URL } from '../utils/ApiUrls';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useCreate } from '../hooks/core/useCreate';
+import { CustomButton } from '../components/common/Custom';
+import FontsFamily from '../constents/FontsFamily';
+
+type GroupCreationPayload = {
+ groupName: string;
+ description: string;
+ minParticipants: number;
+ maxParticipants: number;
+ contributionAmount: number;
+ cycleType: string;
+ customCycleDays: number;
+ visibility: 'PUBLIC' | 'PRIVATE';
+ withdrawalMethod: 'SEQUENTIAL' | 'RANDOM';
+ lateFeeMode: 'DAILY' | 'MONTHLY';
+ lateFeeValue: number;
+ entryFee: number;
+};
// --- Helper: Get Cycle Label ---
const getCycleLabel = (cycleType: string, customCycleDays: string | number) => {
- if (cycleType === "CUSTOM") {
- if (String(customCycleDays) === "1") return "Daily";
- if (String(customCycleDays) === "7") return "Weekly";
+ if (cycleType === 'CUSTOM') {
+ if (String(customCycleDays) === '1') return 'Daily';
+ if (String(customCycleDays) === '7') return 'Weekly';
return `Custom (${customCycleDays} days)`;
}
- return cycleType.charAt(0).toUpperCase() + cycleType.slice(1).toLowerCase().replace("_", " ");
+ return cycleType.charAt(0).toUpperCase() + cycleType.slice(1).toLowerCase().replace('_', ' ');
};
// --- Themed Dialog Component ---
@@ -31,34 +48,37 @@ const ThemedDialog = ({
onClose: () => void;
onNavigate: () => void;
}) => {
+ const theme = useTheme();
if (!visible) return null;
const config = isSuccess
? {
- icon: "checkmark-circle" as const,
- color: "#2ecc71",
- title: "Success!",
- buttonText: "Go Home",
- action: onNavigate,
- }
+ icon: 'checkmark-circle' as const,
+ color: theme.colors.primary,
+ title: 'Request Submitted',
+ buttonText: 'Back to Home',
+ action: onNavigate,
+ }
: {
- icon: "close-circle" as const,
- color: "#e74c3c",
- title: "Oops!",
- buttonText: "Try Again",
- action: onClose,
- };
+ icon: 'close-circle' as const,
+ color: theme.colors.error,
+ title: 'Submission Failed',
+ buttonText: 'Review and Retry',
+ action: onClose,
+ };
return (
-
-
- {config.title}
- {message}
-
- {config.buttonText}
-
+
+
+ {config.title}
+ {message}
+
@@ -66,116 +86,127 @@ const ThemedDialog = ({
};
export default function GroupReview({ route, navigation }: any) {
- const { form } = route.params;
- const [isLoading, setIsLoading] = useState(false);
- const [isSuccess, setIsSuccess] = useState(false);
+ const { form } = route.params as { form: GroupCreationPayload };
+ const theme = useTheme();
const [modalVisible, setModalVisible] = useState(false);
- const [feedbackMessage, setFeedbackMessage] = useState("");
+ const [feedbackMessage, setFeedbackMessage] = useState('');
+ const [isSuccess, setIsSuccess] = useState(false);
+
+ const { mutate: createGroup, isPending: isLoading } = useCreate(GROUP_CREATION_URL);
const handleNavigateHome = () => {
setModalVisible(false);
- navigation.reset({ index: 0, routes: [{ name: "Home" }] });
+ navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
};
- const handleSubmit = async () => {
- setIsLoading(true);
- try {
- const res = (await apiCall(GROUP_CREATION_URL, "POST", form)) as {
- statusCode?: number;
- message?: string;
- };
- if (res?.statusCode === 201) {
+ const handleSubmit = () => {
+ const payload: GroupCreationPayload = {
+ groupName: String(form.groupName || '').trim(),
+ description: String(form.description || '').trim(),
+ minParticipants: Number(form.minParticipants) || 0,
+ maxParticipants: Number(form.maxParticipants) || 0,
+ contributionAmount: Number(form.contributionAmount) || 0,
+ cycleType: String(form.cycleType || 'MONTHLY'),
+ customCycleDays: String(form.cycleType || '').toUpperCase() === 'CUSTOM'
+ ? Number(form.customCycleDays) || 0
+ : 0,
+ visibility: (String(form.visibility || 'PUBLIC').toUpperCase() === 'PRIVATE' ? 'PRIVATE' : 'PUBLIC'),
+ withdrawalMethod: (String(form.withdrawalMethod || 'SEQUENTIAL').toUpperCase() === 'RANDOM' ? 'RANDOM' : 'SEQUENTIAL'),
+ lateFeeMode: (String(form.lateFeeMode || 'DAILY').toUpperCase() === 'MONTHLY' ? 'MONTHLY' : 'DAILY'),
+ lateFeeValue: Number(form.lateFeeValue) || 0,
+ entryFee: Number(form.entryFee) || 0,
+ };
+
+ createGroup(payload, {
+ onSuccess: (res) => {
setIsSuccess(true);
- setFeedbackMessage(res.message || "Your group has been created successfully.");
- } else {
- throw new Error(res?.message || "Failed to create the group.");
- }
- } catch (error: any) {
- setIsSuccess(false);
- setFeedbackMessage(error.message || "An unexpected error occurred.");
- } finally {
- setIsLoading(false);
- setModalVisible(true);
- }
+ setFeedbackMessage(res.message || 'Your circle request was submitted for admin approval.');
+ setModalVisible(true);
+ },
+ onError: (error) => {
+ setIsSuccess(false);
+ setFeedbackMessage(error.message || 'Failed to submit your request. Please try again.');
+ setModalVisible(true);
+ },
+ });
};
- const TimelineSection: React.FC<{ icon: any; title: string; children: React.ReactNode }> = ({
- icon,
- title,
- children,
- }) => (
+ const TimelineSection = ({ icon, title, children }: { icon: any; title: string; children: React.ReactNode }) => (
-
-
+
+
-
+
- {title}
- {children}
+ {title}
+ {children}
);
const renderRow = (label: string, value: string | number | null) => {
- if (value === null || value === undefined || value === "") return null;
+ if (value === null || value === undefined || value === '') return null;
return (
-
- {label}
- {value}
+
+ {label}
+ {value}
);
};
return (
-
-
+
+
- {form.groupName || "Group Review"}
- Confirm the details to launch your group.
+ {form.groupName || 'Circle Review'}
+
+ Review all details before submitting for admin approval.
+
- {renderRow("Name", form.groupName)}
+ {renderRow('Name', form.groupName)}
{form.description ? (
- Description
- {form.description}
+ Description
+ {form.description}
) : null}
-
- {renderRow("Visibility", form.visibility)}
- {renderRow("Distribution", form.withdrawalMethod)}
- {renderRow("Min Members", form.minParticipants)}
- {renderRow("Max Members", form.maxParticipants)}
+
+ {renderRow('Visibility', form.visibility)}
+ {renderRow('Distribution', form.withdrawalMethod)}
+ {renderRow('Min Members', form.minParticipants)}
+ {renderRow('Max Members', form.maxParticipants)}
-
- {renderRow("Contribution", `₹${form.contributionAmount}`)}
- {renderRow("Cycle", getCycleLabel(form.cycleType, form.customCycleDays))}
- {/* Show days only if neither Daily nor Weekly */}
- {form.cycleType === "CUSTOM" && !["1", "7"].includes(String(form.customCycleDays))
- && renderRow("Cycle Days", `${form.customCycleDays} days`)}
+
+ {renderRow('Contribution', `₹${form.contributionAmount}`)}
+ {renderRow('Entry Fee', `₹${form.entryFee}`)}
+ {renderRow('Cycle', getCycleLabel(form.cycleType, form.customCycleDays))}
+ {form.cycleType === 'CUSTOM' && !['1', '7'].includes(String(form.customCycleDays)) &&
+ renderRow('Cycle Days', `${form.customCycleDays} days`)}
-
- {renderRow("Fee Mode", form.lateFeeMode)}
- {renderRow("Fee Value", `₹${form.lateFeeValue}`)}
+
+ {renderRow('Fee Mode', form.lateFeeMode)}
+ {renderRow('Fee Value', `₹${form.lateFeeValue}`)}
-
- {isLoading ? (
-
- ) : (
- <>
-
- Create Group
- >
- )}
-
+
void; onView: (group: Group) => void; isGrid?: boolean; }) => {
- const isFull = item.currentParticipants >= item.maxParticipants;
- const cardStyle = isGrid ? styles.gridCard : styles.listCard;
- const [loading, setLoading] = useState(false)
- const navigation = useNavigation()
+const PUBLIC_GROUPS_PARAMS = {
+ page: 0,
+ size: 10,
+};
+const getGroupsFromResponse = (response: any): Group[] => {
+ if (!response) return [];
+ if (Array.isArray(response?.data?.content)) return response.data.content;
+ if (Array.isArray(response?.data)) return response.data;
+ return [];
+};
+
+// --- UI Components ---
+const GroupCard = ({
+ item,
+ onJoin,
+ isJoined,
+ isJoining
+}: {
+ item: Group;
+ onJoin: () => void;
+ isJoined: boolean;
+ isJoining: boolean;
+}) => {
+ const theme = useTheme();
+ const isFull = item.currentParticipants >= item.maxParticipants;
+ const isPending = Boolean(item.joinRequestPending);
+ const canJoin = !isJoined && !isPending && !isFull;
+ console.log("item>>>>>>>>>>>>>>", item);
return (
-
- {/* navigation.navigate("GroupDetails" as never)}> */}
-
-
- {item.groupName}
- {item.description}
+
+
+
+
+ {item.groupName}
+
+
+ {item.description}
+
-
- {item.currentParticipants}/{item.maxParticipants} Members
- {item.cycleType}
-
- {/* */}
-
- {/* onView(item)}>
- View
- */}
- {
- setLoading(true);
- await onJoin(item.id);
- setLoading(false);
- }}
- disabled={isFull || loading} // Also disable button while loading
- >
- {loading ? : {isFull ? "Full" : "Join"}}
-
+
+
+
+
+
+
+ {item.currentParticipants}/{item.maxParticipants}
+
+
+
+
+
+ {item.cycleType === 'CUSTOM' && item.customCycleDays
+ ? (item.customCycleDays === 1 ? 'Daily' : item.customCycleDays === 7 ? 'Weekly' : `${item.customCycleDays} Days`)
+ : item.cycleType}
+
+
+ {item.entryFee && item.entryFee > 0 ? (
+
+
+
+ ₹{item.entryFee}
+
+
+ ) : null}
+
+ {!isJoined && (
+ 0 ? 'Pay & Join' : 'Join Circle')}
+ onPress={onJoin}
+ disabled={!canJoin || isJoining}
+ loading={isJoining}
+ style={styles.joinBtn}
+ type={!canJoin ? 'secondary' : 'primary'}
+ />
+ )}
);
};
-const FeaturedGroupCard = ({ item }: { item: Group }) => {
- return (
-
-
-
- {item.groupName}
- by {item.createdByName}
-
-
- )
-};
-
export default function GroupScreen() {
- const [activeTab, setActiveTab] = useState("Public");
- const [searchQuery, setSearchQuery] = useState("");
- const [isGridView, setIsGridView] = useState(false);
- const navigation = useNavigation>();
+ const theme = useTheme();
+ const navigation = useNavigation();
+ const insets = useSafeAreaInsets();
+ const userRole = useSelector((state: any) => state.user?.role);
+ const [activeTab, setActiveTab] = useState<'Public' | 'Joined'>('Public');
- const [myGroups, setMyGroups] = useState([]);
- const [joinedGroups, setJoinedGroups] = useState([]);
- const [publicGroups, setPublicGroups] = useState([]);
- const [featuredGroups, setFeaturedGroups] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [role, setRole] = useState(null);
- const [refreshing, setRefreshing] = useState(false)
- const [alertInfo, setAlertInfo] = useState({
- visible: false,
- title: '',
- message: '',
- buttons: [] as { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[],
- alertType: 'info' as 'info' | 'success' | 'error',
+ const [searchQuery, setSearchQuery] = useState('');
+ const [joiningGroupId, setJoiningGroupId] = useState(null);
+
+ // Data Fetching
+ const { data: publicGroupsRes, isLoading: publicLoading, refetch: refetchPublic } = useFetch(
+ PUBLIC_GROUPS_URL,
+ PUBLIC_GROUPS_PARAMS
+ );
+ const { data: joinedGroupsRes, isLoading: joinedLoading, refetch: refetchJoined } = useFetch(GET_JOINED_GROUP_URL);
+ const { data: userStatusRes } = useFetch(USER_STATUS);
+ const { mutate: joinGroup, isPending: isJoining } = useCreate('');
+
+ const role = String(userStatusRes?.data?.role || userRole || '').trim().toUpperCase();
+ const isOrganizer = role === 'ORGANIZER';
+
+ const publicGroups = useMemo(() => {
+ const groups = getGroupsFromResponse(publicGroupsRes);
+ return groups.filter((g) => {
+ const visibility = String(g.visibility || "").toUpperCase();
+ const status = String(g.status || "").toUpperCase();
+ return visibility === "PUBLIC" && status === "APPROVED";
});
+ }, [publicGroupsRes]);
- const fetchGroups = async () => {
- if (loading) return;
- setLoading(true);
- setError(null);
- try {
- const res = await apiCall(PUBLIC_GROUPS_URL, "GET") as { data?: Group[] };
- if (res && res.data) {
- setPublicGroups(res.data);
- setFeaturedGroups(res.data.slice(0, 5));
- } else {
- throw new Error("No data received");
- }
- } catch (e) {
- setError("Failed to fetch groups. Please try again.");
- } finally {
- setLoading(false);
- }
- };
+ const joinedGroups = useMemo(() => getGroupsFromResponse(joinedGroupsRes), [joinedGroupsRes]);
-
- const fetchJoinedGroups = async () => {
- if (loading) return;
- setLoading(true);
- setError(null);
- try {
- console.log("first-????-")
- const res = await apiCall(GET_JOINED_GROUP_URL, "GET") as { data?: Group[] };
- console.log("first---", res)
- if (res && res.data) {
- setJoinedGroups(res.data);
-
- } else {
- throw new Error("No data received");
- }
- } catch (e) {
- setError("Failed to fetch groups. Please try again.");
- } finally {
- setLoading(false);
- }
- };
-
- useFocusEffect(React.useCallback(() => {
- fetchGroups()
- fetchJoinedGroups()
- }, []));
-
- const filterData = (data: Group[]) => {
+ const filteredData = useMemo(() => {
+ const data = activeTab === 'Public' ? publicGroups : joinedGroups;
if (!searchQuery.trim()) return data;
- return data.filter(g =>
- g.groupName.toLowerCase().includes(searchQuery.toLowerCase())
- );
- };
+ return data.filter((g: Group) => String(g.groupName || "").toLowerCase().includes(searchQuery.toLowerCase()));
+ }, [activeTab, joinedGroups, publicGroups, searchQuery]);
- const handleJoinGroup = async (groupId: string, userId: string) => {
- console.log("Joining group:", groupId, "User:", userId);
- try {
- const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`;
- console.log("Joining group URL:", url);
- const res = await apiCall(url, "POST") as any;
+ const [refreshing, setRefreshing] = useState(false);
+ const onRefresh = useCallback(() => {
+ setRefreshing(true);
+ const promises = [refetchPublic(), refetchJoined()];
+ Promise.all(promises).finally(() => setRefreshing(false));
+ }, [refetchPublic, refetchJoined]);
- console.log("JOIN_GROUP_URL response:", res);
- if (res.statusCode === 200) {
- setAlertInfo({
- visible: true,
- title: 'Success',
- message: 'Your request to join the group has been sent.',
- buttons: [
- { text: 'OK', onPress: () => setAlertInfo({ ...alertInfo, visible: false }), buttonColor: "#28a745", textColor: "#fff" }
- ],
- alertType: 'success',
- });
- } else {
- ToastAndroid.show(res.message || "Failed to join group.", ToastAndroid.SHORT);
- }
- } catch (error) {
- console.error("Join Group Error:", error);
- }
- };
-
- const userStatus = async () => {
- try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
-
- return;
- }
- const token = credentials.password;
-
- setLoading(true);
- setError(null);
- const response = await fetch(USER_STATUS, {
- method: "GET",
- headers: {
- Accept: "*/*",
- Authorization: `Bearer ${token}`,
- },
- });
-
- const json: ApiResponse = await response.json();
-
- console.log("👤 Profile Status Response:", json);
- if (json.status === "success") {
- setRole(json.data.role);
-
- if (json.data.profileStatus === false) {
-
- }
- } else {
- setError(json.message || "Failed to fetch status");
- }
- } catch (err) {
- setError("Something went wrong. Please try again.");
- } finally {
- setLoading(false);
- }
- };
useFocusEffect(
useCallback(() => {
- userStatus();
+ onRefresh();
}, [])
);
-
-
- const handleViewGroup = (group: Group) => {
- // navigation.navigate('GroupDetails', { group });
- ToastAndroid.show(`Viewing details for ${group.groupName}`, ToastAndroid.SHORT);
- };
-
- const onRefresh = () => {
- setRefreshing(true)
- fetchGroups()
- setTimeout(() => {
- setRefreshing(false)
- }, 20000);
- }
-
- const renderContent = () => {
- let data: Group[];
- switch (activeTab) {
- case "My Groups": data = myGroups; break;
- case "Joined": data = joinedGroups; break;
- case "Public": data = publicGroups; break;
- default: data = [];
- }
-
- if (loading && !publicGroups.length) return ;
- if (error) return {error};
-
- const filteredData = filterData(data);
- if (filteredData.length === 0 && !loading) return No groups found.;
-
- return (
- item.id}
- renderItem={({ item }) => (
- {
- console.log('mmmm')
- if (activeTab === 'Public') {
- handleJoinGroup(item.id, 'currentUserId');
- }
- }}
- onView={handleViewGroup}
- isGrid={isGridView}
- />
- )}
- columnWrapperStyle={isGridView ? { justifyContent: 'space-between' } : null}
- contentContainerStyle={{ paddingBottom: 150 }}
- refreshControl={
-
- }
- // ListFooterComponent={}
- />
+ const handleJoin = (groupId: string | number) => {
+ console.log("Hitting here");
+ setJoiningGroupId(groupId);
+ joinGroup(
+ { resource: `${JOIN_GROUP_URL}groups/${String(groupId)}/join/10`, payload: {} } as any,
+ {
+ onSuccess: () => {
+ setJoiningGroupId(null);
+ ToastAndroid.show('Join request sent successfully!', ToastAndroid.SHORT);
+ refetchPublic();
+ },
+ onError: (err) => {
+ setJoiningGroupId(null);
+ ToastAndroid.show(err.message || 'Failed to send request', ToastAndroid.SHORT);
+ },
+ }
);
};
+ const renderSkeletons = () => (
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+ );
+
return (
- {/* Fixed Header */}
-
- Explore Groups
-
-
-
+
+
+ Explore Circles
+
+
+
+
+ {!isOrganizer && (
+
+ {(['Public', 'Joined'] as const).map((tab) => (
+ setActiveTab(tab)}
+ style={[
+ styles.tab,
+ activeTab === tab && { backgroundColor: theme.colors.card, borderRadius: 10, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 }
+ ]}
+ >
+
+ {tab}
+
+
+ ))}
+
+ )}
- {/* Scrollable Content */}
-
-
-
-
-
- {role === "ORGANIZER" ? (["Public"].map((tab) => (
- setActiveTab(tab)}>
- {tab}
-
- ))) : (["Joined", "Public"].map((tab) => (
- setActiveTab(tab)}>
- {tab}
-
- )))}
-
- setIsGridView(!isGridView)} style={styles.layoutToggleButton}>
-
-
-
-
- {/* The list now renders inside a simple View, as FlatList handles its own scroll */}
- {renderContent()}
-
- {role === "ORGANIZER" && (
- } onPress={() => navigation.navigate('GroupCreation')} />
- )}
- String(item.id)}
+ renderItem={({ item }) => (
+ item?.entryFee && item?.entryFee > 0 ? navigation.navigate('PaymentMode' as any, {
+ groupId: item.id,
+ routeFrom: 'GroupScreen',
+ routeData: item
+ }) : handleJoin(item.id)}
+ isJoining={isJoining && joiningGroupId === item.id}
+ />
+ )}
+ contentContainerStyle={styles.listContent}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ !publicLoading && !joinedLoading ? (
+
+
+
+ {searchQuery ? 'No circles match your search' : 'No circles found here'}
+
+
+ ) : renderSkeletons()
+ }
/>
-
+
+ {isOrganizer && (
+ }
+ onPress={() => navigation.navigate('GroupCreation')}
+ />
+ )}
);
}
const styles = StyleSheet.create({
+ topContainer: {
+ paddingHorizontal: 14,
+ paddingTop: 10,
+ paddingBottom: 8,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 14,
+ },
+ title: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ iconButton: {
+ width: 38,
+ height: 38,
+ borderRadius: 12,
+ borderWidth: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ tabs: {
+ flexDirection: 'row',
+ padding: 4,
+ borderRadius: 14,
+ borderWidth: 1,
+ marginTop: 12,
+ marginBottom: 4,
+ },
+ tab: {
+ flex: 1,
+ paddingVertical: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ tabText: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ listContent: {
+ paddingHorizontal: 14,
+ paddingBottom: 86,
+ },
+ card: {
+ borderRadius: 16,
+ borderWidth: 1,
+ padding: 14,
+ marginBottom: 12,
+ },
+ listCard: {
+ width: '100%',
+ },
- headerContainer: { backgroundColor: '#f5f6fa', paddingHorizontal: 16 },
- title: { fontSize: responsiveFontSize(2.2), fontWeight: "600", color: colors.Group },
- searchContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 20, paddingHorizontal: 10, elevation: 5, marginVertical: 20 , borderWidth:2,borderColor:colors.pastelGreen },
- searchIcon: { marginRight: 8 },
- searchInput: { flex: 1, height: 45, fontSize: responsiveFontSize(1.6), color: '#333' },
- sectionTitle: { fontSize: responsiveFontSize(2.0), fontWeight: 'bold', color: colors.Group, marginBottom: 12, paddingHorizontal: 16 },
- featuredList: { paddingLeft: 16, marginBottom: 20 },
- featuredCard: { width: width * 0.45, marginRight: 15, backgroundColor: '#fff', borderRadius: 12, elevation: 3, overflow: 'hidden' },
- featuredImage: { width: '100%', height: 100 },
- featuredContent: { padding: 10 },
- featuredName: { fontSize: responsiveFontSize(1.4), fontWeight: 'bold', color: '#333' },
- featuredCreator: { fontSize: responsiveFontSize(1.2), color: '#777', marginTop: 2 },
- tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 },
- segmentedControl: { flex: 1, flexDirection: 'row', borderRadius: 10, padding: 4, marginRight: 10 },
- segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' },
- segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 5, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 , borderRadius:15 },
- segmentText: { fontSize: responsiveFontSize(1.4), fontWeight: '600', color: '#555' },
- segmentTextActive: { color: '#333' },
- layoutToggleButton: { padding: 8 },
- fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: colors.pastelGreen, borderRadius: 30, elevation: 4 },
- listCard: { backgroundColor: colors.pastelGreen, padding: 16, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5 },
- gridCard: { backgroundColor: colors.pastelGreen, padding: 12, borderRadius: 20, marginBottom: 12,marginHorizontal:15, elevation: 5, width: (width - 48) / 2 },
- groupName: { fontSize: responsiveFontSize(1.6), fontWeight: 'bold', color: '#333', width: '50%' },
- groupDescription: { fontSize: responsiveFontSize(1.4), color: '#666', marginVertical: 4, width: '40%' },
- groupMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 8, marginBottom: 10 },
- groupMetaText: { fontSize: responsiveFontSize(1.2), color: '#888', fontWeight: '500', textTransform: 'capitalize' },
- emptyText: { textAlign: 'center', marginTop: 40, fontSize: responsiveFontSize(1.6) , color: '#888' },
- emptyFeaturedText: { marginLeft: 20, color: '#888' },
- buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
- viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 },
- viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: responsiveFontSize(1.4) },
- joinButton: { flex: 1, backgroundColor: colors.white, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 },
- joinButtonDisabled: { backgroundColor: '#bdc3c7' },
- joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: responsiveFontSize(1.4) },
+ cardHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ },
+ groupInfo: {
+ flex: 1,
+ marginRight: 10,
+ },
+ groupName: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 2,
+ },
+ groupDescription: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ groupAvatar: {
+ width: 48,
+ height: 48,
+ borderRadius: 12,
+ borderWidth: 1,
+ },
+ cardDivider: {
+ height: 1,
+ marginVertical: 10,
+ },
+ cardMeta: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 12,
+ },
+ metaItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ metaText: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ joinBtn: {
+ paddingVertical: 10,
+ marginVertical: 0,
+ borderRadius: 10,
+ },
+
+ skeletonList: {
+ flexDirection: 'column',
+ gap: 16,
+ },
+ emptyState: {
+ alignItems: 'center',
+ marginTop: 64,
+ },
+ emptyText: {
+ marginTop: 16,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ fab: {
+ position: 'absolute',
+ right: 16,
+ borderRadius: 30,
+ width: 56,
+ height: 56,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
});
diff --git a/src/screens/InviteScreen.tsx b/src/screens/InviteScreen.tsx
index e0a2a1a..f2679a5 100644
--- a/src/screens/InviteScreen.tsx
+++ b/src/screens/InviteScreen.tsx
@@ -1,21 +1,24 @@
-import { NativeStackScreenProps } from '@react-navigation/native-stack';
-import React, { useState, useEffect, useCallback, FC } from 'react';
+import React, { useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
- TouchableOpacity,
- Alert,
- ActivityIndicator,
ToastAndroid,
} from 'react-native';
-import { RootStackParamList } from '../navigation/AppNavigator'; // Adjust path
-import { apiCall } from '../utils/ApiHelper'; // Adjust path
-import AppView from '../components/AppView'; // Adjust path
-import colors from '../components/colors'; // Adjust path
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { RootStackParamList } from '../navigation/AppNavigator';
+import { useFetch } from '../hooks/core/useFetch';
+import { useCreate } from '../hooks/core/useCreate';
import { BASE_URL } from '../utils/ApiUrls';
+import AppView from '../components/AppView';
+import { useTheme } from '../hooks/ui/useTheme';
+import { CustomButton } from '../components/common/Custom';
+import SkeletonLoader from '../components/common/SkeletonLoader';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import LucideIcon from '../components/common/LucideIcon';
+import * as Keychain from 'react-native-keychain';
+import FontsFamily from '../constents/FontsFamily';
// --- Type Definitions ---
interface Participant {
@@ -31,179 +34,301 @@ interface GroupDetails {
groupName: string;
description: string;
contributionAmount: number;
+ minParticipants: number;
maxParticipants: number;
- cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
+ entryFee: number;
+ latePaymentCharge: number;
+ lateFeeMode: string;
+ withdrawalMethod: string;
+ cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM' | 'YEARLY' | 'HALF_YEARLY';
+ customCycleDays: number | null;
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
organizerFirstName: string;
organizerLastName: string | null;
+ organizerMobile: string;
+ organizerEmail: string;
totalParticipants: number;
participants: Participant[];
}
-type InviteScreenProps = NativeStackScreenProps; // Assuming 'Invite' is in your stack
+type InviteScreenProps = NativeStackScreenProps;
-// --- Main Screen Component ---
-const InviteScreen: FC = ({ route }) => {
- const { groupId,code} = route.params;
-
- const [groupDetails, setGroupDetails] = useState(null);
- const [loading, setLoading] = useState(true);
- const [joining, setJoining] = useState(false);
- const [error, setError] = useState(null);
-
- const fetchGroupDetails = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const response = await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
- if (response.status === 'success' && response.data) {
- setGroupDetails(response.data);
- } else {
- throw new Error(response.message || 'Failed to fetch group invitation details.');
- }
- } catch (e: any) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- }, [groupId]);
+const InviteScreen: React.FC = ({ route, navigation }) => {
+ const { groupId, code } = route.params;
+ const theme = useTheme();
useEffect(() => {
- fetchGroupDetails();
- }, [fetchGroupDetails]);
+ const checkAuth = async () => {
+ try {
+ const credentials = await Keychain.getGenericPassword();
+ if (!credentials) {
+ navigation.replace('Login');
+ }
+ } catch (error) {
+ console.error('Auth check error:', error);
+ navigation.replace('Login');
+ }
+ };
+ checkAuth();
+ }, [navigation]);
- const handleJoinGroup = async () => {
+ const { data: detailsRes, isLoading, error } = useFetch(
+ `${BASE_URL}fund/group/details?groupId=${groupId}`
+ );
+ console.log(detailsRes, "detailsRes")
+ const { mutate: joinGroup, isPending: isJoining } = useCreate(
+ `${BASE_URL}fund/groups/join/invite/${code}`
+ );
+
+ const groupDetails: GroupDetails | null = detailsRes?.data || null;
+
+ const handleJoin = () => {
if (!groupDetails) return;
-
- setJoining(true);
- try {
- // Replace with your actual API call to join the group
- // For now, it just shows an alert.
- await apiCall(`${BASE_URL}fund/groups/join/invite/${code}`, 'POST');
- ToastAndroid.show(`You have requested to join "${groupDetails.groupName}".`, ToastAndroid.SHORT);
- fetchGroupDetails(); // Refresh details after joining
- } catch (e: any) {
- ToastAndroid.show(e.message || 'An unexpected error occurred.', ToastAndroid.SHORT);
- } finally {
- setJoining(false);
- }
+ joinGroup({}, {
+ onSuccess: () => {
+ ToastAndroid.show(`Join request sent for "${groupDetails.groupName}".`, ToastAndroid.SHORT);
+ navigation.navigate('Home');
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err.message || 'Failed to join circle', ToastAndroid.SHORT);
+ },
+ });
};
- if (loading) return ;
- if (error) return {error};
- if (!groupDetails) return null;
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (error || !groupDetails) {
+ return (
+
+
+
+ Invalid Invitation
+
+ {error instanceof Error ? error.message : 'This invitation link might be expired or invalid.'}
+
+ navigation.navigate('Home')} type="outline" />
+
+
+ );
+ }
const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim();
+ const isJoinable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
return (
-
- You're Invited!
- Join the group {groupDetails.groupName}
+
+
+
+
+
+ You're Invited!
+
+ Join {groupDetails.groupName} and start saving together.
+
+
- {/* Group Info Card */}
-
- Group Details
+
+ Circle Overview
-
+
+
+
+
+
-
+
- {/* Participants Card */}
-
- Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants})
- {groupDetails.participants.length > 0 ? (
- groupDetails.participants.map(p => (
-
- {`${p.firstName} ${p.lastName || ''}`.trim()}
- {p.role === 'ORGANIZER' && Organizer}
-
- ))
- ) : (
- Be the first to join!
- )}
-
-
-
- {/* Floating Join Button */}
-
-
- {joining ? (
-
+
+
+ Members ({groupDetails.totalParticipants}/{groupDetails.maxParticipants})
+
+ {groupDetails.participants.length > 0 ? (
+ groupDetails.participants.map((p) => (
+
+
+
+
+ {p.firstName.charAt(0)}
+
+
+
+ {`${p.firstName} ${p.lastName || ''}`.trim()}
+
+
+ {p.role === 'ORGANIZER' && (
+
+ Organizer
+
+ )}
+
+ ))
) : (
-
- {groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`}
-
+ Be the first to join!
)}
-
-
+
+
+
+
);
};
-// Helper component for displaying rows
-const InfoRow = ({ label, value }: { label: string; value: string | number }) => (
-
- {label}
- {value}
-
-);
+const InfoRow = ({ label, value }: { label: string; value: string | number }) => {
+ const theme = useTheme();
+ return (
+
+ {label}
+ {value}
+
+ );
+};
const styles = StyleSheet.create({
- centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' },
- errorText: { color: '#EF4444', fontSize: responsiveFontSize(1.2), textAlign: 'center', padding: 20 },
- contentContainer: { padding: 16, paddingBottom: 100 }, // Add padding to avoid overlap with button
-
- headerTitle: { fontSize: responsiveFontSize(3.2), fontWeight: 'bold', color: '#111827', textAlign: 'center' },
- headerSubtitle: { fontSize: responsiveFontSize(1.8), color: '#4B5563', textAlign: 'center', marginBottom: 24, marginTop: 4 },
-
- card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
- cardTitle: { fontSize: responsiveFontSize(1.8), fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
- infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
- infoLabel: { fontSize: responsiveFontSize(1.6), color: '#6B7280' },
- infoValue: { fontSize: responsiveFontSize(1.6), fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
-
- participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10 },
- participantName: { fontSize: responsiveFontSize(1.6), color: '#1F2937' },
- organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12 },
- organizerBadgeText: { color: '#92400E', fontSize: responsiveFontSize(1.2), fontWeight: '500' },
- noParticipantsText: { fontSize: responsiveFontSize(1.6), color: '#6B7280', textAlign: 'center', paddingVertical: 10 },
-
- floatingButtonContainer: {
- position: 'absolute',
- bottom: 20,
- left: 20,
- right: 20,
- alignItems: 'center',
+ scrollContent: {
+ paddingBottom: 40,
},
- joinButton: {
- backgroundColor: colors.primary,
- borderRadius: 14,
- paddingVertical: 16,
- paddingHorizontal: 32,
- elevation: 5,
+ header: {
+ alignItems: 'center',
+ marginBottom: 32,
+ marginTop: 16,
+ },
+ inviteBadge: {
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 4,
- width: '80%',
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+ headerTitle: {
+ fontSize: responsiveFontSize(3.2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ headerSubtitle: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ marginTop: 8,
+ paddingHorizontal: 20,
+ lineHeight: 24,
+ },
+ card: {
+ borderRadius: 16,
+ borderWidth: 1,
+ padding: 20,
+ marginBottom: 16,
+ },
+ cardTitle: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 16,
+ },
+ infoRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingVertical: 10,
+ },
+ infoLabel: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ infoValue: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'capitalize',
+ },
+ participantRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ },
+ participantInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ },
+ avatar: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ justifyContent: 'center',
alignItems: 'center',
},
- joinButtonText: {
- color: '#FFFFFF',
- fontSize: responsiveFontSize(1.8),
- fontWeight: 'bold',
+ avatarText: {
+ fontSize: 14,
+ fontFamily: FontsFamily.nunitoBold,
},
- disabledButton: {
- backgroundColor: '#9CA3AF', // Gray color when disabled
+ participantName: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ organizerBadge: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 8,
+ },
+ organizerBadgeText: {
+ fontSize: 10,
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
+ },
+ emptyText: {
+ textAlign: 'center',
+ paddingVertical: 20,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ joinButton: {
+ marginTop: 16,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ },
+ errorTitle: {
+ fontSize: 24,
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 16,
+ },
+ errorSubtitle: {
+ fontSize: 16,
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ marginTop: 8,
+ marginBottom: 32,
},
});
export default InviteScreen;
-
diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx
index e0a618e..dbc8d98 100644
--- a/src/screens/LoginScreen.tsx
+++ b/src/screens/LoginScreen.tsx
@@ -1,328 +1,179 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
- TextInput,
StyleSheet,
- TouchableOpacity,
+ Animated,
KeyboardAvoidingView,
Platform,
- Animated,
- StatusBar,
- ActivityIndicator,
- Dimensions,
} from 'react-native';
-import { LOGIN_URL } from '../utils/ApiUrls';
-import axios from 'axios';
import { useNavigation } from '@react-navigation/native';
-import type { StackNavigationProp } from '@react-navigation/stack';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
import { RootStackParamList } from '../navigation/AppNavigator';
-import LinearGradient from 'react-native-linear-gradient';
-import Ionicons from '@react-native-vector-icons/ionicons';
-import MaterialRipple from 'react-native-material-ripple';
-import CustomAlert from '../components/CostomAlart';
-import colors from '../components/colors';
-import {
- responsiveHeight,
- responsiveWidth,
- responsiveFontSize
-} from "react-native-responsive-dimensions";
-import { useTheme } from '../theme/ThemeProvider';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useFetch } from '../hooks/core/useFetch';
+import { LOGIN_URL } from '../utils/ApiUrls';
+import { CustomInput, CustomButton } from '../components/common/Custom';
+import AppView from '../components/AppView';
import FontsFamily from '../constents/FontsFamily';
-
-
-const { width } = Dimensions.get('window');
-const BLOCK_SIZE = width / 3;
-
-
-// Note: You will need to install react-native-material-ripple
-// npm install react-native-material-ripple
-
-
export default function LoginScreen() {
const navigation = useNavigation>();
+ const theme = useTheme();
const [mobile, setMobile] = useState('');
- const [loading, setLoading] = useState(false);
- const [alertInfo, setAlertInfo] = useState({
- visible: false,
- title: '',
- message: '',
- buttons: [] as { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[],
- alertType: 'info' as 'info' | 'success' | 'error',
- });
-
-
- const fade = useRef(new Animated.Value(0)).current;
- const slide = useRef(new Animated.Value(30)).current;
- const ctaScale = useRef(new Animated.Value(1)).current;
- const { theme } = useTheme();
+ const [error, setError] = useState('');
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(20)).current;
useEffect(() => {
Animated.parallel([
- Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }),
- Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }),
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ Animated.spring(slideAnim, {
+ toValue: 0,
+ tension: 20,
+ useNativeDriver: true,
+ }),
]).start();
- }, []);
-
-
- const showAlert = (
- title: string,
- message: string,
- buttons: { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[],
- alertType: 'info' | 'success' | 'error' = 'info',
- ) => {
- setAlertInfo({ visible: true, title, message, buttons, alertType });
- };
-
-
- const hideAlert = () => {
- setAlertInfo({ ...alertInfo, visible: false });
- };
+ }, [fadeAnim, slideAnim]);
+ const { refetch: sendOtp, isFetching: loading } = useFetch(
+ LOGIN_URL,
+ { mobile },
+ {
+ enabled: false,
+ retry: false,
+ }
+ );
const handleContinue = async () => {
- if (mobile.length === 10) {
- setLoading(true);
- try {
- const res = await axios.get(LOGIN_URL, { params: { mobile } });
- console.log("login res -",res)
- if (res.data.statusCode === 200) {
- navigation.navigate('OtpVarify', { mobile });
- } else {
- showAlert('Login Failed', res.data.message || 'Could not send OTP.', [{ text: 'Try Again', onPress: hideAlert }], 'error');
- }
- } catch (error: any) {
- showAlert('An Error Occurred', 'Failed to send OTP. Please check your connection and try again.', [{ text: 'OK', onPress: hideAlert }], 'error');
- } finally {
- setLoading(false);
+ if (!/^\d{10}$/.test(mobile)) {
+ setError('Please enter a 10-digit mobile number');
+ return;
+ }
+
+ setError('');
+ try {
+ const { data } = await sendOtp();
+ if (data?.statusCode === 200) {
+ navigation.navigate('OtpVarify', { mobile });
+ } else {
+ setError(data?.message || 'Failed to send OTP. Please try again.');
}
- } else {
- showAlert('Invalid Number', 'Please enter a valid 10-digit mobile number.', [{ text: 'Got it', onPress: hideAlert }], 'info');
+ } catch (err) {
+ setError('Something went wrong. Please check your connection.');
}
};
-
- const colorsBlocks = [
- theme.colors.logincard,
- theme.colors.logincard1,
- theme.colors.logincard1,
- theme.colors.logincard,
- theme.colors.logincard,
- theme.colors.logincard1,
- theme.colors.logincard1,
- theme.colors.logincard,
- theme.colors.logincard,
- ];
-
-
return (
-
-
-
-
- {/* Funky grid blocks background (Unchanged) */}
-
- {colorsBlocks.map((bgColor, index) => (
-
-
- ))}
-
-
-
+
-
-
-
- CircleCollect
- Welcome Back!
-
- Let's get you signed in to your community.
-
-
+
+
+ CircleCollect
+ Welcome back
+
+ Enter your mobile number to sign in to your community.
+
+
+ {
+ setMobile(text.replace(/\D/g, ''));
+ if (error) setError('');
+ }}
+ keyboardType="phone-pad"
+ maxLength={10}
+ icon="call-outline"
+ error={error}
+ />
-
-
-
-
-
-
-
-
-
-
-
- {loading ? (
-
- ) : (
-
- Continue
-
-
- )}
-
-
-
- Your privacy is our priority. Your information is always secure.
+ />
+
+
+
+
+ Your information is secure and private.
-
-
+
+
-
-
-
-
+
);
}
-
const styles = StyleSheet.create({
container: {
flex: 1,
},
- gridContainer: {
- ...StyleSheet.absoluteFillObject,
- flexDirection: 'row',
- flexWrap: 'wrap',
- },
- gridBlock: {
- width: BLOCK_SIZE,
- height: BLOCK_SIZE,
- margin: 2,
- borderRadius: 16,
- },
- elevatedBlock: {
- shadowColor: '#ffffffff',
- shadowOffset: { width: 1, height: 5 },
- shadowOpacity: 1,
- shadowRadius: 15,
- elevation: 10,
- },
- keyboardAvoidingContainer: {
+ keyboardView: {
flex: 1,
- },
- contentContainer: {
- flex: 1,
- justifyContent: 'space-between',
- padding: 24,
- },
- topSection: {
- flex: 0.8,
justifyContent: 'center',
+ },
+ content: {
+ paddingHorizontal: 12,
+ },
+ headerSection: {
alignItems: 'center',
+ marginBottom: 48,
},
brand: {
- fontSize: responsiveFontSize(3),
- color: colors.primary,
- textAlign: 'center',
- letterSpacing: 2,
- marginBottom: 16,
- textShadowColor: 'rgba(0, 0, 0, 0.1)',
- textShadowOffset: { width: 1, height: 1 },
- textShadowRadius: 2,
+ fontSize: responsiveFontSize(2.2),
fontFamily: FontsFamily.nunitoExtraBold,
+ letterSpacing: 1.5,
+ textTransform: 'uppercase',
+ marginBottom: 16,
},
title: {
- fontSize: responsiveFontSize(5),
- fontFamily: FontsFamily.nunitoBold,
-
- textAlign: 'center',
- marginBottom: 14,
+ fontSize: responsiveFontSize(3.5),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 8,
},
subtitle: {
fontSize: responsiveFontSize(1.6),
- fontFamily: FontsFamily.nunitoRegular,
+ fontFamily: FontsFamily.nunitoRegular,
textAlign: 'center',
-
- lineHeight: 26,
- maxWidth: 320,
+ paddingHorizontal: 24,
+ lineHeight: 24,
},
- inputWrapper: {
- flexDirection: 'row',
- alignItems: 'center',
- borderRadius: 36,
- borderWidth: 2,
- paddingHorizontal: responsiveHeight(2),
- elevation: 8,
- },
- inputIcon: {
- marginRight: 16,
- },
- input: {
- flex: 1,
- fontSize: responsiveFontSize(1.8),
- fontFamily: FontsFamily.nunitoSemiBold,
- color: colors.textLight,
- height: responsiveHeight(5),
- },
- bottomSection: {
- alignItems: 'center',
- paddingBottom: Platform.OS === 'ios' ? 20 : 10,
+ formSection: {
+ marginBottom: 32,
},
button: {
- width: '100%',
- borderRadius: 30,
- elevation: 14,
- marginTop: 20,
+ marginTop: 16,
},
- buttonGradient: {
-
- borderRadius:42,
- paddingVertical: 14,
- flexDirection: 'row',
- justifyContent: 'center',
+ footerSection: {
alignItems: 'center',
},
- buttonContent: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- buttonText: {
- color: colors.white,
- fontSize: responsiveFontSize(1.8),
- fontFamily: FontsFamily.nunitoBold,
- },
footerText: {
- fontSize: responsiveFontSize(1.4),
- color: 'rgba(0,0,0,0.5)',
- marginTop: 28,
- textAlign: 'center',
- maxWidth: 300,
+ fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
},
-});
\ No newline at end of file
+});
diff --git a/src/screens/NotificationRedirection.tsx b/src/screens/NotificationRedirection.tsx
index 8449698..6342f68 100644
--- a/src/screens/NotificationRedirection.tsx
+++ b/src/screens/NotificationRedirection.tsx
@@ -92,4 +92,4 @@ const styles = StyleSheet.create({
},
});
-export default NotificationRedirection;
\ No newline at end of file
+export default NotificationRedirection;
diff --git a/src/screens/OrgnizerRagister.tsx b/src/screens/OrgnizerRagister.tsx
index 8d9e02c..b71efe9 100644
--- a/src/screens/OrgnizerRagister.tsx
+++ b/src/screens/OrgnizerRagister.tsx
@@ -1,154 +1,181 @@
-import React, { useState } from "react";
+import React, { useState } from 'react';
import {
- View,
- Text,
- TextInput,
- Pressable,
StyleSheet,
- Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
ToastAndroid,
-} from "react-native";
-import axios from "axios";
-import { ORGRAGISTRATION } from "../utils/ApiUrls";
-import * as Keychain from "react-native-keychain";
-import { responsiveFontSize } from "react-native-responsive-dimensions";
+ View,
+ Text,
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useCreate } from '../hooks/core/useCreate';
+import { ORGRAGISTRATION } from '../utils/ApiUrls';
+import { CustomInput, CustomButton } from '../components/common/Custom';
+import FontsFamily from '../constents/FontsFamily';
+import AppView from '../components/AppView';
-const theme = {
- primary: "#111",
- accent: "#FF7A00",
- background: "#F9F9F9",
- text: "#1A1A1A",
- subText: "#6B6B6B",
- outline: "#E5E5E5",
+type OrganizerForm = {
+ bankAccountNumber: string;
+ bankName: string;
+ branchCode: string;
};
+type OrganizerFormErrors = Partial>;
+
+const isValidBankName = (value: string) => /^[A-Za-z][A-Za-z\s.&-]{1,}$/.test(value.trim());
+const isValidAccountNumber = (value: string) => /^\d{9,18}$/.test(value.trim());
+const isValidIfsc = (value: string) => /^[A-Z]{4}0[A-Z0-9]{6}$/.test(value.trim().toUpperCase());
+
export default function OrganizerRegisterScreen() {
- const [bankAccountNumber, setBankAccountNumber] = useState("");
- const [bankName, setBankName] = useState("");
- const [branchCode, setBranchCode] = useState("");
- const [loading, setLoading] = useState(false);
-const handleSubmit = async () => {
- if (!bankAccountNumber || !bankName || !branchCode) {
- ToastAndroid.show('All fields are required.', ToastAndroid.SHORT);
- return;
- }
+ const theme = useTheme();
+ const navigation = useNavigation();
- try {
- setLoading(true);
+ const [form, setForm] = useState({
+ bankAccountNumber: '',
+ bankName: '',
+ branchCode: '',
+ });
+ const [errors, setErrors] = useState({});
- // 🔑 get token from Keychain
- const creds = await Keychain.getGenericPassword();
- const token = creds ? creds.password : null;
+ const { mutate: register, isPending: isLoading } = useCreate(ORGRAGISTRATION);
- if (!token) {
- ToastAndroid.show('No token found. Please log in again.', ToastAndroid.SHORT);
- setLoading(false);
+ const handleChange = (field: keyof OrganizerForm, value: string) => {
+ let nextValue = value;
+ if (field === 'bankAccountNumber') nextValue = value.replace(/\D/g, '');
+ if (field === 'branchCode') nextValue = value.toUpperCase();
+
+ setForm(prev => ({ ...prev, [field]: nextValue }));
+ if (errors[field]) {
+ setErrors(prev => ({ ...prev, [field]: '' }));
+ }
+ };
+
+ const validateForm = () => {
+ const nextErrors: OrganizerFormErrors = {};
+
+ if (!isValidBankName(form.bankName)) {
+ nextErrors.bankName = 'Enter a valid bank name (min 2 letters).';
+ }
+ if (!isValidAccountNumber(form.bankAccountNumber)) {
+ nextErrors.bankAccountNumber = 'Account number must be 9-18 digits.';
+ }
+ if (!isValidIfsc(form.branchCode)) {
+ nextErrors.branchCode = 'Enter a valid IFSC (e.g. HDFC0001234).';
+ }
+
+ setErrors(nextErrors);
+ return Object.keys(nextErrors).length === 0;
+ };
+
+ const handleSubmit = () => {
+ if (!validateForm()) {
+ ToastAndroid.show('Please fix the highlighted fields.', ToastAndroid.SHORT);
return;
}
- const payload = {
- bankAccountNumber,
- bankName,
- branchCode,
- };
-
- const res = await axios.post(
- ORGRAGISTRATION, // your API endpoint
- payload,
- {
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`, // 👈 token added here
- },
- }
- );
-
- console.log("✅ Organizer Registered:", res.data);
- ToastAndroid.show('Organizer registered successfully!', ToastAndroid.SHORT);
- // Optionally, reset form fields
- setBankAccountNumber("");
- setBankName("");
- setBranchCode("");
- } catch (err: any) {
- console.error("❌ Register Error:", err?.response?.data || err.message);
- } finally {
- setLoading(false);
- }
-};
-
+ register(form, {
+ onSuccess: () => {
+ ToastAndroid.show('You are now registered as an Organizer!', ToastAndroid.SHORT);
+ navigation.navigate('Home');
+ },
+ onError: (err) => {
+ ToastAndroid.show(err.message || 'Registration failed', ToastAndroid.SHORT);
+ },
+ });
+ };
return (
-
-
- Organizer Registration 📝
+
+
+
+
+ Organizer Registration
+
+ Provide your banking details to start managing your own circles.
+
+
-
+
+ handleChange('bankName', val)}
+ icon="business-outline"
+ error={errors.bankName}
+ />
-
+ handleChange('bankAccountNumber', val)}
+ keyboardType="numeric"
+ maxLength={18}
+ icon="card-outline"
+ error={errors.bankAccountNumber}
+ />
-
+ handleChange('branchCode', val)}
+ autoCapitalize="characters"
+ maxLength={11}
+ icon="code-working-outline"
+ error={errors.branchCode}
+ />
+
-
- {loading ? "Submitting..." : "Submit"}
-
-
-
+
+
+
+
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
- justifyContent: "center",
- padding: 20,
+ paddingBottom: 40,
+ },
+ header: {
+ marginBottom: 32,
+ marginTop: 16,
},
title: {
- fontSize: responsiveFontSize(2.8),
- fontWeight: "700",
- color: theme.text,
- marginBottom: 20,
- textAlign: "center",
+ fontSize: responsiveFontSize(3.2),
+ fontFamily: FontsFamily.nunitoBold,
},
- input: {
+ subtitle: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 8,
+ lineHeight: 24,
+ },
+ form: {
+ borderRadius: 20,
borderWidth: 1,
- borderColor: theme.outline,
- borderRadius: 12,
- padding: 14,
- marginBottom: 15,
- fontSize: responsiveFontSize(1.6),
- backgroundColor: "#fff",
+ padding: 24,
+ marginBottom: 24,
},
button: {
- backgroundColor: theme.primary,
- paddingVertical: 16,
- borderRadius: 12,
- alignItems: "center",
- },
- buttonText: {
- color: "#fff",
- fontWeight: "600",
- fontSize: responsiveFontSize(1.6),
+ marginTop: 'auto',
},
});
diff --git a/src/screens/OtpVarificationScreen.tsx b/src/screens/OtpVarificationScreen.tsx
index 3398f32..d8380ca 100644
--- a/src/screens/OtpVarificationScreen.tsx
+++ b/src/screens/OtpVarificationScreen.tsx
@@ -1,396 +1,254 @@
-import { CommonActions, RouteProp, useNavigation, useRoute } from "@react-navigation/native";
-import axios from "axios";
-import React, { useEffect, useRef, useState } from "react";
+import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
- Pressable,
Animated,
Platform,
- NativeSyntheticEvent,
- TextInputKeyPressEventData,
KeyboardAvoidingView,
- StatusBar,
- Dimensions,
-} from "react-native";
-import { RootStackParamList } from "../navigation/AppNavigator";
-import * as Keychain from "react-native-keychain";
-import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
-import colors from "../components/colors";
-import CustomAlert from "../components/CostomAlart";
-import { getFcmToken } from "../utils/firebase/firebaseServices";
-import { useTheme } from "../theme/ThemeProvider";
-import FontsFamily from "../constents/FontsFamily";
+ ToastAndroid,
+ TouchableOpacity,
+} from 'react-native';
+import { useNavigation, useRoute, RouteProp, CommonActions } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import * as Keychain from 'react-native-keychain';
+import { RootStackParamList } from '../navigation/AppNavigator';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useCreate } from '../hooks/core/useCreate';
+import { LOGIN_URL, VARIFY_OTP_URL } from '../utils/ApiUrls';
+import { getFcmToken } from '../utils/firebase/firebaseServices';
+import { CustomButton } from '../components/common/Custom';
+import AppView from '../components/AppView';
+import FontsFamily from '../constents/FontsFamily';
const CELL_COUNT = 6;
-const { width } = Dimensions.get('window');
-const BLOCK_SIZE = width / 3;
-type OtpVerifyRouteProp = RouteProp;
-
-interface AlertButton {
- text: string;
- onPress: () => void;
- style?: "cancel" | "destructive" | "default";
-}
+type OtpVerifyRouteProp = RouteProp;
export default function OtpVerificationScreen() {
- const [code, setCode] = useState(Array(CELL_COUNT).fill(""));
- const [timer, setTimer] = useState(30);
- const [isResending, setIsResending] = useState(false);
- const [alertConfig, setAlertConfig] = useState<{
- visible: boolean;
- title: string;
- message: string;
- buttons: AlertButton[];
- alertType: 'error' | 'success' | 'info';
- }>({
- visible: false,
- title: "",
- message: "",
- buttons: [],
- alertType: "info",
- });
- const { theme } = useTheme();
-
- const inputs = useRef>([]);
+ const theme = useTheme();
+ const navigation = useNavigation();
const route = useRoute();
const { mobile } = route.params;
- const navigation = useNavigation();
- // Refs for auto-closing logic
- const successActionRef = useRef<(() => void) | null>(null);
- const successTimerRef = useRef(null);
+ const [code, setCode] = useState(Array(CELL_COUNT).fill(''));
+ const [timer, setTimer] = useState(30);
+ const inputs = useRef>([]);
- const fade = useRef(new Animated.Value(0)).current;
- const slide = useRef(new Animated.Value(30)).current;
- const ctaScale = useRef(new Animated.Value(1)).current;
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(20)).current;
useEffect(() => {
Animated.parallel([
- Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }),
- Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }),
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ Animated.spring(slideAnim, {
+ toValue: 0,
+ tension: 20,
+ useNativeDriver: true,
+ }),
]).start();
+
+ const interval = setInterval(() => {
+ setTimer((prev) => (prev > 0 ? prev - 1 : 0));
+ }, 1000);
+
+ return () => clearInterval(interval);
}, []);
- useEffect(() => {
- if (timer <= 0) return;
- const interval = setInterval(() => setTimer((prev) => prev - 1), 1000);
- return () => clearInterval(interval);
- }, [timer]);
+ const { mutate: verifyOtp, isPending: verifying } = useCreate(VARIFY_OTP_URL, {
+ onSuccess: async (res) => {
+ if (res?.statusCode === 200) {
+ const { jwt, userId } = res?.data || {};
+ await Keychain.setGenericPassword('auth', jwt, {
+ accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED,
+ });
+ await getFcmToken(userId);
- const hideAlert = () => {
- if (successTimerRef.current) {
- clearTimeout(successTimerRef.current);
- successTimerRef.current = null;
- }
- setAlertConfig(prev => ({ ...prev, visible: false }));
- };
-
- const triggerAlert = (
- title: string,
- message: string,
- buttons: AlertButton[],
- alertType: 'error' | 'success' | 'info' = 'info',
- autoCloseAction?: () => void
- ) => {
- setAlertConfig({ visible: true, title, message, buttons, alertType });
-
- if (alertType === 'success' && autoCloseAction) {
- successActionRef.current = autoCloseAction;
- successTimerRef.current = setTimeout(() => {
- successActionRef.current?.();
- }, 2000);
- }
- };
-
- const saveToken = async (token: string) => {
- try {
- await Keychain.setGenericPassword("auth", token, { accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED });
- console.log("✅ Token saved securely");
- } catch (error) {
- console.error("❌ Error saving token:", error);
- }
- };
-
- const onSubmit = async (otp: string, mobile: string) => {
- if (otp.length !== CELL_COUNT) {
- triggerAlert(
- 'Incomplete Code',
- 'Please enter all 6 digits of the OTP.',
- [{ text: 'OK', onPress: hideAlert }],
- 'error'
- );
- return;
- }
- try {
- const res = await axios.post(VARIFY_OTP_URL, { mobile, otp }, {
- headers: { "Content-Type": "application/json", Accept: "application/json" },
- });
-
- if (res.data?.statusCode === 200) {
- const { jwt } = res.data?.data || {};
- console.log("✅ OTP Verified:", res.data);
- await saveToken(jwt);
- await getFcmToken(res.data?.data?.userId);
-
- let hasActionRun = false;
- const successAction = () => {
- if (hasActionRun) return;
- hasActionRun = true;
- hideAlert();
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{ name: 'Home' }],
- })
- );
- };
-
- triggerAlert(
- 'Success!',
- 'OTP verified successfully. Redirecting...',
- [{ text: 'OK', onPress: successAction }],
- 'success',
- successAction
+ ToastAndroid.show('Verified successfully', ToastAndroid.SHORT);
+ navigation.dispatch(
+ CommonActions.reset({
+ index: 0,
+ routes: [{ name: 'Home' }],
+ })
);
+ } else {
+ ToastAndroid.show(res?.message || 'Invalid OTP', ToastAndroid.SHORT);
}
- } catch (err: any) {
- console.error("❌ OTP Verification Error:", err?.response?.data || err.message);
- triggerAlert(
- 'Verification Failed',
- err?.response?.data?.message || "Invalid OTP. Please try again.",
- [{ text: 'Try Again', onPress: hideAlert, style: 'cancel' }],
- 'error'
- );
- }
- };
+ },
+ onError: () => {
+ ToastAndroid.show('Verification failed. Please try again.', ToastAndroid.SHORT);
+ },
+ });
- const resendOtp = async () => {
- try {
- setIsResending(true);
+ const { mutate: resendOtp, isPending: resending } = useCreate(LOGIN_URL, {
+ onSuccess: () => {
setTimer(30);
- await axios.get(LOGIN_URL, { params: { mobile } })
-
- setCode(Array(CELL_COUNT).fill(""));
+ setCode(Array(CELL_COUNT).fill(''));
inputs.current[0]?.focus();
+ ToastAndroid.show('OTP Resent', ToastAndroid.SHORT);
+ },
+ onError: () => {
+ ToastAndroid.show('Failed to resend OTP', ToastAndroid.SHORT);
+ },
+ });
- let hasActionRun = false;
- const successAction = () => {
- if (hasActionRun) return;
- hasActionRun = true;
- hideAlert();
- };
+ const handleTextChange = (text: string, index: number) => {
+ const newCode = [...code];
+ newCode[index] = text.slice(-1);
+ setCode(newCode);
- triggerAlert(
- 'OTP Sent',
- 'A new code has been sent to your mobile number.',
- [{ text: 'OK', onPress: successAction }],
- 'success',
- successAction
- );
- } catch (err: any) {
- console.error("❌ Resend OTP Error:", err?.response?.data || err.message);
- triggerAlert(
- 'Error',
- 'Failed to resend OTP. Try again later.',
- [{ text: 'OK', onPress: hideAlert, style: 'cancel' }],
- 'error'
- );
- } finally {
- setIsResending(false);
+ if (text && index < CELL_COUNT - 1) {
+ inputs.current[index + 1]?.focus();
+ }
+
+ if (newCode.every((cell) => cell !== '')) {
+ verifyOtp({ mobile, otp: newCode.join('') });
}
};
- const submitIfComplete = (arr: string[]) => {
- if (arr.join("").length === CELL_COUNT) {
- onSubmit(arr.join(""), mobile);
+ const handleKeyPress = (e: any, index: number) => {
+ if (e.nativeEvent.key === 'Backspace' && !code[index] && index > 0) {
+ inputs.current[index - 1]?.focus();
}
};
- const updateIndex = (idx: number, val: string) => {
- const v = val.replace(/[^0-9]/g, "");
- const next = [...code];
- next[idx] = v.slice(-1);
- setCode(next);
- if (v && idx < CELL_COUNT - 1) inputs.current[idx + 1]?.focus();
- submitIfComplete(next);
- };
-
- const handleFirstCellChange = (val: string) => {
- const digits = val.replace(/\D/g, "");
- if (digits.length <= 1) {
- updateIndex(0, digits);
- return;
- }
- const next = Array(CELL_COUNT).fill("").map((_, i) => digits[i] ?? "");
- setCode(next);
- const firstEmpty = next.findIndex((c) => !c);
- if (firstEmpty >= 0) {
- inputs.current[firstEmpty]?.focus();
- } else {
- inputs.current[CELL_COUNT - 1]?.blur();
- }
- submitIfComplete(next);
- };
-
- const handleKeyPress = (idx: number, e: NativeSyntheticEvent) => {
- if (e.nativeEvent.key === "Backspace" && !code[idx] && idx > 0) {
- inputs.current[idx - 1]?.focus();
- }
- };
-
- const colorsBlocks = [
- theme.colors.logincard,
- theme.colors.logincard1,
- theme.colors.logincard1,
- theme.colors.logincard,
- theme.colors.logincard,
- theme.colors.logincard1,
- theme.colors.logincard1,
- theme.colors.logincard,
- theme.colors.logincard,
- ];
-
return (
-
-
-
-
- {colorsBlocks.map((bgColor, index) => (
-
- ))}
-
-
-
-
-
-
-
- Verify Your Code
- We’ve sent a 6-digit verification code to +91 {mobile}.
-
-
- {Array.from({ length: CELL_COUNT }).map((_, i) => (
- { inputs.current[i] = r; }}
- // UPDATED CELL STYLE
- style={[
- styles.cell,
- code[i] ? styles.cellFilled : null,
- {
- backgroundColor: theme.colors.optCard,
- color: theme.colors.reverseText,
- }
- ]}
-
- keyboardType="number-pad"
- maxLength={i === 0 ? CELL_COUNT : 1}
- value={code[i]}
- onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))}
- onKeyPress={(e) => handleKeyPress(i, e)}
- placeholder="*"
- placeholderTextColor="#BDBDBD"
- textContentType="oneTimeCode"
- />
- ))}
-
+
+
+
+
+ Verification
+
+ Enter the 6-digit code sent to {'\n'}
+ +91 {mobile}
+
-
- Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()}
- onPressOut={() => Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()}
- onPress={() => onSubmit(code.join(""), mobile)}
- style={[styles.button, { backgroundColor: theme.colors.background }]}
- >
- Verify & Continue
-
+
+ {code.map((digit, index) => (
+ { inputs.current[index] = ref }}
+ style={[
+ styles.otpInput,
+ {
+ backgroundColor: theme.colors.surface,
+ color: theme.colors.textPrimary,
+ borderColor: digit ? theme.colors.primary : theme.colors.border,
+ },
+ ]}
+ keyboardType="number-pad"
+ maxLength={1}
+ value={digit}
+ placeholder="0"
+ placeholderTextColor={theme.colors.textMuted}
+ onChangeText={(text) => handleTextChange(text, index)}
+ onKeyPress={(e) => handleKeyPress(e, index)}
+ selectionColor={theme.colors.primary}
+ />
+ ))}
+
- {timer > 0 ? (
- Resend code in 00:{timer.toString().padStart(2, "0")}
- ) : (
-
-
- {isResending ? "Resending..." : "Resend Code"}
+
+ verifyOtp({ mobile, otp: code.join('') })}
+ loading={verifying}
+ style={styles.button}
+ />
+
+
+ {timer > 0 ? (
+
+ Resend code in 00:{timer.toString().padStart(2, '0')}
-
- )}
-
-
+ ) : (
+ resendOtp({ params: { mobile } })} disabled={resending}>
+
+ {resending ? 'Sending...' : 'Resend Code'}
+
+
+ )}
+
+
+
-
+
);
}
const styles = StyleSheet.create({
- container: { flex: 1 },
- gridContainer: {
- ...StyleSheet.absoluteFillObject,
+ container: {
+ flex: 1,
+ },
+ keyboardView: {
+ flex: 1,
+ justifyContent: 'center',
+ },
+ content: {
+ paddingHorizontal: 12,
+ },
+ headerSection: {
+ alignItems: 'center',
+ marginBottom: 48,
+ },
+ title: {
+ fontSize: responsiveFontSize(3.5),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 12,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ lineHeight: 24,
+ },
+ otpSection: {
flexDirection: 'row',
- flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ marginBottom: 40,
},
- gridBlock: {
- width: BLOCK_SIZE,
- height: BLOCK_SIZE,
- margin: 2,
- borderRadius: 16,
+ otpInput: {
+ width: 48,
+ height: 56,
+ borderRadius: 12,
+ borderWidth: 1.5,
+ textAlign: 'center',
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
},
- elevatedBlock: {
- shadowColor: '#ffffffff',
- shadowOffset: { width: 1, height: 5 },
- shadowOpacity: 1,
- shadowRadius: 15,
- elevation: 10,
+ actionSection: {
+ alignItems: 'center',
},
- keyboardAvoidingContainer: { flex: 1, justifyContent: 'center' },
- contentPanel: {
- marginHorizontal: 24,
- borderRadius: 32,
- padding: 28,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 10 },
- shadowOpacity: 0.14,
- shadowRadius: 24,
- elevation: 12,
+ button: {
+ width: '100%',
+ marginBottom: 24,
},
- topSection: { alignItems: 'center', marginBottom: 24 },
- title: { fontSize: 28, color: colors.textDark, textAlign: 'center', marginBottom: 12 ,fontFamily: FontsFamily.nunitoExtraBold},
- subtitle: { fontSize: 16, color: colors.textLight, textAlign: 'center', marginBottom: 32, lineHeight: 24, maxWidth: 300 ,fontFamily: FontsFamily.nunitoMedium},
-
- otpRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- width: '100%',
- paddingHorizontal: 0, // Removed padding to use full width
+ resendContainer: {
+ alignItems: 'center',
},
-
- // FIX: Updated Cell Styling for clean smooth radius
- cell: {
- width: 45,
- height: 52,
- borderRadius: 14, // Smoother radius
- textAlign: "center",
- fontSize: 20,
- fontWeight: "600",
- elevation: 0, // Removed elevation for flat look (or keep if you want shadow)
- overflow: 'hidden',
+ timerText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
},
- cellFilled: {
- // Optional: Add specific style when filled if needed, handled inline above
+ resendText: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
+ textDecorationLine: 'underline',
},
-
- bottomSection: { paddingBottom: Platform.OS === "ios" ? 20 : 10, paddingTop: 10 },
- button: { backgroundColor: colors.pastelPink, paddingVertical: 18, borderRadius: 30, alignItems: "center", justifyContent: "center",},
- buttonText: { color: colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold },
- footerText: { textAlign: "center", fontFamily: FontsFamily.nunitoBold, fontSize: 14, marginTop: 20 },
});
diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx
index d41038b..5c84e78 100644
--- a/src/screens/SplashScreen.tsx
+++ b/src/screens/SplashScreen.tsx
@@ -1,133 +1,87 @@
-import React, { useEffect, useRef } from "react";
-import { View, Text, StyleSheet, ActivityIndicator, Animated, Alert, ToastAndroid } from "react-native";
-import * as Keychain from "react-native-keychain";
-import { useNavigation } from "@react-navigation/native";
-import { RootStackParamList } from "../navigation/AppNavigator";
-import { PROFILE_URL } from "../utils/ApiUrls";
-import { useDispatch } from "react-redux";
-import { clearUser, setUserData } from "../redux/userSlice";
-import colors from "../components/colors";
-import { StackNavigationProp } from "@react-navigation/stack";
+import React, { useEffect, useRef } from 'react';
+import { View, Text, StyleSheet, ActivityIndicator, Animated } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { useDispatch } from 'react-redux';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { RootStackParamList } from '../navigation/AppNavigator';
+import { PROFILE_URL } from '../utils/ApiUrls';
+import { setUserData, clearUser } from '../redux/userSlice';
+import { useTheme } from '../hooks/ui/useTheme';
+import { useFetch } from '../hooks/core/useFetch';
+import FontsFamily from '../constents/FontsFamily';
const SplashScreen = () => {
const navigation = useNavigation>();
const dispatch = useDispatch();
-
-
- const fadeAnim = useRef(new Animated.Value(1)).current;
+ const theme = useTheme();
+ const fadeAnim = useRef(new Animated.Value(0)).current;
- const fetchUserProfile = async (dispatch: any, navigation: any) => {
- try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- return null;
- }
-
- const token = credentials.password;
- const response = await fetch(PROFILE_URL, {
- method: "GET",
- headers: {
- Accept: "*/*",
- Authorization: `Bearer ${token}`,
- },
- });
-
- const result = await response.json();
-
- if (response.status === 200 && result.status === "success") {
- // Save user data in Redux
- dispatch(
- setUserData({
- id: result.data.id,
- userId: result.data.userId,
- username: result.data.username,
- firstName: result.data.firstName,
- lastName: result.data.lastName,
- gender: result.data.gender,
- mobile: result.data.mobile,
- email: result.data.email,
- address: result.data.address,
- bankAccountNumber: result.data.bankAccountNumber,
- bankName: result.data.bankName,
- branchCode: result.data.branchCode,
- profilePicture: result.data.filePath,
- filePath: result.data.filePath,
- imageBlob: result.data.imageBlob,
- role: result.data.role,
- organizerId: result.data.organizerId,
- organizerStatus: result.data.organizerStatus,
- profileStatus: true,
- })
- );
- return result.data.role;
- } else if (response.status === 401 || result.status === 401) {
- // Unauthorized: reset credentials and navigate to login
- await Keychain.resetGenericPassword();
- dispatch(clearUser());
- ToastAndroid.show(result.message || "Session expired. Please login again.", ToastAndroid.SHORT);
- navigation.replace("Login");
- return null;
- } else {
- console.warn("Unexpected response:", result);
- return null;
- }
- } catch (error) {
- console.error("❌ Error fetching profile:", error);
- ToastAndroid.show("Failed to fetch profile. Please try again.", ToastAndroid.SHORT);
- return null;
- }
- };
+ const { data, isError, isLoading, error } = useFetch(PROFILE_URL, {}, {
+ retry: false,
+ staleTime: 0,
+ });
useEffect(() => {
- const startSplash = async () => {
- const role = await fetchUserProfile(dispatch, navigation);
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ }).start();
+ }, []);
- Animated.timing(fadeAnim, {
- toValue: 0,
- duration: 1500,
- useNativeDriver: true,
- delay: 1500,
- }).start(() => {
- // Navigate based on role
- if (!role) {
- navigation.replace("Login");
- } else if (role === "USER") {
- navigation.replace("Home");
- } else if (role === "ORGANIZER") {
- navigation.replace("Home"); // Replace with Organizer screen if you have one
+ useEffect(() => {
+ if (!isLoading) {
+ const timer = setTimeout(() => {
+ if (data && data.status === 'success') {
+ const userData = data.data;
+ dispatch(
+ setUserData({
+ ...userData,
+ profilePicture: userData.filePath,
+ profileStatus: true,
+ })
+ );
+ navigation.replace('Home');
} else {
- navigation.replace("Login");
+ // If error is 401 or no data (not logged in)
+ dispatch(clearUser());
+ navigation.replace('Login');
}
- });
- };
+ }, 1500); // Minimum splash time
- startSplash();
- }, [navigation, fadeAnim, dispatch, ]);
+ return () => clearTimeout(timer);
+ }
+ }, [isLoading, data, isError]);
return (
-
-
- Circle Collect
-
-
+
+
+ CircleCollect
+ Your Community Finance Hub
+
+
);
};
-export default SplashScreen;
-
const styles = StyleSheet.create({
container: {
flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: "#FFFFFF",
+ justifyContent: 'center',
+ alignItems: 'center',
},
- title: {
- fontSize: 28,
- fontWeight: "800",
- color: colors.Home,
- letterSpacing: 1,
+ brand: {
+ fontSize: responsiveFontSize(4),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ letterSpacing: 2,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 8,
},
});
+
+export default SplashScreen;
diff --git a/src/screens/home/DeclineModal.tsx b/src/screens/home/DeclineModal.tsx
index 93bd75e..12fafe4 100644
--- a/src/screens/home/DeclineModal.tsx
+++ b/src/screens/home/DeclineModal.tsx
@@ -1,74 +1,111 @@
-import React, { useState } from "react";
-import { View, Text, TextInput, Button, Modal, StyleSheet } from "react-native";
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, Modal, TouchableWithoutFeedback, Keyboard } from 'react-native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { CustomInput, CustomButton } from '../../components/common/Custom';
+import FontsFamily from '../../constents/FontsFamily';
-const DeclineModal = ({ visible, onClose, onSubmit }: {
+const DeclineModal = ({
+ visible,
+ onClose,
+ onSubmit,
+}: {
visible: boolean;
onClose: () => void;
onSubmit: (reason: string) => void;
}) => {
- const [reason, setReason] = useState("");
+ const [reason, setReason] = useState('');
+ const theme = useTheme();
+
+ const handleSubmit = () => {
+ if (reason.trim()) {
+ onSubmit(reason);
+ setReason('');
+ }
+ };
return (
-
-
-
- Decline Request
-
-
-
-
+
+
+
+
+
+ Decline Request
+
+ Please provide a reason for declining this join request.
+
+
+
+
+
+
+
+
+
+
-
+
);
};
+export default DeclineModal;
+
const styles = StyleSheet.create({
overlay: {
flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: "rgba(0,0,0,0.5)",
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'rgba(0,0,0,0.4)',
+ padding: 20,
},
container: {
- width: "80%",
- backgroundColor: "white",
- padding: 20,
- borderRadius: 10,
+ width: '100%',
+ maxWidth: 400,
+ padding: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 12,
+ elevation: 5,
},
title: {
- fontSize: 18,
- marginBottom: 10,
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 20,
+ lineHeight: 20,
},
input: {
- borderWidth: 1,
- borderColor: "#ccc",
- borderRadius: 5,
- padding: 8,
- marginBottom: 15,
+ height: 100,
+ textAlignVertical: 'top',
+ marginBottom: 24,
},
actions: {
- flexDirection: "row",
- justifyContent: "space-between",
+ flexDirection: 'row',
+ gap: 12,
+ },
+ button: {
+ flex: 1,
},
});
-
-export default DeclineModal;
diff --git a/src/screens/home/GroupList.tsx b/src/screens/home/GroupList.tsx
index a23fb7d..4ea711d 100644
--- a/src/screens/home/GroupList.tsx
+++ b/src/screens/home/GroupList.tsx
@@ -1,193 +1,241 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useMemo } from 'react';
import {
+ Keyboard,
+ KeyboardAvoidingView,
View,
Text,
StyleSheet,
TouchableOpacity,
+ TouchableWithoutFeedback,
TextInput,
FlatList,
- ActivityIndicator,
LayoutAnimation,
- UIManager,
Platform,
- Alert,
- ToastAndroid,
-} from "react-native";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from "../../components/colors";
-import * as keychain from "react-native-keychain";
-import { BASE_URL } from "../../utils/ApiUrls";
+ UIManager,
+} from 'react-native';
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { MY_GROUPS_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
-// Enable LayoutAnimation for Android
-if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
+if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
-
const GroupCard = ({ item }: { item: any }) => {
const [expanded, setExpanded] = useState(false);
+ const theme = useTheme();
+
const toggleExpand = () => {
- LayoutAnimation.easeInEaseOut();
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setExpanded(!expanded);
};
- const initial = item.groupName ? item.groupName.charAt(0).toUpperCase() : "?";
+
+ const initial = item.groupName ? item.groupName.charAt(0).toUpperCase() : '?';
+
return (
-
-
-
- {initial}
+
+
+
+ {initial}
-
- {item.groupName}
- {item.description}
-
- {item.visibility} • {item.cycleType}
+
+ {item.groupName}
+
+ {item.visibility} • {item.cycleType} • {item.description || 'No description'}
-
-
-
-
+
+
{expanded && (
-
- {Object.entries(item).map(([key, value]) => (
-
- {key}:{" "}
- {Array.isArray(value) ? value.join("-") : String(value)}
-
- ))}
+
+
+ Status
+ {item.status}
+
+
+ Members
+ {item.memberCount || 0}
+
+ {item.baseAmount && (
+
+ Contribution
+ ₹{item.baseAmount}
+
+ )}
)}
);
};
-// ✅ Group List with Search
const GroupList = () => {
- const [groups, setGroups] = useState([]);
- const [filteredGroups, setFilteredGroups] = useState([]);
- const [loading, setLoading] = useState(true);
- const [search, setSearch] = useState("");
+ const theme = useTheme();
+ const [search, setSearch] = useState('');
- useEffect(() => {
- const fetchGroups = async () => {
- const credentials = await keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
- }
- const token = credentials.password;
- try {
- const res = await fetch(`${BASE_URL}fund/getAll/my/groups`, {
- headers: { Accept: "*/*", Authorization: `Bearer ${token}` },
- });
- console.log("my groups response : ", res);
- const data = await res.json();
- setGroups(data.data || []);
- setFilteredGroups(data.data || []);
- } catch (e) {
- console.error("API error", e);
- } finally {
- setLoading(false);
- }
- };
- fetchGroups();
- }, []);
+ const { data, isLoading } = useFetch(MY_GROUPS_URL);
- useEffect(() => {
- if (search.trim() === "") {
- setFilteredGroups(groups);
- } else {
- setFilteredGroups(
- groups.filter((g) =>
- g.groupName.toLowerCase().includes(search.toLowerCase())
- )
- );
- }
- }, [search, groups]);
+ const filteredGroups = useMemo(() => {
+ const groups = data?.data || [];
+ if (!search.trim()) return groups;
+ return groups.filter((g: any) =>
+ g.groupName.toLowerCase().includes(search.toLowerCase())
+ );
+ }, [data?.data, search]);
- if (loading) return ;
+ if (isLoading) {
+ return (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ );
+ }
return (
- <>
-
-
-
-
+
+
+
+
+
+
+ {search.length > 0 && (
+ setSearch('')}>
+
+
+ )}
+
- item.id}
- renderItem={({ item }) => }
- ListEmptyComponent={
- No groups found
- }
- />
- >
+ item.id.toString()}
+ renderItem={({ item }) => }
+ contentContainerStyle={styles.listContent}
+ ListEmptyComponent={
+
+
+
+ {search ? 'No circles match your search' : 'You haven\'t created any circles yet'}
+
+
+ }
+ />
+
+
+
);
};
export default GroupList;
+
const styles = StyleSheet.create({
-
- searchContainer: {
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: colors.white,
+ container: {
+ flex: 1,
+ },
+ loadingContainer: {
+ padding: 16,
+ },
+ searchBar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ height: 48,
borderRadius: 12,
+ borderWidth: 1,
paddingHorizontal: 12,
- marginVertical: 10,
- height: 45,
- shadowColor: "#000",
- shadowOpacity: 0.05,
- shadowRadius: 5,
- elevation: 2,
+ marginHorizontal: 16,
+ marginVertical: 12,
},
- searchInput: { flex: 1, marginLeft: 8, color: colors.textDark },
-
- groupCard: {
- backgroundColor: colors.white,
- padding: 14,
- borderRadius: 14,
+ searchInput: {
+ flex: 1,
+ marginLeft: 8,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ padding: 0,
+ },
+ listContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 24,
+ },
+ card: {
+ borderRadius: 16,
+ borderWidth: 1,
marginBottom: 12,
- shadowColor: "#000",
- shadowOpacity: 0.05,
- shadowRadius: 4,
- elevation: 2,
+ overflow: 'hidden',
},
- groupLeft: { flexDirection: "row", alignItems: "center" },
- groupAvatar: {
- width: 50,
- height: 50,
- borderRadius: 25,
- backgroundColor: colors.pastelPink,
- justifyContent: "center",
- alignItems: "center",
+ cardHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ },
+ avatar: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ justifyContent: 'center',
+ alignItems: 'center',
marginRight: 12,
},
- avatarText: { fontSize: 18, fontWeight: "700", color: colors.black },
- groupName: { fontSize: 16, fontWeight: "600", color: colors.textDark },
- groupDescription: { fontSize: 13, color: colors.textLight, marginTop: 2 },
- groupMeta: { fontSize: 12, color: colors.black, marginTop: 2 },
- detailsBox: {
- marginTop: 10,
- padding: 10,
- borderRadius: 8,
- backgroundColor: "#f9f9f9",
+ avatarText: {
+ fontSize: 18,
+ fontFamily: FontsFamily.nunitoBold,
},
- detailText: { fontSize: 12, color: colors.textDark, marginBottom: 4 },
- emptyText: { textAlign: "center", color: colors.textLight, marginTop: 20 },
- centered: { flex: 1, justifyContent: "center", alignItems: "center" },
- sectionTitle: { fontSize: 18, fontWeight: "600", color: colors.textDark },
-});
\ No newline at end of file
+ headerInfo: {
+ flex: 1,
+ },
+ groupName: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ groupMeta: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
+ },
+ details: {
+ padding: 12,
+ borderTopWidth: 1,
+ gap: 8,
+ },
+ detailRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ detailLabel: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ detailValue: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ emptyState: {
+ alignItems: 'center',
+ marginTop: 64,
+ },
+ emptyText: {
+ marginTop: 12,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ textAlign: 'center',
+ },
+});
diff --git a/src/screens/home/GroupsTabs.tsx b/src/screens/home/GroupsTabs.tsx
index cd2cad7..aa80ef5 100644
--- a/src/screens/home/GroupsTabs.tsx
+++ b/src/screens/home/GroupsTabs.tsx
@@ -1,24 +1,22 @@
-import React, { useState, useEffect } from "react";
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
-} from "react-native";
-import colors from "../../components/colors";
-import GroupList from "./GroupList";
-import RequestList from "./RequestList";
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import GroupList from './GroupList';
+import RequestList from './RequestList';
+import FontsFamily from '../../constents/FontsFamily';
// ✅ Tab Layout
const GroupsTabs = () => {
- const [activeTab, setActiveTab] = useState("MyGroups");
+ const [activeTab, setActiveTab] = useState('MyGroups');
+ const theme = useTheme();
const renderContent = () => {
switch (activeTab) {
- case "MyGroups":
+ case 'MyGroups':
return ;
- case "Requests":
+ case 'Requests':
return ;
default:
return null;
@@ -26,101 +24,59 @@ const GroupsTabs = () => {
};
return (
- <>
-
- {/* Tabs */}
-
- {["MyGroups", "Requests"].map((tab) => (
+
+
+ {['MyGroups', 'Requests'].map((tab) => (
setActiveTab(tab)}
+ activeOpacity={0.7}
>
- {tab === "MyGroups" ? "My Groups" : tab}
+ {tab === 'MyGroups' ? 'My Circles' : 'Join Requests'}
))}
-
- {/* Content */}
- {renderContent()}
-
- >
+
+ {renderContent()}
+
+
);
};
export default GroupsTabs;
const styles = StyleSheet.create({
-
+ container: {
+ flex: 1,
+ },
tabs: {
- flexDirection: "row",
- justifyContent: "space-around",
- marginVertical: 10,
+ flexDirection: 'row',
borderBottomWidth: 1,
- borderBottomColor: "#ddd",
+ marginHorizontal: 16,
},
- tabButton: { paddingVertical: 10, flex: 1, alignItems: "center" },
- tabText: { fontSize: 16, color: colors.textLight },
- activeTabButton: { borderBottomWidth: 2, borderBottomColor: colors.Home },
- activeTabText: { color: colors.Home, fontWeight: "600" },
-
- searchContainer: {
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: colors.white,
- borderRadius: 12,
- paddingHorizontal: 12,
- marginVertical: 10,
- height: 45,
- shadowColor: "#000",
- shadowOpacity: 0.05,
- shadowRadius: 5,
- elevation: 2,
+ tabButton: {
+ paddingVertical: 12,
+ marginRight: 24,
+ alignItems: 'center',
},
- searchInput: { flex: 1, marginLeft: 8, color: colors.textDark },
-
- groupCard: {
- backgroundColor: colors.white,
- padding: 14,
- borderRadius: 14,
- marginBottom: 12,
- shadowColor: "#000",
- shadowOpacity: 0.05,
- shadowRadius: 4,
- elevation: 2,
+ tabText: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
},
- groupLeft: { flexDirection: "row", alignItems: "center" },
- groupAvatar: {
- width: 50,
- height: 50,
- borderRadius: 25,
- backgroundColor: colors.Home + "33",
- justifyContent: "center",
- alignItems: "center",
- marginRight: 12,
+ content: {
+ flex: 1,
+ marginTop: 8,
},
- avatarText: { fontSize: 18, fontWeight: "700", color: colors.Home },
- groupName: { fontSize: 16, fontWeight: "600", color: colors.textDark },
- groupDescription: { fontSize: 13, color: colors.textLight, marginTop: 2 },
- groupMeta: { fontSize: 12, color: colors.Home, marginTop: 2 },
- detailsBox: {
- marginTop: 10,
- padding: 10,
- borderRadius: 8,
- backgroundColor: "#f9f9f9",
- },
- detailText: { fontSize: 12, color: colors.textDark, marginBottom: 4 },
- emptyText: { textAlign: "center", color: colors.textLight, marginTop: 20 },
- centered: { flex: 1, justifyContent: "center", alignItems: "center" },
- sectionTitle: { fontSize: 18, fontWeight: "600", color: colors.textDark },
});
diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx
index 35964a9..dffd64c 100644
--- a/src/screens/home/Home.tsx
+++ b/src/screens/home/Home.tsx
@@ -1,200 +1,117 @@
-import React, { useEffect, useState, useCallback } from "react";
+import React, { useCallback, useMemo } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
- UIManager,
- Platform,
FlatList,
ListRenderItem,
- Alert,
RefreshControl,
- StatusBar,
-} from "react-native";
-import AppView from "../../components/AppView";
-import colors from "../../components/colors";
-import { useDispatch, useSelector } from "react-redux";
-import { useNavigation } from "@react-navigation/native";
-import { DASHBOARD_URL, PROFILE_URL, USER_STATUS } from "../../utils/ApiUrls";
-import { setUserData } from "../../redux/userSlice";
-import { TabNavigationProp } from "../../navigation/BottomTabs";
-import UserProfileStatus from "./UserProfileStatus";
-import AnimatedCard from "../../components/common/AnimatedCard";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import { apiCall } from "../../utils/ApiHelper";
-import CustomAlert, { CustomAlertProps } from "../../components/CostomAlart";
-import { useTheme } from "../../theme/ThemeProvider";
-import FontsFamily from "../../constents/FontsFamily";
-import { responsiveFontSize, responsiveHeight } from "react-native-responsive-dimensions";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
+} from 'react-native';
+import { useDispatch, useSelector } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import LucideIcon from '../../components/common/LucideIcon';
-if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
- UIManager.setLayoutAnimationEnabledExperimental(true);
-}
-
-type UserProfile = {
- id: string;
- userId: number;
- firstName: string | null;
- lastName: string | null;
- imageBlob: string | null;
- role?: string;
- organizerStatus?: string;
- profileStatus?: boolean;
-};
+import AppView from '../../components/AppView';
+import { DASHBOARD_URL, PROFILE_URL, USER_STATUS } from '../../utils/ApiUrls';
+import { setUserData } from '../../redux/userSlice';
+import { TabNavigationProp } from '../../navigation/BottomTabs';
+import UserProfileStatus from './UserProfileStatus';
+import AnimatedCard from '../../components/common/AnimatedCard';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import FontsFamily from '../../constents/FontsFamily';
interface ActionItem {
id: string;
title: string;
action: string;
- tab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
- iconName?: any;
- backOne?: string;
- backTwo?: string;
- subtitle?: string;
+ tab?: 'My Groups' | 'Joined Groups' | 'Requests' | 'Your Requests';
+ iconName: string;
+ subtitle: string;
}
const organizerActions: ActionItem[] = [
- { id: "1", title: "My Groups", subtitle: "Manage groups", action: "NAVIGATE_MANAGEMENT", tab: "My Groups", iconName: "people-circle-outline", backOne: "#decbfcff", backTwo: "#B799FF" },
- { id: "2", title: "Joined Groups", subtitle: "View groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "people-outline", backOne: "#D7E9F7", backTwo: "#82AAE3" },
- { id: "3", title: "Requests", subtitle: "Check requests", action: "NAVIGATE_MANAGEMENT", tab: "Requests", iconName: "notifications-outline", backOne: "#DFF6DD", backTwo: "#88D18A" },
- { id: "4", title: "Create Group", subtitle: "Start new group", action: "NAVIGATE_CREATE_GROUP", iconName: "add-circle-outline", backOne: "#FFF4D2", backTwo: "#FFB344" },
- { id: "5", title: "Your Requests", subtitle: "Track requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", iconName: "mail-open-outline", backOne: "#FFF4D2", backTwo: "#FFB344" },
- { id: "6", title: "Transactions", subtitle: "View history", action: "TRANSACTION", iconName: "receipt-outline", backOne: "#decbfcff", backTwo: "#B799FF" },
+ { id: '1', title: 'My Groups', subtitle: 'Manage your circles', action: 'NAVIGATE_MANAGEMENT', tab: 'My Groups', iconName: 'people-circle-outline' },
+ { id: '2', title: 'Joined Groups', subtitle: 'Circles you belong to', action: 'NAVIGATE_MANAGEMENT', tab: 'Joined Groups', iconName: 'people-outline' },
+ { id: '3', title: 'Requests', subtitle: 'Pending join requests', action: 'NAVIGATE_MANAGEMENT', tab: 'Requests', iconName: 'notifications-outline' },
+ { id: '4', title: 'Create Group', subtitle: 'Start a new circle', action: 'NAVIGATE_CREATE_GROUP', iconName: 'add-circle-outline' },
+ { id: '5', title: 'Your Requests', subtitle: 'Status of your applications', action: 'NAVIGATE_MANAGEMENT', tab: 'Your Requests', iconName: 'mail-open-outline' },
+ { id: '6', title: 'Transactions', subtitle: 'View payment history', action: 'TRANSACTION', iconName: 'receipt-outline' },
];
const userActions: ActionItem[] = [
- { id: "1", title: "Joined Groups", subtitle: "See your groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "people-outline", backOne: "#D7E9F7", backTwo: "#82AAE3" },
- { id: "2", title: "Your Requests", subtitle: "Track requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", iconName: "mail-open-outline", backOne: "#FFF4D2", backTwo: "#FFB344" },
- { id: "3", title: "Transactions", subtitle: "View history", action: "TRANSACTION", iconName: "receipt-outline", backOne: "#decbfcff", backTwo: "#B799FF" },
- { id: "4", title: "Create Group", subtitle: "Start new group", action: "NAVIGATE_CREATE_GROUP", iconName: "add-circle-outline", backOne: "#DFF6DD", backTwo: "#88D18A" },
+ { id: '1', title: 'Joined Groups', subtitle: 'Circles you belong to', action: 'NAVIGATE_MANAGEMENT', tab: 'Joined Groups', iconName: 'people-outline' },
+ { id: '2', title: 'Your Requests', subtitle: 'Status of your applications', action: 'NAVIGATE_MANAGEMENT', tab: 'Your Requests', iconName: 'mail-open-outline' },
+ { id: '3', title: 'Transactions', subtitle: 'View payment history', action: 'TRANSACTION', iconName: 'receipt-outline' },
+ { id: '4', title: 'Create Group', subtitle: 'Start a new circle', action: 'NAVIGATE_CREATE_GROUP', iconName: 'add-circle-outline' },
];
-
const Home: React.FC = () => {
const user = useSelector((state: any) => state.user);
const notifications = useSelector((state: any) => state.notifications.notifications);
const navigation = useNavigation();
const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+ const theme = useTheme();
- const actions = user.role === "ORGANIZER" ? organizerActions : userActions;
- const [apiUser, setApiUser] = useState(null);
- const [alertVisible, setAlertVisible] = useState(false);
- const [alertAction, setAlertAction] = useState(null);
- const [userDashboard, setUserDashboard] = useState(null);
- const [refreshing, setRefreshing] = useState(false);
- const [statusData, setStatusData] = useState(null);
- const insets = useSafeAreaInsets();
- const {theme} = useTheme();
- const getDashboardData = async () => {
- try {
- const result = (await apiCall(DASHBOARD_URL, "GET")) as any;
- if (result.status === "success") {
- console.log("dashboard -----",result)
- setUserDashboard(result.data);
- }
- } catch (error) {
- console.error("❌ Error fetching dashboard data:", error);
+ // API Hooks
+ const { data: profileData, refetch: refetchProfile, isFetching: isFetchingProfile } = useFetch(PROFILE_URL);
+ const { data: dashboardData, refetch: refetchDashboard, isFetching: isFetchingDashboard } = useFetch(DASHBOARD_URL);
+ const { data: statusData, refetch: refetchStatus, isFetching: isFetchingStatus } = useFetch(USER_STATUS);
+
+ // Sync profile with Redux
+ React.useEffect(() => {
+ if (profileData?.status === 'success') {
+ dispatch(setUserData({ ...profileData.data, profileStatus: true }));
}
- };
+ }, [profileData, dispatch]);
- const getUserStatus = async () => {
- const result = await apiCall(USER_STATUS, "GET") as any;
- setStatusData(result.data);
- }
+ const onRefresh = useCallback(() => {
+ refetchProfile();
+ refetchDashboard();
+ refetchStatus();
+ }, [refetchProfile, refetchDashboard, refetchStatus]);
- const fetchUserProfile = async () => {
- try {
- const result = (await apiCall(PROFILE_URL, "GET")) as any;
- console.log("profile data", result);
- if (result.status === "success") {
- setApiUser(result.data);
- dispatch(setUserData({ ...result.data, profileStatus: true }));
- }
- } catch (error) {
- console.error("❌ Error fetching profile:", error);
- }
- };
+ const actions = useMemo(() =>
+ user.role === 'ORGANIZER' ? organizerActions : userActions,
+ [user.role]);
- const fetchData = useCallback(async () => {
- await Promise.all([fetchUserProfile(), getDashboardData(), getUserStatus()]);
- }, []);
-
- const onRefresh = useCallback(async () => {
- setRefreshing(true);
- await fetchData();
- setRefreshing(false);
- }, [fetchData]);
-
- useEffect(() => {
- const unsubscribe = navigation.addListener("focus", fetchData);
- fetchData();
- return unsubscribe;
- }, [navigation, fetchData]);
-
- const handleActionPress = (item: ActionItem) => {
- if (item.action === "NAVIGATE_MANAGEMENT" && item.tab) {
- navigation.navigate("Management", { actionTab: item.tab });
- } else if (item.action === "TRANSACTION") {
- navigation.navigate("TransactionList" as never);
- } else if (item.action === "NAVIGATE_CREATE_GROUP") {
- switch (apiUser?.organizerStatus) {
- case "NOT_APPLIED":
- setAlertAction({
- visible: true,
- title: "Become an Organizer",
- message: "You need to be an organizer to create groups.",
- buttons: [
- {
- text: "Continue",
- onPress: () => {
- setAlertVisible(false);
- navigation.navigate("BecomeOrg" as never);
- },
- },
- { text: "Close", onPress: () => setAlertVisible(false), style: "cancel" },
- ],
- alertType: "error",
- onClose: () => setAlertAction(null),
- });
- setAlertVisible(true);
- break;
- case "PENDING":
- Alert.alert("Pending Approval", "Your organizer request is still pending.");
- break;
- case "APPROVED":
- navigation.navigate("GroupCreation" as never);
- break;
- default:
- Alert.alert("Unknown Status", "Could not determine your organizer status.");
- break;
- }
- }
- };
-
const unreadCount = notifications.filter((item: any) => !item.read).length;
- const renderGridItem: ListRenderItem = ({ item }) => (
- handleActionPress(item)}
- />
- );
+ const handleActionPress = (item: ActionItem) => {
+ if (item.action === 'NAVIGATE_MANAGEMENT' && item.tab) {
+ navigation.navigate('Management', { actionTab: item.tab });
+ } else if (item.action === 'TRANSACTION') {
+ navigation.navigate('TransactionList' as never);
+ } else if (item.action === 'NAVIGATE_CREATE_GROUP') {
+ const organizerStatus = profileData?.data?.organizerStatus || user.organizerStatus;
- const getProfileImageUri = () => {
- const blob = user.imageBlob || apiUser?.imageBlob;
- return blob ? { uri: `data:image/jpeg;base64,${blob}` } : undefined;
+ if (organizerStatus === 'APPROVED') {
+ navigation.navigate('GroupCreation' as never);
+ } else if (organizerStatus === 'PENDING') {
+ navigation.navigate('BecomeOrg' as never); // View status
+ } else {
+ navigation.navigate('BecomeOrg' as never); // Apply now
+ }
+ }
};
+ const renderGridItem: ListRenderItem = ({ item }) => (
+
+ handleActionPress(item)}
+ />
+
+ );
+
return (
-
-
+
{
ListHeaderComponent={
<>
-
-
- Hello,
+
+ Hello,
+
+ {user.firstName || profileData?.data?.firstName || 'User'}
- {user.firstName || apiUser?.firstName || "User"}
- navigation.navigate("Notification" as never)} style={{ marginTop: responsiveHeight(2) ,}}>
+ navigation.navigate('Notification' as never)}
+ style={[styles.iconButton, { backgroundColor: theme.colors.surface }]}
+ >
{unreadCount > 0 && (
-
- {unreadCount}
+
+ {unreadCount}
)}
-
+
-
- Quick Actions
-
+
+ Quick Actions
>
}
- contentContainerStyle={{ paddingBottom: 100 }}
+ contentContainerStyle={{
+ paddingBottom: insets.bottom + 86,
+ paddingHorizontal: 6,
+ }}
refreshControl={
}
/>
-
- {alertAction && (
- setAlertVisible(false)}
- />
- )}
-
+
);
};
@@ -263,41 +174,56 @@ const styles = StyleSheet.create({
flex: 1,
},
header: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingHorizontal: 20,
- paddingTop: 24,
- paddingBottom: 16,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 10,
+ marginTop: 10,
+ marginBottom: 16,
},
- greeting: { fontSize: 22,fontFamily: FontsFamily.nunitoLight },
- sectionTitle: { fontSize: 20, fontWeight: "600", color: colors.textDark, flex: 1 ,fontFamily: FontsFamily.nunitoSemiBold},
- actionHeader: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- marginHorizontal: 20,
- marginBottom: 10,
- marginTop: 16
+ greeting: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoRegular,
},
- payAllButton: {
- paddingHorizontal: 14,
- paddingVertical: 6,
- backgroundColor: colors.primary,
- borderRadius: 8,
+ userName: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ marginTop: -4,
+ },
+ iconButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ sectionHeader: {
+ paddingHorizontal: 10,
+ marginBottom: 12,
+ marginTop: 16,
+ },
+ sectionTitle: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ cardContainer: {
+ flex: 1,
+ padding: 6,
},
- payAllText: { color: "#fff", fontWeight: "bold", fontSize: 14 },
badge: {
- position: "absolute",
- right: -6,
- top: -4,
- backgroundColor: colors.danger,
+ position: 'absolute',
+ right: 8,
+ top: 8,
borderRadius: 8,
- width: 16,
+ minWidth: 16,
height: 16,
- justifyContent: "center",
- alignItems: "center",
+ justifyContent: 'center',
+ alignItems: 'center',
zIndex: 1,
+ paddingHorizontal: 4,
},
- badgeText: { color: colors.white, fontSize: 10, fontWeight: "bold" },
-});
\ No newline at end of file
+ badgeText: {
+ fontSize: 10,
+ fontFamily: FontsFamily.nunitoBold,
+ },
+});
diff --git a/src/screens/home/RequestList.tsx b/src/screens/home/RequestList.tsx
index 1e4f04b..a2558fb 100644
--- a/src/screens/home/RequestList.tsx
+++ b/src/screens/home/RequestList.tsx
@@ -1,23 +1,28 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useMemo } from 'react';
import {
+ Keyboard,
+ KeyboardAvoidingView,
View,
Text,
StyleSheet,
- ScrollView,
+ FlatList,
TouchableOpacity,
- RefreshControl,
- Alert,
- ActivityIndicator,
+ TouchableWithoutFeedback,
TextInput,
- ToastAndroid
+ Alert,
+ ToastAndroid,
+ Platform,
} from 'react-native';
-import { Ionicons } from '@react-native-vector-icons/ionicons';
-import colors from '../../components/colors';
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { useUpdate } from '../../hooks/core/useUpdate';
+import { REQUESTS_URL, APPROVE_REQUEST_URL } from '../../utils/ApiUrls';
import DeclineModal from './DeclineModal';
-import * as keychain from 'react-native-keychain'
-import { BASE_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
-// Define TypeScript interfaces
interface GroupRequest {
requestId: string;
groupName: string;
@@ -28,589 +33,339 @@ interface GroupRequest {
status: string;
}
-interface ApiResponse {
- message: string;
- status: string;
- statusCode: number;
- data: GroupRequest[];
-}
-
-const RequestList: React.FC = () => {
- const [requests, setRequests] = useState([]);
- const [filteredRequests, setFilteredRequests] = useState([]);
- const [refreshing, setRefreshing] = useState(false);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [searchQuery, setSearchQuery] = useState('');
- const [processingRequest, setProcessingRequest] = useState(null);
- const [showDeclineModal, setShowDeclineModal] = useState<{ modal: boolean, requestId: string | null }>({ modal: false, requestId: null });
- const [selectedRequestId, setSelectedRequestId] = useState(null);
-
- // Fetch data from API
- const fetchRequests = async (): Promise => {
-
- const credentials = await keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
- }
- const token = credentials.password;
- try {
- setRefreshing(true);
- setError(null);
-
- const response = await fetch(
- `${BASE_URL}`,
- {
- method: "GET",
- headers: {
- accept: "*/*",
- Authorization: `Bearer ${token}`
- }
- }
- );
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data: ApiResponse = await response.json();
-
- if (data.status === "success") {
- setRequests(data.data);
- setFilteredRequests(data.data);
- } else {
- throw new Error("Failed to fetch requests");
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
- setError(errorMessage);
- console.error("API Error:", error);
- } finally {
- setLoading(false);
- setRefreshing(false);
- }
- };
-
- useEffect(() => {
- fetchRequests();
- }, []);
-
- // Filter requests based on search query
- useEffect(() => {
- if (searchQuery.trim() === '') {
- setFilteredRequests(requests);
- } else {
- const query = searchQuery.toLowerCase();
- const filtered = requests.filter(request =>
- request.groupName.toLowerCase().includes(query) ||
- request.userName.toLowerCase().includes(query) ||
- request.mobile.includes(query) ||
- request.status.toLowerCase().includes(query)
- );
- setFilteredRequests(filtered);
- }
- }, [searchQuery, requests]);
-
- const handleAccept = async (requestId: string): Promise => {
- Alert.alert(
- "Accept Request",
- "Are you sure you want to accept this request?",
- [
- { text: "Cancel", style: "cancel" },
- {
- text: "Accept",
- onPress: async () => {
- try {
- setProcessingRequest(requestId);
-
- // Make API call to approve the request
- const response = await fetch(
- `http://192.168.1.5:9060/api/fund/requests/${requestId}/approve`,
- {
- method: "PUT",
- headers: {
- "accept": "*/*",
- "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU"
- }
- }
- );
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- // Parse response
- const data = await response.json();
-
- if (data.status === "success") {
- // Remove the accepted request from the list
- const updatedRequests = requests.filter(req => req.requestId !== requestId);
- setRequests(updatedRequests);
- ToastAndroid.show(data.message || "Request accepted successfully", ToastAndroid.SHORT);
- fetchRequests();
- } else {
- throw new Error(data.message || "Failed to accept request");
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
- console.error("Accept Error:", error);
- } finally {
- setProcessingRequest(null);
- }
- }
- }
- ]
- );
- };
-
- const handleDeclineConfirm = async (requestedId: string | null, reason: string) => {
- setShowDeclineModal({ modal: false, requestId: null });
- try {
- setProcessingRequest(requestedId);
-
- // Make API call to decline the request
- const response = await fetch(
- `http://192.168.1.5:9060/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
- {
- method: "PUT",
- headers: {
- "accept": "*/*",
- "Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU"
- }
- }
- );
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- // Parse response
- const data = await response.json();
-
- if (data.status === "success") {
- // Remove the declined request from the list
- const updatedRequests = requests.filter(req => req.requestId !== selectedRequestId);
- setRequests(updatedRequests);
- ToastAndroid.show(data.message || "Request declined successfully", ToastAndroid.SHORT);
-
- fetchRequests();
- } else {
- throw new Error(data.message || "Failed to decline request");
- }
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
-
- console.error("Decline Error:", error);
- } finally {
- setProcessingRequest(null);
- }// call your API
- };
-
-
-
- const onRefresh = (): void => {
- fetchRequests();
- };
+const RequestCard = ({
+ request,
+ onAccept,
+ onDecline,
+ isProcessing,
+}: {
+ request: GroupRequest;
+ onAccept: () => void;
+ onDecline: () => void;
+ isProcessing: boolean;
+}) => {
+ const theme = useTheme();
const getInitials = (name: string): string => {
return name
.split(' ')
- .map(word => word.charAt(0))
+ .map((word) => word.charAt(0))
.join('')
.toUpperCase()
.substring(0, 2);
};
- const clearSearch = (): void => {
- setSearchQuery('');
+ return (
+
+
+
+
+ {getInitials(request.groupName)}
+
+
+
+ {request.groupName}
+ Circle Join Request
+
+
+ {request.status}
+
+
+
+
+
+
+
+
+ {request.userName}
+
+
+
+ {request.mobile}
+
+
+
+
+
+ Decline
+
+
+
+ {isProcessing ? 'Processing' : 'Accept'}
+
+
+
+
+ );
+};
+
+const RequestList: React.FC = () => {
+ const theme = useTheme();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [showDeclineModal, setShowDeclineModal] = useState<{ modal: boolean; requestId: string | null }>({
+ modal: false,
+ requestId: null,
+ });
+
+ const { data, isLoading, refetch } = useFetch(REQUESTS_URL);
+ const { mutate: updateRequest, isPending: isUpdating } = useUpdate('');
+
+ const filteredRequests = useMemo(() => {
+ const requests = data?.data || [];
+ if (!searchQuery.trim()) return requests;
+ const query = searchQuery.toLowerCase();
+ return requests.filter(
+ (request: GroupRequest) =>
+ request.groupName.toLowerCase().includes(query) ||
+ request.userName.toLowerCase().includes(query) ||
+ request.mobile.includes(query) ||
+ request.status.toLowerCase().includes(query)
+ );
+ }, [data?.data, searchQuery]);
+
+ const handleAccept = (requestId: string) => {
+ Alert.alert('Accept Request', 'Are you sure you want to approve this join request?', [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Accept',
+ onPress: () => {
+ updateRequest(
+ {
+ resource: `${APPROVE_REQUEST_URL}${requestId}/approve`,
+ payload: {}
+ } as any, // useUpdate might need adjustments if not handling dynamic resources well
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Request accepted successfully', ToastAndroid.SHORT);
+ refetch();
+ },
+ onError: (error) => {
+ ToastAndroid.show(error.message || 'Failed to accept request', ToastAndroid.SHORT);
+ },
+ }
+ );
+ },
+ },
+ ]);
};
- if (loading) {
- return (
-
-
- Loading requests...
-
+ const handleDeclineConfirm = (requestId: string | null, reason: string) => {
+ setShowDeclineModal({ modal: false, requestId: null });
+ updateRequest(
+ {
+ resource: `${APPROVE_REQUEST_URL}${requestId}/reject?reason=${reason}`,
+ payload: {}
+ } as any,
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Request declined successfully', ToastAndroid.SHORT);
+ refetch();
+ },
+ onError: (error) => {
+ ToastAndroid.show(error.message || 'Failed to decline request', ToastAndroid.SHORT);
+ },
+ }
);
- }
+ };
- if (error) {
+ if (isLoading) {
return (
-
-
- Error loading requests
- {error}
-
- Try Again
-
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
);
}
return (
-
-
- Pending Requests
-
- {filteredRequests.length} pending group request{filteredRequests.length !== 1 ? 's' : ''}
- {searchQuery ? ` matching "${searchQuery}"` : ''}
-
-
-
- {/* Search Bar */}
-
-
-
- {searchQuery ? (
-
-
-
- ) : null}
-
-
-
- }
- contentContainerStyle={filteredRequests.length === 0 ? styles.emptyScrollView : undefined}
- >
- {filteredRequests.length === 0 ? (
-
- {searchQuery ? (
- <>
-
- No matching requests
-
- No requests found for "{searchQuery}"
-
-
- Clear Search
-
- >
- ) : (
- <>
-
- No pending requests
-
- You're all caught up! There are no pending group requests.
-
- >
+
+
+
+
+
+
+ {searchQuery.length > 0 && (
+ setSearchQuery('')}>
+
+
)}
- ) : (
- filteredRequests.map((request) => (
-
-
-
-
-
- {getInitials(request.groupName)}
-
-
-
- {request.groupName}
- Group Join Request
-
-
-
- {request.status}
-
+
+ item.requestId}
+ renderItem={({ item }) => (
+ handleAccept(item.requestId)}
+ onDecline={() => setShowDeclineModal({ modal: true, requestId: item.requestId })}
+ isProcessing={isUpdating}
+ />
+ )}
+ contentContainerStyle={styles.listContent}
+ ListEmptyComponent={
+
+
+
+ {searchQuery ? 'No requests match your search' : 'No pending requests at the moment'}
+
+ }
+ />
-
-
-
-
- {request.userName}
-
-
-
-
- {request.mobile}
-
-
-
- setShowDeclineModal({ modal: true, requestId: request.requestId })}
- disabled={processingRequest === request.requestId}
- >
- {processingRequest === request.requestId ? (
-
- ) : (
- Decline
- )}
-
-
- handleAccept(request.requestId)}
- disabled={processingRequest === request.requestId}
- >
- {processingRequest === request.requestId ? (
-
- ) : (
- Accept
- )}
-
-
-
- ))
- )}
-
- {showDeclineModal.modal && (
- setShowDeclineModal({ modal: false, requestId: null })}
- onSubmit={(reason) => handleDeclineConfirm(showDeclineModal.requestId, reason)}
- />
- )}
-
+ setShowDeclineModal({ modal: false, requestId: null })}
+ onSubmit={(reason) => handleDeclineConfirm(showDeclineModal.requestId, reason)}
+ />
+
+
+
);
};
+
const styles = StyleSheet.create({
container: {
- paddingBottom: '100%',
- },
- centerContainer: {
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#f5f7fb',
- padding: 20,
},
- loadingText: {
- fontSize: 16,
- color: '#666',
- marginTop: 12,
+ loadingContainer: {
+ padding: 16,
},
- errorText: {
- fontSize: 18,
- fontWeight: '600',
- color: '#e74c3c',
- marginTop: 16,
- marginBottom: 8,
- },
- errorSubText: {
- fontSize: 14,
- color: '#7f8c8d',
- textAlign: 'center',
- marginBottom: 20,
- },
- retryButton: {
- backgroundColor: colors.pastelPink,
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 8,
- },
- retryButtonText: {
- color: '#fff',
- fontWeight: '600',
- },
- header: {
- // backgroundColor: '#fff',
- // // padding: 20,
- // borderBottomWidth: 1,
- // borderBottomColor: '#eee',
- // shadowColor: '#000',
- // shadowOffset: { width: 0, height: 2 },
- // shadowOpacity: 0.1,
- // shadowRadius: 3,
- // elevation: 3,
- },
- headerTitle: {
- fontSize: 24,
- fontWeight: 'bold',
- color: '#2c3e50',
- marginBottom: 5,
- },
- headerSubtitle: {
- fontSize: 14,
- color: '#7f8c8d',
- },
- // Search styles
- searchContainer: {
+ searchBar: {
flexDirection: 'row',
alignItems: 'center',
- backgroundColor: '#fff',
- marginVertical: 16,
- marginBottom: 8,
- borderRadius: 10,
+ height: 48,
+ borderRadius: 12,
+ borderWidth: 1,
paddingHorizontal: 12,
- height: 50,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.1,
- shadowRadius: 2,
- elevation: 2,
- },
- searchIcon: {
- marginRight: 8,
+ marginHorizontal: 16,
+ marginVertical: 12,
},
searchInput: {
flex: 1,
- height: '100%',
- fontSize: 16,
- color: '#2c3e50',
+ marginLeft: 8,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
padding: 0,
},
- clearButton: {
- padding: 4,
- },
- clearSearchButton: {
- marginTop: 16,
- paddingVertical: 8,
+ listContent: {
paddingHorizontal: 16,
- backgroundColor: '#ecf0f1',
- borderRadius: 6,
+ paddingBottom: 24,
},
- clearSearchText: {
- color: '#7f8c8d',
- fontWeight: '500',
- },
- emptyScrollView: {
- flexGrow: 1,
- justifyContent: 'center',
- },
- requestCard: {
- backgroundColor: '#fff',
- borderRadius: 12,
- marginBottom: 16,
+ card: {
+ borderRadius: 16,
+ borderWidth: 1,
padding: 16,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 3,
+ marginBottom: 16,
},
cardHeader: {
flexDirection: 'row',
- justifyContent: 'space-between',
alignItems: 'center',
- marginBottom: 12,
- },
- groupInfo: {
- flexDirection: 'row',
- alignItems: 'center',
- flex: 1,
},
avatar: {
- width: 50,
- height: 50,
- borderRadius: 25,
- backgroundColor: colors.pastelPink,
+ width: 48,
+ height: 48,
+ borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
avatarText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold',
+ fontSize: 18,
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ headerInfo: {
+ flex: 1,
},
groupName: {
- fontSize: 18,
- fontWeight: '600',
- color: '#2c3e50',
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
requestText: {
- fontSize: 14,
- color: '#7f8c8d',
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
marginTop: 2,
},
statusBadge: {
- paddingHorizontal: 10,
- paddingVertical: 5,
- borderRadius: 12,
- },
- pendingBadge: {
- backgroundColor: '#fff4e6',
- },
- otherBadge: {
- backgroundColor: '#e8f5e9',
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 8,
},
statusText: {
- fontSize: 12,
- fontWeight: '600',
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoBold,
},
divider: {
height: 1,
- backgroundColor: '#ecf0f1',
marginVertical: 12,
},
userInfo: {
+ gap: 6,
+ },
+ infoRow: {
flexDirection: 'row',
alignItems: 'center',
- marginBottom: 8,
+ gap: 8,
},
userName: {
- fontSize: 16,
- color: '#2c3e50',
- marginLeft: 8,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
userDetail: {
- fontSize: 14,
- color: '#7f8c8d',
- marginLeft: 8,
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
},
actions: {
flexDirection: 'row',
- justifyContent: 'flex-end',
marginTop: 16,
+ gap: 12,
},
- button: {
- paddingVertical: 10,
- paddingHorizontal: 20,
- borderRadius: 8,
- marginLeft: 12,
- minWidth: 100,
+ actionButton: {
+ flex: 1,
+ height: 40,
+ borderRadius: 10,
+ justifyContent: 'center',
alignItems: 'center',
},
- acceptButton: {
- backgroundColor: '#2ecc71',
- },
declineButton: {
- backgroundColor: '#fff',
borderWidth: 1,
- borderColor: '#e74c3c',
},
- acceptButtonText: {
- color: '#fff',
- fontWeight: '600',
+ acceptButton: {},
+ acceptText: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
},
- declineButtonText: {
- color: '#e74c3c',
- fontWeight: '600',
+ declineText: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoBold,
},
emptyState: {
alignItems: 'center',
- justifyContent: 'center',
- padding: 40,
+ marginTop: 64,
},
- emptyStateText: {
- fontSize: 18,
- fontWeight: '600',
- color: '#7f8c8d',
- marginTop: 16,
- marginBottom: 8,
- },
- emptyStateSubtext: {
- fontSize: 14,
- color: '#bdc3c7',
+ emptyText: {
+ marginTop: 12,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
textAlign: 'center',
},
});
-export default RequestList;
\ No newline at end of file
+export default RequestList;
diff --git a/src/screens/home/UserProfileStatus.tsx b/src/screens/home/UserProfileStatus.tsx
index 797a80d..dc1af69 100644
--- a/src/screens/home/UserProfileStatus.tsx
+++ b/src/screens/home/UserProfileStatus.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState, useEffect } from "react";
+import React, { useMemo, useState, useEffect } from 'react';
import {
StyleSheet,
Text,
@@ -9,55 +9,38 @@ import {
Image,
ScrollView,
ActivityIndicator,
-} from "react-native";
-import { useNavigation } from "@react-navigation/native";
-import colors from "../../components/colors";
-import LinearGradient from "react-native-linear-gradient";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import { apiCall } from "../../utils/ApiHelper";
-import { PAY_ALL_URL } from "../../utils/ApiUrls";
-import {
- responsiveHeight,
- responsiveWidth,
- responsiveFontSize
-} from "react-native-responsive-dimensions";
-import { useTheme } from "../../theme/ThemeProvider";
-import FontsFamily from "../../constents/FontsFamily";
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useCreate } from '../../hooks/core/useCreate';
+import { PAY_ALL_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
-type StatusData = {
+export type StatusData = {
role?: string;
organizerStatus?: string;
profileStatus?: boolean;
};
-type DashboardData = {
+export type DashboardData = {
totalPaidAmountMonth?: number;
multiplePayAmount?: number;
- completeStatusMyGroupCount?: number;
activeStatusMyGroupCount?: number;
- createdGroupActiveCount?: number;
- createdGroupCompleteCount?: number;
myCreatedGroups?: any[];
myJoinedGroups?: Array;
- requestList?: any[];
- myRequests?: Array<{ status: string }>;
+ completeStatusMyGroupCount?: number;
dueGroupDetails?: Array<{
groupId: string;
groupName: string;
baseAmount: number;
lateFee: number;
- waivedAmount: number;
totalDue: number;
- dueDate: string;
- isLate: boolean;
- currentCycle: number;
- lateFeeMode: string;
- lateFeeValue: number;
- hasPaid: boolean;
}>;
};
-type UserProfileStatusProps = {
+export type UserProfileStatusProps = {
profileImage?: { uri: string };
statusData: StatusData | null;
userDashboard: DashboardData | null;
@@ -68,53 +51,54 @@ const UserProfileStatus: React.FC = ({
statusData,
userDashboard,
}) => {
- const navigation = useNavigation();
- const [showModal, setShowModal] = useState(false);
+ const navigation = useNavigation();
+ const theme = useTheme();
- // Payment UI states
+ const [showModal, setShowModal] = useState(false);
const [showPayDialog, setShowPayDialog] = useState(false);
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
- const [submitting, setSubmitting] = useState(false);
- const {theme} = useTheme();
- // console.log("user me : ", userDashboard)
- // Preselect all due groups at mount and on change
+
useEffect(() => {
if (userDashboard?.dueGroupDetails) {
setSelectedGroupIds(userDashboard.dueGroupDetails.map((g) => g.groupId));
}
}, [userDashboard?.dueGroupDetails]);
- // Show profile completion modal only if profile is incomplete
-
- console.log("userDashboard : ", userDashboard)
useEffect(() => {
- if (statusData && !statusData.profileStatus) setShowModal(true);
+ if (statusData && !statusData.profileStatus) {
+ setShowModal(true);
+ }
}, [statusData]);
- // Top grid info items
+ const { mutate: payAll, isPending: submitting } = useCreate(PAY_ALL_URL, {
+ onSuccess: (res) => {
+ if (res?.status) {
+ ToastAndroid.show('Payment initiated successfully', ToastAndroid.SHORT);
+ setShowPayDialog(false);
+ } else {
+ ToastAndroid.show(res?.message || 'Payment failed', ToastAndroid.SHORT);
+ }
+ },
+ onError: () => {
+ ToastAndroid.show('Something went wrong during payment', ToastAndroid.SHORT);
+ },
+ });
+
const infoItems = useMemo(
- () => [
- { icon: , label: "Role", value: statusData?.role || "-" },
- { icon: , label: "Active Groups", value: userDashboard?.activeStatusMyGroupCount?.toString() || "0" },
- { icon: , label: "Joined Groups", value: userDashboard?.myJoinedGroups?.length?.toString() || "0" },
- { icon: , label: "My Groups", value: userDashboard?.myCreatedGroups?.length?.toString() || "0" },
- { icon: , label: "Total Due", value: `₹${userDashboard?.multiplePayAmount || 0}` },
- { icon: , label: "Completed", value: userDashboard?.completeStatusMyGroupCount?.toString() || "0" },
- ],
- [userDashboard, statusData]
-);
+ () => [
+ { icon: 'person-outline', label: 'Role', value: statusData?.role || '-' },
+ { icon: 'pie-chart-outline', label: 'Active', value: userDashboard?.activeStatusMyGroupCount?.toString() || '0' },
+ { icon: 'book-outline', label: 'Joined', value: userDashboard?.myJoinedGroups?.length?.toString() || '0' },
+ { icon: 'people-outline', label: 'My Circles', value: userDashboard?.myCreatedGroups?.length?.toString() || '0' },
+ ],
+ [userDashboard, statusData]
+ );
-
- // Utility for info color
- const getValueColor = (value: string) => {
- if (value.includes("₹") || value === "Active" || value === "Yes ✅") return "#4CAF50";
- if (value === "Inactive" || value === "No ❌") return "#FF6B6B";
- return "#fff";
- };
-
- // Dues and payment logic
- const dueGroups = userDashboard?.dueGroupDetails || [];
- const totalDue = dueGroups.reduce((sum, item) => sum + (item.totalDue || 0), 0);
+ const dueGroups = useMemo(() => userDashboard?.dueGroupDetails || [], [userDashboard?.dueGroupDetails]);
+ const totalDue = useMemo(
+ () => dueGroups.reduce((sum, item) => sum + (item.totalDue || 0), 0),
+ [dueGroups]
+ );
const totalDueSelected = useMemo(
() =>
dueGroups
@@ -125,220 +109,149 @@ const UserProfileStatus: React.FC = ({
const handleToggleGroup = (groupId: string) => {
setSelectedGroupIds((prev) =>
- prev.includes(groupId)
- ? prev.filter((id) => id !== groupId)
- : [...prev, groupId]
+ prev.includes(groupId) ? prev.filter((id) => id !== groupId) : [...prev, groupId]
);
};
- const handlePayAll = async () => {
- if (selectedGroupIds.length === 0) {
- ToastAndroid.show("Select at least one group", ToastAndroid.SHORT);
- return;
- }
- setSubmitting(true);
- try {
- const response = await apiCall(PAY_ALL_URL, 'POST', { groupIds: selectedGroupIds }, undefined, "Payment failed. Try again.") as any;
-
- console.log("-=-=-=-=-=-=-=-=-=-=-=", response)
- if (response.status) {
- ToastAndroid.show("Payment initiated!", ToastAndroid.LONG);
- setShowPayDialog(false);
- } else {
- ToastAndroid.show("Payment failed. Try again.", ToastAndroid.SHORT);
- }
- } catch {
- ToastAndroid.show("Network error. Try again.", ToastAndroid.SHORT);
- }
- setSubmitting(false);
- };
-
- // Bottom bar
const renderDueBar = () => (
-
-
- Total Due: ₹{totalDue}
-
+
+
+ Total Outstanding
+ ₹{totalDue}
+
setShowPayDialog(true)}
disabled={dueGroups.length === 0}
activeOpacity={0.8}
>
-
- Pay All
+
+ Pay Selected
);
- // Payment dialog modal
- const renderPayDialog = () => (
- setShowPayDialog(false)}>
-
-
-
-
- Select Groups to Pay
- setShowPayDialog(false)}>
-
-
-
-
- {dueGroups.map((group) => (
- handleToggleGroup(group.groupId)}
- activeOpacity={0.85}
- >
-
-
- {group.groupName}
-
- ₹{group.baseAmount}
- {group.lateFee > 0 ? ` (Late: ₹${group.lateFee})` : ""}
-
-
- {/* ₹{group.totalDue} */}
-
- ))}
-
-
-
- Total : ₹{totalDueSelected}
-
-
- {submitting ? (
-
- ) : (
- <>
-
- Pay Selected
- >
- )}
-
-
-
-
-
- );
-
return (
-
-
-
-
- Profile Overview
+
+
+
+ navigation.navigate('Profile')}
+ style={styles.avatarWrapper}
+ >
+ {profileImage ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {statusData?.profileStatus ? 'Verified' : 'Incomplete'}
+
+
-
-
-
- {statusData?.profileStatus ? "Verified" : "Pending"}
-
-
-
-
-
-
- {infoItems.map((item, idx) => (
-
-
-
- {item.icon}
-
-
- {item.label}
-
-
-
- {item.value}
-
+
+
+
+ {infoItems.map((item) => (
+
+ {item.label}
+ {item.value}
))}
-
-
- navigation.navigate("Profile" as never)}
- activeOpacity={0.8}
- >
-
- {profileImage !== undefined ? (
-
- ) : (
-
-
-
- )}
-
-
- Total Paid Amount
- {userDashboard?.totalPaidAmountMonth}
-
-
-
-
+
{renderDueBar()}
- {renderPayDialog()}
+ setShowPayDialog(false)}>
+
+
+
+ Select Payments
+ setShowPayDialog(false)}>
+
+
+
+
+ {dueGroups.map((group) => (
+ handleToggleGroup(group.groupId)}
+ >
+
+
+ {group.groupName}
+
+ Amount: ₹{group.baseAmount} {group.lateFee > 0 ? `+ ₹${group.lateFee} fee` : ''}
+
+
+ ₹{group.totalDue}
+
+ ))}
+
+
+
+ Payable
+ ₹{totalDueSelected}
+
+ payAll({ groupIds: selectedGroupIds })}
+ disabled={submitting || selectedGroupIds.length === 0}
+ >
+ {submitting ? (
+
+ ) : (
+ Confirm Payment
+ )}
+
+
+
+
+
{showModal && (
-
-
-
- Complete Your Profile
-
- Your profile is incomplete. Update your details to unlock all features.
-
-
- {
- setShowModal(false);
- navigation.navigate("Profile" as never);
- }}
- >
-
- Complete Profile
-
- setShowModal(false)}
- >
-
- Remind Later
-
+
+
+
+
+ Complete Your Profile
+
+ Update your identity and bank details to start earning and managing circles.
+
+ {
+ setShowModal(false);
+ navigation.navigate('Profile');
+ }}
+ >
+ Update Now
+
+ setShowModal(false)}>
+ I'll do it later
+
@@ -351,307 +264,226 @@ export default UserProfileStatus;
const styles = StyleSheet.create({
container: {
- marginHorizontal: 8,
- marginBottom: 16,
+ paddingHorizontal: 10,
+ marginTop: 4,
},
card: {
- borderRadius: 24,
- overflow: "hidden",
- elevation: 8,
- shadowColor: "#000",
- shadowOpacity: 0.3,
- shadowRadius: 10,
- shadowOffset: { width: 0, height: 4 },
- },
- headerSection: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingTop: 16,
- paddingHorizontal: 18,
- marginBottom: 16,
- },
- titleWithIcon: {
- flexDirection: 'row',
- alignItems: 'center'
- },
- headerIcon: {
- marginRight: 8
- },
- header: {
- fontSize: responsiveWidth(4.5),
- fontFamily: FontsFamily.nunitoBold,
- color: "#fff"
- },
- statusBadge: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: 'rgba(255,255,255,0.2)',
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 20,
- },
- statusBadgeText: {
- color: "#fff",
- fontSize: responsiveWidth(2),
- fontWeight: '600',
- marginLeft: 4
- },
- contentSection: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- paddingHorizontal: 16,
- paddingBottom: 16,
- },
- leftSection: {
- flex: 1,
- marginRight: 16
- },
- infoGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 10
- },
- infoCard: {
- padding: 12,
- flex: 1,
- minWidth: '45%',
- marginBottom: 8,
- borderRadius:12
- },
- infoCardOne: {
- backgroundColor: 'rgba(255,255,255,0.15)',
- borderRadius: 14,
- padding: 12,
+ borderRadius: 16,
borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.2)',
- // flex: 1,
- minWidth: '45%',
- marginTop: 21,
+ overflow: 'hidden',
},
- infoCardHeader: {
+ headerRow: {
flexDirection: 'row',
+ padding: 14,
alignItems: 'center',
- marginBottom: 6
- },
- iconContainer: {
- width: responsiveWidth(8),
- height: responsiveHeight(3.5),
- borderRadius: responsiveWidth(2),
- justifyContent: 'center',
- alignItems: 'center',
- marginRight: 8,
- },
- infoLabel: {
- fontSize: responsiveWidth(3),
- fontFamily: FontsFamily.nunitoSemiBold,
- flexShrink: 1,
- },
- infoValue: {
- fontFamily: FontsFamily.nunitoBold,
- fontSize: responsiveWidth(3),
- textAlign: 'left',
},
profileSection: {
- width: 90,
alignItems: 'center',
- justifyContent: 'center',
+ marginRight: 14,
},
- profileContainer: {
- alignItems: 'center'
+ avatarWrapper: {
+ width: 72,
+ height: 72,
+ borderRadius: 36,
+ overflow: 'hidden',
},
avatar: {
- width: 80,
- height: 80,
- borderRadius: 40,
- borderWidth: 3,
- borderColor: "#fff",
+ width: 72,
+ height: 72,
},
avatarPlaceholder: {
- width: 80,
- height: 80,
- borderRadius: 40,
- backgroundColor: 'rgba(255,255,255,0.2)',
+ width: 72,
+ height: 72,
justifyContent: 'center',
alignItems: 'center',
- borderWidth: 3,
- borderColor: "#fff",
},
- profileBadge: {
- position: 'absolute',
- bottom: 0,
- right: 0,
- width: 26,
- height: 26,
- backgroundColor: '#fff',
- borderRadius: 13,
- justifyContent: 'center',
+ badgeLabel: {
+ flexDirection: 'row',
alignItems: 'center',
+ marginTop: 8,
+ },
+ dot: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ marginRight: 6,
+ },
+ badgeText: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ statsSection: {
+ flex: 1,
+ },
+ grid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 10,
+ },
+ gridItem: {
+ width: '45%',
+ },
+ itemLabel: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ itemValue: {
+ fontSize: responsiveFontSize(1.7),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 2,
},
dueBar: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- padding: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 14,
+ paddingVertical: 10,
borderTopWidth: 1,
},
- dueBarText: {
-fontFamily: FontsFamily.nunitoExtraBold,
- fontSize: responsiveWidth(4),
-
+ dueBarLabel: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
},
- payAllButton: {
- flexDirection: "row",
- alignItems: "center",
- paddingHorizontal: 16,
- paddingVertical: 10,
- borderRadius: 20,
+ dueBarValue: {
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ },
+ payButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: 10,
gap: 6,
},
- payAllButtonText: {
- fontFamily: FontsFamily.nunitoBold,
- fontSize: responsiveWidth(2.5)
- },
- payModalOverlay: {
- flex: 1,
- backgroundColor: "rgba(0,0,0,0.3)",
- justifyContent: "center",
- alignItems: "center",
- },
- payModalBox: {
- width: "90%",
- borderRadius: 24,
- overflow: "hidden",
- alignItems: "center",
- shadowColor: "#000",
- shadowOpacity: 0.15,
- shadowRadius: 24,
- elevation: 12,
- },
- payDialogHeader: {
- flexDirection: "row",
- alignItems: "center",
- width: "100%",
- justifyContent: "space-between",
- paddingVertical: 14,
- paddingHorizontal: 18,
- borderBottomWidth: 1,
- borderColor: "rgba(255,255,255,0.1)",
- },
- payDialogTitle: {
-
- fontSize: responsiveWidth(4),
- fontFamily: FontsFamily.nunitoBold,
-
- },
- payGroupOption: {
- flexDirection: "row",
- alignItems: "center",
- gap: 16,
- paddingVertical: 12,
- paddingHorizontal: 14,
- borderRadius: 12,
- marginBottom: 8,
- },
- payOptionSelected: {
-
- borderWidth: 1,
- },
- payGroupName: {
- fontFamily: FontsFamily.nunitoSemiBold,
- fontSize: responsiveWidth(3)
- },
- payDueText: {
- fontSize: responsiveWidth(3),
- fontFamily: FontsFamily.nunitoRegular,
- marginTop: 2
- },
- payAmount: {
- fontWeight: "bold",
- fontSize: responsiveWidth(2.5),
- color: "#4CAF50"
- },
- payDialogFooter: {
- flexDirection: "row",
- alignItems: "center",
- width: "100%",
- justifyContent: "space-between",
- paddingVertical: 16,
- paddingHorizontal: 18,
- },
- payDialogTotal: {
-fontFamily: FontsFamily.nunitoBold,
- fontSize: responsiveWidth(3.5)
- },
- payConfirmButton: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 8,
- paddingHorizontal: 18,
- borderRadius: 12,
- elevation: 4,
-
- gap: 8,
- },
payButtonText: {
- color: "#ffffffff",
- fontWeight: "bold",
- fontSize: responsiveWidth(3),
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoBold,
},
modalOverlay: {
flex: 1,
- backgroundColor: "rgba(0,0,0,0.6)",
- justifyContent: "center",
- alignItems: "center",
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ justifyContent: 'flex-end',
},
- modalBox: {
- width: "85%",
- backgroundColor: "#fff",
- borderRadius: 24,
- padding: 24,
- alignItems: "center",
- elevation: 12,
+ modalContent: {
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ paddingBottom: 24,
+ maxHeight: '80%',
},
- modalTitle: {
- fontSize: responsiveWidth(2.2),
- fontWeight: "700",
- color: colors.primary,
- marginBottom: 8,
- textAlign: 'center',
- },
- modalText: {
- textAlign: "center",
- color: "#666",
- fontSize: responsiveWidth(1.4),
- lineHeight: 20,
- marginBottom: 20,
- },
- modalButtons: {
- width: '100%',
- gap: 10,
- },
- button: {
+ modalHeader: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 14,
- borderRadius: 12,
- gap: 8,
+ justifyContent: 'space-between',
+ padding: 16,
+ borderBottomWidth: 1,
},
- primaryButton: {
- backgroundColor: colors.primary,
+ modalTitle: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
},
- secondaryButton: {
- backgroundColor: "#f5f5f5",
+ groupsList: {
+ padding: 10,
+ },
+ groupItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 14,
+ borderRadius: 16,
borderWidth: 1,
- borderColor: '#e0e0e0',
+ marginBottom: 8,
},
- buttonText: {
- color: "#fff",
- fontWeight: "600",
- fontSize:responsiveWidth(2.5),
+ groupInfo: {
+ flex: 1,
+ marginLeft: 12,
},
- secondaryText: {
- color: "#666",
+ groupName: {
+ fontSize: responsiveFontSize(1.7),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
-});
\ No newline at end of file
+ groupSub: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ groupAmount: {
+ fontSize: responsiveFontSize(1.7),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ modalFooter: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: 16,
+ borderTopWidth: 1,
+ },
+ totalLabel: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ totalValue: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ },
+ confirmButton: {
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ borderRadius: 12,
+ minWidth: 160,
+ alignItems: 'center',
+ },
+ confirmText: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ alertOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ },
+ alertBox: {
+ width: '100%',
+ borderRadius: 24,
+ padding: 24,
+ alignItems: 'center',
+ },
+ alertIcon: {
+ width: 64,
+ height: 64,
+ borderRadius: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ alertTitle: {
+ fontSize: responsiveFontSize(2.5),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 8,
+ },
+ alertMsg: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ lineHeight: 22,
+ marginBottom: 24,
+ },
+ alertButton: {
+ width: '100%',
+ paddingVertical: 16,
+ borderRadius: 12,
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ alertButtonText: {
+ fontSize: responsiveFontSize(1.7),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ laterButton: {
+ paddingVertical: 8,
+ },
+ laterText: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+});
diff --git a/src/screens/home/user-home/CycleDashboard.tsx b/src/screens/home/user-home/CycleDashboard.tsx
index fb99680..6ff98b9 100644
--- a/src/screens/home/user-home/CycleDashboard.tsx
+++ b/src/screens/home/user-home/CycleDashboard.tsx
@@ -9,8 +9,9 @@ import {
UIManager,
Platform,
} from 'react-native';
-import { Ionicons } from "@react-native-vector-icons/ionicons"; import colors from "../../../components/colors"; // Assuming a colors file
+import LucideIcon from '../../../components/common/LucideIcon';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import { useTheme } from "../../../hooks/ui/useTheme";
// Enable LayoutAnimation for Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
@@ -71,9 +72,18 @@ interface CycleDashboardData {
const formatDate = (dateStr: string | null): string => {
if (!dateStr) return 'N/A';
+ // If dateStr is an array (as per interface), handle it
+ if (Array.isArray(dateStr)) {
+ const [year, month, day] = dateStr;
+ return `${day}-${month}-${year}`;
+ }
- const [year, month, day] = dateStr.split('-');
- return `${day}-${month}-${year}`;
+ if (typeof dateStr === 'string' && dateStr.includes('-')) {
+ const [year, month, day] = dateStr.split('-');
+ return `${day}-${month}-${year}`;
+ }
+
+ return String(dateStr);
};
const formatCurrency = (amount: number): string => {
@@ -88,47 +98,56 @@ const formatCurrency = (amount: number): string => {
// --- Child Components ---
const CycleItem = ({ item, isExpanded, onToggle }: { item: Cycle, isExpanded: boolean, onToggle: () => void }) => {
+ const theme = useTheme();
+
return (
-
+
- Cycle {item.cycleNumber}
-
- {formatDate(item.cycleStart)} - {formatDate(item.cycleEnd)}
+ Cycle {item.cycleNumber}
+
+ {formatDate(item.cycleStart as any)} - {formatDate(item.cycleEnd as any)}
-
+
{isExpanded && (
-
+
{/* Withdrawal Info */}
{item.withdrawal && (
- Withdrawal
-
-
+ Withdrawal
+
+
- {item.withdrawal.userName}
- {formatCurrency(item.withdrawal.totalAmount)}
+ {item.withdrawal.userName}
+ {formatCurrency(item.withdrawal.totalAmount)}
- {formatDate(item.withdrawal.paidDate)}
+ {formatDate(item.withdrawal.paidDate as any)}
)}
{/* Contributions List */}
- Contributions ({item.paidParticipants}/{item.totalParticipants})
+ Contributions ({item.paidParticipants}/{item.totalParticipants})
{item.contributions.map(contribution => (
-
-
+
+
- {contribution.userName}
- {formatDate(contribution.paidDate)}
- {contribution.status}
+ {contribution.userName}
+ {formatDate(contribution.paidDate as any)}
+ {contribution.status}
- {formatCurrency(contribution.totalAmount)}
+ {formatCurrency(contribution.totalAmount)}
))}
@@ -143,6 +162,7 @@ const CycleItem = ({ item, isExpanded, onToggle }: { item: Cycle, isExpanded: bo
const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
const [expandedCycle, setExpandedCycle] = useState(data.currentCycle);
+ const theme = useTheme();
const handleToggle = (cycleNumber: number) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
@@ -152,19 +172,19 @@ const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
console.log("expanded cycle : ", data)
const renderHeader = () => (
- {data.groupName}
+ {data.groupName}
- Total Collected
- {formatCurrency(data.totalCollected)}
+ Total Collected
+ {formatCurrency(data.totalCollected)}
- Status
- {data.status}
+ Status
+ {data.status}
- Cycles
- {data.currentCycle}/{data.totalParticipants}
+ Cycles
+ {data.currentCycle}/{data.totalParticipants}
@@ -204,7 +224,6 @@ const styles = StyleSheet.create({
groupName: {
fontSize: responsiveFontSize(2.4),
fontWeight: 'bold',
- color: '#1e293b',
marginBottom: 15,
},
summaryRow: {
@@ -216,17 +235,14 @@ const styles = StyleSheet.create({
},
summaryLabel: {
fontSize: responsiveFontSize(1.4),
- color: '#64748b',
marginBottom: 4,
},
summaryValue: {
fontSize: responsiveFontSize(1.6),
fontWeight: '600',
- color: '#0f172a',
},
// Cycle Item Card
cycleCard: {
- backgroundColor: '#fff',
borderRadius: 12,
marginBottom: 15,
elevation: 2,
@@ -245,18 +261,15 @@ const styles = StyleSheet.create({
cycleTitle: {
fontSize: responsiveFontSize(1.8),
fontWeight: '600',
- color: '#1e293b',
},
cycleDateRange: {
fontSize: responsiveFontSize(1.3),
- color: '#64748b',
marginTop: 2,
},
cycleDetails: {
paddingHorizontal: 16,
paddingBottom: 16,
borderTopWidth: 1,
- borderTopColor: '#f1f5f9',
},
section: {
marginTop: 16,
@@ -264,14 +277,12 @@ const styles = StyleSheet.create({
sectionTitle: {
fontSize: responsiveFontSize(1.6),
fontWeight: '600',
- color: '#334155',
marginBottom: 12,
},
// Withdrawal Card
withdrawalCard: {
flexDirection: 'row',
alignItems: 'center',
- backgroundColor: colors.pastelPink, // Example color
padding: 12,
borderRadius: 8,
},
@@ -282,15 +293,12 @@ const styles = StyleSheet.create({
withdrawalUser: {
fontSize: responsiveFontSize(1.5),
fontWeight: 'bold',
- color: colors.Home
},
withdrawalAmount: {
fontSize: responsiveFontSize(1.4),
- color: colors.Home
},
withdrawalDate: {
fontSize: responsiveFontSize(1.2),
- color: '#475569'
},
// Contribution Item
contributionItem: {
@@ -301,20 +309,16 @@ const styles = StyleSheet.create({
borderRadius: 6,
marginBottom: 10,
borderBottomWidth: 1,
- borderBottomColor: '#f1f5f9',
},
contributorName: {
fontSize: responsiveFontSize(1.5),
- color: '#1e293b',
},
contributionDate: {
fontSize: responsiveFontSize(1.2),
- color: '#64748b',
},
contributionAmount: {
fontSize: responsiveFontSize(1.5),
fontWeight: '500',
- color: '#1e293b',
},
});
diff --git a/src/screens/home/user-home/GroupDetails.tsx b/src/screens/home/user-home/GroupDetails.tsx
index c5913ac..33100c8 100644
--- a/src/screens/home/user-home/GroupDetails.tsx
+++ b/src/screens/home/user-home/GroupDetails.tsx
@@ -1,77 +1,37 @@
-import React, { useEffect, useState, useCallback } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import {
+ FlatList,
+ RefreshControl,
StyleSheet,
Text,
- View,
- TouchableOpacity,
ToastAndroid,
- RefreshControl,
+ TouchableOpacity,
+ View,
ScrollView,
- Modal,
+ Image,
+ Modal
} from "react-native";
-import { useRoute } from "@react-navigation/native";
-import AppView from "../../../components/AppView";
-import colors from "../../../components/colors";
-import Ionicons from '@react-native-vector-icons/ionicons';
-import Header from "../../../components/common/Header";
+import LinearGradient from "react-native-linear-gradient";
+
+import { useTheme } from "../../../hooks/ui/useTheme";
+import { useNavigation, useRoute } from "@react-navigation/native";
import { apiCall } from "../../../utils/ApiHelper";
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
+import LucideIcon from '../../../components/common/LucideIcon';
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+
import ContributionScreen from "../../ContributionScreen";
import CycleDashboard from "./CycleDashboard";
-
-
-const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }: any) => {
- const [open, setOpen] = useState(false);
- const selectedLabel = items.find((item: any) => item.value === selectedValue)?.label || placeholder;
-
-
-
- return (
-
- setOpen(true)} style={styles.dropdownHeader}>
- {selectedLabel}
-
-
-
-
- setOpen(false)}
- >
- setOpen(false)}>
-
-
- {items.map((item: any) => (
- {
- onValueChange(item.value);
- setOpen(false);
- }}
- style={[
- styles.dropdownItem,
- item.value === selectedValue && styles.selectedItem
- ]}
- >
- {item.label}
-
- ))}
-
-
-
-
-
- );
-};
-
+import SkeletonLoader from "../../../components/common/SkeletonLoader";
+import FontsFamily from "../../../constents/FontsFamily";
+import AppView from "../../../components/AppView";
+// import CustomDropdown from "../../../components/CustomDropdown";
const GroupDetails = () => {
+ const theme = useTheme();
const route = useRoute();
+ const navigation = useNavigation();
-
- console.log(" route id : ", route)
const [memberData, setMemberData] = useState([]);
const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = useState(false);
@@ -82,7 +42,6 @@ const GroupDetails = () => {
const [cycleItems, setCycleItems] = useState([]);
const [selectedUser, setSelectedUser] = useState() as string | any;
-
const group = {
id: route.params.item?.id,
name: route.params.item?.groupName,
@@ -93,24 +52,18 @@ const GroupDetails = () => {
members: memberData,
};
-
-
const getPaymentStatus = async () => {
- const response = await apiCall(`${BASE_URL}payments/group/${group.id || route?.params?.groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number, allPaid: boolean, selectedUser: string }; message?: string };
- console.log("paymentStatus", response.data)
+ const response = await apiCall(`${BASE_URL}payments/group/${group.id || route?.params?.groupId}/payment/paid`, 'GET') as any;
if (response.status === 'success') {
setSelectedUser(response.data);
}
}
-
-
const getCycleDashboard = async () => {
try {
const response = await apiCall(`${BASE_URL}payments/groups/${group.id || route?.params?.groupId}/cycle-dashboard`, "GET") as any;
const responseData = response.data;
setCycleDashboardData(responseData || []);
- console.log("cycle data", responseData)
if (responseData?.cycles) {
const items = responseData.cycles.map((cycle: any) => ({
label: `Cycle ${cycle.cycleNumber}`,
@@ -126,18 +79,15 @@ const GroupDetails = () => {
}
};
-
const fetchGroupDetails = async () => {
try {
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id || route?.params?.groupId}`, "GET") as any;
- console.log("Group Members Response: ", res);
setMemberData(res.data);
} catch (error: any) {
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
}
};
-
const handleContribution = async () => {
try {
const res = await apiCall(`${CONTRIBUTE}${group.id || route?.params?.groupId}/calculate-contribution`, 'GET') as any;
@@ -147,7 +97,6 @@ const GroupDetails = () => {
}
};
-
const fetchAllData = useCallback(async () => {
setLoading(true);
await Promise.all([
@@ -158,7 +107,6 @@ const GroupDetails = () => {
setLoading(false);
}, [group.id || route?.params?.groupId]);
-
useEffect(() => {
if (group.id || route?.params?.groupId) {
fetchAllData();
@@ -192,92 +140,200 @@ const GroupDetails = () => {
const selectedCycleData = cycleDashboardData?.cycles?.find((c: any) => c.cycleNumber === selectedCycleNumber);
- // const selectedCycleData = cycleDashboardData?.cycles.find((c: any) => c.cycleNumber === selectedCycleNumber);
+ const renderSkeletons = () => (
+
+
+
+
+
+ );
return (
-
- {selectedUser?.firstName && (
-
-
- Winner: {`${selectedUser.firstName} ${selectedUser.lastName || ''}`}
-
-
- )}
+
+ {/* Standard Header */}
+
+ navigation.goBack()} style={[styles.iconButton, { backgroundColor: theme.colors.card }]}>
+
+
+
+ Circle Wallet
+
+
+
+
+
}
contentContainerStyle={styles.scrollContent}
+ showsVerticalScrollIndicator={false}
>
-
- {group.description || route.name}
+ {loading ? renderSkeletons() : (
+
+ {/* Premium Digital Wallet Card */}
+
+ {/* Background Elements for Depth - Removed as per user request */}
+ {/*
+ */}
-
-
- {memberData.length ?? 0}
- Members
-
-
- ₹{cycleDashboardData?.contributionAmount || group.collected}
- Contribution
-
-
- ₹{cycleDashboardData?.contributionAmount * memberData.length || group.goal}
- Total Amount
-
-
+
+
+ Total Balance
+
+ ₹{cycleDashboardData?.contributionAmount * memberData.length || group.goal}
+
+
+
+
+
+
+
+
+ Group Name
+ {group.name}
+
+
+
+
+ {cycleDashboardData?.status || group?.status}
+
+
+
+
+
- {cycleItems.length > 0 && (
-
- )}
+ {/* Winner Notification Strip */}
+ {selectedUser?.firstName && (
+
+
+
+
+
+
+ Cycle Winner:
+ {selectedUser.firstName} {selectedUser.lastName}
+
+
+
+
+ )}
- {selectedCycleData && cycleDashboardData && (
-
- )}
-
-
-
- Members
-
- {cycleDashboardData?.status || group?.status}
-
-
-
-
- {memberData.map((member: any) => (
-
-
-
-
- {member.firstName} {member.role === "Organizer" ? "(Organizer)" : ""}
-
+ {/* Stats Row (Mini) */}
+
+
+
+
+
+
+ {memberData.length}
+ Members
+
+
+
+
+
+
+
+ ₹{cycleDashboardData?.contributionAmount || group.collected}
+ Per Head
+
+
+
+ {/* Cycle Timeline */}
+ {cycleItems.length > 0 && (
+
+ Cycle Statement
+ {/* */}
+ {selectedCycleData && cycleDashboardData && (
+
+
+
+ )}
+
+ )}
+
+ {/* Members List - Contact Style */}
+
+ Circle Contacts
+
+
+ {memberData.map((member: any, index) => (
+
+
+
+ {member.firstName?.charAt(0)}{member.lastName?.charAt(0)}
+
+ {member.role === "Organizer" && (
+
+
+
+ )}
+
+
+
+ {member.firstName} {member.lastName}
+
+
+ {member.role === "Organizer" ? "Group Admin" : "Member"}
+
+
+
+
+
+
+ ))}
+
+
+
+ {/* Contribute Button */}
+ {(cycleDashboardData?.status === 'ACTIVE' || cycleDashboardData?.status === 'DELAY') && !payable?.hasPaid && (
+ setOpenModal(true)}
+ style={[styles.payButton, { backgroundColor: theme.colors.primary }]}
+ >
+
+
+ Pay Contribution
+
+
+ )}
- ))}
-
-
- {(cycleDashboardData?.status === 'ACTIVE' || cycleDashboardData?.status === 'DELAY') && !payable?.hasPaid && (
- setOpenModal(true)} style={styles.button}>
- Contribute
-
)}
+
{openModal && (
setOpenModal(false)}
onConfirm={() => {
- confirmPayment();
setOpenModal(false);
+ navigation.navigate('PaymentMode' as any, {
+ groupId: group.id || route?.params?.groupId,
+ });
}}
/>
)}
@@ -285,155 +341,262 @@ const GroupDetails = () => {
);
};
-
export default GroupDetails;
-
const styles = StyleSheet.create({
- dropdownWrapper: {
- marginHorizontal: 15,
- marginBottom: 20,
- },
- dropdownHeader: {
+ header: {
flexDirection: 'row',
+ alignItems: 'center',
justifyContent: 'space-between',
- alignItems: 'center',
- padding: 12,
- borderWidth: 1,
- borderColor: '#ddd',
- borderRadius: 8,
- backgroundColor: '#fff',
- },
- headerText: {
- fontSize: 15,
- color: colors.black,
- },
- modalOverlay: {
- flex: 1,
- justifyContent: 'center',
- backgroundColor: 'rgba(0,0,0,0.4)',
- padding: 20,
- },
- dropdownListContainer: {
- backgroundColor: '#fff',
- borderRadius: 8,
- padding: 10,
- maxHeight: 300,
- },
- dropdownItem: {
+ paddingHorizontal: 16,
paddingVertical: 12,
- paddingHorizontal: 10,
},
- selectedItem: {
- backgroundColor: colors.pastelOrange,
- borderRadius: 6,
- },
- itemText: {
- fontSize: 15,
- color: colors.black,
- },
- description: {
- fontSize: 14,
- color: "#475569",
- marginBottom: 20,
- lineHeight: 20,
- paddingHorizontal: 15,
- },
- statsRow: {
- flexDirection: "row",
- justifyContent: "space-around",
- marginBottom: 20,
- paddingHorizontal: 15,
- },
- statBox: {
- alignItems: 'center',
- flex: 1,
- },
- statValue: {
- fontSize: 18,
- fontWeight: '700',
- color: '#1e293b',
- },
- statLabel: {
- fontSize: 12,
- color: '#64748b',
- marginTop: 4,
- },
- membersHeader: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: 'center',
- paddingHorizontal: 15,
- marginTop: 20,
- marginBottom: 10,
- },
- sectionTitle: {
- fontSize: 18,
- fontWeight: "600",
- color: "#0f172a",
- },
- statusBadge: {
- backgroundColor: colors.pastelOrange,
+ iconButton: {
+ width: 40,
+ height: 40,
borderRadius: 20,
- paddingHorizontal: 12,
- paddingVertical: 6,
- elevation: 2,
- },
- statusText: {
- fontSize: 14,
- fontWeight: '600',
- color: colors.black,
- },
- memberCard: {
- flexDirection: "row",
+ justifyContent: 'center',
alignItems: 'center',
- backgroundColor: '#fff',
- padding: 12,
- borderRadius: 12,
- marginBottom: 10,
- elevation: 2,
- marginHorizontal: 15,
},
- memberInfo: {
- marginLeft: 10,
+ iconButtonSpacer: {
+ width: 40,
},
- memberName: {
- fontSize: 15,
- fontWeight: "600",
- color: "#1e293b",
- },
- button: {
- backgroundColor: colors.pastelOrange,
- marginHorizontal: 15,
- marginTop: 20,
- marginBottom: 30,
- elevation: 5,
- paddingVertical: 14,
- borderRadius: 10,
- alignItems: "center",
- },
- buttonText: {
- fontSize: 16,
- fontWeight: "600",
- color: "#000",
+ headerTitle: {
+ fontSize: responsiveFontSize(2),
},
scrollContent: {
- paddingBottom: 50,
+ paddingBottom: 100,
},
- floatingWinnerContainer: {
- position: 'absolute',
- bottom: 80,
- alignSelf: 'center',
- backgroundColor: colors.pastelBlue,
- paddingHorizontal: 15,
- paddingVertical: 8,
- borderRadius: 20,
- elevation: 5,
+ contentPadding: {
+ paddingHorizontal: 16,
+ paddingTop: 10,
+ },
+ skeletonContainer: {
+ paddingHorizontal: 16,
+ marginTop: 20,
+ },
+ // Wallet Card
+ walletCard: {
+ borderRadius: 24,
+ padding: 24,
+ position: 'relative',
+ overflow: 'hidden',
+ minHeight: 180,
+ justifyContent: 'space-between',
+ elevation: 10,
+ shadowColor: '#16213E',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.4,
+ shadowRadius: 16,
+ marginBottom: 24,
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.1)',
+ },
+ cardHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
zIndex: 10,
},
- floatingWinnerText: {
- color: colors.black,
- fontWeight: 'bold',
- fontSize: 16,
+ iconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: 'rgba(255,255,255,0.1)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.2)',
+ },
+ cardLabel: {
+ color: 'rgba(255,255,255,0.6)',
+ fontSize: responsiveFontSize(1.5),
+ textTransform: 'uppercase',
+ letterSpacing: 1.5,
+ marginBottom: 4,
+ },
+ cardBalance: {
+ color: '#fff',
+ fontSize: responsiveFontSize(4),
+ marginVertical: 10,
+ },
+ cardFooter: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-end',
+ },
+ cardFooterLabel: {
+ color: 'rgba(255,255,255,0.6)',
+ fontSize: responsiveFontSize(1.4),
+ marginBottom: 4,
+ },
+ cardFooterValue: {
+ color: '#fff',
+ fontSize: responsiveFontSize(2),
+ },
+ cardStatusBadge: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 20, // Pill shape
+ },
+ cardStatusText: {
+ fontSize: responsiveFontSize(1.4),
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ cardCircle1: {
+ position: 'absolute',
+ top: -60,
+ right: -60,
+ width: 240,
+ height: 240,
+ borderRadius: 120,
+ backgroundColor: 'rgba(255,255,255,0.03)',
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.05)',
+ },
+ cardCircle2: {
+ position: 'absolute',
+ bottom: -40,
+ left: -40,
+ width: 180,
+ height: 180,
+ borderRadius: 90,
+ backgroundColor: 'rgba(113, 31, 247, 0.1)', // Hint of primary color
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.05)',
+ },
+ // Winner Strip
+ winnerStrip: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ borderRadius: 16,
+ marginBottom: 20,
+ borderWidth: 1,
+ },
+ winnerIconBox: {
+ width: 32,
+ height: 32,
+ borderRadius: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ winnerContent: {
+ flex: 1,
+ },
+ winnerText: {
+ fontSize: responsiveFontSize(1.8),
+ },
+ // Mini Stats
+ miniStatsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 24,
+ },
+ miniStatItem: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 16,
+ marginHorizontal: 4,
+ elevation: 2,
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ },
+ iconCircle: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ miniStatValue: {
+ fontSize: responsiveFontSize(2),
+ },
+ miniStatLabel: {
+ fontSize: responsiveFontSize(1.4),
+ },
+ // Generic Section
+ sectionContainer: {
+ marginBottom: 24,
+ },
+ sectionHeader: {
+ fontSize: responsiveFontSize(2),
+ marginBottom: 12,
+ paddingLeft: 4,
+ },
+ // Contacts List
+ contactsContainer: {
+ borderRadius: 20,
+ overflow: 'hidden',
+ elevation: 2,
+ shadowOpacity: 0.1,
+ },
+ contactRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ },
+ contactAvatar: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ avatarInitials: {
+ fontSize: responsiveFontSize(1.8),
+ },
+ contactBadge: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ backgroundColor: '#FFD700', // Gold for organizer
+ width: 16,
+ height: 16,
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 1.5,
+ borderColor: '#fff',
+ },
+ contactInfo: {
+ flex: 1,
+ },
+ contactName: {
+ fontSize: responsiveFontSize(1.8),
+ marginBottom: 2,
+ },
+ contactRole: {
+ fontSize: responsiveFontSize(1.4),
+ },
+ contactAction: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ // Pay Button
+ payButton: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 18,
+ borderRadius: 50, // Pill shape
+ elevation: 8,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ marginBottom: 20,
+ },
+ payButtonText: {
+ fontSize: responsiveFontSize(2),
},
});
diff --git a/src/screens/home/user-home/UserHome.tsx b/src/screens/home/user-home/UserHome.tsx
index d9e40b8..86555d6 100644
--- a/src/screens/home/user-home/UserHome.tsx
+++ b/src/screens/home/user-home/UserHome.tsx
@@ -11,4 +11,4 @@ const UserHome = () => {
export default UserHome
-const styles = StyleSheet.create({})
\ No newline at end of file
+const styles = StyleSheet.create({})
diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx
index 458b72c..5e1968a 100644
--- a/src/screens/management/EditGroupScreen.tsx
+++ b/src/screens/management/EditGroupScreen.tsx
@@ -1,909 +1,814 @@
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
View,
Text,
- TextInput,
StyleSheet,
- ScrollView,
TouchableOpacity,
- Alert,
+ Image,
ActivityIndicator,
+ ToastAndroid,
+ FlatList,
+ Modal,
+ Platform,
+ TouchableWithoutFeedback,
+ Keyboard,
+ TextInput,
+ ScrollView,
+ Dimensions,
+ KeyboardAvoidingView,
Share,
- Image
-} from "react-native";
-import { NativeStackScreenProps } from "@react-navigation/native-stack";
-import { RootStackParamList } from "../../navigation/AppNavigator";
-import { apiCall } from "../../utils/ApiHelper";
-import AppView from "../../components/AppView";
-import { Picker } from "@react-native-picker/picker";
-import colors from "../../components/colors";
-import Clipboard from "@react-native-clipboard/clipboard";
-import { CompositeScreenProps, useNavigation } from "@react-navigation/native";
-import { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
-import { BASE_URL, UPLOAD_GROUP_IMAGE } from "../../utils/ApiUrls";
-import { launchImageLibrary, Asset } from "react-native-image-picker";
-import * as Keychain from "react-native-keychain";
-import Ionicons from "react-native-vector-icons/Ionicons";
-import { CustomButton } from "../../components/common/Custom";
-import { StackNavigationProp } from "@react-navigation/stack";
-import WinnerUser from "./WinnerUser";
-import CustomAlert from "../../components/CostomAlart";
+ RefreshControl,
+} from 'react-native';
+import { useRoute, useNavigation } from '@react-navigation/native';
+import { Picker } from '@react-native-picker/picker';
+import Clipboard from '@react-native-clipboard/clipboard';
+import { launchImageLibrary } from 'react-native-image-picker';
+import * as Keychain from 'react-native-keychain';
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { useUpdate } from '../../hooks/core/useUpdate';
+import { useInvite } from '../../hooks/core/useInvite';
+import { BASE_URL, UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
+import WinnerUser from './WinnerUser';
+
+const { width } = Dimensions.get('window');
+
+// --- TYPE DEFINITIONS ---
interface Participant {
id: string;
firstName: string;
lastName: string | null;
mobile: string;
- role: "ORGANIZER" | "USER";
-}
-
-export interface GroupDetails {
- id: string;
- groupName: string;
- description: string;
- startDate: [number, number, number] | null;
- endDate: [number, number, number] | null;
- contributionAmount: number;
- minParticipants: number;
- maxParticipants: number;
- portalCharge: number | null;
- latePaymentCharge: number | null;
- lateFeeMode: "DAILY" | "FIXED" | null;
- lateFeeValue: number | null;
- cycleType: "QUARTERLY" | "MONTHLY" | "WEEKLY" | "CUSTOM";
- customCycleDays: number;
- withdrawalMethod: "RANDOM" | "SEQUENTIAL";
- status: "PENDING" | "ACTIVE" | "COMPLETED" | "APPROVED" | "DELAY";
- visibility: "PUBLIC" | "PRIVATE";
- organizerId: string;
- totalParticipants: number;
- participants: Participant[];
- imageBlob: string;
+ role: 'ORGANIZER' | 'USER';
}
export interface EditGroupPayload {
groupName: string;
description: string;
maxParticipants: number;
- minParticipants?: number;
- contributionAmount: number;
- customCycleDays: number | null;
- cycleType: "QUARTERLY" | "MONTHLY" | "WEEKLY" | "CUSTOM";
+ minParticipants: number;
+ lateFeeMode: 'DAILY' | 'FIXED' | string;
+ lateFeeValue: number;
latePaymentCharge: number;
- visibility: "PUBLIC" | "PRIVATE";
- lateFeeMode: "DAILY" | "FIXED" | null;
- lateFeeValue: number | null;
+ visibility: 'PUBLIC' | 'PRIVATE';
+ contributionAmount: number;
+ cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM' | 'HALF_YEARLY' | 'YEARLY';
+ entryFee: number;
+ customCycleDays: number | null;
}
-type EditGroupScreenProps = CompositeScreenProps<
- BottomTabScreenProps,
- NativeStackScreenProps
->;
+type EditSection = 'groupInfo' | 'financialRules' | 'configuration' | null;
-type EditingCard = "groupInfo" | "financialRules" | "configuration" | null;
+const EditGroupScreen: React.FC = () => {
+ const theme = useTheme();
+ const styles = React.useMemo(() => getStyles(theme), [theme]);
+ const route = useRoute();
+ const navigation = useNavigation();
+ const { groupId } = route?.params || {};
-type GroupManagementNavProp = StackNavigationProp;
-
-const EditGroupScreen: React.FC = ({ route }) => {
- const { groupId } = route?.params as { groupId: string };
- const navigation = useNavigation();
-
- const [editingCard, setEditingCard] = useState(null);
- const [groupDetails, setGroupDetails] = useState(null);
- const [editableData, setEditableData] = useState({
- groupName: "",
- description: "",
- maxParticipants: 0,
- contributionAmount: 0,
- customCycleDays: null,
- cycleType: "MONTHLY",
- latePaymentCharge: 0,
- visibility: "PRIVATE",
- lateFeeMode: null,
- lateFeeValue: null
- });
-
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
- const [starting, setStarting] = useState(false);
- const [inviting, setInviting] = useState(false);
- const [error, setError] = useState(null);
- const [paymentStatus, setPaymentStatus] = useState(null);
- const [currentCycle, setCurrentCycle] = useState(null);
+ // State
+ const [activeSection, setActiveSection] = useState(null);
+ const [editableData, setEditableData] = useState(null);
+ const [originalData, setOriginalData] = useState(null);
+ const [changedFields, setChangedFields] = useState>(new Set());
const [selectedImage, setSelectedImage] = useState(null);
- const [imageUpdating, setImageUpdating] = useState(false);
- const [selectedUser, setSelectedUser] = useState(null);
- const [selectedUserModal, setSelectedUserModel] = useState(false);
+ const [imageUpdating, setImageUpdating] = useState(false);
+ const [selectedUserModal, setSelectedUserModel] = useState(false);
+ const [isActivating, setIsActivating] = useState(false);
+ const [showInviteTooltip, setShowInviteTooltip] = useState(false);
- // Custom Alert state
- const [alertInfo, setAlertInfo] = useState<{
- visible: boolean;
- title: string;
- message: string;
- buttons: { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[];
- alertType: "info" | "success" | "error";
- }>({
- visible: false,
- title: "",
- message: "",
- buttons: [],
- alertType: "info"
+ // --- DATA FETCHING ---
+ const {
+ data: groupDetailsRes,
+ isLoading: isGroupLoading,
+ isRefetching: isRefetchingGroup,
+ refetch: refetchGroup,
+ } = useFetch(
+ `${BASE_URL}fund/group/details?groupId=${groupId}`,
+ undefined,
+ { enabled: !!groupId }
+ );
+
+ const groupDetails = groupDetailsRes?.data;
+
+ // Initialize data
+ useEffect(() => {
+ if (groupDetails) {
+ const newData: EditGroupPayload = {
+ groupName: groupDetails.groupName || '',
+ description: groupDetails.description || '',
+ maxParticipants: groupDetails.maxParticipants || 0,
+ minParticipants: groupDetails.minParticipants || 0,
+ contributionAmount: groupDetails.contributionAmount || 0,
+ customCycleDays: groupDetails.customCycleDays || 0,
+ cycleType: groupDetails.cycleType || 'MONTHLY',
+ latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
+ visibility: groupDetails.visibility || 'PUBLIC',
+ lateFeeMode: groupDetails.lateFeeMode ?? null,
+ lateFeeValue: groupDetails.lateFeeValue ?? 0,
+ entryFee: groupDetails.entryFee ?? 0,
+ };
+ setOriginalData(newData);
+ setEditableData(newData);
+ setChangedFields(new Set());
+
+ if (groupDetails.status === 'APPROVED') {
+ setShowInviteTooltip(true);
+ const timer = setTimeout(() => setShowInviteTooltip(false), 5000);
+ return () => clearTimeout(timer);
+ }
+ }
+
+ // DEBUG: Log Token
+ const logToken = async () => {
+ try {
+ const credentials = await Keychain.getGenericPassword();
+ if (credentials) {
+ console.log("🔐 AUTH TOKEN:", credentials.password);
+ } else {
+ console.log("🔐 No credentials stored");
+ }
+ } catch (error) {
+ console.error("Error retrieving token:", error);
+ }
+ };
+ logToken();
+
+ }, [groupDetails]);
+
+ const groupStatusFromDetails = groupDetails?.status;
+ const { data: paymentStatusRes, refetch: refetchPaymentStatus } = useFetch(
+ `${BASE_URL}payments/group/${groupId}/payment/paid`,
+ undefined,
+ { enabled: !!groupId && (groupStatusFromDetails === 'ACTIVE' || groupStatusFromDetails === 'DELAY') }
+ );
+
+ const paymentStatus = paymentStatusRes?.data;
+
+ const { mutate: generateInvite, isPending: isInviting } = useInvite({
+ onSuccess: (res) => {
+ if (res.data) {
+ Clipboard.setString(res.data);
+ ToastAndroid.show('Invite link copied', ToastAndroid.SHORT);
+ Share.share({ message: `Join my group: ${res.data}` });
+ }
+ },
+ onError: (error: any) => {
+ ToastAndroid.show(error?.message || 'Failed to generate invite', ToastAndroid.SHORT);
+ }
});
- const initializeEditableData = (data: GroupDetails) => {
- setEditableData({
- groupName: data.groupName,
- description: data.description,
- maxParticipants: data.maxParticipants,
- contributionAmount: data.contributionAmount,
- customCycleDays: data.customCycleDays,
- cycleType: data.cycleType,
- latePaymentCharge: data.latePaymentCharge ?? 0,
- visibility: data.visibility,
- lateFeeMode: data.lateFeeMode ?? null,
- lateFeeValue: data.lateFeeValue ?? null
- });
- };
+ // --- MUTATIONS ---
+ const { mutate: updateGroup, isPending: isSaving } = useUpdate(
+ `${BASE_URL}fund/groups/${groupId}/edit`,
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Group updated successfully', ToastAndroid.SHORT);
+ setActiveSection(null);
+ refetchGroup();
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err?.message || 'Failed to update group', ToastAndroid.SHORT);
+ },
+ }
+ );
- const fetchGroupDetails = useCallback(async () => {
- setLoading(true);
- setError(null);
+ const activateGroup = async () => {
+ setIsActivating(true);
try {
- const response = (await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, "GET")) as {
- status: string;
- data?: GroupDetails;
- message?: string;
- };
- if (response.status === "success" && response.data) {
- setGroupDetails(response.data);
- console.log( "chasvgvyi",response.data)
- initializeEditableData(response.data);
+ const credentials = await Keychain.getGenericPassword();
+ const token = credentials ? credentials.password : '';
+ const response = await fetch(`${BASE_URL}fund/group/start?groupId=${groupId}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ ToastAndroid.show('Group activated successfully', ToastAndroid.SHORT);
+ refetchGroup();
} else {
- throw new Error(response.message || "Failed to fetch group data.");
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || 'Activation failed');
}
- } catch (e: any) {
- setError(e.message);
+ } catch (error: any) {
+ ToastAndroid.show(error?.message || 'Activation failed', ToastAndroid.SHORT);
} finally {
- setLoading(false);
- }
- }, [groupId]);
-
- const getPaymentStatus = async () => {
- const response = (await apiCall(`${BASE_URL}payments/group/${groupId}/payment/paid`, "GET")) as {
- status: string;
- data?: { currentCycle: number; allPaid: boolean; selectedUser: any };
- message?: string;
- };
- console.log("selectedUser api response : ", response)
- if (response.status === "success") {
- setPaymentStatus(response.data?.allPaid || false);
- setCurrentCycle(response.data?.currentCycle || null);
- setSelectedUser(response.data);
+ setIsActivating(false);
}
};
- useEffect(() => {
- getPaymentStatus();
- }, []);
-
- useEffect(() => {
- fetchGroupDetails();
- }, [fetchGroupDetails]);
-
- const handleCancel = () => {
- if (groupDetails) initializeEditableData(groupDetails);
- setEditingCard(null);
- };
-
- const handleSave = async () => {
- if (!groupDetails) return;
+ // --- HANDLERS ---
+ const handleSave = () => {
+ if (!editableData || !groupDetails) return;
if (editableData.maxParticipants < groupDetails.totalParticipants) {
- setAlertInfo({
- visible: true,
- title: "Invalid Input",
- message: `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`,
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
+ ToastAndroid.show('Capacity cannot be less than current participants', ToastAndroid.SHORT);
return;
}
- setSaving(true);
- try {
- await apiCall(`${BASE_URL}fund/groups/${groupDetails.id}/edit`, "PUT", editableData);
- setAlertInfo({
- visible: true,
- title: "Success",
- message: "Group has been updated.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "success"
- });
- setEditingCard(null);
- await fetchGroupDetails();
- } catch (e: any) {
- setAlertInfo({
- visible: true,
- title: "Error",
- message: e.message || "An unexpected error occurred.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- } finally {
- setSaving(false);
+ // Construct payload strictly matching user requirements
+ const payloadData: EditGroupPayload = {
+ groupName: editableData.groupName,
+ description: editableData.description,
+ maxParticipants: Number(editableData.maxParticipants || 0),
+ minParticipants: Number(editableData.minParticipants || 0),
+ lateFeeMode: editableData.lateFeeMode || 'DAILY',
+ lateFeeValue: Number(editableData.lateFeeValue || 0),
+ latePaymentCharge: Number(editableData.latePaymentCharge || 0),
+ visibility: editableData.visibility || 'PUBLIC',
+ contributionAmount: Number(editableData.contributionAmount || 0),
+ cycleType: editableData.cycleType || 'MONTHLY',
+ entryFee: Number(editableData.entryFee || 0),
+ customCycleDays: editableData.cycleType === 'CUSTOM' ? Number(editableData.customCycleDays || 0) : null,
+ };
+
+ updateGroup(payloadData);
+ };
+
+ const [refreshing, setRefreshing] = useState(false);
+ const onRefresh = useCallback(() => {
+ setRefreshing(true);
+ const promises = [refetchGroup()];
+ if (groupStatusFromDetails === 'ACTIVE') {
+ promises.push(refetchPaymentStatus());
}
- };
-
- const handleDistribute = async () => {
- navigation.navigate("RandomSelectUserScreen", { groupId, cycleNumber: currentCycle || 0 });
- };
-
- const handleStartGroup = async () => {
- if (!groupDetails) return;
-
- setAlertInfo({
- visible: true,
- title: "Confirm Start",
- message: "Are you sure you want to start this group? This action cannot be undone.",
- alertType: "info",
- buttons: [
- { text: "Cancel", onPress: () => setAlertInfo({ ...alertInfo, visible: false }), style: "cancel" },
- {
- text: "Start",
- onPress: async () => {
- setAlertInfo({ ...alertInfo, visible: false });
- setStarting(true);
- try {
- await apiCall(`${BASE_URL}fund/group/start?groupId=${groupDetails.id}`, "GET");
- setAlertInfo({
- visible: true,
- title: "Success",
- message: "Group has been started successfully.",
- buttons: [
- {
- text: "OK",
- onPress: () => {
- setAlertInfo({ ...alertInfo, visible: false });
- navigation.goBack();
- }
- }
- ],
- alertType: "success"
- });
- await fetchGroupDetails();
- } catch (e: any) {
- setAlertInfo({
- visible: true,
- title: "Error",
- message: e.message || "An unexpected error occurred.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- } finally {
- setStarting(false);
- }
- },
- style: "destructive"
- }
- ]
- });
- };
-
- const handleInvite = async () => {
- if (!groupDetails) return;
- setInviting(true);
- try {
- const response = (await apiCall(`${BASE_URL}invite/generate?groupId=${groupDetails.id}`, "GET")) as { data?: string };
- const inviteLink = response.data;
-
- if (inviteLink) {
- Clipboard.setString(inviteLink);
- setAlertInfo({
- visible: true,
- title: "Link Copied",
- message: "Invitation link copied to clipboard.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "success"
- });
- await Share.share({
- message: `Join my group on CircleCollect! ${inviteLink}`,
- url: inviteLink,
- title: "CircleCollect Group Invitation"
- });
- } else {
- throw new Error("Could not generate invitation link.");
- }
- } catch (error: any) {
- setAlertInfo({
- visible: true,
- title: "Invitation Error",
- message: error.message || "Failed to generate invitation.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- } finally {
- setInviting(false);
- }
- };
-
- const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
- setEditableData(prev => ({ ...prev, [field]: value }));
- };
-
- const pickImage = () => {
- launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
- if (response.didCancel) return;
-
- if (response.errorCode) {
- setAlertInfo({
- visible: true,
- title: "Image Error",
- message: response.errorMessage || "Something went wrong",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
-
- if (!response.assets || response.assets.length === 0) {
- setAlertInfo({
- visible: true,
- title: "No Image",
- message: "No image selected",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
-
- const selectedImage: Asset = response.assets[0];
- const maxSizeKB = 1000;
- const maxSizeBytes = maxSizeKB * 1024;
-
- if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) {
- const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2);
- setAlertInfo({
- visible: true,
- title: "File Too Large",
- message: `Selected image is ${currentSizeKB}KB. Please select an image smaller than ${maxSizeKB}KB.`,
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
-
- if (!selectedImage.uri) {
- setAlertInfo({
- visible: true,
- title: "Invalid File",
- message: "Invalid image file",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
- setSelectedImage(selectedImage.uri);
- });
- };
+ Promise.all(promises).finally(() => setRefreshing(false));
+ }, [refetchGroup, refetchPaymentStatus, groupStatusFromDetails]);
const handleUpdateImage = async () => {
+ if (!selectedImage) return;
+ setImageUpdating(true);
try {
const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- setAlertInfo({
- visible: true,
- title: "Not Logged In",
- message: "You are not logged in.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
- const token = credentials.password;
-
- if (!selectedImage) {
- setAlertInfo({
- visible: true,
- title: "No Image",
- message: "Please select an image first.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
- return;
- }
-
- setImageUpdating(true);
- const fileExtension = selectedImage.split(".").pop();
+ const token = credentials ? credentials.password : '';
+ const fileExtension = selectedImage.split('.').pop();
const formData = new FormData();
- formData.append("file", {
+ formData.append('file', {
uri: selectedImage,
- type: "image/jpeg",
- name: `group_${groupId}_profile.${fileExtension || "jpg"}`
- });
+ type: 'image/jpeg',
+ name: `group_${groupId}.${fileExtension || 'jpg'}`,
+ } as any);
- const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`;
- const response = await fetch(url, {
- method: "PUT",
+ const response = await fetch(`${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`, {
+ method: 'PUT',
headers: {
- "Content-Type": "multipart/form-data",
- "Authorization": `Bearer ${token}`
+ 'Content-Type': 'multipart/form-data',
+ Authorization: `Bearer ${token}`,
},
- body: formData as any
+ body: formData,
});
- if (response?.status === 200) {
- await fetchGroupDetails();
- setAlertInfo({
- visible: true,
- title: "Success",
- message: "Group image uploaded successfully.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "success"
- });
+ if (response.status === 200) {
+ ToastAndroid.show('Image updated', ToastAndroid.SHORT);
+ refetchGroup();
+ setSelectedImage(null);
} else {
- throw new Error("Failed to upload image.");
+ throw new Error('Upload failed');
}
-
- } catch (error: any) {
- setAlertInfo({
- visible: true,
- title: "Upload Error",
- message: error.message || "Error updating group image.",
- buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }],
- alertType: "error"
- });
+ } catch (e) {
+ ToastAndroid.show('Failed to upload image', ToastAndroid.SHORT);
} finally {
setImageUpdating(false);
}
};
- const getProfileImage = () => {
- return selectedImage
- ? selectedImage
- : groupDetails?.imageBlob
- ? `data:image/jpeg;base64,${groupDetails.imageBlob}`
- : "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
+ const handleInvite = () => {
+ generateInvite(groupId);
};
- const renderField = (
- field: keyof EditGroupPayload,
- label: string,
- displayValue: string | number,
- isEditing: boolean
- ) => {
- const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
+ const pickImage = () => {
+ launchImageLibrary({ mediaType: 'photo', quality: 0.5 }, (res) => {
+ if (res.assets?.[0]?.uri) setSelectedImage(res.assets[0].uri);
+ });
+ };
- const getInputForField = () => {
- switch (field) {
- case "groupName":
- return (
- handleUpdateField("groupName", text)}
- />
- );
- case "description":
- return (
- handleUpdateField("description", text)}
- multiline
- />
- );
- case "contributionAmount":
- return (
-
- handleUpdateField(
- "contributionAmount",
- text ? Number(text.replace(/[^0-9]/g, "")) : 0
- )
- }
- keyboardType="numeric"
- />
- );
- case "maxParticipants":
- return (
-
- handleUpdateField(
- "maxParticipants",
- text ? Number(text.replace(/[^0-9]/g, "")) : 0
- )
- }
- keyboardType="numeric"
- />
- );
- case "latePaymentCharge":
- return (
-
- handleUpdateField(
- "latePaymentCharge",
- text ? Number(text.replace(/[^0-9]/g, "")) : 0
- )
- }
- keyboardType="numeric"
- />
- );
- case "cycleType":
- return (
- handleUpdateField("cycleType", itemValue)}
- style={styles.picker}
- >
- {["MONTHLY", "WEEKLY", "QUARTERLY", "CUSTOM"].map(c => (
-
- ))}
-
- );
- case "customCycleDays":
- if (editableData.cycleType !== "CUSTOM") return null;
- return (
-
- handleUpdateField(
- "customCycleDays",
- text ? Number(text.replace(/[^0-9]/g, "")) : 0
- )
- }
- keyboardType="numeric"
- />
- );
- case "visibility":
- return (
- handleUpdateField("visibility", itemValue)}
- style={styles.picker}
- >
-
-
-
- );
- default:
- return null;
- }
- };
+ const handleDistribute = () => {
+ navigation.navigate('RandomSelectUserScreen', { groupId, cycleNumber: paymentStatus?.currentCycle || 0 });
+ };
+
+ const handleFieldChange = (field: keyof EditGroupPayload, value: any) => {
+ if (!editableData) return;
+ const newData = { ...editableData, [field]: value };
+ setEditableData(newData);
+
+ const newChangedFields = new Set(changedFields);
+ if (originalData && originalData[field] !== value) {
+ newChangedFields.add(field);
+ } else {
+ newChangedFields.delete(field);
+ }
+ setChangedFields(newChangedFields);
+ };
+
+ // --- RENDER HELPERS ---
+ const renderField = (field: keyof EditGroupPayload, label: string) => {
+ if (!editableData) return null;
+
+ const isNum = ['contributionAmount', 'latePaymentCharge', 'maxParticipants', 'customCycleDays', 'minParticipants', 'entryFee', 'lateFeeValue'].includes(field);
return (
-
- {label}
- {isEditing ? (
- {getInputForField()}
+
+ {label}
+ {field === 'cycleType' || field === 'visibility' || field === 'lateFeeMode' ? (
+
+ {(field === 'cycleType'
+ ? ['MONTHLY', 'QUARTERLY', 'HALF_YEARLY', 'YEARLY', 'CUSTOM']
+ : field === 'lateFeeMode'
+ ? ['DAILY', 'MONTHLY']
+ : ['PUBLIC', 'PRIVATE']
+ ).map((option) => (
+ handleFieldChange(field, option)}
+ >
+
+ {option}
+
+
+ ))}
+
) : (
- {displayValue}
+ handleFieldChange(field, isNum ? Number(text.replace(/[^0-9.]/g, '')) : text)}
+ keyboardType={isNum ? 'decimal-pad' : 'default'}
+ multiline={field === 'description'}
+ numberOfLines={field === 'description' ? 3 : 1}
+ textAlignVertical={field === 'description' ? 'top' : 'center'}
+ />
)}
);
};
- const renderCard = (
- title: string,
- fields: Array<{ field: keyof EditGroupPayload; label: string; displayValue: () => string | number }>,
- cardKey: EditingCard
- ) => {
- const isEditingThisCard = editingCard === cardKey;
- const isEditable = groupDetails?.status === "PENDING" || groupDetails?.status === "APPROVED";
+ // --- LIST SECTIONS ---
+ const SECTIONS = [
+ { id: 'cover', type: 'cover' },
+ { id: 'stats', type: 'stats' },
+ { id: 'info', type: 'card', title: 'Group Info', section: 'groupInfo' as EditSection },
+ { id: 'financial', type: 'card', title: 'Financial Rules', section: 'financialRules' as EditSection },
+ { id: 'config', type: 'card', title: 'Configuration', section: 'configuration' as EditSection },
+ { id: 'participants', type: 'participants' },
+ { id: 'actions', type: 'actions' },
+ ];
- return (
-
-
- {title}
- {isEditable && !editingCard && (
- setEditingCard(cardKey)} style={styles.editButton}>
- Update
+ const renderItem = ({ item }: { item: any }) => {
+ if (!groupDetails) return null;
+
+ if (item.type === 'cover') {
+ const coverImageUri = selectedImage || (groupDetails.imageUrl ? groupDetails.imageUrl : `https://api.dicebear.com/7.x/shapes/png?seed=${encodeURIComponent(groupDetails.groupName)}`);
+
+ return (
+
+
+
+
+
+ navigation.goBack()} style={styles.iconBtn}>
+
+
+ Manage Group
+ {groupDetails.status === 'APPROVED' ? (
+
+
+
+
+
+ {showInviteTooltip && (
+ setShowInviteTooltip(false)}
+ style={styles.inviteTooltip}
+ >
+ Invite members
+
+
+ )}
+
+ ) : (
+
+ )}
+
+
+
+ {groupDetails.groupName}
+ {groupDetails.description && (
+
+ {groupDetails.description}
+
+ )}
+
+ {groupDetails.status}
+ {groupDetails.visibility}
+
+
+
+
+
+
+
+ {selectedImage && (
+
+
+ Upload Image
+
+
+
+ {imageUpdating ? : }
+
+
+ )}
+
+ );
+ }
+
+ if (item.type === 'stats') {
+ return (
+
+
+ {groupDetails.totalParticipants}
+ Participants
+
+
+
+ ₹{groupDetails.contributionAmount}
+ Contribution
+
+
+
+ {groupDetails.cycleType}
+ Cycle
+
+
+ );
+ }
+
+ if (item.type === 'card') {
+ const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
+ const fields = item.section === 'groupInfo'
+ ? ['groupName', 'description', 'visibility']
+ : item.section === 'financialRules'
+ ? ['contributionAmount', 'entryFee', 'latePaymentCharge', 'lateFeeMode']
+ : ['maxParticipants', 'minParticipants', 'cycleType', ...(groupDetails.customCycleDays ? ['customCycleDays'] : [])];
+
+ const getDisplayValue = (field: string) => {
+ const val = groupDetails[field as keyof typeof groupDetails];
+ if (field.includes('Amount') || field.includes('Fee') || field.includes('Charge') || field.includes('Value')) return `₹${val ?? 0}`;
+ return String(val ?? '-');
+ };
+
+ return (
+ isEditable && setActiveSection(item.section)}
+ activeOpacity={isEditable ? 0.7 : 1}
+ >
+
+ {item.title}
+ {isEditable && }
+
+
+ {fields.map((f: string) => (
+
+ {f.replace(/([A-Z])/g, ' $1').trim()}:
+ {getDisplayValue(f)}
+
+ ))}
+
+
+ );
+ }
+
+ if (item.type === 'participants') {
+ return (
+
+ Participants ({groupDetails.participants.length})
+ {groupDetails.participants.slice(0, 3).map((p: any) => (
+
+ {p.firstName[0]}
+
+ {p.firstName} {p.lastName}
+ {p.role}
+
+
+ ))}
+ {groupDetails.participants.length > 3 && (
+
+ View All Participants
)}
+ );
+ }
- {fields.map(({ field, label, displayValue }) =>
- renderField(field, label, displayValue(), isEditingThisCard)
- )}
+ if (item.type === 'actions') {
+ const isApproved = groupDetails.status === 'APPROVED';
+ const notEnoughParticipants = groupDetails.totalParticipants < groupDetails.minParticipants;
- {isEditingThisCard && (
-
-
- Cancel
+ return (
+
+ {isApproved && notEnoughParticipants && (
+
+
+
+ To start the group, a minimum of {groupDetails.minParticipants} participants are needed.
+
+
+ )}
+
+ {paymentStatus?.cycleStatus === 'PENDING_SELECTION' && (
+
+ Distribute Funds
-
- {saving ? : Save}
+ )}
+ {paymentStatus?.cycleStatus === 'READY_FOR_WITHDRAWAL' && paymentStatus?.selectedUser && (
+ setSelectedUserModel(true)} style={styles.secondaryBtn}>
+ View Winner
-
- )}
-
- );
+ )}
+
+ );
+ }
+ return null;
};
- if (loading)
+ if (isGroupLoading) {
return (
-
-
-
- );
- if (error)
- return (
-
- {error}
-
+
+
+
+
+
+
+
);
+ }
+
if (!groupDetails) return null;
- const isInvitable = groupDetails.status === "APPROVED";
- const canStartGroup = groupDetails.status === "APPROVED" && groupDetails.totalParticipants >= groupDetails.minParticipants;
-
return (
-
-
- Manage Group
- {isInvitable && (
-
- {inviting ? : Invite}
-
- )}
-
-
-
-
-
- {}} />
-
-
-
-
-
-
-
-
-
- {renderCard(
- "Group Info",
- [
- { field: "groupName", label: "Group Name", displayValue: () => groupDetails.groupName },
- { field: "description", label: "Description", displayValue: () => groupDetails.description }
- ],
- "groupInfo"
- )}
-
- {groupDetails.status === "ACTIVE"
- ? renderCard(
- "Financial Rules",
- [
- {
- field: "contributionAmount",
- label: "Contribution",
- displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString("en-IN")}`
- },
- {
- field: "latePaymentCharge",
- label: "Late Fee",
- displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString("en-IN")}`
- },
- {
- field: "maxParticipants",
- label: "Current Participants",
- displayValue: () => `${groupDetails.totalParticipants}`
- }
- ],
- "financialRules"
- )
- : renderCard(
- "Financial Rules",
- [
- {
- field: "contributionAmount",
- label: "Contribution",
- displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString("en-IN")}`
- },
- {
- field: "maxParticipants",
- label: "Max Participants",
- displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`
- },
- {
- field: "minParticipants" as keyof EditGroupPayload,
- label: "Min Participants",
- displayValue: () => `${groupDetails.minParticipants}`
- },
- {
- field: "latePaymentCharge",
- label: "Late Fee",
- displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString("en-IN")}`
- }
- ],
- "financialRules"
- )}
-
- {renderCard(
- "Configuration",
- [
- { field: "cycleType", label: "Payment Cycle", displayValue: () => groupDetails.cycleType },
- ...(groupDetails.cycleType === "CUSTOM"
- ? [
- {
- field: "customCycleDays" as keyof EditGroupPayload,
- label: "Custom Days",
- displayValue: () => `${groupDetails.customCycleDays} days`
- }
- ]
- : []),
- { field: "visibility", label: "Visibility", displayValue: () => groupDetails.visibility }
- ],
- "configuration"
- )}
-
-
- Participants ({groupDetails.totalParticipants})
- {groupDetails.participants.length > 0 ? (
- groupDetails.participants.map(p => (
-
-
-
- {`${p.firstName} ${p.lastName || ""}`.trim()}
- {p.role === "ORGANIZER" && (
-
- Organizer
-
- )}
-
- {p.mobile}
-
-
- ))
- ) : (
- No participants yet.
- )}
-
-
-
- {/* Fixed-Position Action Buttons */}
- {canStartGroup && (
-
-
- {starting ? : Activate Group}
-
-
- )}
-
- {selectedUser?.cycleStatus !== "PENDING_PAYMENTS" &&
- (groupDetails.status === "ACTIVE" || groupDetails.status === "DELAY" )&&
- selectedUser?.cycleStatus === "PENDING_SELECTION" && (
-
-
- {starting ? : Distribute}
-
-
- )}
-
- {selectedUser?.selectedUser != null && selectedUser?.cycleStatus === "READY_FOR_WITHDRAWAL" && (
-
- setSelectedUserModel(true)} />
-
- )}
-
- {selectedUserModal && setSelectedUserModel(false)} />}
-
-
+ item.id}
+ contentContainerStyle={{ paddingBottom: 150 }}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
/>
+
+ {/* Floating Activate Button */}
+ {groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants && (
+
+ activateGroup()}
+ style={styles.floatingActivateBtn}
+ disabled={isActivating}
+ >
+ {isActivating ? (
+
+ ) : (
+ <>
+
+ Activate Group
+
+ >
+ )}
+
+
+ )}
+
+ {/* --- EDIT MODAL --- */}
+ setActiveSection(null)}
+ >
+
+
+
+
+ {activeSection === 'groupInfo' ? 'Group Information' :
+ activeSection === 'financialRules' ? 'Financial Rules' : 'Configuration'}
+
+ setActiveSection(null)} style={styles.closeBtn}>
+
+
+
+
+
+
+ {activeSection === 'groupInfo' && (
+ <>
+ {renderField('groupName', 'Group Name')}
+ {renderField('description', 'Description')}
+ {renderField('visibility', 'Visibility')}
+ >
+ )}
+ {activeSection === 'financialRules' && (
+ <>
+ {renderField('contributionAmount', 'Contribution Amount')}
+ {renderField('entryFee', 'Entry Fee')}
+ {renderField('latePaymentCharge', 'Late Payment Charge')}
+ {renderField('lateFeeMode', 'Late Fee Mode')}
+ >
+ )}
+ {activeSection === 'configuration' && (
+ <>
+ {renderField('maxParticipants', 'Max Participants')}
+ {renderField('minParticipants', 'Min Participants')}
+ {renderField('cycleType', 'Payment Cycle')}
+ {editableData?.cycleType === 'CUSTOM' && renderField('customCycleDays', 'Cycle Days')}
+ >
+ )}
+
+
+
+
+
+ {isSaving ? : Save Changes}
+
+
+
+
+
+
+ {selectedUserModal && (
+ setSelectedUserModel(false)}
+ />
+ )}
);
};
-const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: "#F3F4F6" },
- contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
- centered: { flex: 1, justifyContent: "center", alignItems: "center" },
- errorText: { color: "#EF4444", fontSize: 16, textAlign: "center" },
- headerContainer: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingHorizontal: 16,
- paddingTop: 16,
- paddingBottom: 8,
- borderBottomWidth: 1,
- borderBottomColor: "#E5E7EB"
- },
- avatarContainer: { position: "relative", marginVertical: 16 },
- avatar: {
- width: 120,
- height: 120,
- borderRadius: 60,
- borderWidth: 4,
- borderColor: "#e2e8f0"
- },
- cameraIcon: {
- position: "absolute",
- bottom: 0,
- backgroundColor: colors.pastelOrange,
- borderRadius: 20,
- width: 40,
- height: 40,
- justifyContent: "center",
- alignItems: "center",
- borderWidth: 3,
- borderColor: "#fff",
- elevation: 2
- },
- headerTitle: { fontSize: 28, fontWeight: "bold", color: "#111827" },
- card: { backgroundColor: "#FFFFFF", borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: "#374151", shadowOpacity: 0.05, shadowRadius: 10 },
- cardHeader: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingBottom: 10,
- marginBottom: 12
- },
- cardTitle: { fontSize: 18, fontWeight: "bold", color: "#1F2937" },
- editButton: { padding: 8, backgroundColor: colors.pastelOrange, borderRadius: 15, elevation: 5 },
- editButtonText: { color: colors.black, fontSize: 15, fontWeight: "bold" },
- fieldContainer: { borderBottomWidth: 1, borderBottomColor: "#F3F4F6", paddingBottom: 12, marginBottom: 12 },
- infoLabel: { fontSize: 16, color: "#6B7280", marginBottom: 4 },
- infoValue: { fontSize: 16, fontWeight: "600", color: "#111827", textTransform: "capitalize" },
- input: { backgroundColor: "#F9FAFB", borderWidth: 1, borderColor: "#D1D5DB", borderRadius: 10, padding: 14, fontSize: 16, color: "#111827" },
- textArea: { height: 100, textAlignVertical: "top" },
- picker: { backgroundColor: "#F9FAFB", borderWidth: 1, borderColor: "#D1D5DB", borderRadius: 10 ,color:colors.black},
- participantRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: "#F3F4F6" },
- participantName: { fontSize: 16, fontWeight: "600", color: "#111827" },
- participantMobile: { fontSize: 14, color: "#6B7280", marginTop: 4 },
- organizerBadge: { backgroundColor: "#FEF3C7", paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 },
- organizerBadgeText: { color: "#92400E", fontSize: 12, fontWeight: "500" },
- noParticipantsText: { fontSize: 15, color: "#6B7280", textAlign: "center", paddingVertical: 20 },
- cardActions: { flexDirection: "row", justifyContent: "flex-end", marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: "#E5E7EB" },
- button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: "center", justifyContent: "center" },
- saveButton: { backgroundColor: colors.pastelOrange },
- cancelButton: { backgroundColor: "#E5E7EB" },
- inviteButton: { backgroundColor: colors.pastelOrange },
- buttonText: { color: colors.black, fontSize: 15, fontWeight: "600" },
- floatingButtonContainer: {
- position: "absolute",
- bottom: 85,
+const getStyles = (theme: any) => StyleSheet.create({
+ // Cover
+ coverSection: { height: 280, position: 'relative' },
+ coverImage: { width: '100%', height: '100%', resizeMode: 'cover' },
+ coverOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)' },
+ coverHeader: { position: 'absolute', top: 30, left: 20, right: 20, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
+ iconBtn: { padding: 8, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 20 },
+ headerTitle: { color: '#fff', fontSize: 18, fontFamily: FontsFamily.nunitoBold },
+ coverContent: { position: 'absolute', bottom: 30, left: 20, right: 20 },
+ groupNameTitle: { color: '#fff', fontSize: 24, fontFamily: FontsFamily.nunitoBold, marginBottom: 8 },
+ descContainer: { backgroundColor: 'rgba(255,255,255,0.15)', padding: 10, borderRadius: 8, borderLeftWidth: 3, borderLeftColor: '#fff', marginBottom: 12 },
+ descText: { color: '#eee', fontSize: 14, fontFamily: FontsFamily.nunitoRegular },
+ badgesRow: { flexDirection: 'row', gap: 10 },
+ badge: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
+ badgeText: { color: '#fff', fontSize: 12, fontWeight: 'bold' },
+ cameraBtn: { position: 'absolute', bottom: -20, right: 30, width: 44, height: 44, borderRadius: 22, backgroundColor: '#fff', elevation: 4, justifyContent: 'center', alignItems: 'center' },
+ uploadFloatBtn: { position: 'absolute', bottom: 40, right: 30, width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' },
+ uploadFloatWrapper: { position: 'absolute', bottom: 40, right: 30, alignItems: 'center' },
+ uploadTooltip: { position: 'absolute', bottom: 58, right: 0, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, elevation: 6, zIndex: 10, minWidth: 100, alignItems: 'center' },
+ uploadTooltipText: { fontSize: 12, fontFamily: FontsFamily.nunitoBold },
+ uploadTooltipArrow: { position: 'absolute', bottom: -5, width: 0, height: 0, borderLeftWidth: 6, borderRightWidth: 6, borderTopWidth: 6, borderLeftColor: 'transparent', borderRightColor: 'transparent' },
+ uploadFloatBtnInner: { width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' },
+
+ // Stats
+ statsContainer: { flexDirection: 'row', backgroundColor: theme.colors.card, margin: 20, marginTop: 30, borderRadius: 16, padding: 20, elevation: 2, justifyContent: 'space-between', alignItems: 'center' },
+ statBox: { alignItems: 'center', flex: 1 },
+ statValue: { fontSize: 18, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
+ statLabel: { fontSize: 12, fontFamily: FontsFamily.nunitoRegular, color: theme.colors.textSecondary },
+ statDivider: { width: 1, height: 30, backgroundColor: '#eee' },
+
+ // Cards
+ cardContainer: { backgroundColor: theme.colors.card, marginHorizontal: 20, marginBottom: 16, borderRadius: 16, padding: 16, elevation: 2 },
+ cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' },
+ cardTitle: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
+ cardBody: {},
+ cardFieldText: { fontSize: 14, color: theme.colors.textPrimary, marginBottom: 6, fontFamily: FontsFamily.nunitoMedium },
+ moreText: { fontSize: 12, color: theme.colors.textSecondary, marginTop: 4, fontStyle: 'italic' },
+
+ // Participants
+ participantRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
+ avatarPlaceholder: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#6366F1', justifyContent: 'center', alignItems: 'center', marginRight: 12 },
+ pName: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
+ pRole: { fontSize: 12, color: theme.colors.textSecondary },
+ viewAllBtn: { paddingVertical: 8, alignItems: 'center', borderTopWidth: 1, borderTopColor: '#f0f0f0', marginTop: 8 },
+ viewAllText: { color: theme.colors.primary, fontFamily: FontsFamily.nunitoBold },
+
+ // Actions
+ actionsContainer: { padding: 20 },
+ primaryBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center', elevation: 2, marginBottom: 12 },
+ btnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold },
+ secondaryBtn: { backgroundColor: '#f0f0f0', padding: 16, borderRadius: 12, alignItems: 'center' },
+ secondaryBtnText: { color: theme.colors.textPrimary, fontSize: 16, fontFamily: FontsFamily.nunitoBold },
+
+ // Modal
+ modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
+ modalContainer: { backgroundColor: theme.colors.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '80%', padding: 20 },
+ modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
+ modalTitle: { fontSize: 20, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
+ closeBtn: { padding: 8, backgroundColor: '#f5f5f5', borderRadius: 20 },
+
+ // New Styles
+ hintContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFBEB', padding: 12, borderRadius: 12, marginBottom: 16, borderLeftWidth: 4, borderLeftColor: '#F59E0B' },
+ hintText: { color: '#B45309', fontSize: 13, fontFamily: FontsFamily.nunitoMedium, marginLeft: 8, flex: 1 },
+
+ floatingBtnContainer: {
+ position: 'absolute',
+ bottom: 90,
left: 20,
right: 20,
- alignItems: "center",
- zIndex: 1
+ alignItems: 'center',
},
- activateButton: {
- backgroundColor: colors.pastelOrange,
- borderRadius: 14,
+ floatingActivateBtn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: theme.colors.primary,
paddingVertical: 16,
- paddingHorizontal: 32,
+ paddingHorizontal: 24,
+ borderRadius: 16,
+ width: '100%',
elevation: 8,
- shadowColor: "#000",
+ shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
- shadowRadius: 4,
- width: "90%",
- alignItems: "center"
+ shadowRadius: 8,
},
- activateButtonText: {
- color: "#000000ff",
+ // Invite & Tooltip
+ inviteContainer: {
+ position: 'relative',
+ alignItems: 'center'
+ },
+ inviteTooltip: {
+ position: 'absolute',
+ top: 45,
+ right: -10,
+ backgroundColor: '#d5d5d5ff',
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 8,
+ width: 140,
+ zIndex: 100,
+ elevation: 10,
+ },
+ tooltipText: {
+ color: '#494949ff',
+ fontSize: 11,
+ fontFamily: FontsFamily.nunitoSemiBold,
+ textAlign: 'center',
+ },
+ tooltipArrow: {
+ position: 'absolute',
+ top: -6,
+ right: 20,
+ width: 0,
+ height: 0,
+ borderLeftWidth: 6,
+ borderRightWidth: 6,
+ borderBottomWidth: 6,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ borderBottomColor: '#334155',
+ },
+ floatingBtnText: {
+ color: '#fff',
fontSize: 18,
- fontWeight: "bold"
- }
+ fontFamily: FontsFamily.nunitoBold,
+ letterSpacing: 0.5,
+ },
+ modalContent: { flex: 1 },
+ modalFieldContainer: { marginBottom: 16 },
+ modalLabel: { fontSize: 14, color: theme.colors.textSecondary, marginBottom: 8, fontFamily: FontsFamily.nunitoBold },
+ modalInput: { borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 12, padding: 14, fontSize: 16, color: theme.colors.textPrimary, backgroundColor: '#f9f9f9', fontFamily: FontsFamily.nunitoMedium },
+ chipContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
+ chip: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, backgroundColor: '#f0f0f0', borderWidth: 1, borderColor: '#e0e0e0' },
+ activeChip: { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary },
+ chipText: { fontSize: 14, fontFamily: FontsFamily.nunitoMedium, color: theme.colors.textSecondary },
+ activeChipText: { color: theme.colors.white, fontFamily: FontsFamily.nunitoBold },
+ modalFooter: { borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 16 },
+ saveBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center' },
+ saveBtnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold }
});
export default EditGroupScreen;
diff --git a/src/screens/management/ManageJoinedGroup.tsx b/src/screens/management/ManageJoinedGroup.tsx
index 0f9c7c0..ae591ef 100644
--- a/src/screens/management/ManageJoinedGroup.tsx
+++ b/src/screens/management/ManageJoinedGroup.tsx
@@ -15,4 +15,4 @@ export default function ManageJoinedGroup({ route }: ManageJoinedGroupProps) {
)
}
-const styles = StyleSheet.create({})
\ No newline at end of file
+const styles = StyleSheet.create({})
diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx
index 8ea888a..72c8291 100644
--- a/src/screens/management/ManagementScreen.tsx
+++ b/src/screens/management/ManagementScreen.tsx
@@ -1,178 +1,400 @@
- // src/screens/ManagementScreen.tsx
-import React, { useState, useCallback, FC, useEffect } from "react";
+import React, { useState, useMemo, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
- ActivityIndicator,
- Alert,
+ ScrollView,
ToastAndroid,
-} from "react-native";
-import { useNavigation, useFocusEffect, useRoute, RouteProp } from "@react-navigation/native";
-import { StackNavigationProp } from "@react-navigation/stack";
-import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls";
-import colors from "../../components/colors";
-import AppView from "../../components/AppView";
-import { apiCall } from "../../utils/ApiHelper";
-import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types';
-import { MyGroupCard } from "../../components/MyGroupCard";
-import { JoinedGroupCard } from "../../components/JoinedGroupCard";
-import { RequestCard } from "../../components/RequestCard";
-import { useSelector } from "react-redux";
-import { UserRequested } from "../../components/UserRequested";
-import { RootTabParamList } from "../../navigation/BottomTabs";
-import { RootStackParamList } from "../../navigation/AppNavigator";
+ RefreshControl,
+} from 'react-native';
+import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
+import { useSelector } from 'react-redux';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import LucideIcon from '../../components/common/LucideIcon';
+import {
+ APPROVE_REQUEST_URL,
+ DECLINE_REQUEST_URL,
+ GET_JOINED_GROUP_URL,
+ MY_GROUPS_URL,
+ REQUESTS_URL,
+ USER_REQUEST
+} from '../../utils/ApiUrls';
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { useUpdate } from '../../hooks/core/useUpdate';
+import { MyGroup, JoinRequest } from '../../types';
+import MyGroupCard from '../../components/MyGroupCard';
+import { JoinedGroupCard } from '../../components/JoinedGroupCard';
+import { RequestCard } from '../../components/RequestCard';
+import { UserRequested } from '../../components/UserRequested';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
+import FontsFamily from '../../constents/FontsFamily';
+import { RootTabParamList } from '../../navigation/BottomTabs';
-// --- TYPE DEFINITIONS ---
-type GroupManagementNavProp = StackNavigationProp;
type ManagementRouteProp = RouteProp;
-type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
+type ActiveTab = 'My Groups' | 'Joined Groups' | 'Requests' | 'Your Requests';
+type GroupStatusFilter = 'PENDING' | 'APPROVED' | 'ACTIVE' | 'DELAY' | 'COMPLETED' | 'REJECTED' | 'CANCELLED';
-const ManagementScreen: FC = () => {
+const GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
+ 'PENDING',
+ 'APPROVED',
+ 'ACTIVE',
+ 'DELAY',
+ 'COMPLETED',
+ 'REJECTED',
+ 'CANCELLED',
+];
+
+const JOINED_GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
+ 'ACTIVE',
+ 'DELAY',
+ 'COMPLETED',
+];
+
+const getListFromResponse = (response: any): T[] => {
+ if (!response) return [];
+ if (Array.isArray(response?.data?.content)) return response.data.content;
+ if (Array.isArray(response?.data)) return response.data;
+ return [];
+};
+
+const ManagementScreen: React.FC = () => {
+ const theme = useTheme();
const user = useSelector((state: any) => state.user);
- const navigation = useNavigation();
+ const navigation = useNavigation();
const route = useRoute();
- const [activeTab, setActiveTab] = useState(user.role === "USER" ? "Joined Groups" : "My Groups");
- const [myGroups, setMyGroups] = useState([]);
- const [joinedGroups, setJoinedGroups] = useState([]);
- const [requests, setRequests] = useState([]);
- const [userRequest, setUserRequest] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState(
+ user.role === 'ORGANIZER' ? 'My Groups' : 'Joined Groups'
+ );
+ const [myGroupStatusFilter, setMyGroupStatusFilter] = useState('APPROVED');
+ const [joinedGroupStatusFilter, setJoinedGroupStatusFilter] = useState('ACTIVE');
useEffect(() => {
- const passedTab = route.params?.actionTab;
- if (passedTab) {
- const availableTabs = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
- if (availableTabs.includes(passedTab)) {
- setActiveTab(passedTab);
- }
- }
- }, [route.params?.actionTab, user.role]);
+ if (!route.params?.actionTab) return;
+ const routeTab = route.params.actionTab as ActiveTab;
+ setActiveTab(routeTab);
+ }, [route.params?.actionTab]);
+ const myGroupsParams = useMemo(
+ () => ({ status: myGroupStatusFilter, page: 0, size: 10 }),
+ [myGroupStatusFilter]
+ );
- const fetchData = useCallback(async () => {
+ const joinedGroupsParams = useMemo(
+ () => ({ status: joinedGroupStatusFilter, page: 0, size: 10 }),
+ [joinedGroupStatusFilter]
+ );
- if (loading) return;
+ // Data Fetching
+ const { data: myGroupsRes, isLoading: myLoading, refetch: refetchMy } = useFetch(
+ MY_GROUPS_URL,
+ myGroupsParams,
+ { enabled: user.role === 'ORGANIZER' }
+ );
+ const { data: joinedRes, isLoading: joinedLoading, refetch: refetchJoined } = useFetch(
+ GET_JOINED_GROUP_URL,
+ joinedGroupsParams
+ );
+ const { data: requestsRes, isLoading: requestsLoading, refetch: refetchRequests } = useFetch(
+ REQUESTS_URL,
+ undefined,
+ { enabled: user.role === 'ORGANIZER' }
+ );
+ const { data: userReqRes, isLoading: userReqLoading, refetch: refetchUserReq } = useFetch(
+ USER_REQUEST,
+ undefined,
+ { enabled: user.role !== 'ORGANIZER' }
+ );
- setLoading(true);
- setError(null);
- try {
- if (user.role === "ORGANIZER") {
- const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
- apiCall(MY_GROUPS_URL, "GET") as Promise>,
- apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>,
- apiCall(REQUESTS_URL, "GET") as Promise>,
- ]);
- setMyGroups(myGroupsRes.data || []);
- setJoinedGroups(joinedGroupsRes.data || []);
- setRequests(requestsRes.data || []);
- } else {
- const [joinedGroupsRes, userRequestRes] = await Promise.all([
- apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>,
- apiCall(USER_REQUEST, "GET") as Promise>,
- ]);
- console.log("GET_JOINED_GROUP_URL",joinedGroupsRes)
- setJoinedGroups(joinedGroupsRes.data || []);
- setUserRequest(userRequestRes.data || []);
- }
- } catch (e) {
- console.error("Fetch Data Error:", e);
- setError("Failed to fetch data. Please pull to refresh.");
- } finally {
- setLoading(false);
- }
+ const { mutate: updateRequest } = useUpdate('');
- }, [user.role]);
+ const myGroups = useMemo(() => getListFromResponse(myGroupsRes), [myGroupsRes]);
+ const joinedGroups = useMemo(() => getListFromResponse(joinedRes), [joinedRes]);
+ const incomingRequests = useMemo(() => getListFromResponse(requestsRes), [requestsRes]);
+ const myRequests = useMemo(() => getListFromResponse(userReqRes), [userReqRes]);
+ const [refreshing, setRefreshing] = useState(false);
+ const onRefresh = useCallback(() => {
+ setRefreshing(true);
+ Promise.all([
+ refetchMy(),
+ refetchJoined(),
+ refetchRequests(),
+ refetchUserReq(),
+ ]).finally(() => {
+ setRefreshing(false);
+ });
+ }, [refetchMy, refetchJoined, refetchRequests, refetchUserReq]);
useFocusEffect(
useCallback(() => {
- fetchData();
- }, [fetchData])
+ onRefresh();
+ }, [])
);
- const handleViewGroup = (item: JoinedGroup) => {
+ const tabs: ActiveTab[] = useMemo(() =>
+ user.role === 'ORGANIZER'
+ ? ['My Groups', 'Joined Groups', 'Requests']
+ : ['Joined Groups', 'Your Requests'],
+ [user.role]);
+ const handleApprove = (req: JoinRequest) => {
+ updateRequest({ resource: `${APPROVE_REQUEST_URL}${req.requestId}/approve`, payload: {} } as any, {
+ onSuccess: () => {
+ ToastAndroid.show('Request approved successfully', ToastAndroid.SHORT);
+ refetchRequests();
+ },
+ onError: (err) => ToastAndroid.show(err.message || 'Failed to approve', ToastAndroid.SHORT),
+ });
};
-
- const handleAction = async (url: string, successMessage: string, failureMessage: string) => {
- try {
- // No need to set loading here if the whole screen is already loading
- const res = await apiCall(url, "PUT", {}) as ApiResponse | null;
- if (res && res.statusCode === 200) {
- ToastAndroid.show("Success", ToastAndroid.SHORT);
- fetchData(); // Refresh data after action
- } else {
- throw new Error(res?.message || failureMessage);
- }
- } catch (err: any) {
- console.error("Action Error:", err);
- ToastAndroid.show(err.message || failureMessage, ToastAndroid.SHORT);
- }
+
+ const handleDecline = (req: JoinRequest) => {
+ updateRequest({ resource: `${DECLINE_REQUEST_URL}${req.requestId}/reject?reason=/`, payload: {} } as any, {
+ onSuccess: () => {
+ ToastAndroid.show('Request declined', ToastAndroid.SHORT);
+ refetchRequests();
+ },
+ onError: (err) => ToastAndroid.show(err.message || 'Failed to decline', ToastAndroid.SHORT),
+ });
};
- const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", `${req.userName} has been added.`, "Failed to approve request.");
- const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/", `Request from ${req.userName} has been declined.`, "Failed to decline request.");
const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
- const renderContent = () => {
- if (error) return {error};
-
- if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
- return ;
- }
+ const renderSkeletons = () => (
+
+ {[1, 2, 3].map(i => (
+
+ ))}
+
+ );
+ const getFilteredData = () => {
switch (activeTab) {
- case "My Groups":
- return } keyExtractor={(item) => item.id} ListEmptyComponent={You haven't created any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
- case "Joined Groups":
- return {
- handleViewGroup(item)}} />} keyExtractor={(item) => item.id} ListEmptyComponent={You haven't joined any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
- case "Requests":
- return } keyExtractor={(item) => item.requestId} ListEmptyComponent={No pending join requests.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
- case "Your Requests":
- return } keyExtractor={(item) => item.requestId} ListEmptyComponent={You haven't sent any join requests.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
- default: return null;
+ case 'My Groups': return myGroups;
+ case 'Joined Groups': return joinedGroups;
+ case 'Requests': return incomingRequests;
+ case 'Your Requests': return myRequests;
+ default: return [];
}
};
- const tabs: ActiveTab[] = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
+ const isLoading = myLoading || joinedLoading || requestsLoading || userReqLoading;
return (
-
+
- Group Management
+ Account Hub
+ Manage your circles and requests
-
+
+
{tabs.map((tab) => (
- setActiveTab(tab)}>
- {tab}
+ setActiveTab(tab)}
+ style={[
+ styles.tab,
+ activeTab === tab && { backgroundColor: theme.colors.card, borderRadius: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 }
+ ]}
+ >
+
+ {tab}
+
))}
-
- {renderContent()}
-
+
+ {user.role === 'ORGANIZER' && activeTab === 'My Groups' && (
+
+
+ {GROUP_STATUS_FILTERS.map((status, index) => {
+ const isActive = myGroupStatusFilter === status;
+ return (
+ setMyGroupStatusFilter(status)}
+ style={[
+ styles.statusFilterChip,
+ index !== GROUP_STATUS_FILTERS.length - 1 && styles.statusFilterChipSpaced,
+ {
+ borderColor: isActive ? theme.colors.primary : theme.colors.border,
+ backgroundColor: isActive ? theme.colors.primarySoft : theme.colors.surface,
+ },
+ ]}
+ >
+
+ {status}
+
+
+ );
+ })}
+
+
+ )}
+
+ {activeTab === 'Joined Groups' && (
+
+
+ {JOINED_GROUP_STATUS_FILTERS.map((status, index) => {
+ const isActive = joinedGroupStatusFilter === status;
+ return (
+ setJoinedGroupStatusFilter(status)}
+ style={[
+ styles.statusFilterChip,
+ index !== JOINED_GROUP_STATUS_FILTERS.length - 1 && styles.statusFilterChipSpaced,
+ {
+ borderColor: isActive ? theme.colors.primary : theme.colors.border,
+ backgroundColor: isActive ? theme.colors.primarySoft : theme.colors.surface,
+ },
+ ]}
+ >
+
+ {status}
+
+
+ );
+ })}
+
+
+ )}
+
+ String(item.id || item.requestId || index)}
+ renderItem={({ item }) => {
+ if (activeTab === 'My Groups') return ;
+ if (activeTab === 'Joined Groups') return ;
+ if (activeTab === 'Requests') return ;
+ if (activeTab === 'Your Requests') return ;
+ return null;
+ }}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ !isLoading ? (
+
+
+ Nothing to show here
+
+ ) : renderSkeletons()
+ }
+ />
);
};
const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: '#f5f6fa' },
- header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
- title: { fontSize: 22, fontWeight: "600"},
- segmentedControl: { flexDirection: 'row',borderRadius: 10,marginBottom: 20, padding: 3 },
- segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
- segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
- segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
- segmentTextActive: { color: colors.textDark },
- contentArea: { flex: 1},
- loader: { marginTop: 50 },
- emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
+ header: {
+ paddingHorizontal: 14,
+ paddingTop: 10,
+ paddingBottom: 12,
+ zIndex: 10,
+ },
+ title: {
+ fontSize: responsiveFontSize(3.2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 4,
+ },
+ tabContainer: {
+ flexDirection: 'row',
+ marginHorizontal: 12,
+ padding: 5,
+ borderRadius: 16,
+ marginBottom: 12,
+ zIndex: 10,
+ },
+ tab: {
+ flex: 1,
+ paddingVertical: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ tabText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ statusFilterContainer: {
+ minHeight: 46,
+ maxHeight: 46,
+ marginBottom: 10,
+ },
+ statusFilterRow: {
+ paddingHorizontal: 12,
+ alignItems: 'center',
+ },
+ statusFilterChip: {
+ borderWidth: 1,
+ borderRadius: 999,
+ minHeight: 34,
+ paddingHorizontal: 14,
+ paddingVertical: 0,
+ justifyContent: 'center',
+ alignSelf: 'center',
+ },
+ statusFilterChipSpaced: {
+ marginRight: 8,
+ },
+ statusFilterText: {
+ fontSize: responsiveFontSize(1.25),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ listContent: {
+ paddingBottom: 86,
+ },
+ skeletonContainer: {
+ paddingHorizontal: 12,
+ },
+ emptyState: {
+ alignItems: 'center',
+ marginTop: 64,
+ },
+ emptyText: {
+ marginTop: 16,
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
});
export default ManagementScreen;
diff --git a/src/screens/management/ManagementTabNavigation.tsx b/src/screens/management/ManagementTabNavigation.tsx
index 1703e63..6214c66 100644
--- a/src/screens/management/ManagementTabNavigation.tsx
+++ b/src/screens/management/ManagementTabNavigation.tsx
@@ -2,42 +2,56 @@ import React from 'react';
import { createBottomTabNavigator, BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { Ionicons } from '@react-native-vector-icons/ionicons';
+import LucideIcon from '../../components/common/LucideIcon';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
-import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
-import colors from '../../components/colors';
-
+import { RootStackParamList } from '../../navigation/AppNavigator';
+import { useTheme } from '../../hooks/ui/useTheme';
+import FontsFamily from '../../constents/FontsFamily';
// --- Screen Imports ---
import EditGroupScreen from './EditGroupScreen';
import Payments from './Payments';
import Member from './Member';
-
// --- Type Definitions ---
type Props = NativeStackScreenProps;
const Tab = createBottomTabNavigator();
-
// --- Icon Mapping ---
-const iconMap: { [key: string]: string } = {
- EditGroup: 'trail-sign-outline',
- Payments: 'trending-up',
+const iconMap: { [key: string]: any } = {
+ EditGroup: 'settings-outline',
+ Payments: 'card-outline',
Member: 'people-outline',
};
-
// --- Custom Floating Tab Bar Component ---
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
const insets = useSafeAreaInsets();
+ const theme = useTheme();
return (
- 0 ? insets.bottom : 16 }]}>
+
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = options.tabBarLabel?.toString() ?? options.title ?? route.name;
const isFocused = state.index === index;
-
+
const iconName = iconMap[route.name] || 'help-circle-outline';
const onPress = () => {
@@ -52,33 +66,38 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
}
};
- const onLongPress = () => {
- navigation.emit({
- type: 'tabLongPress',
- target: route.key,
- });
- };
-
return (
-
-
- {label}
-
+ {isFocused && (
+
+ {label}
+
+ )}
);
})}
@@ -86,7 +105,6 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
);
};
-
// --- Main Tab Navigator Component ---
const ManagementTabNavigation: React.FC = ({ route }) => {
const { groupId } = route.params;
@@ -126,50 +144,29 @@ const ManagementTabNavigation: React.FC = ({ route }) => {
);
};
-
-// --- Styles ---
const styles = StyleSheet.create({
tabBarContainer: {
flexDirection: 'row',
position: 'absolute',
- left: 20, // Adjusted for better visual balance
- right: 20, // Adjusted for better visual balance
- backgroundColor: 'white',
- borderRadius: 30,
- paddingVertical: 8,
- paddingHorizontal: 8, // Added padding
+ left: 16,
+ right: 16,
+ paddingVertical: 6,
+ paddingHorizontal: 6,
alignItems: 'center',
- justifyContent: 'space-around',
- // iOS Shadow
- shadowColor: '#000',
- shadowOffset: {
- width: 0,
- height: 4,
- },
- shadowOpacity: 0.1,
- shadowRadius: 10,
- // Android Shadow
- elevation: 8,
+ justifyContent: 'space-between',
},
tabItem: {
- flex: 1, // Allow items to fill the space
+ flex: 1,
+ flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
- paddingVertical: 10, // Increased padding for a better touch area
- borderRadius: 25,
- shadowColor: '#000',
- shadowOffset: {
- width: 0,
- height: 4,
- },
- shadowOpacity: 0.1,
- shadowRadius: 10, // This is for the background color highlight
+ paddingVertical: 9,
+ gap: 8,
+ marginHorizontal: 4,
},
- // The 'activeTab' style is no longer needed as the logic is handled inline
tabLabel: {
fontSize: 12,
- marginTop: 4,
- fontWeight: '600',
+ fontFamily: FontsFamily.nunitoBold,
},
});
diff --git a/src/screens/management/Member.tsx b/src/screens/management/Member.tsx
index 40e724b..cfaf0eb 100644
--- a/src/screens/management/Member.tsx
+++ b/src/screens/management/Member.tsx
@@ -1,23 +1,20 @@
-import React, { useEffect, useState, useCallback } from 'react';
+import React from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
- ActivityIndicator,
- SafeAreaView,
- Image,
- TouchableOpacity,
- Alert,
+ RefreshControl,
} from 'react-native';
-import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming you use Expo or have vector icons installed
-import { apiCall } from '../../utils/ApiHelper'; // Adjust path to your apiCall utility
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
import AppView from '../../components/AppView';
-import colors from '../../components/colors';
-import { RouteProp, useRoute } from '@react-navigation/native';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
import { BASE_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
-// --- Type Definitions ---
interface Member {
id: string;
userId: number;
@@ -31,270 +28,190 @@ interface Member {
role: 'USER' | 'ORGANIZER';
}
-interface ApiResponse {
- message: string;
- status: string;
- statusCode: number;
- data: Member[];
-}
-type RootStackParamList = {
- Member: { groupId: string }; // or number, depending on your data type
-};
+const MemberScreen = ({ route }: any) => {
+ const theme = useTheme();
+ const { groupId } = route?.params || {};
-type MemberScreenRouteProp = RouteProp;
+ const { data: membersRes, isLoading, refetch } = useFetch(
+ `${BASE_URL}fund/group/all/member?groupId=${groupId}`,
+ { enabled: !!groupId }
+ );
-interface Props {
- route: MemberScreenRouteProp;
-}
+ const members: Member[] = membersRes?.data || [];
-// --- Main Screen Component ---
-// This assumes the component receives `groupId` from a navigator (e.g., Tab Navigator)
-const MemberScreen = ({ route }: Props) => {
-
- const { groupId } = route.params;
- const [members, setMembers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const fetchMembers = useCallback(async () => {
- if (!groupId) {
- setError('Group ID is missing.');
- setLoading(false);
- return;
- }
-
- setLoading(true);
- try {
- const response = await apiCall(
- `${BASE_URL}fund/group/all/member?groupId=${groupId}`,
- 'GET'
- );
-
- if (response && response.status === 'success') {
- setMembers(response.data);
- setError(null);
- } else {
- throw new Error(response?.message || 'Failed to fetch members.');
- }
- } catch (e: any) {
- setError(e.message || 'An unexpected network error occurred.');
- } finally {
- setLoading(false);
- }
- }, [groupId]);
-
- useEffect(() => {
- fetchMembers();
- }, [fetchMembers]);
-
- // --- Render Functions ---
const renderItem = ({ item }: { item: Member }) => {
const fullName = `${item.firstName} ${item.lastName || ''}`.trim();
const initial = item.firstName ? item.firstName.charAt(0).toUpperCase() : '?';
const isOrganizer = item.role === 'ORGANIZER';
return (
-
-
-
- {initial}
+
+
+ {initial}
-
- {fullName}
+
+ {fullName}
{isOrganizer && (
-
- Orginzer
+
+ Organizer
)}
- @{item.username}
-
+ @{item.username}
+
+
+ {item.mobile}
+
-
+
);
};
- const renderContent = () => {
- if (loading) {
- return (
-
-
- Loading Members...
-
- );
- }
-
- if (error) {
- return (
-
- no members found
-
- );
- }
-
- if (members.length === 0) {
- return (
-
-
- No Members Yet
- Invite people to join your group.
-
- )
- }
-
- return (
- item.id}
- contentContainerStyle={styles.listContainer}
- showsVerticalScrollIndicator={false}
- />
- );
- };
+ const renderSkeletons = () => (
+
+ {[1, 2, 3, 4, 5].map(i => (
+
+ ))}
+
+ );
return (
-
+
- Group Members
- {members.length} member(s)
+ Group Members
+
+ {members.length} member{members.length !== 1 ? 's' : ''} in this circle
+
- {renderContent()}
+
+ {isLoading ? (
+ renderSkeletons()
+ ) : (
+ item.id}
+ contentContainerStyle={styles.listContainer}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+
+
+ No Members Yet
+
+ Invite people to join your group to get started.
+
+
+ }
+ />
+ )}
);
};
-// --- Styles ---
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#F3F4F6',
- },
header: {
- paddingHorizontal: 20,
- paddingTop: 20,
- paddingBottom: 12,
-
+ paddingHorizontal: 14,
+ paddingVertical: 10,
},
headerTitle: {
- fontSize: 28,
- fontWeight: 'bold',
- color: '#111827',
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
},
headerSubtitle: {
- fontSize: 16,
- color: '#6B7280',
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
marginTop: 4,
},
listContainer: {
- padding: 16,
+ paddingHorizontal: 14,
+ paddingBottom: 86,
+ },
+ skeletonList: {
+ paddingHorizontal: 14,
},
card: {
flexDirection: 'row',
- backgroundColor: '#FFFFFF',
padding: 16,
- borderRadius: 12,
- marginBottom: 16,
+ borderRadius: 16,
+ marginBottom: 12,
alignItems: 'center',
- shadowColor: '#374151',
- shadowOpacity: 0.08,
- shadowRadius: 10,
- shadowOffset: { width: 0, height: 4 },
- elevation: 3,
+ borderWidth: 1,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
},
avatarPlaceholder: {
- width: 52,
- height: 52,
- borderRadius: 26,
- backgroundColor: '#D1D5DB',
+ width: 48,
+ height: 48,
+ borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
avatarText: {
- color: '#4B5563',
- fontSize: 24,
- fontWeight: 'bold',
- },
- organizerIcon: {
- borderRadius: 10,
- padding: 2,
- borderWidth: 2,
- borderColor: colors.pastelOrange
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
},
infoContainer: {
flex: 1,
},
+ nameRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
name: {
- fontSize: 18,
- fontWeight: '700',
- color: '#111827',
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ badge: {
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 4,
+ },
+ badgeText: {
+ fontSize: responsiveFontSize(1),
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
},
username: {
- fontSize: 14,
- color: '#6B7280',
- marginBottom: 8,
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
},
- contactRow: {
+ contactInfo: {
flexDirection: 'row',
alignItems: 'center',
- marginBottom: 4,
+ marginTop: 6,
+ gap: 4,
},
contactText: {
- marginLeft: 8,
- fontSize: 14,
- color: '#374151',
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoMedium,
},
- menuButton: {
- padding: 8,
- marginLeft: 8,
- },
- centered: {
- flex: 1,
- justifyContent: 'center',
+ emptyContainer: {
alignItems: 'center',
- padding: 20,
- },
- loadingText: {
- marginTop: 10,
- fontSize: 16,
- color: '#6B7280',
- },
- errorTitle: {
- fontSize: 22,
- fontWeight: 'bold',
- color: '#EF4444',
- marginBottom: 8,
- },
- errorText: {
- fontSize: 16,
- color: '#374151',
- textAlign: 'center',
- marginBottom: 20,
- },
- retryButton: {
- backgroundColor: '#4F46E5',
- paddingVertical: 12,
- paddingHorizontal: 30,
- borderRadius: 8,
- },
- retryButtonText: {
- color: 'white',
- fontSize: 16,
- fontWeight: 'bold',
+ marginTop: 80,
+ paddingHorizontal: 40,
},
emptyTitle: {
- fontSize: 22,
- fontWeight: 'bold',
- color: '#374151',
- marginBottom: 8,
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 20,
},
- emptyText: {
- fontSize: 16,
- color: '#6B7280',
+ emptySubtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
textAlign: 'center',
- }
+ marginTop: 8,
+ lineHeight: 22,
+ },
});
export default MemberScreen;
diff --git a/src/screens/management/Payments.tsx b/src/screens/management/Payments.tsx
index cd6961e..fac4ed0 100644
--- a/src/screens/management/Payments.tsx
+++ b/src/screens/management/Payments.tsx
@@ -1,22 +1,28 @@
-import React, { useEffect, useState, useCallback } from 'react';
-import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity, Modal, TextInput, ToastAndroid } from 'react-native';
-import { useRoute, RouteProp } from '@react-navigation/native';
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import colors from '../../components/colors';
-import AppView from '../../components/AppView';
-import { apiCall } from '../../utils/ApiHelper';
-import { BASE_URL } from '../../utils/ApiUrls';
+import React, { useState, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ RefreshControl,
+ TouchableOpacity,
+ TextInput,
+ ToastAndroid,
+} from 'react-native';
+import { useRoute } from '@react-navigation/native';
+import LucideIcon from '../../components/common/LucideIcon';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
-
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { useUpdate } from '../../hooks/core/useUpdate';
+import { BASE_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
+import CustomModal from '../../components/common/CustomModal';
+import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
// --- TYPE DEFINITIONS ---
-interface ApiResponse {
- status: 'success' | 'error';
- data: T;
- message?: string;
- statusCode: number;
-}
-
interface Participant {
userId: string;
firstName: string;
@@ -24,13 +30,12 @@ interface Participant {
mobile: string;
contributionAmount: number;
paidAmount: number;
- payAmount?: number;
status: 'SUCCESS' | 'PENDING' | 'FAILED' | 'UNPAID';
lateFee: number;
lateFeeWaived: boolean;
totalPaidAmount: string;
expectedAmount: number;
- waivedAmount:number
+ waivedAmount: number;
}
interface PaymentDetails {
@@ -44,242 +49,262 @@ interface PaymentDetails {
cycleCompleted: boolean;
}
-type PaymentStackParamList = { Payments: { groupId: string } };
-type PaymentsRouteProp = RouteProp;
-
-// The component is a valid React Functional Component, which is required for using hooks.
const Payments: React.FC = () => {
- // --- HOOKS ---
- // All hooks are called at the top level, which is correct.
- const route = useRoute();
- const { groupId } = route.params;
+ const theme = useTheme();
+ const route = useRoute();
+ const { groupId } = route?.params || {};
- const [paymentDetails, setPaymentDetails] = useState(null);
- const [loading, setLoading] = useState(true);
- const [refreshing, setRefreshing] = useState(false);
- const [isGroupActive, setIsGroupActive] = useState(true);
- const [isModalVisible, setModalVisible] = useState(false);
+ const [isWaiverModalVisible, setWaiverModalVisible] = useState(false);
const [currentParticipant, setCurrentParticipant] = useState(null);
const [waiverAmount, setWaiverAmount] = useState('');
- const [cycleData, setCycleData] = useState();
+ const {
+ data: groupDetailsRes,
+ isLoading: isGroupLoading,
+ isRefetching: isRefetchingGroup,
+ refetch: refetchGroup,
+ } = useFetch(
+ `${BASE_URL}fund/group/details?groupId=${groupId}`,
+ undefined,
+ { enabled: !!groupId }
+ );
+ console.log("groupDetailsRes", groupDetailsRes);
+ // Fetch Payment Details
+ const {
+ data: paymentDetailsRes,
+ isLoading: isPaymentsLoading,
+ refetch: refetchPayments,
+ error: paymentError,
+ } = useFetch(`${BASE_URL}payments/current/payment/details/${groupId}`, {
+ enabled: !!groupId,
+ onError: (error: any) => {
+ console.log("payment error", error);
+ },
+ });
- // --- FUNCTIONS ---
- // These are regular functions and do not call hooks inside them, which is correct.
- const getCycleData = async () => {
- try {
- const response = await apiCall(`${BASE_URL}/payments/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse;
- if (response && response.statusCode === 200) {
- setCycleData(response.data);
- }
- } catch (error: any) {
- console.error("Failed to fetch cycle data:", error.message);
+ // Fetch Cycle Dashboard
+ const {
+ isLoading: isCycleLoading,
+ refetch: refetchCycle,
+ } = useFetch(`${BASE_URL}payments/groups/${groupId}/cycle-dashboard`, {
+ enabled: !!groupId,
+ });
+
+ const paymentDetails: PaymentDetails | null = paymentDetailsRes?.data || null;
+ const isGroupActive = paymentDetailsRes?.statusCode !== 400;
+
+ // Waiver Update
+ const { mutate: waiveFee, isPending: isWaiving } = useUpdate(
+ '',
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Waiver applied successfully', ToastAndroid.SHORT);
+ setWaiverModalVisible(false);
+ refetchPayments();
+ refetchCycle();
+ },
+ onError: (error: any) => {
+ ToastAndroid.show(error?.message || 'Failed to apply waiver', ToastAndroid.SHORT);
+ },
}
- };
-
- const fetchPaymentDetails = useCallback(async () => {
- if (!refreshing) setLoading(true);
- setIsGroupActive(true);
-
- try {
- const response = await apiCall(`${BASE_URL}payments/current/payment/details/${groupId}`, 'GET') as ApiResponse;
- // console.log("fchffryjtc", response)
- if (response && response.statusCode === 200) {
- setPaymentDetails(response.data);
- } else if (response && response.statusCode === 400) {
-
- setIsGroupActive(false);
- setPaymentDetails(null);
- } else {
- setIsGroupActive(false);
- }
- } catch (error: any) {
- console.log("payment error: ", error);
- setIsGroupActive(false);
- } finally {
- setLoading(false);
- setRefreshing(false);
- }
- }, [groupId, refreshing]);
-
- // --- EFFECT HOOK ---
- // useEffect is correctly used to call functions when the component mounts or dependencies change.
- useEffect(() => {
- getCycleData();
- fetchPaymentDetails();
- }, [fetchPaymentDetails]);
+ );
+ const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
- getCycleData();
- fetchPaymentDetails();
- }, [fetchPaymentDetails]);
+ Promise.all([
+ refetchPayments(),
+ refetchCycle(),
+ ]).finally(() => {
+ setRefreshing(false);
+ });
+ }, [refetchPayments, refetchCycle]);
const openWaiverDialog = (participant: Participant) => {
setCurrentParticipant(participant);
setWaiverAmount(participant.lateFee.toString());
- setModalVisible(true);
+ setWaiverModalVisible(true);
};
- const handleWaiveFee = async () => {
- console.log("first approch ")
- // if (!currentParticipant || !paymentDetails) console.log("second approch ") ;
-
+ const handleWaiveFee = () => {
const amountToWaive = parseFloat(waiverAmount);
-
if (isNaN(amountToWaive) || amountToWaive < 0) {
- ToastAndroid.show("Please enter a valid waiver amount.", ToastAndroid.SHORT);
+ ToastAndroid.show('Please enter a valid amount', ToastAndroid.SHORT);
return;
}
- if (amountToWaive > currentParticipant.lateFee) {
- ToastAndroid.show(`Waiver cannot exceed the current late fee of ₹${currentParticipant.lateFee}.`, ToastAndroid.SHORT);
+ if (currentParticipant && amountToWaive > currentParticipant.lateFee) {
+ ToastAndroid.show(`Waiver cannot exceed ₹${currentParticipant.lateFee}`, ToastAndroid.SHORT);
return;
}
- try {
- const url = `${BASE_URL}payments/groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
-
- const response = await apiCall(url, 'PUT', {}) as any;
- console.log("vtitttit hbfbf sjbj j b", response)
-
- if (response && response.statusCode === 200) {
- ToastAndroid.show(`₹${amountToWaive} has been waived successfully.`, ToastAndroid.SHORT);
- setModalVisible(false);
- onRefresh();
- } else {
- throw new Error(response?.message || "Failed to apply waiver.");
- }
- } catch (error: any) {
- ToastAndroid.show(error.message || "Failed to apply waiver.", ToastAndroid.SHORT);
- console.error("Waive Fee Error:", error);
- }
+ const url = `${BASE_URL}payments/groups/${groupId}/cycles/${paymentDetails?.currentCycle}/waive/${currentParticipant?.userId}?amountToWaive=${amountToWaive}`;
+ waiveFee({ resource: url, payload: {} } as any);
};
- // --- RENDER LOGIC ---
const renderParticipant = (participant: Participant) => {
- const isPending = participant.status === 'UNPAID';
- const canWaive = participant.lateFee > 0 && isPending
+ const isUnpaid = participant.status === 'UNPAID';
+ const canWaive = participant.lateFee > 0 && isUnpaid;
return (
-
+
- {participant.firstName} {participant.lastName}
- {participant.mobile}
+
+ {participant.firstName} {participant.lastName}
+
+ {participant.mobile}
-
+
{participant.status}
- {participant.status === "UNPAID" &&
- {`₹${participant?.expectedAmount} `}
- }
- {participant.status === "SUCCESS" &&
- {`₹${participant?.totalPaidAmount}`}
- }
-
- {/*
- {isPending ? `₹${participant.payAmount || 0}` : `₹${participant.totalPaidAmount}`}
- */}
+
+ ₹{participant.status === 'SUCCESS' ? participant.totalPaidAmount : participant.expectedAmount}
+
{participant.lateFee > 0 && (
- Late Fee: ₹{participant.lateFee}
+ Late Fee: ₹{participant.lateFee}
+ )}
+ {participant.lateFeeWaived && (
+ Waived: ₹{participant.waivedAmount}
)}
- {/* {participant.lateFeeWaived && Fee Waived} */}
- {
- participant?.lateFeeWaived && Waived Amount : {participant?.waivedAmount}
- }
-
- {canWaive && (
- openWaiverDialog(participant)}>
-
-
- )}
-
+ {canWaive && (
+ openWaiverDialog(participant)} style={styles.menuButton}>
+
+
+ )}
);
};
+ const renderSkeletons = () => (
+
+
+ {[1, 2, 3, 4].map(i => (
+
+ ))}
+
+ );
+
return (
- setModalVisible(false)}
+ setWaiverModalVisible(false)}
+ title="Waive Late Fee"
+ onConfirm={handleWaiveFee}
+ confirmText={isWaiving ? 'Applying...' : 'Apply Waiver'}
>
-
-
- Waive Late Fee
-
- For {currentParticipant?.firstName}. Current late fee is ₹{currentParticipant?.lateFee}.
-
-
-
- setModalVisible(false)}>
- Cancel
-
-
- Apply Waiver
-
-
-
-
-
+
+ For {currentParticipant?.firstName}. Current late fee: ₹{currentParticipant?.lateFee}
+
+
+
- {loading && !refreshing ? (
-
- ) : !isGroupActive ? (
+ {((paymentError as any)?.response?.status === 400 || (paymentError as any)?.statusCode === 400 || !isGroupActive || groupDetailsRes?.data?.status === 'APPROVED' || groupDetailsRes?.data?.status === 'PENDING') ? (
-
-
- This group is not currently active.
+
+
+ Group is not Active
+
+
+ Please activate the group to view payments.
- Tap to Refresh
+ Tap to Refresh
) : (
}
+ contentContainerStyle={{ paddingBottom: 86 }}
+ refreshControl={
+
+ }
>
- {paymentDetails && (
+ {isPaymentsLoading ? (
+ renderSkeletons()
+ ) : (
<>
- {paymentDetails.groupName}
+ {paymentDetails?.groupName}
-
-
- Cycle
- {paymentDetails.currentCycle}
-
-
- Contribution
- ₹ {paymentDetails.contributionAmount}
-
-
- Total Amount
- ₹{paymentDetails.totalDueWithLateFee}
+
+ {paymentDetails && (
+
+
+ Cycle
+
+ #{paymentDetails.currentCycle}
+
+
+
+
+ Contribution
+
+ ₹{paymentDetails.contributionAmount}
+
+
+
+
+ Total Amount
+
+ ₹{paymentDetails.totalDueWithLateFee}
+
+
+ )}
+
+
+ Participants
+ {paymentDetails?.cycleCompleted && (
+
+ Next: {new Date(paymentDetails.nextCycleStart).toLocaleDateString()}
+
+ )}
- Participants
- {paymentDetails?.cycleCompleted &&
- Next Cycle from : {paymentDetails?.nextCycleStart}
- }{paymentDetails.participants.map(renderParticipant)}
+
+ {paymentDetails?.participants.map(renderParticipant)}
>
)}
@@ -289,58 +314,149 @@ const Payments: React.FC = () => {
};
const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: '#f4f6f8' },
- header: { padding: 20, paddingBottom: 15 },
- headerTitle: { color: '#000000', fontSize: 24, fontWeight: 'bold' },
- summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
- summaryBox: { alignItems: 'center' },
- summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
- summaryValue: { fontSize: 18, fontWeight: '700', color: colors.black },
- sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
- participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
- participantInfo: { flex: 1 },
- participantName: { fontSize: 16, fontWeight: '600' },
- participantMobile: { fontSize: 14, color: '#6c757d', marginTop: 2 },
- paymentStatus: { alignItems: 'flex-end', marginHorizontal: 10 },
- statusText: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase' },
- amountText: { fontSize: 14, color: '#495057', marginTop: 2 },
- lateFeeText: { fontSize: 13, color: colors.danger, fontWeight: '600', marginTop: 4 },
- waivedText: { fontSize: 13, color: colors.success, fontWeight: '600', marginTop: 4 },
- menuContainer: { justifyContent: 'center', alignItems: 'center', minWidth: 25 },
- modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
- modalContainer: { width: '85%', backgroundColor: 'white', borderRadius: 15, padding: 20, elevation: 10 },
- modalTitle: { fontSize: 20, fontWeight: 'bold', color: '#333', marginBottom: 5 },
- modalSubtitle: { fontSize: 14, color: '#666', marginBottom: 20 },
- input: { width: '100%', borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 15, paddingVertical: 12, fontSize: 16, marginBottom: 20 },
- modalButtonContainer: { flexDirection: 'row', justifyContent: 'flex-end' },
- modalButton: { paddingVertical: 10, paddingHorizontal: 20, borderRadius: 8, marginLeft: 10 },
- cancelButton: { backgroundColor: '#f0f0f0' },
- cancelButtonText: { color: '#333', fontWeight: 'bold' },
- saveButton: { backgroundColor: colors.primary },
- saveButtonText: { color: 'white', fontWeight: 'bold' },
+ container: {
+ flex: 1,
+ },
+ header: {
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ headerTitle: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ summaryContainer: {
+ flexDirection: 'row',
+ marginHorizontal: 14,
+ padding: 16,
+ borderRadius: 16,
+ borderWidth: 1,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 16,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ },
+ summaryBox: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ divider: {
+ width: 1,
+ height: '60%',
+ backgroundColor: '#E5E7EB',
+ },
+ summaryLabel: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 4,
+ },
+ summaryValue: {
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 14,
+ marginBottom: 10,
+ },
+ sectionTitle: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ nextCycleText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ },
+ participantCard: {
+ flexDirection: 'row',
+ marginHorizontal: 14,
+ marginBottom: 10,
+ borderRadius: 16,
+ padding: 14,
+ alignItems: 'center',
+ borderWidth: 1,
+ },
+ participantInfo: {
+ flex: 1,
+ },
+ participantName: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ participantMobile: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
+ },
+ paymentStatus: {
+ alignItems: 'flex-end',
+ marginRight: 8,
+ },
+ statusText: {
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
+ },
+ amountText: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 2,
+ },
+ lateFeeText: {
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoMedium,
+ marginTop: 4,
+ },
+ waivedText: {
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoMedium,
+ marginTop: 2,
+ },
+ menuButton: {
+ padding: 8,
+ },
+ skeletonContainer: {
+ paddingHorizontal: 14,
+ paddingTop: 14,
+ },
+ modalSubtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 16,
+ },
+ input: {
+ borderRadius: 8,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoMedium,
+ borderWidth: 1,
+ },
fullScreenMessage: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: '#f4f6f8',
- padding: 20,
+ padding: 40,
},
fullScreenMessageText: {
- fontSize: responsiveFontSize(1.8),
- color: '#333',
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
textAlign: 'center',
- marginTop: 15,
- fontWeight: 'bold',
+ marginTop: 20,
},
refreshButton: {
- marginTop: 20,
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 8,
+ marginTop: 16,
+ padding: 12,
},
refreshButtonText: {
- color: colors.black,
- fontWeight: 'bold',
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
},
});
diff --git a/src/screens/management/RandomSelectUserScreen.tsx b/src/screens/management/RandomSelectUserScreen.tsx
index cf57e1b..eb8e784 100644
--- a/src/screens/management/RandomSelectUserScreen.tsx
+++ b/src/screens/management/RandomSelectUserScreen.tsx
@@ -1,336 +1,906 @@
-import React, { useState, useEffect, useRef } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated, Button, ToastAndroid } from 'react-native';
-import { apiCall } from '../../utils/ApiHelper';
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import { useNavigation } from '@react-navigation/native';
-import ContributionDetails from '../ContributionScreen';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Dimensions,
+ Animated,
+ Easing,
+ ToastAndroid,
+} from 'react-native';
+import LucideIcon from '../../components/common/LucideIcon';
+import { useNavigation, useRoute } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useFetch } from '../../hooks/core/useFetch';
+import { useUpdate } from '../../hooks/core/useUpdate';
import { BASE_URL } from '../../utils/ApiUrls';
-import colors from '../../components/colors';
+import FontsFamily from '../../constents/FontsFamily';
+import SkeletonLoader from '../../components/common/SkeletonLoader';
const { width } = Dimensions.get('window');
const NUM_COLUMNS = 3;
-const ITEM_SIZE = width / NUM_COLUMNS - 20;
+const ITEM_SIZE = width / NUM_COLUMNS - 28;
interface User {
id: string;
name: string;
- selected: boolean;
+ selected: boolean;
}
-const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
- const navigation = useNavigation()
- const { groupId, cycleNumber } = route.params;
+// --- Sparkle Particle Component ---
+const SparkleParticle: React.FC<{
+ delay: number;
+ startX: number;
+ startY: number;
+ color: string;
+ size: number;
+}> = ({ delay, startX, startY, color, size }) => {
+ const anim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 1200,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start();
+ }, delay);
+ return () => clearTimeout(timeout);
+ }, [anim, delay]);
+
+ const angle = Math.random() * Math.PI * 2;
+ const distance = 60 + Math.random() * 80;
+
+ return (
+
+ );
+};
+
+// --- Pulse Ring Component ---
+const PulseRing: React.FC<{ delay: number; color: string; size: number }> = ({
+ delay,
+ color,
+ size,
+}) => {
+ const anim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 1000,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+ }, delay);
+ return () => clearTimeout(timeout);
+ }, [anim, delay]);
+
+ return (
+
+ );
+};
+
+const RandomSelectUserScreen: React.FC = () => {
+ const theme = useTheme();
+ const navigation = useNavigation();
+ const route = useRoute();
+ const { groupId, cycleNumber } = route.params || {};
+
const [users, setUsers] = useState([]);
const [highlightedIndex, setHighlightedIndex] = useState(null);
const [winner, setWinner] = useState<{ id: string; name: string } | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [openModal, setOpenModal] = useState(false)
+ const [showWinnerReveal, setShowWinnerReveal] = useState(false);
+ const [phase, setPhase] = useState<'idle' | 'scanning' | 'slowing' | 'reveal'>('idle');
- const scaleAnim = useRef(new Animated.Value(1)).current;
+ // Per-item animated values
+ const itemAnims = useRef([]).current;
+ const winnerScaleAnim = useRef(new Animated.Value(1)).current;
+ const winnerGlowAnim = useRef(new Animated.Value(0)).current;
+ const bannerSlideAnim = useRef(new Animated.Value(100)).current;
+ const bannerOpacity = useRef(new Animated.Value(0)).current;
+ const headerPulseAnim = useRef(new Animated.Value(1)).current;
- useEffect(() => {
- const fetchParticipants = async () => {
- try {
- const response = await apiCall(`${BASE_URL}fund/group/all/member?groupId=${groupId}`, "GET") as any;
-
- if (response.status === 'success' && response.data) {
- const formattedUsers = response.data.map((u: any) => ({
- id: u.id,
- name: `${u.firstName} ${u.lastName}`,
- selected: u?.selected
- }));
- console.log("all mem : ", formattedUsers)
-
- setUsers(formattedUsers);
- } else {
- ToastAndroid.show('Failed to fetch participants.', ToastAndroid.SHORT);
- }
- } catch (error) {
- ToastAndroid.show('Could not connect to the server to fetch participants.', ToastAndroid.SHORT);
- } finally {
- setIsLoading(false);
+ // Ensure we have enough animated values for each item
+ const ensureAnims = useCallback(
+ (count: number) => {
+ while (itemAnims.length < count) {
+ itemAnims.push(new Animated.Value(0));
}
- };
+ },
+ [itemAnims],
+ );
- fetchParticipants();
- }, [groupId]);
+ // --- DATA FETCHING ---
+ const { data: participantsRes, isLoading: isParticipantsLoading } = useFetch(
+ `${BASE_URL}fund/group/all/member?groupId=${groupId}`,
+ {},
+ {
+ enabled: !!groupId,
+ },
+ );
useEffect(() => {
- if (highlightedIndex !== null) {
- Animated.sequence([
- Animated.timing(scaleAnim, { toValue: 1.15, duration: 80, useNativeDriver: true }),
- Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
- ]).start();
+ if (participantsRes?.data) {
+ const formattedUsers = participantsRes.data.map((u: any) => ({
+ id: u.id,
+ name: `${u.firstName} ${u.lastName}`,
+ selected: u?.selected,
+ }));
+ setUsers(formattedUsers);
+ ensureAnims(formattedUsers.length);
}
- }, [highlightedIndex]);
+ }, [participantsRes, ensureAnims]);
- // const handleStartSelection = () => {
- // if (users.length === 0 || isAnimating) return;
+ // --- MUTATIONS ---
+ const { mutate: selectWinnerApi } = useUpdate(
+ `${BASE_URL}fund/method/based/userSelect`,
+ {
+ onSuccess: (res: any) => {
+ if (res.status === 'success' && res.data?.selectedUser?.id) {
+ const winnerData = {
+ id: res.data.selectedUser.id,
+ name: `${res.data.selectedUser.firstName} ${res.data.selectedUser.lastName}`,
+ };
+ const winnerIndex = users.findIndex(u => u.id === winnerData.id);
- // setWinner(null);
- // setIsAnimating(true);
+ if (winnerIndex !== -1) {
+ triggerWinnerReveal(winnerData, winnerIndex);
+ } else {
+ ToastAndroid.show('Winner not found among participants', ToastAndroid.SHORT);
+ resetAnimation();
+ }
+ } else {
+ ToastAndroid.show('Failed to determine a winner', ToastAndroid.SHORT);
+ resetAnimation();
+ }
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err?.message || 'Selection failed', ToastAndroid.SHORT);
+ resetAnimation();
+ },
+ },
+ );
- // let highlightCount = 0;
- // const maxHighlights = 30 + Math.floor(Math.random() * 10); // Add some randomness
+ const resetAnimation = () => {
+ setIsAnimating(false);
+ setPhase('idle');
+ setHighlightedIndex(null);
+ setShowWinnerReveal(false);
+ winnerScaleAnim.setValue(1);
+ winnerGlowAnim.setValue(0);
+ bannerSlideAnim.setValue(100);
+ bannerOpacity.setValue(0);
+ itemAnims.forEach(a => a.setValue(0));
+ };
- // const highlightInterval = setInterval(() => {
- // const randomIndex = Math.floor(Math.random() * users.length);
- // setHighlightedIndex(randomIndex);
- // highlightCount++;
+ // --- HEADER PULSE during animation ---
+ useEffect(() => {
+ if (phase === 'scanning' || phase === 'slowing') {
+ const pulse = Animated.loop(
+ Animated.sequence([
+ Animated.timing(headerPulseAnim, {
+ toValue: 1.05,
+ duration: 600,
+ useNativeDriver: true,
+ }),
+ Animated.timing(headerPulseAnim, {
+ toValue: 1,
+ duration: 600,
+ useNativeDriver: true,
+ }),
+ ]),
+ );
+ pulse.start();
+ return () => pulse.stop();
+ } else {
+ headerPulseAnim.setValue(1);
+ }
+ }, [phase, headerPulseAnim]);
- // if (highlightCount >= maxHighlights) {
- // clearInterval(highlightInterval);
- // fetchWinner();
- // }
- // }, 120);
- // };
+ // --- WINNER REVEAL ANIMATION ---
+ const triggerWinnerReveal = (winnerData: { id: string; name: string }, winnerIdx: number) => {
+ setPhase('reveal');
+ setWinner(winnerData);
+ setHighlightedIndex(winnerIdx);
+ setShowWinnerReveal(true);
+ // 1. Pop all items down except winner
+ const itemAnimations = itemAnims.map((anim, i) => {
+ if (i === winnerIdx) {
+ return Animated.timing(anim, {
+ toValue: 2, // special "winner" state
+ duration: 400,
+ easing: Easing.out(Easing.back(1.5)),
+ useNativeDriver: true,
+ });
+ }
+ return Animated.timing(anim, {
+ toValue: -1, // "dim" state
+ duration: 300,
+ useNativeDriver: true,
+ });
+ });
+ // 2. Winner scale + glow
+ const winnerReveal = Animated.parallel([
+ Animated.spring(winnerScaleAnim, {
+ toValue: 1.2,
+ friction: 4,
+ tension: 80,
+ useNativeDriver: true,
+ }),
+ Animated.timing(winnerGlowAnim, {
+ toValue: 1,
+ duration: 600,
+ useNativeDriver: true,
+ }),
+ ]);
+ // 3. Banner slide in
+ const bannerReveal = Animated.parallel([
+ Animated.spring(bannerSlideAnim, {
+ toValue: 0,
+ friction: 6,
+ tension: 60,
+ useNativeDriver: true,
+ }),
+ Animated.timing(bannerOpacity, {
+ toValue: 1,
+ duration: 500,
+ useNativeDriver: true,
+ }),
+ ]);
+ Animated.sequence([
+ Animated.parallel(itemAnimations),
+ Animated.delay(200),
+ winnerReveal,
+ Animated.delay(300),
+ bannerReveal,
+ ]).start(() => {
+ setTimeout(() => {
+ navigation.replace('EditGroup', { groupId });
+ }, 2500);
+ });
+ };
+
+ // --- MAIN SELECTION ANIMATION (3-phase spotlight roulette) ---
const handleStartSelection = () => {
- // Exclude already selected users
const availableUsers = users.filter(u => !u.selected);
if (availableUsers.length === 0 || isAnimating) {
- ToastAndroid.show('All users are already selected.', ToastAndroid.SHORT);
+ ToastAndroid.show(
+ 'All users are already selected or selection in progress',
+ ToastAndroid.SHORT,
+ );
return;
}
+ resetAnimation();
setWinner(null);
setIsAnimating(true);
+ setPhase('scanning');
- let highlightCount = 0;
- const maxHighlights = 30 + Math.floor(Math.random() * 10);
+ // Phase 1: Fast random scanning (accelerate then sustain)
+ let count = 0;
+ const totalPhase1 = 18;
+ const totalPhase2 = 15;
- const highlightInterval = setInterval(() => {
- const randomIndex = Math.floor(Math.random() * availableUsers.length);
- const selectedUser = availableUsers[randomIndex];
-
- // Get original index in `users` for highlighting
- const originalIndex = users.findIndex(u => u.id === selectedUser.id);
- setHighlightedIndex(originalIndex);
-
- highlightCount++;
-
- if (highlightCount >= maxHighlights) {
- clearInterval(highlightInterval);
- fetchWinner();
- }
- }, 120);
- };
-
- const fetchWinner = async () => {
- const payload = { groupId: groupId };
- try {
- const response = await apiCall(`${BASE_URL}fund/method/based/userSelect`, 'POST', payload) as any;
- console.log("random select user response : ", response)
- if (response.status === 'success' && response.data?.selectedUser?.id) {
- const winnerData = { id: response?.data?.selectedUser?.id, name: `${response.data.selectedUser.firstName} ${response.data.selectedUser.lastName}` };
- const winnerIndex = users.findIndex(u => u.id === winnerData.id);
-
- if (winnerIndex !== -1) {
- setWinner(winnerData);
- setHighlightedIndex(winnerIndex);
- } else {
- ToastAndroid.show('Winner not found among participants.', ToastAndroid.SHORT);
+ const runPhase = (
+ phaseCount: number,
+ getDelay: (i: number) => number,
+ onComplete: () => void,
+ ) => {
+ let i = 0;
+ const step = () => {
+ if (i >= phaseCount) {
+ onComplete();
+ return;
}
- } else {
- ToastAndroid.show('Failed to determine a winner from the API.', ToastAndroid.SHORT);
- }
- navigation.replace('EditGroup', { groupId })
- } catch (error) {
- ToastAndroid.show('Could not connect to the server to fetch the winner.', ToastAndroid.SHORT);
- } finally {
- setIsAnimating(false);
- }
+ const randomIdx = Math.floor(Math.random() * availableUsers.length);
+ const user = availableUsers[randomIdx];
+ const origIdx = users.findIndex(u => u.id === user.id);
+ setHighlightedIndex(origIdx);
+
+ // Animate the highlighted item
+ ensureAnims(users.length);
+ itemAnims.forEach((anim, idx) => {
+ Animated.timing(anim, {
+ toValue: idx === origIdx ? 1 : 0,
+ duration: 60,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ i++;
+ setTimeout(step, getDelay(i));
+ };
+ step();
+ };
+
+ // Phase 1: Fast scanning — starts moderate, gets fast
+ runPhase(
+ totalPhase1,
+ i => Math.max(50, 150 - i * 6),
+ () => {
+ setPhase('slowing');
+ // Phase 2: Dramatic slowdown — each step takes longer
+ runPhase(
+ totalPhase2,
+ i => 100 + i * 40 + Math.pow(i, 1.6) * 8,
+ () => {
+ // Final api call to determine real winner
+ selectWinnerApi({ groupId });
+ },
+ );
+ },
+ );
};
- const renderItem = ({ item, index }: { item: { id: string; name: string, selected: boolean }, index: number }) => {
+ // --- RENDER ITEM ---
+ const renderUserCard = (item: User, index: number) => {
const isHighlighted = index === highlightedIndex;
- const isWinner = winner?.id === item.id && !isAnimating;
+ const isWinnerCard = winner?.id === item.id && phase === 'reveal';
+ ensureAnims(users.length);
+ const itemAnim = itemAnims[index] || new Animated.Value(0);
- const animatedStyle = {
- transform: [{ scale: isHighlighted ? scaleAnim : 1 }],
- };
+ const isDark = theme.mode === 'dark';
+
+ // Dynamic colors
+ const cardBg = isWinnerCard
+ ? isDark ? '#1a3d2a' : '#e8f5ee'
+ : item.selected
+ ? theme.colors.surface
+ : theme.colors.background;
+
+ const borderColor = isWinnerCard
+ ? theme.colors.success
+ : isHighlighted
+ ? isDark ? '#818CF8' : '#6366F1'
+ : theme.colors.borderLight;
+
+ const textColor = isWinnerCard
+ ? theme.colors.success
+ : item.selected
+ ? theme.colors.textMuted
+ : theme.colors.textPrimary;
+
+ const iconColor = isWinnerCard
+ ? theme.colors.success
+ : isHighlighted
+ ? isDark ? '#818CF8' : '#6366F1'
+ : item.selected
+ ? theme.colors.textMuted
+ : theme.colors.textSecondary;
return (
-
-
+ {/* Glow overlay for highlighted */}
+ {isHighlighted && !isWinnerCard && (
+
+ )}
+
+ {/* Winner pulse rings */}
+ {isWinnerCard && showWinnerReveal && (
+
+
+
+
+
+ )}
+
+ {/* Winner sparkle particles */}
+ {isWinnerCard && showWinnerReveal && (
+
+ {Array.from({ length: 12 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {/* Winner crown badge */}
+ {isWinnerCard && (
+
+
+
+ )}
+
+ {/* Already selected badge */}
+ {item.selected && !isWinnerCard && (
+
+
+
+ )}
+
+ {/* Avatar circle */}
+
+
+
+
+ {/* Name */}
+
{item.name}
);
};
- if (isLoading) {
+ // --- STATUS TEXT ---
+ const getPhaseText = () => {
+ switch (phase) {
+ case 'scanning':
+ return 'Scanning participants...';
+ case 'slowing':
+ return 'Narrowing down...';
+ case 'reveal':
+ return '🎉 Winner Found!';
+ default:
+ return 'Tap below to start selection';
+ }
+ };
+
+ // --- LOADING STATE ---
+ if (isParticipantsLoading) {
return (
-
-
- Loading Participants...
-
+
+
+
+
+
+
+ {Array.from({ length: 9 }).map((_, i) => (
+
+ ))}
+
+
);
}
+ // --- MAIN RENDER ---
return (
-
-
- Select a Winner
- Cycle #{cycleNumber}
-
- item.id}
- numColumns={NUM_COLUMNS}
- extraData={{ highlightedIndex, winner }}
- contentContainerStyle={styles.grid}
- />
-
- {winner && !isAnimating && (
-
- 🎉 Winner: {winner.name}
+
+ {/* Header */}
+
+
+ {phase === 'reveal' ? '🏆 Winner!' : '🎯 Winner Selection'}
+
+
+
+
+ Cycle #{cycleNumber}
+
+
+
+ {getPhaseText()}
+
+
-
+ {/* Participants Grid */}
+
+
+ {users.map((user, index) => renderUserCard(user, index))}
+
+
+
+ {/* Footer */}
+
+ {/* Winner Banner */}
+ {winner && phase === 'reveal' && (
+
+
+
+
+
+
+ WINNER
+
+
+ {winner.name}
+
+
+
+
)}
- {
- openModal && setOpenModal(false)} onConfirm={() => setOpenModal(false)} />
- }
-
+ {/* Action Button */}
- {isAnimating ? 'Selecting...' : 'Start Selection'}
+ {isAnimating && phase !== 'reveal' ? (
+
+
+
+ Selecting...
+
+
+ ) : (
+
+
+
+ {winner ? 'Select Again' : 'Start Selection'}
+
+
+ )}
-
+
);
};
const styles = StyleSheet.create({
- centeredContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#f9fafb',
- },
- loadingText: {
- marginTop: 15,
- fontSize: 18,
- color: '#374151',
- fontWeight: '500',
- },
- container: {
- flex: 1,
- backgroundColor: '#f9fafb',
- alignItems: 'center',
- },
header: {
- paddingTop: 50,
- paddingBottom: 20,
- alignItems: 'center'
+ paddingVertical: 20,
+ paddingHorizontal: 16,
+ alignItems: 'center',
+ gap: 8,
},
- title: {
- fontSize: 32,
- fontWeight: 'bold',
- color: '#111827',
+ headerTitle: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ letterSpacing: -0.5,
},
- subtitle: {
- fontSize: 18,
- color: '#6b7280',
- marginTop: 4
+ cycleBadge: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingVertical: 5,
+ paddingHorizontal: 12,
+ borderRadius: 20,
+ },
+ cycleText: {
+ fontSize: responsiveFontSize(1.5),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ phaseText: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ marginTop: 2,
},
grid: {
- paddingHorizontal: 10,
+ paddingHorizontal: 12,
+ paddingBottom: 160,
+ },
+ gridInner: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ skeletonGrid: {
+ padding: 16,
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ gap: 12,
},
gridItem: {
- margin: 8,
+ margin: 6,
width: ITEM_SIZE,
- height: ITEM_SIZE,
+ height: ITEM_SIZE + 10,
justifyContent: 'center',
alignItems: 'center',
- borderRadius: 12,
- backgroundColor: '#fff',
- borderWidth: 1,
- borderColor: '#e5e7eb',
+ borderRadius: 16,
padding: 8,
- // shadowColor: "#000",
- // shadowOffset: {
- // width: 0,
- // height: 2,
- // },
- // shadowOpacity: 0.1,
- // shadowRadius: 3.84,
- // elevation: 5,
+ position: 'relative',
+ overflow: 'visible',
},
- highlightedItem: {
- borderWidth: 5,
- borderColor: '#4f46e5',
- },
- winnerItem: {
- backgroundColor: '#10b981',
- borderColor: '#059669',
+ avatarCircle: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 6,
},
gridText: {
- marginTop: 8,
- color: '#1f2937',
- fontWeight: '600',
textAlign: 'center',
- fontSize: 12,
+ fontSize: responsiveFontSize(1.25),
+ lineHeight: responsiveFontSize(1.7),
},
- highlightedText: {
- color: '#000000ff',
+ selectedBadge: {
+ position: 'absolute',
+ top: 6,
+ right: 6,
+ width: 18,
+ height: 18,
+ borderRadius: 9,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ crownBadge: {
+ position: 'absolute',
+ top: -6,
+ right: -6,
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 10,
+ elevation: 8,
},
footer: {
+ position: 'absolute',
+ bottom: 0,
width: '100%',
- padding: 20,
+ padding: 16,
+ paddingBottom: 20,
borderTopWidth: 1,
- borderTopColor: '#e5e7eb',
alignItems: 'center',
- backgroundColor: '#fff'
+ gap: 10,
},
- button: {
- paddingVertical: 16,
- paddingHorizontal: 50,
- backgroundColor: colors.pastelOrange,
- borderRadius: 30,
- elevation: 5,
- },
- buttonDisabled: {
- backgroundColor: '#9ca3af',
- shadowColor: 'transparent',
- },
- buttonText: {
- fontSize: 18,
- color: colors.black,
- fontWeight: 'bold',
- },
- winnerTextContainer: {
- marginBottom: 20,
- padding: 12,
- backgroundColor: '#d1fae5',
- borderRadius: 12,
+ winnerBanner: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 14,
width: '100%',
- alignItems: 'center',
},
- winnerText: {
- fontSize: 20,
- fontWeight: 'bold',
- color: '#065f46',
+ trophyCircle: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: 'rgba(255,255,255,0.2)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ winnerLabel: {
+ fontSize: responsiveFontSize(1.1),
+ fontFamily: FontsFamily.nunitoBold,
+ letterSpacing: 1.5,
+ },
+ winnerName: {
+ fontSize: responsiveFontSize(1.9),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 1,
+ },
+ actionButton: {
+ width: '100%',
+ paddingVertical: 15,
+ borderRadius: 14,
+ alignItems: 'center',
+ justifyContent: 'center',
+ elevation: 6,
+ shadowColor: '#6366F1',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.25,
+ shadowRadius: 12,
+ },
+ buttonContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ actionButtonText: {
+ fontSize: responsiveFontSize(1.9),
+ fontFamily: FontsFamily.nunitoBold,
},
});
diff --git a/src/screens/management/WinnerUser.tsx b/src/screens/management/WinnerUser.tsx
index 6284f36..f97e78c 100644
--- a/src/screens/management/WinnerUser.tsx
+++ b/src/screens/management/WinnerUser.tsx
@@ -1,43 +1,46 @@
-import React, { useEffect, useRef } from 'react'
-import { StyleSheet, Text, View, Animated, ToastAndroid } from 'react-native'
-import { Ionicons } from '@react-native-vector-icons/ionicons'
-import CustomModal from '../../components/common/CustomModal'
-import { apiCall } from '../../utils/ApiHelper'
-import { BASE_URL } from '../../utils/ApiUrls'
-import colors from '../../components/colors'
+import React, { useEffect, useRef } from 'react';
+import { StyleSheet, Text, View, Animated, ToastAndroid } from 'react-native';
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import CustomModal from '../../components/common/CustomModal';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { useUpdate } from '../../hooks/core/useUpdate';
+import { BASE_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
interface WinnerUserProps {
data: {
- withdrawalDone: boolean
- selectionDone: boolean
- selectionId: string
- currentCycle: number
- pendingParticipants: number
- withdrawalDetails: any
- paidParticipants: number
+ withdrawalDone: boolean;
+ selectionDone: boolean;
+ selectionId: string;
+ currentCycle: number;
+ pendingParticipants: number;
+ withdrawalDetails: any;
+ paidParticipants: number;
selectedUser: {
- firstName: string
- lastName: string
- mobile: string
- id: string
- userId: number
- email: string
- }
- cycleEndDate: number[]
- cycleStartDate: number[]
- allPaid: boolean
- selectionDate: number[]
- totalParticipants: number
- cycleStatus: string
- selectionMethod: string
- canProceedToWithdrawal: boolean
- }
- visible: boolean
- onClose: () => void
- groupId: string | number
+ firstName: string;
+ lastName: string;
+ mobile: string;
+ id: string;
+ userId: number;
+ email: string;
+ };
+ cycleEndDate: number[];
+ cycleStartDate: number[];
+ allPaid: boolean;
+ selectionDate: number[];
+ totalParticipants: number;
+ cycleStatus: string;
+ selectionMethod: string;
+ canProceedToWithdrawal: boolean;
+ };
+ visible: boolean;
+ onClose: () => void;
+ groupId: string | number;
}
const WinnerUser: React.FC = ({ data, visible, onClose, groupId }) => {
+ const theme = useTheme();
const {
withdrawalDone,
selectionDone,
@@ -50,13 +53,11 @@ const WinnerUser: React.FC = ({ data, visible, onClose, groupId
cycleStatus,
selectionMethod,
canProceedToWithdrawal,
- } = data || {}
+ } = data || {};
- const scaleAnim = useRef(new Animated.Value(0)).current
- const pulseAnim = useRef(new Animated.Value(1)).current
- const fadeAnim = useRef(new Animated.Value(0)).current
-
- console.log("winner data : ", data)
+ const scaleAnim = useRef(new Animated.Value(0)).current;
+ const pulseAnim = useRef(new Animated.Value(1)).current;
+ const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (visible) {
@@ -64,190 +65,172 @@ const WinnerUser: React.FC = ({ data, visible, onClose, groupId
toValue: 1,
friction: 5,
useNativeDriver: true,
- }).start()
+ }).start();
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
- toValue: 1.1,
- duration: 600,
+ toValue: 1.05,
+ duration: 800,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
- duration: 600,
+ duration: 800,
useNativeDriver: true,
}),
])
- ).start()
+ ).start();
Animated.timing(fadeAnim, {
toValue: 1,
- duration: 1500,
+ duration: 1000,
useNativeDriver: true,
- }).start()
+ }).start();
} else {
- scaleAnim.setValue(0)
- pulseAnim.setValue(1)
- fadeAnim.setValue(0)
+ scaleAnim.setValue(0);
+ pulseAnim.setValue(1);
+ fadeAnim.setValue(0);
}
- }, [visible])
+ }, [visible]);
- const onConfirm = async () => {
- try {
- const res = await apiCall(`${BASE_URL}payments/withdraw`, 'PUT', { groupId, userId: data?.selectedUser?.id, cycleNumber: currentCycle }) as any
- console.log("res winner user : ", res)
- ToastAndroid.show('Withdrawal successful', ToastAndroid.SHORT)
- onClose()
- } catch (error) {
- console.log('Confirm payment error : ', error)
- } finally {
- onClose()
+ const { mutate: withdrawFunds, isPending } = useUpdate(
+ `${BASE_URL}payments/withdraw`,
+ {
+ onSuccess: () => {
+ ToastAndroid.show('Withdrawal successful', ToastAndroid.SHORT);
+ onClose();
+ },
+ onError: (err) => {
+ ToastAndroid.show(err?.message || 'Withdrawal failed', ToastAndroid.SHORT);
+ },
}
- }
+ );
+
+ const handleWithdraw = () => {
+ withdrawFunds({
+ groupId,
+ userId: selectedUser?.id,
+ cycleNumber: currentCycle,
+ });
+ };
+
+ const renderRow = (icon: any, label: string, value: any, color?: string) => (
+
+
+
+ {label}
+
+ {value}
+
+ );
return (
- {/* Header */}
-
- Cycle {currentCycle} Winner
-
+
+
+
+ Cycle {currentCycle} Winner
+
+ {selectedUser?.firstName} {selectedUser?.lastName}
+
- {/* Winner Name */}
-
- {selectedUser?.firstName} {selectedUser?.lastName}
-
-
- {/* Details */}
-
-
- Mobile:
- {selectedUser?.mobile}
+
+ {renderRow('call-outline', 'Mobile', selectedUser?.mobile)}
+ {renderRow('mail-outline', 'Email', selectedUser?.email || 'N/A')}
+ {renderRow('people-outline', 'Participants', `${paidParticipants}/${totalParticipants}`)}
+ {renderRow(
+ allPaid ? 'checkmark-circle-outline' : 'alert-circle-outline',
+ 'All Paid',
+ allPaid ? 'Yes' : 'No',
+ allPaid ? theme.colors.success : theme.colors.error
+ )}
+ {renderRow(
+ withdrawalDone ? 'cash-outline' : 'time-outline',
+ 'Withdrawal',
+ withdrawalDone ? 'Completed' : 'Pending',
+ withdrawalDone ? theme.colors.success : theme.colors.warning
+ )}
-
-
- Email:
- {selectedUser?.email}
-
-
-
-
- Total Participants:
- {totalParticipants}
-
-
-
-
- Paid Participants:
- {paidParticipants}
-
-
-
-
- Pending Participants:
- {pendingParticipants}
-
-
-
-
- All Paid:
- {allPaid ? 'Yes' : 'No'}
-
-
-
-
- Withdrawal Status:
- {withdrawalDone ? 'Completed' : 'Pending'}
-
-
-
-
- Selection Method:
- {selectionMethod}
-
-
-
-
- Cycle Status:
- {cycleStatus}
-
-
-
+
{selectionDone ? 'Congratulations!' : 'Selection Pending'}
- )
-}
-
-export default WinnerUser
+ );
+};
const styles = StyleSheet.create({
container: {
- padding: 20,
+ padding: 10,
},
header: {
- flexDirection: 'row',
alignItems: 'center',
- gap: 8,
- marginBottom: 16,
+ marginBottom: 24,
+ },
+ trophyContainer: {
+ width: 64,
+ height: 64,
+ borderRadius: 32,
justifyContent: 'center',
- },
- cycle: {
- fontSize: 18,
- fontWeight: '700',
- color: '#ff9800',
- marginHorizontal: 8,
- textAlign: 'center',
- },
- name: {
- fontSize: 22,
- fontWeight: '800',
- color: '#333',
- marginBottom: 20,
- textAlign: 'center',
- },
- row: {
- flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
- label: {
- fontSize: 14,
- fontWeight: '600',
- color: '#555',
- marginLeft: 6,
- marginRight: 4,
+ cycle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ marginBottom: 4,
},
- value: {
- fontSize: 16,
- color: '#000',
- },
- congrats: {
- fontSize: 20,
- fontWeight: '700',
- color: '#4caf50',
- marginTop: 20,
+ name: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
textAlign: 'center',
},
-})
+ detailsCard: {
+ padding: 16,
+ borderRadius: 16,
+ borderWidth: 1,
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 10,
+ borderBottomWidth: 1,
+ },
+ rowLabelContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ label: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ },
+ value: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ congrats: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ textAlign: 'center',
+ marginTop: 24,
+ },
+});
+
+export default WinnerUser;
diff --git a/src/screens/management/YourContacts.tsx b/src/screens/management/YourContacts.tsx
index f16c1f9..a7bf2f5 100644
--- a/src/screens/management/YourContacts.tsx
+++ b/src/screens/management/YourContacts.tsx
@@ -1,28 +1,29 @@
-
-
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
TouchableOpacity,
- Modal,
PermissionsAndroid,
Platform,
NativeModules,
Linking,
- Alert,
ToastAndroid,
-} from "react-native";
-import AppView from "../../components/AppView";
-import colors from "../../components/colors";
+} from 'react-native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import FontsFamily from '../../constents/FontsFamily';
+import CustomModal from '../../components/common/CustomModal';
const { ContactsModule } = NativeModules;
-const APP_URL = "https://play.google.com/store/apps/details?id=com.yourapp"; // replace with your real app url
+const APP_URL = 'https://play.google.com/store/apps/details?id=com.yourapp';
+const WHATSAPP_URL = 'https://play.google.com/store/apps/details?id=com.whatsapp';
const YourContacts = () => {
+ const theme = useTheme();
const [contacts, setContacts] = useState([]);
const [selectedContact, setSelectedContact] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
@@ -33,18 +34,17 @@ const YourContacts = () => {
const handleFetchContacts = async () => {
try {
- if (Platform.OS === "android") {
+ if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
{
- title: "Contacts Permission",
- message: "This app would like to view your contacts.",
- buttonPositive: "OK",
+ title: 'Contacts Permission',
+ message: 'This app would like to view your contacts.',
+ buttonPositive: 'OK',
}
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
- console.log("Contacts permission denied");
return;
}
}
@@ -52,225 +52,174 @@ const YourContacts = () => {
const fetchedContacts = await ContactsModule.getContacts();
setContacts(fetchedContacts);
} catch (error) {
- console.error("Error fetching contacts:", error);
+ console.error('Error fetching contacts:', error);
}
};
- const APP_URL = "https://play.google.com/store/apps/details?id=com.yourapp"; // your app link
- const WHATSAPP_URL = "https://play.google.com/store/apps/details?id=com.whatsapp"; // fallback store link
-
const handleSend = async (contact: any) => {
if (!contact) return;
const message = `Hey ${contact.name}, check out this awesome app! Download here: ${APP_URL}`;
-
- // direct send to contact if you want
- const url = `whatsapp://send?phone=${contact.phone}&text=${encodeURIComponent(
- message
- )}`;
+ const url = `whatsapp://send?phone=${contact.phone}&text=${encodeURIComponent(message)}`;
try {
- const supported = await Linking.canOpenURL("whatsapp://send");
+ const supported = await Linking.canOpenURL('whatsapp://send');
if (supported) {
- // WhatsApp installed
await Linking.openURL(url);
} else {
- // WhatsApp not installed → send user to Play Store/App Store
- if (Platform.OS === "android") {
+ if (Platform.OS === 'android') {
await Linking.openURL(WHATSAPP_URL);
- } else if (Platform.OS === "ios") {
- await Linking.openURL("itms-apps://apps.apple.com/app/whatsapp-messenger/id310633997");
+ } else if (Platform.OS === 'ios') {
+ await Linking.openURL('itms-apps://apps.apple.com/app/whatsapp-messenger/id310633997');
}
}
} catch (err) {
- console.error("Error sending to WhatsApp:", err);
- ToastAndroid.show("Something went wrong while trying to open WhatsApp", ToastAndroid.SHORT);
+ ToastAndroid.show('Could not open WhatsApp', ToastAndroid.SHORT);
}
-
setModalVisible(false);
};
-
const renderContact = ({ item }: { item: any }) => (
-
-
-
- {item.name ? item.name.charAt(0).toUpperCase() : "?"}
+
+
+
+ {item.name ? item.name.charAt(0).toUpperCase() : '?'}
- {item.name}
- {item.phone}
+ {item.name}
+ {item.phone}
{
setSelectedContact(item);
setModalVisible(true);
}}
>
- Invite
+ Invite
);
return (
-
-
- Select Contact
-
+
+
+ Select Contact
+ Invite your friends to join CircleCollect
-
+
index.toString()}
renderItem={renderContact}
+ contentContainerStyle={styles.listContent}
+ showsVerticalScrollIndicator={false}
+ ListEmptyComponent={
+
+ No contacts found
+
+ }
/>
- {/* Send Modal */}
- setModalVisible(false)}
+ onClose={() => setModalVisible(false)}
+ onConfirm={() => handleSend(selectedContact)}
+ confirmText="Send Invite"
+ cancelText="Cancel"
+ confirmButton={theme.colors.primary}
>
-
-
- Send Invite
- {selectedContact && (
-
- Send invite to {selectedContact.name} ({selectedContact.phone})
-
- )}
-
- setModalVisible(false)}
- >
- Cancel
-
- handleSend(selectedContact)}
- >
- Send
-
-
-
+
+ Send Invite
+ {selectedContact && (
+
+ Invite {selectedContact.name} to the app via WhatsApp?
+
+ )}
-
+
);
};
-
-export default YourContacts;
-
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#fff",
- paddingHorizontal: 10,
- paddingTop: 20,
- },
header: {
- fontSize: 20,
- fontWeight: "bold",
- marginBottom: 15,
+ padding: 20,
+ paddingBottom: 10,
+ },
+ headerTitle: {
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ headerSubtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 4,
+ },
+ listContent: {
+ paddingHorizontal: 20,
+ paddingBottom: 20,
},
contactRow: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 12,
- borderBottomWidth: 0.5,
- borderBottomColor: "#ddd",
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 14,
+ borderBottomWidth: 1,
},
avatar: {
- width: 45,
- height: 45,
- borderRadius: 25,
- backgroundColor: colors.pastelOrange,
- justifyContent: "center",
- alignItems: "center",
- marginRight: 12,
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 14,
},
avatarText: {
- color: "#fff",
- fontSize: 18,
- fontWeight: "bold",
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
},
contactInfo: {
flex: 1,
},
contactName: {
- fontSize: 16,
- fontWeight: "600",
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
},
contactPhone: {
- fontSize: 13,
- color: "gray",
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
},
inviteButton: {
- paddingHorizontal: 14,
- paddingVertical: 6,
- borderRadius: 8,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 10,
borderWidth: 1,
- borderColor: colors.Settings,
},
inviteText: {
- color: colors.Settings,
- fontWeight: "bold",
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
},
- modalOverlay: {
- flex: 1,
- backgroundColor: "rgba(0,0,0,0.5)",
- justifyContent: "center",
- alignItems: "center",
- },
- modalContent: {
- width: "80%",
- backgroundColor: "#fff",
- borderRadius: 10,
- padding: 20,
- alignItems: "center",
+ modalBody: {
+ padding: 10,
+ alignItems: 'center',
},
modalTitle: {
- fontSize: 18,
- fontWeight: "bold",
- marginBottom: 10,
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 12,
},
modalText: {
- fontSize: 15,
- marginBottom: 20,
- textAlign: "center",
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ lineHeight: 24,
},
- modalActions: {
- flexDirection: "row",
- justifyContent: "space-between",
- width: "100%",
- },
- cancelButton: {
- flex: 1,
- marginRight: 10,
- paddingVertical: 10,
- borderRadius: 8,
- backgroundColor: "#ddd",
- alignItems: "center",
- },
- cancelText: {
- fontWeight: "bold",
- color: "#333",
- },
- sendButton: {
- flex: 1,
- marginLeft: 10,
- paddingVertical: 10,
- borderRadius: 8,
- backgroundColor: colors.pastelOrange,
- alignItems: "center",
- },
- sendText: {
- fontWeight: "bold",
- color: "#fff",
+ emptyContainer: {
+ alignItems: 'center',
+ marginTop: 40,
},
});
+
+export default YourContacts;
diff --git a/src/screens/notification/Notification.tsx b/src/screens/notification/Notification.tsx
index 697e5d7..4f76790 100644
--- a/src/screens/notification/Notification.tsx
+++ b/src/screens/notification/Notification.tsx
@@ -9,15 +9,11 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import {
removeNotification,
- clearAllNotifications,
- markAsRead,
- markAllAsRead,
} from '../../redux/notificationSlice';
import LinearGradient from 'react-native-linear-gradient';
-import { Ionicons } from '@react-native-vector-icons/ionicons';
+import LucideIcon from '../../components/common/LucideIcon';
import AppView from '../../components/AppView';
import Header from '../../components/common/Header';
-import { useNavigation } from '@react-navigation/native';
import { useNotificationHandler } from '../hook/useNotificationHandler';
import colors from '../../components/colors';
@@ -28,7 +24,6 @@ type FilterType = 'all' | 'JOIN_REQUEST' | 'CONTRIBUTION' | 'CONTRIBUTION_ACK' |
const Notification = () => {
const dispatch = useDispatch();
- const navigation = useNavigation();
const { handleMarkAsReadAndNavigate } = useNotificationHandler();
const notifications = useSelector(
(state: any) => state.notifications.notifications
@@ -41,10 +36,6 @@ const Notification = () => {
dispatch(removeNotification(messageId));
};
- const handleClearAll = () => {
- dispatch(clearAllNotifications());
- };
-
const handleMarkAsRead = (messageId: string, screen: string, groupId: string, cycleNumber: string, contributionId: string) => {
console.log("screen type : ", screen);
handleMarkAsReadAndNavigate(
@@ -63,10 +54,6 @@ const Notification = () => {
);
};
- const handleMarkAllAsRead = () => {
- dispatch(markAllAsRead());
- };
-
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
const now = new Date();
@@ -181,7 +168,8 @@ const Notification = () => {
{/* Header with actions */}
0 ? `(${unreadCount})` : ''}`} arrowColor='#000' titleColor='#000'/>
+ title={`Notifications ${unreadCount > 0 ? `(${unreadCount})` : ''}`}
+ />
{/*
{unreadCount > 0 && (
{
{/* Icon */}
- {
onPress={() => handleDelete(item.messageId)}
style={styles.deleteButton}
>
- {/* Pastel Pink */}
+ {/* Pastel Pink */}
@@ -282,7 +270,7 @@ const Notification = () => {
}}
ListEmptyComponent={
- {/* Pastel Gray */}
+ {/* Pastel Gray */}
No notifications yet
When you get notifications, they'll appear here
@@ -422,4 +410,4 @@ const styles = StyleSheet.create({
textAlign: 'center',
lineHeight: 20,
},
-});
\ No newline at end of file
+});
diff --git a/src/screens/payment/PaymentModeScreen.tsx b/src/screens/payment/PaymentModeScreen.tsx
new file mode 100644
index 0000000..b342da0
--- /dev/null
+++ b/src/screens/payment/PaymentModeScreen.tsx
@@ -0,0 +1,754 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useSelector } from 'react-redux';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ ActivityIndicator,
+ Animated,
+ Image,
+ ToastAndroid,
+ Modal,
+ Easing,
+} from 'react-native';
+import { useNavigation, useRoute } from '@react-navigation/native';
+import { responsiveFontSize } from 'react-native-responsive-dimensions';
+import LucideIcon from '../../components/common/LucideIcon';
+import AppView from '../../components/AppView';
+import { useTheme } from '../../hooks/ui/useTheme';
+import { apiCall } from '../../utils/ApiHelper';
+import { BASE_URL, CONTRIBUTE, PAYMENT_MODES_URL } from '../../utils/ApiUrls';
+import FontsFamily from '../../constents/FontsFamily';
+import Header from '../../components/common/Header';
+import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
+
+interface PaymentMode {
+ id: number;
+ modeCode: string;
+ modeName: string;
+ provider: string;
+ countryCode: string;
+ currencyCode: string;
+ minAmount: number;
+ maxAmount: number;
+ isActive: boolean;
+ isMaintenance: boolean;
+ displayOrder: number;
+ iconUrl: string;
+}
+
+// --- Extracted ModeCard Component (hooks must be in a component, not a render function) ---
+const getBrandColors = (modeCode: string): { bg: string; text: string } => {
+ switch (modeCode) {
+ case 'PAISAPAL': return { bg: '#2BAF2B', text: '#fff' }; // Pesapal green
+ case 'MTN': return { bg: '#FFCC00', text: '#003087' }; // MTN yellow + blue
+ case 'AIRTEL': return { bg: '#ED1C24', text: '#fff' }; // Airtel red
+ default: return { bg: '#6C63FF', text: '#fff' };
+ }
+};
+
+interface ModeCardProps {
+ item: PaymentMode;
+ index: number;
+ isSelected: boolean;
+ isDisabled: boolean;
+ onSelect: (id: number) => void;
+}
+
+const ModeCard: React.FC = ({ item, index, isSelected, isDisabled, onSelect }) => {
+ const theme = useTheme();
+ const cardAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ Animated.timing(cardAnim, {
+ toValue: 1,
+ duration: 400,
+ delay: index * 100,
+ useNativeDriver: true,
+ }).start();
+ }, []);
+
+ const brandColors = getBrandColors(item.modeCode);
+
+ return (
+
+ { if (!isDisabled) onSelect(item.id); }}
+ style={[
+ styles.modeCard,
+ {
+ backgroundColor: isSelected ? theme.colors.primary + '12' : theme.colors.card,
+ borderColor: isSelected ? theme.colors.primary : theme.colors.border,
+ opacity: isDisabled ? 0.5 : 1,
+ },
+ ]}
+ >
+ {/* Selection Indicator */}
+
+ {isSelected && }
+
+
+ {/* Provider Brand Icon */}
+
+
+ {item.provider.charAt(0).toUpperCase()}
+
+
+
+ {/* Info */}
+
+ {item.modeName}
+
+ {item.provider} • {item.currencyCode}
+
+
+ {item.currencyCode} {item.minAmount.toLocaleString()} – {item.maxAmount.toLocaleString()}
+
+
+
+ {/* Badges */}
+
+ {item.isMaintenance && (
+
+ Maintenance
+
+ )}
+ {!item.isActive && !item.isMaintenance && (
+
+ Unavailable
+
+ )}
+
+
+
+ );
+};
+
+// --- Main Screen ---
+const PaymentModeScreen: React.FC = () => {
+ const theme = useTheme();
+ const navigation = useNavigation();
+ const route = useRoute();
+ const { groupId, routeFrom, routeData = null } = route.params || {};
+ const user = useSelector((state: any) => state.user);
+
+ const [modes, setModes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedMode, setSelectedMode] = useState(null);
+ const [paying, setPaying] = useState(false);
+ const [payable, setPayable] = useState();
+ // --- Payment processing modal state ---
+ const [processingVisible, setProcessingVisible] = useState(false);
+ const [processingStatus, setProcessingStatus] = useState<'polling' | 'success' | 'failed' | 'timeout'>('polling');
+ const [processingMessage, setProcessingMessage] = useState('Payment request initiated. Awaiting approval...');
+ const [timeRemaining, setTimeRemaining] = useState(360); // 6 minutes in seconds
+ const pollingRef = useRef | null>(null);
+ const countdownRef = useRef | null>(null);
+ const spinAnim = useRef(new Animated.Value(0)).current;
+ const spinLoop = useRef(null);
+
+ // Animations
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(30)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
+ Animated.timing(slideAnim, { toValue: 0, duration: 500, useNativeDriver: true }),
+ ]).start();
+ }, []);
+
+
+ useEffect(() => {
+ { routeFrom === 'GroupDetails' && handleContribution(); }
+ console.log("data", payable);
+ }, []);
+ const handleContribution = async () => {
+ try {
+ const res = await apiCall(`${CONTRIBUTE}${groupId || route?.params?.groupId}/calculate-contribution`, 'GET') as any;
+ setPayable(res.data);
+ } catch (error) {
+ console.log("Error in handleContribution: ", error);
+ }
+ };
+ // Spin animation for processing modal
+ const startSpinAnimation = () => {
+ spinAnim.setValue(0);
+ spinLoop.current = Animated.loop(
+ Animated.timing(spinAnim, {
+ toValue: 1,
+ duration: 1500,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ })
+ );
+ spinLoop.current.start();
+ };
+
+ const stopSpinAnimation = () => {
+ spinLoop.current?.stop();
+ };
+
+ const cleanupPolling = () => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ if (countdownRef.current) {
+ clearInterval(countdownRef.current);
+ countdownRef.current = null;
+ }
+ stopSpinAnimation();
+ };
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => cleanupPolling();
+ }, []);
+
+ const pollTransactionStatus = (pgOrderNo: string) => {
+ setProcessingVisible(true);
+ setProcessingStatus('polling');
+ setProcessingMessage('Payment request initiated. Awaiting approval...');
+ setTimeRemaining(360);
+ startSpinAnimation();
+
+ const startTime = Date.now();
+ const MAX_DURATION = 6 * 60 * 1000; // 6 minutes
+ const POLL_INTERVAL = 5000; // 5 seconds
+
+ // Countdown timer
+ countdownRef.current = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+ const remaining = Math.max(0, Math.ceil((MAX_DURATION - elapsed) / 1000));
+ setTimeRemaining(remaining);
+ }, 1000);
+
+ // Polling interval
+ pollingRef.current = setInterval(async () => {
+ const elapsed = Date.now() - startTime;
+
+ // Timeout after 6 minutes
+ if (elapsed >= MAX_DURATION) {
+ cleanupPolling();
+ setProcessingStatus('timeout');
+ setProcessingMessage('Payment verification timed out. Please check your transaction history.');
+ return;
+ }
+
+ try {
+ const statusRes = await apiCall(
+ `${BASE_URL}payments/pg/check-status/${pgOrderNo}`,
+ 'GET'
+ ) as any;
+
+ console.log('Poll status response:', JSON.stringify(statusRes));
+
+ const txnStatus = statusRes?.data?.txnStatus;
+
+ if (txnStatus === 'SUCCESS') {
+ cleanupPolling();
+ setProcessingStatus('success');
+ setProcessingMessage('Payment completed successfully!');
+ setTimeout(() => {
+ setProcessingVisible(false);
+ setPaying(false);
+ ToastAndroid.show('Payment successful!', ToastAndroid.SHORT);
+ navigation.goBack();
+ }, 2000);
+ return;
+ }
+
+ if (txnStatus === 'FAILED' || txnStatus === 'REJECTED' || txnStatus === 'CANCELLED') {
+ cleanupPolling();
+ setProcessingStatus('failed');
+ setProcessingMessage(statusRes?.data?.message || 'Payment failed. Please try again.');
+ return;
+ }
+
+ // Still pending — update message
+ setProcessingMessage(statusRes?.message || 'Waiting for payment confirmation...');
+ } catch (err) {
+ console.log('Poll error:', err);
+ // Don't stop polling on network errors, keep trying
+ }
+ }, POLL_INTERVAL);
+ };
+
+ const handleCloseProcessingModal = () => {
+ cleanupPolling();
+ setProcessingVisible(false);
+ setPaying(false);
+ };
+
+ const fetchPaymentModes = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await apiCall(PAYMENT_MODES_URL, 'GET') as any;
+ if (res?.status === 'success' && Array.isArray(res.data)) {
+ const sorted = res.data.sort((a: PaymentMode, b: PaymentMode) => a.displayOrder - b.displayOrder);
+ setModes(sorted);
+ } else {
+ ToastAndroid.show('Failed to load payment methods', ToastAndroid.SHORT);
+ }
+ } catch (error) {
+ ToastAndroid.show('Error loading payment methods', ToastAndroid.SHORT);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchPaymentModes();
+ }, [fetchPaymentModes]);
+ const handlePaymentForRouteFormGroupScreen = async () => {
+ try {
+
+ setPaying(true);
+ const payload = {
+ groupId: groupId || 0,
+ paymentId: selectedMode || 0,
+ mobileNumber: '',
+ };
+ console.log('payload', JSON.stringify(payload));
+
+ const res = await apiCall(
+ `${BASE_URL}payments/pg/entry-fee/${groupId}?groupId=${groupId}&paymentId=${selectedMode}&mobileNumber=`,
+ 'POST',
+ payload
+ ) as any;
+
+ if (res?.status === 'success' || res?.statusCode === 200) {
+ const pgOrderNo = res?.data?.pgOrderNo;
+ if (pgOrderNo) {
+ // Start polling for transaction status
+ pollTransactionStatus(pgOrderNo);
+ } else {
+ ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ } else {
+ ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ } catch (error) {
+ ToastAndroid.show('An error occurred during payment', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ }
+ const handlePaymentForRouteFormGroupDetails = async () => {
+ try {
+
+ setPaying(true);
+ const payload = {
+ groupId: groupId || 0,
+ amount: payable.totalPayable || 0,
+ paymentId: selectedMode || 0,
+ mobileNumber: '',
+ };
+ console.log('payload', JSON.stringify(payload));
+
+ const res = await apiCall(
+ `${BASE_URL}payments/contribute`,
+ 'POST',
+ payload
+ ) as any;
+
+ if (res?.status === 'success' || res?.statusCode === 200) {
+ const pgOrderNo = res?.data?.pgOrderNo;
+ if (pgOrderNo) {
+ // Start polling for transaction status
+ pollTransactionStatus(pgOrderNo);
+ } else {
+ ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ } else {
+ ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ } catch (error) {
+ ToastAndroid.show('An error occurred during payment', ToastAndroid.SHORT);
+ setPaying(false);
+ }
+ }
+ const handleConfirmPayment = async () => {
+ if (!selectedMode) {
+ ToastAndroid.show('Please select a payment method', ToastAndroid.SHORT);
+ return;
+ }
+ switch (routeFrom) {
+ case 'GroupDetails':
+ handlePaymentForRouteFormGroupDetails();
+ break;
+ case 'GroupScreen':
+ handlePaymentForRouteFormGroupScreen();
+ break;
+ default:
+ break;
+ }
+
+ };
+
+ return (
+
+
+
+
+ {/* Amount Summary */}
+ {routeFrom === 'GroupDetails' &&
+
+
+ Total Payable
+
+
+ {payable?.currencyCode || 'UGX'} {Number(payable?.totalPayable || 0).toLocaleString()}
+
+ }
+ {routeFrom === 'GroupScreen' &&
+
+
+ Entry Fee
+
+
+ {payable?.currencyCode || 'UGX'} {Number(routeData?.entryFee || 0).toLocaleString()}
+
+ }
+
+ {/* Payment Methods */}
+ {loading ? (
+
+
+ Loading payment methods...
+
+ ) : modes.length === 0 ? (
+
+
+ No payment methods available
+
+ ) : (
+ String(item.id)}
+ renderItem={({ item, index }) => (
+
+ )}
+ contentContainerStyle={styles.listContent}
+ showsVerticalScrollIndicator={false}
+ />
+ )}
+
+
+ {/* Bottom CTA */}
+ {!loading && modes.length > 0 && (
+
+
+ {paying ? (
+
+ ) : (
+ <>
+
+ Confirm & Pay
+ >
+ )}
+
+
+ )}
+
+ {/* Processing Modal — non-dismissable while polling */}
+ {
+ // Only allow closing if not actively polling
+ if (processingStatus !== 'polling') handleCloseProcessingModal();
+ }}
+ >
+
+
+ {/* Icon / Spinner */}
+ {processingStatus === 'polling' ? (
+
+
+
+ ) : processingStatus === 'success' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {/* Title */}
+
+ {processingStatus === 'polling' ? 'Processing Payment' :
+ processingStatus === 'success' ? 'Payment Successful' :
+ processingStatus === 'timeout' ? 'Verification Timed Out' : 'Payment Failed'}
+
+
+ {/* Message */}
+
+ {processingMessage}
+
+
+ {/* Countdown timer while polling */}
+ {processingStatus === 'polling' && (
+
+
+
+ {Math.floor(timeRemaining / 60)}:{String(timeRemaining % 60).padStart(2, '0')} remaining
+
+
+ )}
+
+ {/* Close button — only visible when NOT polling */}
+ {processingStatus !== 'polling' && (
+
+
+ {processingStatus === 'success' ? 'Done' : 'Close'}
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default PaymentModeScreen;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ amountCard: {
+ padding: 16,
+ borderRadius: 16,
+ borderWidth: 1,
+ marginBottom: 20,
+ },
+ amountRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ marginBottom: 6,
+ },
+ amountLabel: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ amountValue: {
+ fontSize: responsiveFontSize(3),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ },
+ listContent: {
+ paddingBottom: 100,
+ gap: 10,
+ },
+ modeCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 16,
+ borderWidth: 1.5,
+ },
+ radioOuter: {
+ width: 22,
+ height: 22,
+ borderRadius: 11,
+ borderWidth: 2,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ radioInner: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ },
+ modeIconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ modeIconImage: {
+ width: 32,
+ height: 32,
+ borderRadius: 6,
+ },
+ modeInfo: {
+ flex: 1,
+ },
+ modeName: {
+ fontSize: responsiveFontSize(1.7),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ modeProvider: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ marginTop: 2,
+ },
+ modeLimits: {
+ fontSize: responsiveFontSize(1.1),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
+ },
+ badgeContainer: {
+ marginLeft: 8,
+ },
+ badge: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 6,
+ },
+ badgeText: {
+ fontSize: responsiveFontSize(1),
+ fontFamily: FontsFamily.nunitoBold,
+ textTransform: 'uppercase',
+ },
+ loaderContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: 12,
+ },
+ loaderText: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: 12,
+ },
+ emptyText: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ bottomBar: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderTopWidth: 1,
+ },
+ confirmButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 16,
+ borderRadius: 14,
+ },
+ confirmText: {
+ color: '#fff',
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+});
+
+const modalStyles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ },
+ card: {
+ width: '100%',
+ borderRadius: 20,
+ paddingVertical: 36,
+ paddingHorizontal: 24,
+ alignItems: 'center',
+ },
+ iconCircle: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: responsiveFontSize(2.2),
+ fontFamily: FontsFamily.nunitoExtraBold,
+ marginTop: 20,
+ textAlign: 'center',
+ },
+ message: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ marginTop: 10,
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+ timerContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ marginTop: 16,
+ paddingHorizontal: 14,
+ paddingVertical: 6,
+ borderRadius: 20,
+ backgroundColor: 'rgba(128,128,128,0.1)',
+ },
+ timerText: {
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ closeButton: {
+ marginTop: 24,
+ paddingVertical: 14,
+ paddingHorizontal: 40,
+ borderRadius: 12,
+ },
+ closeButtonText: {
+ color: '#fff',
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+});
diff --git a/src/screens/profile/BecomeOrg.tsx b/src/screens/profile/BecomeOrg.tsx
index c3bceae..512627a 100644
--- a/src/screens/profile/BecomeOrg.tsx
+++ b/src/screens/profile/BecomeOrg.tsx
@@ -3,192 +3,226 @@ import {
View,
Text,
StyleSheet,
- Alert,
- ActivityIndicator,
ScrollView,
- KeyboardAvoidingView,
- Platform,
TouchableOpacity,
ToastAndroid,
} from "react-native";
-import * as Keychain from "react-native-keychain";
-import { CustomInput, CustomButton } from "../../components/common/Custom";
-import colors from "../../components/colors";
+import LucideIcon from '../../components/common/LucideIcon';
import { useNavigation } from "@react-navigation/native";
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+
+import { CustomInput, CustomButton } from "../../components/common/Custom";
+import { useTheme } from "../../hooks/ui/useTheme";
+import { useCreate } from "../../hooks/core/useCreate";
import AppView from "../../components/AppView";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
-import { BASE_URL } from "../../utils/ApiUrls";
+import FontsFamily from "../../constents/FontsFamily";
+import { APPLY_ORGANIZER_URL } from "../../utils/ApiUrls";
+
+type OrganizerApplyErrors = {
+ idName?: string;
+ idNumber?: string;
+ issueDate?: string;
+};
+
+const isValidIdType = (value: string) => /^[A-Za-z][A-Za-z\s-]{1,}$/.test(value.trim());
+const isValidIdNumber = (value: string) => /^[A-Za-z0-9-]{6,24}$/.test(value.trim());
+const formatIssueDateInput = (value: string) => {
+ const digits = value.replace(/\D/g, "").slice(0, 8);
+
+ if (digits.length <= 2) return digits;
+ if (digits.length <= 4) return `${digits.slice(0, 2)}-${digits.slice(2)}`;
+ return `${digits.slice(0, 2)}-${digits.slice(2, 4)}-${digits.slice(4)}`;
+};
+
+const isValidIssueDate = (value: string) => {
+ if (!/^\d{2}-\d{2}-\d{4}$/.test(value)) return false;
+ const [day, month, year] = value.split("-").map(Number);
+ const parsed = new Date(Date.UTC(year, month - 1, day));
+ const isRealDate =
+ parsed.getUTCFullYear() === year &&
+ parsed.getUTCMonth() + 1 === month &&
+ parsed.getUTCDate() === day;
+ if (!isRealDate) return false;
+
+ const today = new Date();
+ const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
+ return parsed.getTime() <= todayUtc;
+};
const BecomeOrg = () => {
+ const theme = useTheme();
const navigation = useNavigation();
+
const [idName, setIdName] = useState("");
const [idNumber, setIdNumber] = useState("");
const [issueDate, setIssueDate] = useState("");
- const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState({});
- const handleSubmit = async () => {
- if (!idName || !idNumber || !issueDate) {
- ToastAndroid.show("Please fill all fields", ToastAndroid.SHORT);
- return;
- }
-
- try {
- setLoading(true);
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show("You are not logged in. Please log in to continue.", ToastAndroid.SHORT);
- setLoading(false);
- // Optionally navigate to login screen
- // navigation.navigate("Login");
- return;
- }
- const token = credentials.password;
-
- const response = await fetch(
- `${BASE_URL}organizer/registration/apply`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
- idName,
- idNumber,
- issueDate,
- }),
- }
- );
-
- const result = await response.json();
-
- if (response.ok && result.status === "success") {
- ToastAndroid.show("Organizer registration application submitted successfully!", ToastAndroid.SHORT);
- setIdName("");
- setIdNumber("");
- setIssueDate("");
- navigation.navigate("Home" as never);
+ const { mutate: applyOrganizer, isPending: loading } = useCreate(APPLY_ORGANIZER_URL, {
+ onSuccess: (res) => {
+ console.log("api res",res)
+ if (res.status === "success") {
+ ToastAndroid.show("Application submitted successfully!", ToastAndroid.SHORT);
+ navigation.navigate("Home");
} else {
- ToastAndroid.show(result.message || "Failed to submit application", ToastAndroid.SHORT);
+ ToastAndroid.show(res.message || "Failed to submit application", ToastAndroid.SHORT);
}
- } catch (error) {
- console.error("❌ Registration error:", error);
- ToastAndroid.show("An unexpected error occurred. Please try again.", ToastAndroid.SHORT);
- } finally {
- setLoading(false);
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err.message || "An error occurred", ToastAndroid.SHORT);
+ }
+ });
+
+ const handleChange = (field: keyof OrganizerApplyErrors, value: string) => {
+ if (field === "idName") setIdName(value);
+ if (field === "idNumber") setIdNumber(value);
+ if (field === "issueDate") setIssueDate(value);
+ if (errors[field]) {
+ setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
+ const validateForm = () => {
+ const nextErrors: OrganizerApplyErrors = {};
+
+ if (!isValidIdType(idName)) {
+ nextErrors.idName = "Enter a valid ID type (min 2 letters).";
+ }
+ if (!isValidIdNumber(idNumber)) {
+ nextErrors.idNumber = "ID number must be 6-24 chars (A-Z, 0-9, -).";
+ }
+ if (!isValidIssueDate(issueDate)) {
+ nextErrors.issueDate = "Use valid date DD-MM-YYYY (not future).";
+ }
+
+ setErrors(nextErrors);
+ return Object.keys(nextErrors).length === 0;
+ };
+
+ const handleSubmit = () => {
+ if (!validateForm()) {
+ ToastAndroid.show("Please fix the highlighted fields.", ToastAndroid.SHORT);
+ return;
+ }
+ applyOrganizer({
+ idName: idName.trim(),
+ idNumber: idNumber.trim(),
+ issueDate: issueDate.trim(),
+ });
+ };
+
return (
-
+
navigation.goBack()} style={styles.backButton}>
-
+
- Become an Organizer
-
+ Become Organizer
-
-
- Submit Your ID for Verification
-
+
+ ID Verification
+
Please provide your official identification details to become a verified organizer.
handleChange("idName", val)}
icon="person-outline"
- error={""}
+ error={errors.idName}
/>
handleChange("idNumber", val.toUpperCase())}
+ autoCapitalize="characters"
icon="card-outline"
- error=""
+ error={errors.idNumber}
/>
handleChange("issueDate", formatIssueDateInput(val))}
icon="calendar-outline"
- error=""
+ error={errors.issueDate}
/>
-
- {loading && (
-
- )}
-
-
+
+
);
};
-export default BecomeOrg;
-
const styles = StyleSheet.create({
- container: {
- flexGrow: 1,
- padding: 20,
- justifyContent: "center",
- },
- title: {
- fontSize: 24,
- fontWeight: "700",
- marginBottom: 10,
- textAlign: "center",
- color: colors.textDark,
- },
- subtitle: {
- fontSize: 16,
- textAlign: "center",
- color: colors.textDark,
- marginBottom: 30,
- },
header: {
flexDirection: "row",
alignItems: "center",
- paddingHorizontal: 16,
- paddingVertical: 12,
- backgroundColor: '#FFF',
- borderBottomWidth: 1,
- borderBottomColor: '#E0E0E0',
+ paddingHorizontal: 24,
+ paddingVertical: 16,
},
backButton: {
- padding: 5,
+ marginRight: 16,
+ padding: 4,
},
headerTitle: {
+ fontSize: responsiveFontSize(2.4),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ scrollView: {
flex: 1,
- fontSize: 20,
- fontWeight: "600",
- color: "#000",
+ },
+ scrollContent: {
+ flexGrow: 1,
+ paddingHorizontal: 20,
+ paddingTop: 10,
+ justifyContent: "flex-start",
+ paddingBottom: 40,
+ },
+ card: {
+ padding: 24,
+ borderRadius: 24,
+ borderWidth: 1,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOpacity: 0.04,
+ shadowRadius: 12,
+ shadowOffset: { width: 0, height: 4 },
+ },
+ title: {
+ fontSize: responsiveFontSize(2.6),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 8,
textAlign: "center",
},
- placeholder: {
- width: 24, // to balance the back button
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: "center",
+ marginBottom: 30,
},
+ submitBtn: {
+ marginTop: 12,
+ }
});
+
+export default BecomeOrg;
diff --git a/src/screens/profile/EditBankDetails.tsx b/src/screens/profile/EditBankDetails.tsx
index ae7b220..dae2e43 100644
--- a/src/screens/profile/EditBankDetails.tsx
+++ b/src/screens/profile/EditBankDetails.tsx
@@ -3,24 +3,35 @@ import {
StyleSheet,
View,
Text,
- Alert,
- ActivityIndicator,
TouchableOpacity,
ToastAndroid,
} from "react-native";
import { useNavigation, useRoute } from "@react-navigation/native";
+import LucideIcon from '../../components/common/LucideIcon';
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+
import { CustomButton, CustomInput } from "../../components/common/Custom";
-import colors from "../../components/colors";
+import { useTheme } from "../../hooks/ui/useTheme";
+import { useUpdate } from "../../hooks/core/useUpdate";
import AppView from "../../components/AppView";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
+import FontsFamily from "../../constents/FontsFamily";
import { UPDATE_BANK_URL } from "../../utils/ApiUrls";
-import { apiCall } from "../../utils/ApiHelper";
+
+type BankFormErrors = {
+ bankName?: string;
+ bankAccountNumber?: string;
+ branchCode?: string;
+};
+
+const isValidBankName = (value: string) => /^[A-Za-z][A-Za-z\s.&-]{1,}$/.test(value.trim());
+const isValidAccountNumber = (value: string) => /^\d{9,18}$/.test(value.trim());
+const isValidIfsc = (value: string) => /^[A-Z]{4}0[A-Z0-9]{6}$/.test(value.trim().toUpperCase());
const EditBankDetails = () => {
+ const theme = useTheme();
const navigation = useNavigation();
const route = useRoute();
- // Get initial data from route parameters passed from the Profile screen
const {
currentBankName,
currentAccountNumber,
@@ -30,99 +41,124 @@ const EditBankDetails = () => {
currentBankName?: string;
currentAccountNumber?: string;
currentBranchCode?: string;
- onUpdate:()=>void
+ onUpdate: () => void;
};
- // Initialize state with the passed-in details
const [bankName, setBankName] = useState(currentBankName || "");
const [bankAccountNumber, setBankAccountNumber] = useState(currentAccountNumber || "");
const [branchCode, setBranchCode] = useState(currentBranchCode || "");
- const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState({});
- // The update function remains the same, using the local state
- const handleUpdateBankDetails = async () => {
- if (!bankName || !bankAccountNumber || !branchCode) {
- ToastAndroid.show("Please fill in all bank details.", ToastAndroid.SHORT);
+ const { mutate: updateBank, isPending: loading } = useUpdate(UPDATE_BANK_URL, {
+ onSuccess: () => {
+ ToastAndroid.show("Bank details updated successfully!", ToastAndroid.SHORT);
+ onUpdate();
+ navigation.goBack();
+ },
+ onError: (error: any) => {
+ ToastAndroid.show(error.message || "Failed to update bank details", ToastAndroid.SHORT);
+ }
+ });
+
+ const handleChange = (field: keyof BankFormErrors, value: string) => {
+ if (field === "bankName") setBankName(value);
+ if (field === "bankAccountNumber") setBankAccountNumber(value.replace(/\D/g, ""));
+ if (field === "branchCode") setBranchCode(value.toUpperCase());
+ if (errors[field]) {
+ setErrors((prev) => ({ ...prev, [field]: "" }));
+ }
+ };
+
+ const validateForm = () => {
+ const nextErrors: BankFormErrors = {};
+
+ if (!isValidBankName(bankName)) {
+ nextErrors.bankName = "Enter a valid bank name (min 2 letters).";
+ }
+ if (!isValidAccountNumber(bankAccountNumber)) {
+ nextErrors.bankAccountNumber = "Account number must be 9-18 digits.";
+ }
+ if (!isValidIfsc(branchCode)) {
+ nextErrors.branchCode = "Enter a valid IFSC (e.g. HDFC0001234).";
+ }
+
+ setErrors(nextErrors);
+ return Object.keys(nextErrors).length === 0;
+ };
+
+ const handleUpdate = () => {
+ if (!validateForm()) {
+ ToastAndroid.show("Please fix the highlighted fields.", ToastAndroid.SHORT);
return;
}
-
- try {
- setLoading(true);
- const response = await apiCall(UPDATE_BANK_URL, 'PUT', {
- bankName,
- bankAccountNumber,
- branchCode,
- }) as { status: string; message?: string };
- console.log("Update Bank Response:", response);
- if (response.status === "success") {
- ToastAndroid.show(response.message || "Bank details updated successfully!", ToastAndroid.SHORT);
- navigation.goBack();
- onUpdate(); // Call the onUpdate function to refresh profile data
- } else {
- ToastAndroid.show(response.message || "Could not update bank details.", ToastAndroid.SHORT);
- }
- } catch (error: any) {
- console.error("Error updating bank details:", error);
- ToastAndroid.show(error.message || "An unexpected error occurred. Please try again.", ToastAndroid.SHORT);
- } finally {
- setLoading(false);
- }
+ updateBank({
+ bankName: bankName.trim(),
+ bankAccountNumber: bankAccountNumber.trim(),
+ branchCode: branchCode.trim(),
+ });
};
return (
navigation.goBack()} style={styles.backButton}>
-
+
- Edit Bank Details
+ Edit Bank Details
- Update Your Bank Information
+
+ Bank Information
+
+ Update your account details for receiving payments.
+
-
-
-
-
-
-
-
- handleChange("bankName", val)}
+ icon="business-outline"
+ error={errors.bankName}
/>
- navigation.goBack()}
- style={styles.cancelButton}
- buttonBackground={colors.pastelBlue}
+
+ handleChange("bankAccountNumber", val)}
+ icon="card-outline"
+ keyboardType="numeric"
+ maxLength={18}
+ error={errors.bankAccountNumber}
/>
+
+ handleChange("branchCode", val)}
+ autoCapitalize="characters"
+ maxLength={11}
+ icon="barcode-outline"
+ error={errors.branchCode}
+ />
+
+
+
+ navigation.goBack()}
+ type="outline"
+ style={styles.cancelButton}
+ />
+
@@ -133,33 +169,50 @@ const styles = StyleSheet.create({
header: {
flexDirection: "row",
alignItems: "center",
- padding: 16,
+ paddingHorizontal: 24,
+ paddingVertical: 16,
},
backButton: {
marginRight: 16,
+ padding: 4,
},
headerTitle: {
- fontSize: 22,
- fontWeight: "600",
- color: colors.Profile,
+ fontSize: responsiveFontSize(2.4),
+ fontFamily: FontsFamily.nunitoBold,
},
container: {
flex: 1,
- padding: 16,
+ paddingHorizontal: 20,
+ paddingTop: 10,
+ },
+ card: {
+ padding: 24,
+ borderRadius: 24,
+ borderWidth: 1,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOpacity: 0.04,
+ shadowRadius: 12,
+ shadowOffset: { width: 0, height: 4 },
},
sectionTitle: {
- fontSize: 18,
- fontWeight: "700",
- color: "#1e293b",
- marginBottom: 20,
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
+ marginBottom: 8,
+ },
+ sectionSubtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 24,
},
buttonContainer: {
- marginTop: 24,
+ marginTop: 12,
+ },
+ saveButton: {
+ marginBottom: 4,
},
cancelButton: {
- marginTop: 12,
- borderWidth: 1,
- borderColor: "#cbd5e1",
+ marginTop: 4,
},
});
diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx
index fe245de..87035af 100644
--- a/src/screens/profile/Profile.tsx
+++ b/src/screens/profile/Profile.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useCallback } from "react";
import {
StyleSheet,
View,
@@ -7,913 +7,883 @@ import {
Image,
TouchableOpacity,
Alert,
- ActivityIndicator,
- StatusBar,
- TextInput,
- KeyboardAvoidingView,
- Platform,
RefreshControl,
ToastAndroid,
} from "react-native";
import * as Keychain from "react-native-keychain";
-import { Ionicons } from "@react-native-vector-icons/ionicons";
+import LucideIcon from '../../components/common/LucideIcon';
import { launchImageLibrary } from "react-native-image-picker";
-import { CustomButton, CustomInput } from "../../components/common/Custom";
-import colors from "../../components/colors";
-import SwitchNavigator from "./SwitchNavigator";
-import { useFocusEffect, useNavigation } from "@react-navigation/native";
-import { ApiResponse } from "../home/UserProfileStatus";
-import { BASE_URL, PUT_USER_PROFILE, PUT_USER_PROFILE_IMG, USER_STATUS } from "../../utils/ApiUrls";
-import AppView from "../../components/AppView";
+import { useNavigation } from "@react-navigation/native";
import { useDispatch } from "react-redux";
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { useTheme } from "../../hooks/ui/useTheme";
+import { useFetch } from "../../hooks/core/useFetch";
+import { useUpdate } from "../../hooks/core/useUpdate";
+import { CustomButton, CustomInput } from "../../components/common/Custom";
+import AppView from "../../components/AppView";
+import SkeletonLoader from "../../components/common/SkeletonLoader";
+import FontsFamily from "../../constents/FontsFamily";
import { clearUser, setUserData } from "../../redux/userSlice";
-import { StackNavigationProp } from "@react-navigation/stack";
-import { RootStackParamList } from "../../navigation/AppNavigator";
-import { firebase } from "@react-native-firebase/messaging";
-import FirebaseService, { deleteFcmToken, getFcmToken } from "../../utils/firebase/firebaseServices";
import { clearAllNotifications } from "../../redux/notificationSlice";
+import { deleteFcmToken, getFcmToken } from "../../utils/firebase/firebaseServices";
+import {
+ PROFILE_URL,
+ PUT_USER_PROFILE,
+ PUT_USER_PROFILE_IMG,
+ USER_STATUS,
+ BASE_URL,
+ USER_MOBILE_MONEY_URL,
+} from "../../utils/ApiUrls";
+import SwitchNavigator from "./SwitchNavigator";
-
-type UserProfile = {
+interface UserProfile {
id: string;
userId: number;
- firstName: string | null;
- lastName: string | null;
- mobile: string | null;
- gender: string | null;
- email: string | null;
- username: string | null;
- address: string | null;
- bankName: string | null;
- branchCode: string | null;
- filePath: string | null;
- bankAccountNumber: string | null;
- imageBlob: string | null;
- organizerStatus?: string;
- role?: string;
- onUpdate:()=>void
+ firstName: string;
+ lastName: string;
+ mobile: string;
+ gender: string;
+ email: string;
+ username: string;
+ address: string;
+ bankName: string;
+ branchCode: string;
+ bankAccountNumber: string;
+ imageBlob: string;
+ role: string;
+ organizerStatus: string;
+}
+
+type ProfileEditErrors = {
+ firstName?: string;
+ lastName?: string;
+ username?: string;
+ email?: string;
+ address?: string;
+ gender?: string;
};
+type ProfileEditField = "firstName" | "lastName" | "username" | "email" | "address" | "gender";
+
+type GenderOption = {
+ value: "Male" | "Female" | "Other";
+ label: string;
+ subtitle: string;
+ icon: "man-outline" | "woman-outline" | "sparkles-outline";
+};
+
+type MobileMoneyForm = {
+ provider: string;
+ mobileNumber: string;
+ currency: string;
+ countryCode: string;
+};
+
+type MobileMoneyErrors = Partial>;
+
+const GENDER_OPTIONS: GenderOption[] = [
+ { value: "Male", label: "Male", subtitle: "He / Him", icon: "man-outline" },
+ { value: "Female", label: "Female", subtitle: "She / Her", icon: "woman-outline" },
+ { value: "Other", label: "Other", subtitle: "They / Them", icon: "sparkles-outline" },
+];
+
+const isValidPersonName = (value: string) => /^[A-Za-z][A-Za-z\s'-]{1,}$/.test(value.trim());
+const isValidUsername = (value: string) => /^[A-Za-z0-9._-]{3,24}$/.test(value.trim());
+const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
+const normalizeGender = (value?: string) => {
+ const normalized = value?.trim().toLowerCase() || "";
+ if (!normalized) return "";
+ if (["male", "m", "man"].includes(normalized)) return "Male";
+ if (["female", "f", "woman"].includes(normalized)) return "Female";
+ if (["other", "non-binary", "nonbinary", "prefer not to say"].includes(normalized)) return "Other";
+ return value?.trim() || "";
+};
const Profile = () => {
- const navigation = useNavigation>();
- const dispatch = useDispatch()
- const [user, setUser] = useState(null);
- const [fetching, setFetching] = useState(true);
+ const theme = useTheme();
+ const navigation = useNavigation();
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
const [editing, setEditing] = useState(false);
- const [imageUpdating, setImageUpdating] = useState(false);
- const [loading, setLoading] = useState(false);
- const [statusData, setStatusData] = useState(null);
- const [error, setError] = useState(null);
- const [refreshing, setRefreshing] = useState(false);
+ const [editForm, setEditForm] = useState>({});
+ const [editErrors, setEditErrors] = useState({});
+ const [showMobileMoneyForm, setShowMobileMoneyForm] = useState(false);
+ const [mobileMoneyForm, setMobileMoneyForm] = useState({
+ provider: "",
+ mobileNumber: "",
+ currency: "",
+ countryCode: "",
+ });
+ const [mobileMoneyErrors, setMobileMoneyErrors] = useState({});
-
- const fetchUserProfile = async () => {
- try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
+ // --- API HOOKS ---
+ const {
+ data: profileData,
+ isLoading: isProfileLoading,
+ refetch: refetchProfile,
+ isFetching: isFetchingProfile
+ } = useFetch(PROFILE_URL, {
+ onSuccess: (res: any) => {
+ if (res.data) {
+ dispatch(setUserData({ ...res.data, profileStatus: true }));
+ setEditForm({ ...res.data, gender: normalizeGender(res.data.gender) });
}
- const token = credentials.password;
-
-
- setFetching(true);
- const response = await fetch(
- `${BASE_URL}user/profile/view`,
- {
- method: "GET",
- headers: {
- Accept: "*/*",
- Authorization: `Bearer ${token}`,
- },
- }
- );
-
-
- const result = await response.json();
- console.log("👤 Profile Response:", result);
- if (result.status === "success") {
- setUser(result.data);
- dispatch(setUserData({
- id: result?.data.id,
- userId: result?.data.userId,
- username: result?.data.username,
- firstName: result?.data.firstName,
- lastName: result?.data.lastName,
- gender: result?.data.gender,
- mobile: result?.data.mobile,
- email: result?.data.email,
- address: result?.data.address,
- bankAccountNumber: result?.data.bankAccountNumber,
- bankName: result?.data.bankName,
- branchCode: result?.data.branchCode,
- profilePicture: result?.data.filePath, // or keep as `filePath`
- filePath: result?.data.filePath,
- imageBlob: result?.data.imageBlob,
- role: result?.data.role,
- organizerId: result?.data.organizerId,
- organizerStatus: result?.data.organizerStatus,
- profileStatus: true, // set true once profile is fetched
- }));
- } else {
- ToastAndroid.show("Failed to fetch user profile", ToastAndroid.SHORT);
- }
- } catch (error) {
- console.error("❌ Error fetching profile:", error);
- } finally {
- setFetching(false);
}
- };
+ });
+ const { data: statusData, refetch: refetchStatus } = useFetch(USER_STATUS);
- const updateProfile = async () => {
- try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
- }
- const token = credentials.password;
-
-
- const response = await fetch(
- PUT_USER_PROFILE,
- {
- method: "PUT",
- headers: {
- Accept: "*/*",
- "Content-Type": "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
- address: user?.address || null,
- email: user?.email || null,
- firstName: user?.firstName || null,
- gender: user?.gender || null,
- lastName: user?.lastName || null,
- username: user?.username || null,
- }),
- }
- );
-
-
- const result = await response.json();
- if (result.status === "success") {
- ToastAndroid.show("Profile updated successfully", ToastAndroid.SHORT);
- setEditing(false);
- fetchUserProfile(); // reload updated data
- } else {
- ToastAndroid.show("Failed to update profile", ToastAndroid.SHORT);
- }
- } catch (error) {
- console.error("❌ Error updating profile:", error);
- ToastAndroid.show("Could not update profile", ToastAndroid.SHORT);
+ const { mutate: updateProfile, isPending: isUpdatingProfile } = useUpdate(PUT_USER_PROFILE, {
+ onSuccess: () => {
+ ToastAndroid.show("Profile updated", ToastAndroid.SHORT);
+ setEditErrors({});
+ setEditing(false);
+ refetchProfile();
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err.message || "Update failed", ToastAndroid.SHORT);
}
- };
+ });
-
- const pickImage = () => {
- launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
- if (response.didCancel) return;
- if (response.errorCode) {
- ToastAndroid.show(response.errorMessage || "Something went wrong", ToastAndroid.SHORT);
- } else if (response.assets && response.assets.length > 0) {
- const selectedImage = response.assets[0];
- setUser((prev: any) =>
- prev ? { ...prev, filePath: selectedImage.uri ?? null } : prev
- );
- }
- });
- };
-
-
- const handleUpdateImage = async () => {
- try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
- }
- const token = credentials.password;
-
-
- if (!user?.filePath) {
- ToastAndroid.show("Please select an image first", ToastAndroid.SHORT);
- return;
- }
-
-
- setImageUpdating(true);
-
-
- const formData = new FormData();
- formData.append("file", {
- uri: user?.filePath,
- type: "image/jpeg",
- name: "profile.jpg",
- } as any);
-
-
- const response = await fetch(
- PUT_USER_PROFILE_IMG,
- {
- method: "PUT",
- headers: {
- Accept: "application/json",
- Authorization: `Bearer ${token}`,
- },
- body: formData,
- }
- );
-
-
- const result = await response.json();
- if (result.status === "success") {
- ToastAndroid.show("Profile image updated successfully", ToastAndroid.SHORT);
-
- // If API returns updated image, set it
- if (result.data?.imageBlob) {
- setUser((prev: any) => prev ? { ...prev, imageBlob: result.data.imageBlob, filePath: null } : prev);
- } else {
- // Otherwise, keep the local filePath until next fetch
- setUser((prev: any) => prev ? { ...prev, filePath: user?.filePath } : prev);
- }
-
-
- // Refresh profile from API as well
- fetchUserProfile();
- } else {
- ToastAndroid.show(result.message || "Failed to update profile image", ToastAndroid.SHORT);
- }
- } catch (error) {
- console.error("❌ Error updating profile image:", error);
- ToastAndroid.show("Could not update profile image", ToastAndroid.SHORT);
- } finally {
- setImageUpdating(false);
+ const { mutate: updateImage, isPending: isUpdatingImage } = useUpdate(PUT_USER_PROFILE_IMG, {
+ onSuccess: () => {
+ ToastAndroid.show("Image updated", ToastAndroid.SHORT);
+ refetchProfile();
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err.message || "Image update failed", ToastAndroid.SHORT);
}
- };
+ });
+ const { mutate: updateMobileMoney, isPending: isUpdatingMobileMoney } = useUpdate(USER_MOBILE_MONEY_URL, {
+ onSuccess: () => {
+ ToastAndroid.show("Mobile money updated", ToastAndroid.SHORT);
+ setMobileMoneyErrors({});
+ setShowMobileMoneyForm(false);
+ refetchProfile();
+ },
+ onError: (err: any) => {
+ ToastAndroid.show(err.message || "Mobile money update failed", ToastAndroid.SHORT);
+ },
+ });
- const userStatus = async () => {
+ const user = profileData?.data;
+ const status = statusData?.data;
+ const selectedGender = normalizeGender(editForm.gender || user?.gender);
+ const scrollBottomInset = 64 + Math.max(insets.bottom, 8) + 20;
+
+ const onRefresh = useCallback(() => {
+ refetchProfile();
+ refetchStatus();
+ }, [refetchProfile, refetchStatus]);
+
+ const handleLogout = async () => {
try {
- const credentials = await Keychain.getGenericPassword();
- if (!credentials) {
- ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
- return;
- }
- const token = credentials.password;
-
-
- setLoading(true);
- setError(null);
- const response = await fetch(USER_STATUS, {
- method: "GET",
- headers: {
- Accept: "*/*",
- Authorization: `Bearer ${token}`,
- },
- });
-
-
- const json: ApiResponse = await response.json();
-
-
- console.log("👤 Profile Status Response:", json);
- if (json.status === "success") {
- setStatusData(json.data);
- console.log("statusData response :", json);
-
-
- } else {
- setError(json.message || "Failed to fetch status");
- }
- } catch (err) {
- setError("Something went wrong. Please try again.");
- } finally {
- setLoading(false);
- }
- };
-
-
- const logout = async (navigation: any) => {
- try {
- const credentials = await Keychain.getGenericPassword();
-
-
- if (!credentials) {
- ToastAndroid.show('No user is currently logged in', ToastAndroid.SHORT);
- return;
- }
const fcmToken = await getFcmToken(user?.userId?.toString());
- const res = await fetch(`${BASE_URL}token/remove?fcmToken=${fcmToken}`, {
- method: "PUT",
- headers: {
- Accept: "*/*",
- Authorization: `Bearer ${credentials.password}`,
- },
- });
- console.log(" logout api response : ", res)
- await deleteFcmToken(user?.userId?.toString());
+ if (fcmToken) {
+ await fetch(`${BASE_URL}token/remove?fcmToken=${fcmToken}`, {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${(await Keychain.getGenericPassword() as any).password}`,
+ },
+ });
+ await deleteFcmToken(user?.userId?.toString());
+ }
+
dispatch(clearAllNotifications());
- // Clear stored credentials
await Keychain.resetGenericPassword();
- dispatch(clearUser())
- ToastAndroid.show("You have been logged out", ToastAndroid.SHORT);
+ dispatch(clearUser());
navigation.replace("Login");
} catch (error) {
console.error("Logout error:", error);
}
};
-
- const confirmLogout = (navigation: any) => {
- Alert.alert(
- "Logout",
- "Are you sure you want to log out?",
- [
- { text: "Cancel", style: "cancel" },
- { text: "Yes, Logout", style: "destructive", onPress: () => logout(navigation) },
- ]
- );
+ const confirmLogout = () => {
+ Alert.alert("Logout", "Are you sure you want to log out?", [
+ { text: "Cancel", style: "cancel" },
+ { text: "Yes, Logout", style: "destructive", onPress: handleLogout },
+ ]);
};
- console.log("statusData?.role :", statusData?.role);
- console.log("statusData?.organizerStatus :", statusData?.organizerStatus);
+ const pickImage = () => {
+ launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => {
+ if (response.assets && response.assets.length > 0) {
+ const file = response.assets[0];
+ const formData = new FormData();
+ formData.append("file", {
+ uri: file.uri,
+ type: file.type || "image/jpeg",
+ name: file.fileName || "profile.jpg",
+ } as any);
+ updateImage(formData);
+ }
+ });
+ };
- useFocusEffect(
- useCallback(() => {
- userStatus();
- }, [])
+ const handleEditChange = (field: ProfileEditField, value: string) => {
+ setEditForm((prev) => ({ ...prev, [field]: value }));
+ if (editErrors[field]) {
+ setEditErrors((prev) => ({ ...prev, [field]: "" }));
+ }
+ };
+
+ const validateProfileForm = () => {
+ const nextErrors: ProfileEditErrors = {};
+ const firstName = editForm.firstName?.trim() || "";
+ const lastName = editForm.lastName?.trim() || "";
+ const username = editForm.username?.trim() || "";
+ const email = editForm.email?.trim() || "";
+ const address = editForm.address?.trim() || "";
+ const gender = normalizeGender(editForm.gender);
+
+ if (!isValidPersonName(firstName)) {
+ nextErrors.firstName = "Enter a valid first name (min 2 letters).";
+ }
+ if (!isValidPersonName(lastName)) {
+ nextErrors.lastName = "Enter a valid last name (min 2 letters).";
+ }
+ if (!isValidUsername(username)) {
+ nextErrors.username = "Username must be 3-24 chars (letters, numbers, . _ -).";
+ }
+ if (!isValidEmail(email)) {
+ nextErrors.email = "Enter a valid email address.";
+ }
+ if (address.length < 5) {
+ nextErrors.address = "Address should be at least 5 characters.";
+ }
+ if (!gender) {
+ nextErrors.gender = "Please choose a gender option.";
+ }
+
+ setEditErrors(nextErrors);
+ return Object.keys(nextErrors).length === 0;
+ };
+
+ const handleSaveProfile = () => {
+ if (!validateProfileForm()) {
+ ToastAndroid.show("Please fix the highlighted fields.", ToastAndroid.SHORT);
+ return;
+ }
+
+ const payload = {
+ firstName: editForm.firstName?.trim() || user?.firstName || "",
+ lastName: editForm.lastName?.trim() || user?.lastName || "",
+ gender: normalizeGender(editForm.gender) || normalizeGender(user?.gender) || "",
+ email: editForm.email?.trim() || user?.email || "",
+ username: editForm.username?.trim() || user?.username || "",
+ address: editForm.address?.trim() || user?.address || "",
+ };
+
+ updateProfile({
+ ...payload,
+ });
+ };
+
+ const handleMobileMoneyChange = (field: keyof MobileMoneyForm, value: string) => {
+ const nextValue = field === "currency" || field === "countryCode" ? value.toUpperCase() : value;
+
+ setMobileMoneyForm((prev) => ({ ...prev, [field]: nextValue }));
+ if (mobileMoneyErrors[field]) {
+ setMobileMoneyErrors((prev) => ({ ...prev, [field]: "" }));
+ }
+ };
+
+ const validateMobileMoneyForm = () => {
+ const nextErrors: MobileMoneyErrors = {};
+ const provider = mobileMoneyForm.provider.trim();
+ const mobileNumber = mobileMoneyForm.mobileNumber.trim();
+ const currency = mobileMoneyForm.currency.trim().toUpperCase();
+ const countryCode = mobileMoneyForm.countryCode.trim().toUpperCase();
+
+ if (provider.length < 2) {
+ nextErrors.provider = "Provider name should be at least 2 letters.";
+ }
+ if (!/^\d{8,15}$/.test(mobileNumber)) {
+ nextErrors.mobileNumber = "Enter a valid mobile number (8-15 digits).";
+ }
+ if (!/^[A-Z]{3}$/.test(currency)) {
+ nextErrors.currency = "Currency must be 3 letters, e.g. USD.";
+ }
+ if (!/^[A-Z]{2,4}$/.test(countryCode)) {
+ nextErrors.countryCode = "Country code should be 2-4 letters.";
+ }
+
+ setMobileMoneyErrors(nextErrors);
+ return Object.keys(nextErrors).length === 0;
+ };
+
+ const handleSaveMobileMoney = () => {
+ if (!validateMobileMoneyForm()) {
+ ToastAndroid.show("Please fix mobile money fields.", ToastAndroid.SHORT);
+ return;
+ }
+
+ updateMobileMoney({
+ provider: mobileMoneyForm.provider.trim(),
+ mobileNumber: mobileMoneyForm.mobileNumber.trim(),
+ currency: mobileMoneyForm.currency.trim().toUpperCase(),
+ countryCode: mobileMoneyForm.countryCode.trim().toUpperCase(),
+ });
+ };
+
+ const renderDetailRow = (icon: any, label: string, value: string) => (
+
+
+
+
+
+ {label}
+ {value || "Not provided"}
+
+
);
-
- useEffect(() => {
- userStatus();
- fetchUserProfile();
- }, []);
-
-
- const onRefresh = () => {
- setRefreshing(true);
- // Simulate a network request or data fetching
- fetchUserProfile()
- setTimeout(() => {
- setRefreshing(false);
- }, 2000); // 2 seconds delay
- };
-
+ const renderSkeletons = () => (
+
+
+
+
+
+ );
return (
-
- {/* Header */}
+
- User Profile
- confirmLogout(navigation)} style={styles.refreshButton}>
- Logout
-
+ Profile
+
+
- {/* if (fetching || !user) {
- return ( */}
- {fetching || !user &&
-
-
- Loading profile...
-
-
- }
- }
+ keyboardShouldPersistTaps="handled"
+ keyboardDismissMode="on-drag"
+ showsVerticalScrollIndicator={false}
>
-
- }
- >
-
-
- {/* Profile Card */}
-
- {/* Profile Image Section */}
- {
- statusData?.role === "USER" && statusData?.organizerStatus !== "NOT_APPLIED" ? (
-
- Your Role: {statusData.role} | Organizer Status: {statusData.organizerStatus}
-
- ) : (
- statusData?.role !== "ORGANIZER" && (
-
- )
- )
- }
-
-
-
+ {isProfileLoading ? renderSkeletons() : (
+ <>
+
+
console.log("Error loading image")}
- resizeMode='contain'
/>
- {user?.filePath}
-
-
+
+
-
-
-
+ {user?.firstName} {user?.lastName}
+ {status?.role || user?.role}
+ {status?.role === "USER" && status?.organizerStatus !== "APPROVED" && (
+
+
+
+ )}
{!editing ? (
- <>
- {/* Profile Details */}
-
-
-
- Personal Information
- Become Organizer
-
-
-
- setEditing(true)}
- icon="create"
- style={styles.editButton}
- buttonBackground={colors.white}
- />
-
+
+
+
+ Personal Information
+ {
+ setEditForm({ ...(user || {}), gender: normalizeGender(user?.gender) });
+ setEditErrors({});
+ setEditing(true);
+ }}
+ >
+
+
-
-
-
-
- Name
- {user?.firstName} {user?.lastName}
-
-
-
-
-
-
- Email
- {user?.email || "Not provided"}
-
-
-
-
-
-
-
- Mobile
- {user?.mobile || "Not provided"}
-
-
-
-
-
-
-
- Usernames
- {user?.username}
-
-
-
-
-
-
-
- Address
- {user?.address || "Not provided"}
-
-
-
-
-
-
-
- Gender
- {user?.gender || "Not specified"}
-
-
-
-
-
-
-
- Role
- {user?.role || "User"}
-
-
-
- {/* BANK DETAILS SECTION */}
-
-
- Bank Information
- {/* Navigate to a screen you will create for editing bank details */}
-
- {
- navigation.navigate("EditBankDetails", {
- currentBankName: user?.bankName,
- currentAccountNumber: user?.bankAccountNumber,
- currentBranchCode: user?.branchCode,
- onUpdate: () =>fetchUserProfile() // Refresh profile data after update
-
- })
- }}
-
- icon="create-outline"
- style={styles.editButton}
- buttonBackground={colors.white}
- />
-
-
-
-
-
- Bank Name
- {user?.bankName || "Not provided"}
-
-
-
-
-
- Branch Code
- {user?.branchCode || "Not provided"}
-
-
-
-
-
- Account Number
- {user?.bankAccountNumber || "Not provided"}
-
-
-
-
+ {renderDetailRow("mail-outline", "Email", user?.email)}
+ {renderDetailRow("call-outline", "Mobile", user?.mobile)}
+ {renderDetailRow("at-outline", "Username", user?.username)}
+ {renderDetailRow("location-outline", "Address", user?.address)}
+ {renderDetailRow("male-female-outline", "Gender", user?.gender)}
- >
+
+
+
+ Bank Information
+ navigation.navigate("EditBankDetails", {
+ currentBankName: user?.bankName,
+ currentAccountNumber: user?.bankAccountNumber,
+ currentBranchCode: user?.branchCode,
+ onUpdate: refetchProfile
+ })}>
+
+
+
+
+ {renderDetailRow("business-outline", "Bank Name", user?.bankName)}
+ {renderDetailRow("barcode-outline", "Branch (IFSC)", user?.branchCode)}
+ {renderDetailRow("card-outline", "Account Number", user?.bankAccountNumber)}
+
+
+
+
+ Mobile Money
+ {
+ if (showMobileMoneyForm) {
+ setShowMobileMoneyForm(false);
+ setMobileMoneyErrors({});
+ return;
+ }
+ setMobileMoneyForm((prev) => ({
+ ...prev,
+ mobileNumber: prev.mobileNumber || user?.mobile || "",
+ }));
+ setShowMobileMoneyForm(true);
+ }}
+ >
+
+
+
+
+ {!showMobileMoneyForm ? (
+ <>
+
+ Add your mobile money wallet details for faster payment collection.
+
+ {
+ setMobileMoneyForm((prev) => ({
+ ...prev,
+ mobileNumber: prev.mobileNumber || user?.mobile || "",
+ }));
+ setShowMobileMoneyForm(true);
+ }}
+ type="outline"
+ icon="wallet-outline"
+ style={styles.mobileMoneyButton}
+ />
+ >
+ ) : (
+
+ handleMobileMoneyChange("provider", t)}
+ icon="business-outline"
+ placeholder="Enter provider, e.g. MTN, Airtel"
+ error={mobileMoneyErrors.provider}
+ />
+ handleMobileMoneyChange("mobileNumber", t)}
+ icon="call-outline"
+ error={mobileMoneyErrors.mobileNumber}
+ keyboardType="phone-pad"
+ />
+ handleMobileMoneyChange("currency", t)}
+ icon="cash-outline"
+ error={mobileMoneyErrors.currency}
+ autoCapitalize="characters"
+ maxLength={3}
+ />
+ handleMobileMoneyChange("countryCode", t)}
+ icon="flag-outline"
+ error={mobileMoneyErrors.countryCode}
+ autoCapitalize="characters"
+ maxLength={4}
+ />
+
+ {
+ setShowMobileMoneyForm(false);
+ setMobileMoneyErrors({});
+ }}
+ type="outline"
+ style={{ flex: 1 }}
+ />
+
+
+
+ )}
+
+
) : (
- <>
- {/* Edit Form */}
-
- Edit Profile
+
+
+ Edit Profile
setUser({ ...user, firstName: text })}
- icon="person"
+ value={editForm.firstName}
+ onChangeText={(t) => handleEditChange("firstName", t)}
+ icon="person-outline"
+ error={editErrors.firstName}
/>
-
setUser({ ...user, lastName: text })}
- icon="person"
+ value={editForm.lastName}
+ onChangeText={(t) => handleEditChange("lastName", t)}
+ icon="person-outline"
+ error={editErrors.lastName}
/>
-
handleEditChange("username", t)}
+ icon="at-outline"
+ error={editErrors.username}
+ autoCapitalize="none"
+ />
+
+ Gender
+
+ Choose your profile badge.
+
+
+ {GENDER_OPTIONS.map((option) => {
+ const isSelected = selectedGender === option.value;
+
+ return (
+ handleEditChange("gender", option.value)}
+ style={[
+ styles.genderCard,
+ {
+ borderColor: isSelected ? theme.colors.primary : theme.colors.border,
+ backgroundColor: isSelected ? theme.colors.primarySoft : theme.colors.background,
+ },
+ ]}
+ >
+
+
+
+
+
+ {option.label}
+
+
+ {option.subtitle}
+
+
+
+
+ );
+ })}
+
+
+
+
+ {selectedGender ? `${selectedGender} selected` : "Tap a card to select gender"}
+
+
+ {editErrors.gender ? (
+ {editErrors.gender}
+ ) : null}
+
+ setUser({ ...user, email: text })}
- icon="mail"
+ value={editForm.email}
+ onChangeText={(t) => handleEditChange("email", t)}
+ icon="mail-outline"
+ error={editErrors.email}
keyboardType="email-address"
/>
-
setUser({ ...user, username: text })}
- icon="at"
- />
-
- setUser({ ...user, address: text })}
- icon="location"
+ value={editForm.address}
+ onChangeText={(t) => handleEditChange("address", t)}
+ icon="location-outline"
+ error={editErrors.address}
multiline
/>
-
- Gender
-
- {["Male", "Female", "Other"].map((option) => (
- setUser({ ...user, gender: option })}
- >
-
- {option}
-
-
- ))}
-
-
-
-
-
-
+
setEditing(false)}
- icon="close"
- style={styles.cancelButton}
- buttonBackground={colors.white}
+ onPress={() => {
+ setEditErrors({});
+ setEditing(false);
+ }}
+ type="outline"
+ style={{ flex: 1 }}
+ />
+
- >
+
)}
-
-
-
+ >
+ )}
+
);
};
-
const styles = StyleSheet.create({
- container: {
- flex: 1,
- // backgroundColor: colors.pastelBlue,
- paddingBottom: '30%',
- },
- loadingContainer: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: "#f8fafc"
- },
- loadingText: {
- marginTop: 10,
- fontSize: 16,
- color: "#64748b"
- },
header: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- padding: 16,
- borderBottomLeftRadius: 20,
- borderBottomRightRadius: 20,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 14,
+ paddingVertical: 10,
},
headerTitle: {
- fontSize: 22, fontWeight: "600", color: colors.black
+ fontSize: responsiveFontSize(2.8),
+ fontFamily: FontsFamily.nunitoBold,
},
- refreshButton: {
+ logoutBtn: {
+ padding: 6,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ },
+ avatarWrapper: {
+ alignItems: 'center',
+ marginTop: 8,
+ marginBottom: 16,
+ },
+ avatarBorder: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ borderWidth: 1,
padding: 4,
- flexDirection: "row",
- },
-
-
- profileCard: {
- borderRadius: 20,
- margin: 16,
- // backgroundColor: "#fff",
- // padding: 20,
- // shadowColor: "#000",
- // shadowOpacity: 0.1,
- // shadowRadius: 10,
- // shadowOffset: { width: 0, height: 4 },
- // elevation: 5,
- },
- profileImageSection: {
- alignItems: "center",
- marginBottom: 24,
- paddingBottom: 20,
- borderBottomWidth: 1,
- borderBottomColor: "#f1f5f9",
- },
- avatarContainer: {
- position: "relative",
+ position: 'relative',
marginBottom: 16,
},
avatar: {
- width: 120,
- height: 130,
+ width: '100%',
+ height: '100%',
borderRadius: 60,
- borderWidth: 4,
- borderColor: "#e2e8f0",
},
- cameraIcon: {
- position: "absolute",
- right: 0,
+ cameraBtn: {
+ position: 'absolute',
bottom: 0,
- backgroundColor: colors.pastelBlue,
- borderRadius: 20,
- width: 40,
- height: 40,
- justifyContent: "center",
- alignItems: "center",
- borderWidth: 3,
- borderColor: "#fff",
- elevation: 2,
+ right: 0,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ elevation: 4,
+ shadowColor: '#000',
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ shadowOffset: { width: 0, height: 2 },
},
- updateImageButton: {
- width: "80%",
- paddingHorizontal: 10,
- borderRadius: 25,
- elevation: 5,
-
+ userName: {
+ fontSize: responsiveFontSize(2.4),
+ fontFamily: FontsFamily.nunitoBold,
},
- detailsSection: {
- marginBottom: "40%",
+ userRole: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoMedium,
+ marginTop: 4,
},
- sectionTitle: {
- fontSize: 18,
- fontWeight: "700",
- color: "#1e293b",
- // marginBottom: 16,
- paddingBottom: 8,
- borderBottomWidth: 1,
- borderBottomColor: "#f1f5f9",
+ switchContainer: {
+ marginHorizontal: 14,
+ marginBottom: 14,
},
- detailRow: {
- flexDirection: "row",
- alignItems: "center",
- marginBottom: 16,
+ content: {
paddingHorizontal: 12,
},
- detailIcon: {
- marginRight: 20,
- elevation: 5
+ card: {
+ padding: 18,
+ borderRadius: 18,
+ borderWidth: 1,
+ marginBottom: 14,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOpacity: 0.04,
+ shadowRadius: 12,
+ shadowOffset: { width: 0, height: 4 },
+ },
+ cardHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 14,
+ },
+ cardTitle: {
+ fontSize: responsiveFontSize(2),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ detailRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ iconBox: {
+ width: 36,
+ height: 36,
+ borderRadius: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ detailTextContainer: {
+ flex: 1,
},
detailLabel: {
- fontSize: 12,
- color: "#64748b",
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
marginBottom: 2,
},
- detailText: {
- fontSize: 16,
- color: "#334155",
- fontWeight: "500",
+ detailValue: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
- editButton: {
- elevation: 5,
- paddingHorizontal: 10,
- paddingVertical: 4,
- borderRadius: 15,
- borderWidth: 2,
- borderColor: colors.pastelBlue
+ mobileMoneyHint: {
+ fontSize: responsiveFontSize(1.45),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 10,
+ },
+ mobileMoneyButton: {
+ marginTop: 2,
+ },
+ mobileMoneyActions: {
+ flexDirection: 'row',
+ gap: 12,
+ marginTop: 6,
},
editSection: {
- marginBottom: '100%',
- flex: 1,
+ paddingHorizontal: 12,
},
- editButtons: {
+ editActions: {
+ flexDirection: 'row',
gap: 12,
+ marginTop: 14,
},
- saveButton: {
- marginTop: 8,
+ genderSection: {
+ marginBottom: 14,
},
- cancelButton: {
- marginTop: 4,
+ genderLabel: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ marginBottom: 4,
+ },
+ genderHint: {
+ fontSize: responsiveFontSize(1.3),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginBottom: 8,
+ },
+ genderGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between',
+ gap: 10,
+ },
+ genderCard: {
+ width: '48.5%',
borderWidth: 1,
- borderColor: "#cbd5e1",
+ borderRadius: 14,
+ paddingHorizontal: 10,
+ paddingVertical: 10,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
},
- inputContainer: {
- marginBottom: 16,
+ genderIconBadge: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ justifyContent: 'center',
+ alignItems: 'center',
},
- inputLabel: {
- fontSize: 14,
- fontWeight: "600",
- color: "#374151",
- marginBottom: 6,
- },
- inputWrapper: {
- flexDirection: "row",
- alignItems: "center",
- position: "relative",
- },
- inputIcon: {
- position: "absolute",
- left: 12,
- zIndex: 1,
- },
- input: {
- borderWidth: 1,
- borderColor: "#d1d5db",
- borderRadius: 12,
- padding: 12,
- backgroundColor: "#fff",
- color: "#1f2937",
- fontSize: 16,
+ genderTextBlock: {
flex: 1,
},
- inputWithIcon: {
- paddingLeft: 40,
+ genderTitle: {
+ fontSize: responsiveFontSize(1.4),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
- buttonSecondary: {
- backgroundColor: "#f1f5f9",
+ genderSubTitle: {
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 1,
},
- buttonDanger: {
- backgroundColor: "#ef4444",
- },
- buttonOutline: {
- backgroundColor: "transparent",
+ genderPreview: {
+ marginTop: 10,
borderWidth: 1,
- borderColor: "#6366f1",
+ borderRadius: 10,
+ paddingVertical: 8,
+ paddingHorizontal: 10,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
},
- buttonText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "600",
+ genderPreviewText: {
+ fontSize: responsiveFontSize(1.25),
+ fontFamily: FontsFamily.nunitoRegular,
},
- buttonTextSecondary: {
- color: "#334155",
- },
- buttonTextDanger: {
- color: "#fff",
- },
- buttonTextOutline: {
- color: "#6366f1",
- },
- genderRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- marginTop: 8,
- marginBottom: 16,
- gap: 12,
- },
- genderButton: {
- flex: 1,
- paddingVertical: 12,
- backgroundColor: "#f1f5f9",
- borderRadius: 12,
- alignItems: "center",
- borderWidth: 1,
- borderColor: "#e2e8f0",
- },
- genderButtonActive: {
- backgroundColor: colors.pastelBlue,
- },
- genderButtonText: {
- color: "#64748b",
- fontWeight: "500",
- fontSize: 16,
- },
- genderButtonTextActive: {
- color: colors.black,
- fontWeight: "700"
- },
-
- buttonIcon: {
- marginRight: 8,
+ genderError: {
+ marginTop: 6,
+ fontSize: responsiveFontSize(1.2),
+ fontFamily: FontsFamily.nunitoSemiBold,
},
+ skeletonContainer: {
+ paddingHorizontal: 14,
+ marginTop: 14,
+ }
});
-
export default Profile;
diff --git a/src/screens/profile/SwitchNavigator.tsx b/src/screens/profile/SwitchNavigator.tsx
index 5a26a3a..a0a7cc3 100644
--- a/src/screens/profile/SwitchNavigator.tsx
+++ b/src/screens/profile/SwitchNavigator.tsx
@@ -1,12 +1,11 @@
-import React, { useState } from "react";
-import { View, Text, StyleSheet, Switch } from "react-native";
+import React from "react";
+import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useNavigation } from "@react-navigation/native";
-import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+import LucideIcon from '../../components/common/LucideIcon';
-type RootStackParamList = {
- BecomeOrg: undefined;
- Profile: undefined;
-};
+import { useTheme } from "../../hooks/ui/useTheme";
+import FontsFamily from "../../constents/FontsFamily";
type SwitchNavigatorProps = {
role: string;
@@ -14,54 +13,85 @@ type SwitchNavigatorProps = {
};
const SwitchNavigator: React.FC = ({ role, status }) => {
- const navigation =
- useNavigation>();
- const [isEnabled, setIsEnabled] = useState(false);
+ const theme = useTheme();
+ const navigation = useNavigation();
+ const canApply = role === "USER" && (status === "NOT_APPLIED" || !status);
+ const prettyStatus = status ? status.replace(/_/g, " ") : "NOT APPLIED";
- const toggleSwitch = () => {
- const newValue = !isEnabled;
- setIsEnabled(newValue);
-
- if (newValue) {
- navigation.navigate("BecomeOrg"); // Navigate when ON
- } else {
- navigation.navigate("Profile"); // Navigate when OFF
- }
- };
-
- console.log("status :", status);
- console.log("role :", role);
+ const primaryText = canApply ? "Become an Organizer" : "Organizer Application";
+ const secondaryText = canApply
+ ? "Tap to apply and unlock organizer tools"
+ : `Current status: ${prettyStatus}`;
return (
-
-
- {!isEnabled && role !== "USER"
- ? "Become an Organizer"
- : `Request Organizer Role ${status}`}
-
-
-
+ navigation.navigate("BecomeOrg")}
+ style={[styles.container, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}
+ >
+
+
+
+
+
+ {primaryText}
+ {secondaryText}
+
+
+ { prettyStatus !== "PENDING" &&
+
+ }
+
);
};
-export default SwitchNavigator;
-
const styles = StyleSheet.create({
container: {
flexDirection: "row",
- justifyContent: 'flex-end',
- paddingHorizontal: 16,
- paddingVertical: 10,
- gap: 10,
+ alignItems: 'center',
+ paddingHorizontal: 14,
+ paddingVertical: 13,
+ borderRadius: 16,
+ borderWidth: 1,
+ elevation: 1,
+ shadowColor: '#000',
+ shadowOpacity: 0.02,
+ shadowRadius: 8,
+ shadowOffset: { width: 0, height: 2 },
},
- text: {
- fontSize: 18,
- flexShrink: 1, // Wrap text if too long
+ leadingIcon: {
+ width: 34,
+ height: 34,
+ borderRadius: 17,
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: 10,
+ },
+ textBlock: {
+ flex: 1,
+ },
+ primaryText: {
+ fontSize: responsiveFontSize(1.8),
+ fontFamily: FontsFamily.nunitoSemiBold,
+ },
+ secondaryText: {
+ fontSize: responsiveFontSize(1.35),
+ fontFamily: FontsFamily.nunitoRegular,
+ marginTop: 2,
+ },
+ arrowBox: {
+ width: 30,
+ height: 30,
+ borderRadius: 15,
+ borderWidth: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ marginLeft: 10,
},
});
+
+export default SwitchNavigator;
diff --git a/src/screens/profile/Transaction.tsx b/src/screens/profile/Transaction.tsx
index a4ee84f..c3edcf6 100644
--- a/src/screens/profile/Transaction.tsx
+++ b/src/screens/profile/Transaction.tsx
@@ -1,17 +1,69 @@
-import { StyleSheet, Text, View } from 'react-native'
-import React from 'react'
-import AppView from '../../components/AppView'
+import React from 'react';
+import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
+import LucideIcon from '../../components/common/LucideIcon';
+import { useNavigation } from "@react-navigation/native";
+import { responsiveFontSize } from "react-native-responsive-dimensions";
+
+import AppView from '../../components/AppView';
+import { useTheme } from "../../hooks/ui/useTheme";
+import FontsFamily from "../../constents/FontsFamily";
const Transaction = () => {
- return (
-
- Transaction
-
- )
-}
+ const theme = useTheme();
+ const navigation = useNavigation();
-export default Transaction
+ return (
+
+
+ navigation.goBack()} style={styles.backButton}>
+
+
+ Transactions
+
+
+
+
+ Coming Soon
+
+ Your transaction history will appear here.
+
+
+
+ );
+};
const styles = StyleSheet.create({
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 24,
+ paddingVertical: 16,
+ },
+ backButton: {
+ marginRight: 16,
+ padding: 4,
+ },
+ headerTitle: {
+ fontSize: responsiveFontSize(2.4),
+ fontFamily: FontsFamily.nunitoBold,
+ },
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 40,
+ },
+ title: {
+ fontSize: responsiveFontSize(2.4),
+ fontFamily: FontsFamily.nunitoBold,
+ marginTop: 20,
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: responsiveFontSize(1.6),
+ fontFamily: FontsFamily.nunitoRegular,
+ textAlign: 'center',
+ }
+});
-})
\ No newline at end of file
+export default Transaction;
diff --git a/src/screens/setting/Setting.tsx b/src/screens/setting/Setting.tsx
index 1526ee2..73fdd7d 100644
--- a/src/screens/setting/Setting.tsx
+++ b/src/screens/setting/Setting.tsx
@@ -13,4 +13,4 @@ const Setting = () => {
export default Setting
-const styles = StyleSheet.create({})
\ No newline at end of file
+const styles = StyleSheet.create({})
diff --git a/src/screens/transaction/TransactionList.tsx b/src/screens/transaction/TransactionList.tsx
index bfcd4eb..067013a 100644
--- a/src/screens/transaction/TransactionList.tsx
+++ b/src/screens/transaction/TransactionList.tsx
@@ -198,4 +198,4 @@ const styles = StyleSheet.create({
emptyContainer: { alignItems: "center", justifyContent: "center", },
emptyTitle: { fontSize: 18, fontWeight: "600", color: "#374151" },
emptySubtitle: { marginTop: 8, color: "#6b7280" },
-});
\ No newline at end of file
+});
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
index 920eb06..fb1496b 100644
--- a/src/theme/theme.ts
+++ b/src/theme/theme.ts
@@ -1,75 +1,115 @@
export type ThemeMode = 'light' | 'dark';
+export const palette = {
+ black: '#000000',
+ greyDark: '#6F6F73',
+ greyMedium: '#9A9A9A',
+ greyLight: '#B0B0B0',
+ greyExtraLight: '#E6E6E6',
+ white: '#FFFFFF',
+};
+
+export const spacing = {
+ xs: 4,
+ sm: 6,
+ md: 10,
+ lg: 14,
+ xl: 18,
+ xxl: 24,
+ xxxl: 32,
+};
+
+export const borderRadius = {
+ sm: 4,
+ md: 8,
+ lg: 12,
+ xl: 16,
+ xxl: 24,
+ full: 100,
+};
+
export interface ThemeColors {
primary: string;
+ primarySoft: string;
+ onPrimary: string;
secondary: string;
background: string;
+ surface: string;
card: string;
textPrimary: string;
textSecondary: string;
- success: string;
- border: string;
- logincard?: string;
- logincard1?: string;
+ textMuted: string;
reverseBackground: string;
- reverseText?: string;
- optCard?: string;
- gray?: string;
- black?: string;
- white?: string;
- red?: string;
- green?: string;
+ reverseText: string;
+ border: string;
+ borderLight: string;
+ white: string;
+ black: string;
+ error: string;
+ success: string;
+ warning: string;
}
export interface Theme {
mode: ThemeMode;
colors: ThemeColors;
+ spacing: typeof spacing;
+ borderRadius: typeof borderRadius;
+ palette: typeof palette;
}
export const light: Theme = {
mode: 'light',
+ spacing,
+ borderRadius,
+ palette,
colors: {
- primary: '#F4721E',
- secondary: '#FF9A62',
- reverseBackground: '#000000',
- background: '#FFFFFF',
- card: '#F3F4F6',
- textPrimary: '#000000',
- reverseText: '#FFFFFF',
- textSecondary: '#666666',
- success: '#008e02',
- border: '#E5E7EB',
- logincard: '#e0e0e0',
- logincard1: '#66666eb3',
- optCard: '#66666eb3',
- gray:'#e0e0e0ff',
- black:'#000000',
- white:'#FFFFFF',
- red:'#FF0000',
- green:'#00C853'
+ primary: '#1C1C1F',
+ primarySoft: 'rgba(28, 28, 31, 0.1)',
+ onPrimary: palette.white,
+ secondary: '#5E5E64',
+ background: palette.white,
+ surface: '#F4F4F6',
+ card: palette.white,
+ textPrimary: palette.black,
+ textSecondary: '#5E5E64',
+ textMuted: '#7B7B82',
+ reverseBackground: palette.black,
+ reverseText: palette.white,
+ border: '#DADAE0',
+ borderLight: '#ECECF1',
+ white: palette.white,
+ black: palette.black,
+ error: '#B94747',
+ success: '#2E8A57',
+ warning: '#9B6E19',
},
};
export const dark: Theme = {
mode: 'dark',
+ spacing,
+ borderRadius,
+ palette,
colors: {
- primary: '#F4721E',
- secondary: '#FF9A62',
- reverseBackground: '#FFFFFF',
- background: '#000000', // Fixed: Dark mode = Black bg
- card: '#111214',
- textPrimary: '#FFFFFF',
- textSecondary: '#A1A1A1',
- reverseText: '#000000',
- success: '#00C853',
- border: '#222427',
- logincard: '#000000',
- logincard1: '#8d8d8f',
- optCard: '#e0e0e0',
- gray:'#66666eb3',
- black:'#000000',
- white:'#FFFFFF',
- red:'#FF0000',
- green:'#00C853'
+ primary: '#E4E4E8',
+ primarySoft: 'rgba(228, 228, 232, 0.18)',
+ onPrimary: '#101013',
+ secondary: '#C1C1C8',
+ background: '#0D0D11',
+ surface: '#1A1A1F',
+ card: '#141419',
+ textPrimary: '#F5F5F8',
+ textSecondary: '#C1C1C8',
+ textMuted: '#9A9AA2',
+ reverseBackground: palette.white,
+ reverseText: palette.black,
+ border: '#2D2D35',
+ borderLight: '#22222A',
+ white: palette.white,
+ black: palette.black,
+ error: '#B86666',
+ success: '#3B8E62',
+ warning: '#A87B2F',
},
};
diff --git a/src/types.ts b/src/types.ts
index fe88e58..bc6b8f9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -16,8 +16,8 @@ export interface MyGroup {
currentParticipants: number;
maxParticipants: number;
contributionAmount: number;
- cycleType: "MONTHLY" | "QUARTERLY" | "YEARLY";
- status: "PENDING" | "ACTIVE" | "COMPLETED";
+ cycleType: "CUSTOM" | "DAILY" | "WEEKLY" | "MONTHLY" | "QUARTERLY" | "HALF_YEARLY" | "YEARLY";
+ status: "PENDING" | "APPROVED" | "ACTIVE" | "DELAY" | "COMPLETED" | "REJECTED" | "CANCELLED";
}
export interface JoinedGroup {
diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx
index d16a1ab..432d641 100644
--- a/src/utils/ApiHelper.tsx
+++ b/src/utils/ApiHelper.tsx
@@ -1,5 +1,5 @@
import axios, { AxiosRequestConfig } from "axios";
-import { Alert, ToastAndroid } from "react-native";
+import { ToastAndroid } from "react-native";
import * as Keychain from "react-native-keychain";
/**
@@ -16,13 +16,23 @@ export async function apiCall(
config?: AxiosRequestConfig,
responseErrorMessage?:string
): Promise {
+ const normalizedMethod = method.toUpperCase();
+ const requestPayload =
+ normalizedMethod === "GET" || normalizedMethod === "DELETE"
+ ? config?.params ?? null
+ : data ?? null;
+
+ console.log(`[HOOK_API][${normalizedMethod}] URL:`, url);
+ console.log(`[HOOK_API][${normalizedMethod}] PAYLOAD:`, requestPayload);
+
try {
// Get saved credentials from Keychain
const creds = await Keychain.getGenericPassword();
console.log("Keychain Credentials Found:", creds); // Log if creds were found
+ const isMultipart = typeof FormData !== "undefined" && data instanceof FormData;
const headers = {
- "Content-Type": "application/json",
+ "Content-Type": isMultipart ? "multipart/form-data" : "application/json",
...(creds ? { Authorization: `Bearer ${creds.password}` } : {}),
...config?.headers,
};
@@ -35,11 +45,22 @@ export async function apiCall(
headers,
});
- // --- Response Logging ---
- console.log(`--- API Response:${url}///// [${response}] `);
+ console.log(`[HOOK_API][${normalizedMethod}] SUCCESS URL:`, url);
+ console.log(`[HOOK_API][${normalizedMethod}] SUCCESS META:`, {
+ statusCode: response.status ?? null,
+ status: response.statusText ?? null,
+ message: (response.data as { message?: string })?.message ?? null,
+ });
+ console.log(`[HOOK_API][${normalizedMethod}] SUCCESS RESPONSE:`, response.data);
return response.data;
} catch (error: any) {
+ console.error(`[HOOK_API][${normalizedMethod}] ERROR URL:`, url);
+ console.error(
+ `[HOOK_API][${normalizedMethod}] ERROR RESPONSE:`,
+ error?.response?.data ?? error?.message ?? error
+ );
+
if (error.response) {
// Server responded with a status other than 2xx
// console.error(
@@ -60,8 +81,6 @@ export async function apiCall(
// console.error("API Error:", error);
}
console.log(`API : ${url} API Error:`, error);
- return error.response.status
- // Re-throw the error so that the calling function can handle it if needed
- throw error;
+ return null;
}
}
diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx
index 5cab9ee..5e75c3e 100644
--- a/src/utils/ApiUrls.tsx
+++ b/src/utils/ApiUrls.tsx
@@ -25,4 +25,7 @@ export const UPLOAD_GROUP_IMAGE = `${BASE_URL}fund/groups/`;
export const STORE_FCM_TOKEN = `${BASE_URL}token/store?fcmToken=`
export const REMOVE_FCM_TOKEN = `${BASE_URL}token/remove?fcmToken=`
export const DASHBOARD_URL = `${BASE_URL}fund/dashboard`
-export const PAY_ALL_URL = `${BASE_URL}payments/payments/bulk`
\ No newline at end of file
+export const PAY_ALL_URL = `${BASE_URL}payments/payments/bulk`
+export const APPLY_ORGANIZER_URL = `${BASE_URL}organizer/registration/apply`;
+export const USER_MOBILE_MONEY_URL = `${BASE_URL}user/register/mobile/money`
+export const PAYMENT_MODES_URL = `${BASE_URL}payments/getAll/payment-mode`