feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
CI / quality (push) Waiting to run
CI / quality (push) Waiting to run
- Add src/core/ (config, errors, events, http, observability, storage, query) - Add src/Spreadsheet/ engine with native POI backend and Excel bridge - Add Envelope C spreadsheet flow and tender bidding envelope modules - Consolidate components into src/components/, remove legacy Component/ and Hooks/ - Add registration state/validation, auth session guard, profile edit modules - Add unit tests across axios, redux, bidding, registration, encryption, core - Add CI workflow, docs (architecture, audit, DSC API spec), keystore example - Remove tracked keystore binary; ignore scripts.zip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9acec328a7
commit
1d7646d938
@@ -0,0 +1,31 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: npx jest --ci
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
# Errors are blocking; style warnings are still being burned down.
|
||||||
|
# Tighten to --max-warnings=0 once `npx eslint .` exits 0 on main.
|
||||||
|
- name: Lint
|
||||||
|
run: npx eslint . --quiet
|
||||||
+11
@@ -36,6 +36,14 @@ local.properties
|
|||||||
!debug.keystore
|
!debug.keystore
|
||||||
.kotlin/
|
.kotlin/
|
||||||
|
|
||||||
|
# Signing secrets — never commit keystores or their credentials
|
||||||
|
android/app/appKeyStore
|
||||||
|
android/keystore.properties
|
||||||
|
keystore.properties
|
||||||
|
|
||||||
|
# SonarQube scanner workspace
|
||||||
|
.scannerwork/
|
||||||
|
|
||||||
# node.js
|
# node.js
|
||||||
#
|
#
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -81,3 +89,6 @@ rn-config-output.json
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Local archives
|
||||||
|
scripts.zip
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { LogBox, View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AlertNotificationRoot } from 'react-native-alert-notification';
|
import { AlertNotificationRoot } from 'react-native-alert-notification';
|
||||||
import { PersistGate } from 'redux-persist/integration/react';
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { persistor, store } from './src/Redux/Store.js';
|
import { persistor, store } from './src/Redux/Store';
|
||||||
import Routes from './src/Navigation/Routes.jsx';
|
import Routes from './src/Navigation/Routes';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import colors from './src/Constant/Color';
|
import colors from './src/Constant/Color';
|
||||||
import { TextEncoder } from 'text-encoding';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import AppStatusBar from './src/components/AppStatusBar';
|
import AppStatusBar from './src/components/AppStatusBar';
|
||||||
LogBox.ignoreAllLogs(true);
|
import { ErrorBoundary } from './src/core/errors/ErrorBoundary';
|
||||||
|
import { queryClient } from './src/core/query/queryClient';
|
||||||
|
import AuthSessionGuard from './src/Navigation/AuthSessionGuard';
|
||||||
import { navigationRef } from './src/Navigation/NavigationService';
|
import { navigationRef } from './src/Navigation/NavigationService';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const queryClient = new QueryClient();
|
const insets = useSafeAreaInsets();
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
global.TextEncoder = TextEncoder;
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer ref={navigationRef}>
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<NavigationContainer ref={navigationRef}>
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AlertNotificationRoot>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<Provider store={store}>
|
<AlertNotificationRoot>
|
||||||
<PersistGate persistor={persistor}>
|
<Provider store={store}>
|
||||||
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
|
<PersistGate persistor={persistor}>
|
||||||
<AppStatusBar />
|
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
|
||||||
<Routes />
|
<AppStatusBar />
|
||||||
</View>
|
<AuthSessionGuard />
|
||||||
</PersistGate>
|
<Routes />
|
||||||
</Provider>
|
</View>
|
||||||
</AlertNotificationRoot>
|
</PersistGate>
|
||||||
</GestureHandlerRootView>
|
</Provider>
|
||||||
</QueryClientProvider>
|
</AlertNotificationRoot>
|
||||||
</NavigationContainer>
|
</GestureHandlerRootView>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</NavigationContainer>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +1,149 @@
|
|||||||
This is a new [**React Native**](http://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](http://github.com/react-native-community/cli).
|
# ProcureLinc — Mobile App
|
||||||
|
|
||||||
# Getting Started
|
ProcureLinc is a React Native mobile application for an **e-procurement platform** that lets registered contractors take part in **e-auctions** and **e-tenders** from their phone. It covers the full journey: contractor onboarding with eKYC, browsing live auctions/tenders, purchasing/EMD payments, document submission, and secure bidding.
|
||||||
|
|
||||||
> **Note**: Make sure you have completed the [Set Up Your Environment](http://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
- **Platform:** Android & iOS (React Native 0.79, React 19)
|
||||||
|
- **App ID:** `com.procure`
|
||||||
|
- **Backend:** REST + WebSocket APIs (base URL configured in [src/Services/ApiCall/ApiUrls.tsx](src/Services/ApiCall/ApiUrls.tsx))
|
||||||
|
|
||||||
## Step 1: Start Metro
|
---
|
||||||
|
|
||||||
First, you will need to run **Metro**, the JavaScript build tool for React Native.
|
## Features
|
||||||
|
|
||||||
To start the Metro dev server, run the following command from the root of your React Native project:
|
- **Contractor registration** — multi-stage onboarding (Stage 1–3) including partnership details.
|
||||||
|
- **eKYC verification** — Organization PAN, Aadhaar (OTP), and GST verification flows; KYC web view.
|
||||||
|
- **Authentication & security** — login, reset password, two-factor auth, **device binding**, **DSC (Digital Signature Certificate) binding**, and request/response encryption.
|
||||||
|
- **e-Auctions** — live auction listings, forward & reverse auctions, item-wise & auction-wise EMD documents, technical document templates, purchase + transaction status.
|
||||||
|
- **e-Tenders** — live tender listings, tender details, proceed-to-bid, and **Envelope A / B / C** plus **QCBS / LCS** submission with file uploads.
|
||||||
|
- **Real-time bidding** — STOMP over SockJS WebSocket updates.
|
||||||
|
- **Dashboard & profile** — auction/tender dashboard counts, contractor profile view/edit, purchased tenders & auctions.
|
||||||
|
- **Smooth UX** — native-stack screen transitions, animated custom bottom tab bar, Lottie animations, and theming (light/dark via `useTheme`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Area | Library |
|
||||||
|
|------|---------|
|
||||||
|
| Framework | React Native 0.79, React 19 |
|
||||||
|
| Navigation | `@react-navigation/native`, `native-stack`, `bottom-tabs`, `drawer` |
|
||||||
|
| State | Redux Toolkit, `react-redux`, `redux-persist` |
|
||||||
|
| Data fetching | TanStack React Query, Axios |
|
||||||
|
| Forms & validation | Formik, Yup |
|
||||||
|
| Real-time | `@stomp/stompjs`, `sockjs-client` |
|
||||||
|
| Animation / UI | Reanimated, Gesture Handler, Lottie, Lucide icons, SVG, Linear Gradient |
|
||||||
|
| Security | `crypto-js`, `jsencrypt`, `react-native-device-info` |
|
||||||
|
| Documents | `exceljs`, `xlsx`, `react-native-blob-util`, `react-native-fs`, document picker |
|
||||||
|
| Device | Permissions, Geolocation, Haptic Feedback, WebView |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── Axios/ # Axios instance & interceptors
|
||||||
|
├── Component/ # Shared components
|
||||||
|
├── components/ # App-level components (e.g. AppStatusBar)
|
||||||
|
├── Constant/ # Colors, NavigationString, static config
|
||||||
|
├── Helper/ # Helper utilities
|
||||||
|
├── Hooks/ # Custom hooks (e.g. useTheme)
|
||||||
|
├── Images/ # Image assets
|
||||||
|
├── Navigation/ # MainStack, Bottom tabs, Drawer, NavigationService
|
||||||
|
├── Permission/ # Runtime permission handling
|
||||||
|
├── Redux/ # Store, slices, persistence
|
||||||
|
├── Screens/ # Feature screens (Auction, Tender, Bidding, Login, etc.)
|
||||||
|
├── Services/ # API calls & endpoint URLs
|
||||||
|
├── Styles/ # Shared styles
|
||||||
|
├── Utils/ # Utilities (encryption, formatting, etc.)
|
||||||
|
├── api/ # API helpers
|
||||||
|
├── lottie/ # Lottie animation files
|
||||||
|
├── socket/ # WebSocket (STOMP/SockJS) setup
|
||||||
|
└── theme/ # Typography & theme tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigation entry points:
|
||||||
|
- [App.tsx](App.tsx) → [src/Navigation/Routes.jsx](src/Navigation/Routes.jsx) → [src/Navigation/MainStack.jsx](src/Navigation/MainStack.jsx)
|
||||||
|
- Bottom tabs (Home / Auction / Tender / Profile): [src/Navigation/Bottom.tsx](src/Navigation/Bottom.tsx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
> **Note:** Complete the [React Native environment setup](https://reactnative.dev/docs/set-up-your-environment) first.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js **>= 18**
|
||||||
|
- JDK 17, Android SDK / Android Studio (for Android)
|
||||||
|
- Xcode + CocoaPods (for iOS, macOS only)
|
||||||
|
|
||||||
|
### 1. Install dependencies
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure the backend URL
|
||||||
|
|
||||||
|
Set the active `BASE_URL` in [src/Services/ApiCall/ApiUrls.tsx](src/Services/ApiCall/ApiUrls.tsx). The default points to the demo environment:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const BASE_URL = `https://eproc.godemo.in/`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start Metro
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Using npm
|
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
# OR using Yarn
|
|
||||||
yarn start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Build and run your app
|
### 4. Run the app
|
||||||
|
|
||||||
With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
|
**Android**
|
||||||
|
|
||||||
### Android
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Using npm
|
|
||||||
npm run android
|
npm run android
|
||||||
|
|
||||||
# OR using Yarn
|
|
||||||
yarn android
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### iOS
|
**iOS** (first run / after native dependency changes, install pods):
|
||||||
|
|
||||||
For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
|
|
||||||
|
|
||||||
The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bundle install
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, and every time you update your native dependencies, run:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
bundle install # first time only
|
||||||
bundle exec pod install
|
bundle exec pod install
|
||||||
```
|
|
||||||
|
|
||||||
For more information, please visit [CocoaPods Getting Started guide](http://guides.cocoapods.org/using/getting-started.html).
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Using npm
|
|
||||||
npm run ios
|
npm run ios
|
||||||
|
|
||||||
# OR using Yarn
|
|
||||||
yarn ios
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
|
---
|
||||||
|
|
||||||
This is one way to run your app — you can also build it directly from Android Studio or Xcode.
|
## Available Scripts
|
||||||
|
|
||||||
## Step 3: Modify your app
|
| Script | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `npm start` | Start the Metro dev server |
|
||||||
|
| `npm run android` | Build & run on Android |
|
||||||
|
| `npm run ios` | Build & run on iOS |
|
||||||
|
| `npm run lint` | Run ESLint |
|
||||||
|
| `npm test` | Run Jest tests |
|
||||||
|
| `npm run create:users` | Run the user-seeding script (`scripts/create-users.js`) |
|
||||||
|
|
||||||
Now that you have successfully run the app, let's make changes!
|
> If a transition or native change doesn't appear, restart Metro with a clean cache: `npx react-native start --reset-cache`.
|
||||||
|
|
||||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](http://reactnative.dev/docs/fast-refresh).
|
---
|
||||||
|
|
||||||
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
## Repository
|
||||||
|
|
||||||
- **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Dev Menu**, accessed via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS).
|
This project is hosted on the internal Git server:
|
||||||
- **iOS**: Press <kbd>R</kbd> in iOS Simulator.
|
|
||||||
|
|
||||||
## Congratulations! :tada:
|
```
|
||||||
|
http://git.lincpay/EProcurement/ProcureLinc-Mobile-App.git
|
||||||
|
```
|
||||||
|
|
||||||
You've successfully run and modified your React Native App. :partying_face:
|
---
|
||||||
|
|
||||||
### Now what?
|
## Troubleshooting
|
||||||
|
|
||||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](http://reactnative.dev/docs/integration-with-existing-apps).
|
- **Metro stale cache** — `npx react-native start --reset-cache`.
|
||||||
- If you're curious to learn more about React Native, check out the [docs](http://reactnative.dev/docs/getting-started).
|
- **Android build issues** — `cd android && ./gradlew clean`, then rebuild.
|
||||||
|
- **iOS pods out of date** — re-run `bundle exec pod install`.
|
||||||
# Troubleshooting
|
- General help: [React Native Troubleshooting](https://reactnative.dev/docs/troubleshooting).
|
||||||
|
|
||||||
If you're having issues getting the above steps to work, see the [Troubleshooting](http://reactnative.dev/docs/troubleshooting) page.
|
|
||||||
|
|
||||||
# Learn More
|
|
||||||
|
|
||||||
To learn more about React Native, take a look at the following resources:
|
|
||||||
|
|
||||||
- [React Native Website](http://reactnative.dev) - learn more about React Native.
|
|
||||||
- [Getting Started](http://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
|
||||||
- [Learn the Basics](http://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
|
||||||
- [Blog](http://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
|
||||||
- [`@facebook/react-native`](http://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
|
||||||
|
|||||||
Binary file not shown.
@@ -72,7 +72,22 @@ def enableProguardInReleaseBuilds = false
|
|||||||
*/
|
*/
|
||||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||||
|
|
||||||
android {
|
/**
|
||||||
|
* Release signing credentials live in android/keystore.properties (gitignored).
|
||||||
|
* See android/keystore.properties.example. In CI, write the file from secrets
|
||||||
|
* before building. Local dev builds without it fall back to the debug key and
|
||||||
|
* MUST NOT be distributed.
|
||||||
|
*/
|
||||||
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def hasReleaseSigning = keystorePropertiesFile.exists()
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
|
||||||
|
} else {
|
||||||
|
logger.warn("WARNING: android/keystore.properties not found — release build will be DEBUG-SIGNED and must not be distributed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
ndkVersion rootProject.ext.ndkVersion
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
@@ -92,25 +107,50 @@ android {
|
|||||||
keyAlias 'androiddebugkey'
|
keyAlias 'androiddebugkey'
|
||||||
keyPassword 'android'
|
keyPassword 'android'
|
||||||
}
|
}
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
release {
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
signingConfig hasReleaseSigning ? signingConfigs.release : signingConfigs.debug
|
||||||
// see http://reactnative.dev/docs/signed-apk-android.
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += [
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/LICENSE",
|
||||||
|
"META-INF/LICENSE.txt",
|
||||||
|
"META-INF/license.txt",
|
||||||
|
"META-INF/NOTICE",
|
||||||
|
"META-INF/NOTICE.txt",
|
||||||
|
"META-INF/notice.txt",
|
||||||
|
"META-INF/*.kotlin_module",
|
||||||
|
"META-INF/versions/**",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// The version of react-native is set by the React Native Gradle Plugin
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
implementation("com.facebook.react:react-android")
|
implementation("com.facebook.react:react-android")
|
||||||
|
|
||||||
|
// Native Apache POI backend for metadata-preserving BOQ editing.
|
||||||
|
implementation("org.apache.poi:poi-ooxml:5.2.5")
|
||||||
|
|
||||||
if (hermesEnabled.toBoolean()) {
|
if (hermesEnabled.toBoolean()) {
|
||||||
implementation("com.facebook.react:hermes-android")
|
implementation("com.facebook.react:hermes-android")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.procure
|
package com.procure
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import com.facebook.react.ReactActivity
|
import com.facebook.react.ReactActivity
|
||||||
import com.facebook.react.ReactActivityDelegate
|
import com.facebook.react.ReactActivityDelegate
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||||
@@ -7,13 +9,26 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
|
|||||||
|
|
||||||
class MainActivity : ReactActivity() {
|
class MainActivity : ReactActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// FLAG_SECURE: the app displays sealed-bid amounts, Aadhaar OTPs and other
|
||||||
|
// regulated PII. Block screenshots, screen recording and the app-switcher
|
||||||
|
// thumbnail in release builds; debug builds stay capturable for QA.
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
window.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
|
WindowManager.LayoutParams.FLAG_SECURE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the main component registered from JavaScript.
|
* Returns the name of the main component registered from JavaScript.
|
||||||
*/
|
*/
|
||||||
override fun getMainComponentName(): String = "procure"
|
override fun getMainComponentName(): String = "procure"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the instance of the [ReactActivityDelegate].
|
* Returns the instance of the [ReactActivityDelegate].
|
||||||
*/
|
*/
|
||||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|||||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||||
import com.facebook.soloader.SoLoader
|
import com.facebook.soloader.SoLoader
|
||||||
|
import com.procure.excel.ExcelPackage
|
||||||
|
|
||||||
class MainApplication : Application(), ReactApplication {
|
class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
@@ -18,8 +19,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
object : DefaultReactNativeHost(this) {
|
object : DefaultReactNativeHost(this) {
|
||||||
override fun getPackages(): List<ReactPackage> =
|
override fun getPackages(): List<ReactPackage> =
|
||||||
PackageList(this).packages.apply {
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
add(ExcelPackage())
|
||||||
// add(MyReactNativePackage())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
override fun getJSMainModuleName(): String = "index"
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package com.procure.excel
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle
|
||||||
|
import org.apache.poi.ss.usermodel.Cell
|
||||||
|
import org.apache.poi.ss.usermodel.CellType
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidation
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidationConstraint.ValidationType
|
||||||
|
import org.apache.poi.ss.usermodel.DateUtil
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
||||||
|
import org.apache.poi.ss.usermodel.Row
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet
|
||||||
|
import org.apache.poi.ss.usermodel.SheetVisibility
|
||||||
|
import org.apache.poi.ss.usermodel.VerticalAlignment
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFCellStyle
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFColor
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFFont
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExcelModule — the native Apache POI bridge.
|
||||||
|
*
|
||||||
|
* Holds one workbook per session. `open` parses xlsx bytes and returns a
|
||||||
|
* WorkbookDTO (values, formulas, styles, merges, hidden lines, protection,
|
||||||
|
* lock flags, validations). `save` applies ONLY edited values back onto the
|
||||||
|
* retained workbook — preserving every other feature — and returns the new
|
||||||
|
* bytes. Formula recalculation is deferred to Excel/HyperFormula.
|
||||||
|
*/
|
||||||
|
class ExcelModule(private val reactContext: ReactApplicationContext) :
|
||||||
|
ReactContextBaseJavaModule(reactContext) {
|
||||||
|
|
||||||
|
private var workbook: Workbook? = null
|
||||||
|
|
||||||
|
override fun getName(): String = "ExcelModule"
|
||||||
|
|
||||||
|
/* ------------------------------- open ------------------------------- */
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun open(base64: String, promise: Promise) {
|
||||||
|
try {
|
||||||
|
close()
|
||||||
|
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||||
|
val wb = XSSFWorkbook(ByteArrayInputStream(bytes))
|
||||||
|
workbook = wb
|
||||||
|
promise.resolve(serializeWorkbook(wb).toString())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
promise.reject("E_OPEN", "Failed to open workbook: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeWorkbook(wb: Workbook): JSONObject {
|
||||||
|
val root = JSONObject()
|
||||||
|
root.put("activeSheet", wb.activeSheetIndex.coerceAtLeast(0))
|
||||||
|
val sheets = JSONArray()
|
||||||
|
for (i in 0 until wb.numberOfSheets) {
|
||||||
|
sheets.put(serializeSheet(wb, i))
|
||||||
|
}
|
||||||
|
root.put("sheets", sheets)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeSheet(wb: Workbook, index: Int): JSONObject {
|
||||||
|
val sheet = wb.getSheetAt(index)
|
||||||
|
val out = JSONObject()
|
||||||
|
out.put("name", wb.getSheetName(index))
|
||||||
|
out.put("index", index)
|
||||||
|
out.put(
|
||||||
|
"state",
|
||||||
|
when (wb.getSheetVisibility(index)) {
|
||||||
|
SheetVisibility.HIDDEN -> "hidden"
|
||||||
|
SheetVisibility.VERY_HIDDEN -> "veryHidden"
|
||||||
|
else -> "visible"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
out.put("protected", sheet.protect)
|
||||||
|
|
||||||
|
// Frozen panes.
|
||||||
|
var frozenRows = 0
|
||||||
|
var frozenCols = 0
|
||||||
|
try {
|
||||||
|
sheet.paneInformation?.let { pane ->
|
||||||
|
if (pane.isFreezePane) {
|
||||||
|
frozenRows = pane.horizontalSplitPosition.toInt()
|
||||||
|
frozenCols = pane.verticalSplitPosition.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) { /* ignore pane read errors */ }
|
||||||
|
out.put("frozenRows", frozenRows)
|
||||||
|
out.put("frozenCols", frozenCols)
|
||||||
|
|
||||||
|
val lastRow = sheet.lastRowNum
|
||||||
|
var maxCol = 0
|
||||||
|
|
||||||
|
val hiddenRows = JSONArray()
|
||||||
|
val rowHeights = JSONObject()
|
||||||
|
val cells = JSONArray()
|
||||||
|
|
||||||
|
// Precompute per-cell data validations.
|
||||||
|
val dvMap = buildValidationMap(sheet)
|
||||||
|
|
||||||
|
for (r in 0..lastRow) {
|
||||||
|
val row = sheet.getRow(r) ?: continue
|
||||||
|
if (row.zeroHeight) hiddenRows.put(r)
|
||||||
|
rowHeights.put(r.toString(), pointsToPx(row.heightInPoints))
|
||||||
|
for (cell in row) {
|
||||||
|
val c = cell.columnIndex
|
||||||
|
if (c > maxCol) maxCol = c
|
||||||
|
val dto = serializeCell(cell, dvMap["$r,$c"]) ?: continue
|
||||||
|
cells.put(dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val colCount = maxCol + 1
|
||||||
|
out.put("rowCount", lastRow + 1)
|
||||||
|
out.put("colCount", colCount)
|
||||||
|
|
||||||
|
val hiddenCols = JSONArray()
|
||||||
|
val colWidths = JSONObject()
|
||||||
|
for (c in 0 until colCount) {
|
||||||
|
if (sheet.isColumnHidden(c)) hiddenCols.put(c)
|
||||||
|
try {
|
||||||
|
colWidths.put(c.toString(), charUnitsToPx(sheet.getColumnWidth(c)))
|
||||||
|
} catch (_: Throwable) { /* default width */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
out.put("hiddenRows", hiddenRows)
|
||||||
|
out.put("hiddenColumns", hiddenCols)
|
||||||
|
out.put("columnWidths", colWidths)
|
||||||
|
out.put("rowHeights", rowHeights)
|
||||||
|
|
||||||
|
val merges = JSONArray()
|
||||||
|
for (region: CellRangeAddress in sheet.mergedRegions) {
|
||||||
|
merges.put(region.formatAsString())
|
||||||
|
}
|
||||||
|
out.put("merges", merges)
|
||||||
|
out.put("cells", cells)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeCell(cell: Cell, dv: JSONObject?): JSONObject? {
|
||||||
|
val style = cell.cellStyle
|
||||||
|
val locked = style?.locked ?: true
|
||||||
|
val numFmt = style?.dataFormatString
|
||||||
|
val comment = try {
|
||||||
|
cell.cellComment?.string?.string
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val styleJson = serializeStyle(cell)
|
||||||
|
|
||||||
|
val isFormula = cell.cellType == CellType.FORMULA
|
||||||
|
var value: Any? = null
|
||||||
|
var formula: String? = null
|
||||||
|
var type = "text"
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (cell.cellType) {
|
||||||
|
CellType.FORMULA -> {
|
||||||
|
formula = cell.cellFormula
|
||||||
|
when (cell.cachedFormulaResultType) {
|
||||||
|
CellType.NUMERIC -> {
|
||||||
|
value = cell.numericCellValue
|
||||||
|
type = if (DateUtil.isCellDateFormatted(cell)) "date" else typeFromNumFmt(numFmt)
|
||||||
|
}
|
||||||
|
CellType.BOOLEAN -> { value = cell.booleanCellValue; type = "boolean" }
|
||||||
|
CellType.STRING -> { value = cell.stringCellValue; type = "text" }
|
||||||
|
CellType.ERROR -> { value = "#ERROR!"; type = "text" }
|
||||||
|
else -> { value = null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CellType.NUMERIC -> {
|
||||||
|
value = cell.numericCellValue
|
||||||
|
type = if (DateUtil.isCellDateFormatted(cell)) "date" else typeFromNumFmt(numFmt)
|
||||||
|
}
|
||||||
|
CellType.STRING -> { value = cell.stringCellValue; type = "text" }
|
||||||
|
CellType.BOOLEAN -> { value = cell.booleanCellValue; type = "boolean" }
|
||||||
|
CellType.ERROR -> { value = "#ERROR!"; type = "text" }
|
||||||
|
else -> { value = null }
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer a validation-derived dropdown type.
|
||||||
|
if (dv != null && dv.optString("kind") == "list") type = "dropdown"
|
||||||
|
|
||||||
|
val hasContent = value != null || formula != null
|
||||||
|
val hasMeta = styleJson != null || comment != null || dv != null || !locked
|
||||||
|
if (!hasContent && !hasMeta) return null
|
||||||
|
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("r", cell.rowIndex)
|
||||||
|
obj.put("c", cell.columnIndex)
|
||||||
|
obj.put("v", value ?: JSONObject.NULL)
|
||||||
|
obj.put("f", formula ?: JSONObject.NULL)
|
||||||
|
obj.put("t", type)
|
||||||
|
if (numFmt != null && numFmt != "General") obj.put("numFmt", numFmt)
|
||||||
|
obj.put("locked", locked)
|
||||||
|
if (comment != null) obj.put("comment", comment)
|
||||||
|
if (styleJson != null) obj.put("style", styleJson)
|
||||||
|
if (dv != null) obj.put("dv", dv)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializeStyle(cell: Cell): JSONObject? {
|
||||||
|
val style = cell.cellStyle ?: return null
|
||||||
|
val json = JSONObject()
|
||||||
|
var any = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
val font = cell.sheet.workbook.getFontAt(style.fontIndexAsInt)
|
||||||
|
if (font.bold) { json.put("bold", true); any = true }
|
||||||
|
if (font.italic) { json.put("italic", true); any = true }
|
||||||
|
if (font.underline.toInt() != 0) { json.put("underline", true); any = true }
|
||||||
|
if (font.strikeout) { json.put("strike", true); any = true }
|
||||||
|
if (font.fontHeightInPoints.toInt() != 11) { json.put("fontSize", font.fontHeightInPoints.toInt()); any = true }
|
||||||
|
if (font is XSSFFont) {
|
||||||
|
font.xssfColor?.let { hexFromColor(it)?.let { hex -> json.put("fontColor", hex); any = true } }
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) { /* font read best-effort */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (style is XSSFCellStyle) {
|
||||||
|
val fill = style.fillForegroundColorColor
|
||||||
|
if (fill is XSSFColor) hexFromColor(fill)?.let { json.put("bgColor", it); any = true }
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) { /* fill read best-effort */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (style.alignment) {
|
||||||
|
HorizontalAlignment.LEFT -> json.put("hAlign", "left")
|
||||||
|
HorizontalAlignment.CENTER -> json.put("hAlign", "center")
|
||||||
|
HorizontalAlignment.RIGHT -> json.put("hAlign", "right")
|
||||||
|
HorizontalAlignment.JUSTIFY -> json.put("hAlign", "justify")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
when (style.verticalAlignment) {
|
||||||
|
VerticalAlignment.TOP -> json.put("vAlign", "top")
|
||||||
|
VerticalAlignment.CENTER -> json.put("vAlign", "middle")
|
||||||
|
VerticalAlignment.BOTTOM -> json.put("vAlign", "bottom")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
if (style.wrapText) json.put("wrap", true)
|
||||||
|
if (style.alignment != HorizontalAlignment.GENERAL || style.wrapText) any = true
|
||||||
|
} catch (_: Throwable) { /* alignment best-effort */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (style.borderTop != BorderStyle.NONE) { json.put("borderTop", true); any = true }
|
||||||
|
if (style.borderRight != BorderStyle.NONE) { json.put("borderRight", true); any = true }
|
||||||
|
if (style.borderBottom != BorderStyle.NONE) { json.put("borderBottom", true); any = true }
|
||||||
|
if (style.borderLeft != BorderStyle.NONE) { json.put("borderLeft", true); any = true }
|
||||||
|
} catch (_: Throwable) { /* border best-effort */ }
|
||||||
|
|
||||||
|
return if (any) json else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildValidationMap(sheet: Sheet): Map<String, JSONObject> {
|
||||||
|
val map = HashMap<String, JSONObject>()
|
||||||
|
try {
|
||||||
|
for (dv: DataValidation in sheet.dataValidations) {
|
||||||
|
val constraint = dv.validationConstraint
|
||||||
|
val json = JSONObject()
|
||||||
|
when (constraint.validationType) {
|
||||||
|
ValidationType.LIST -> {
|
||||||
|
json.put("kind", "list")
|
||||||
|
val values = constraint.explicitListValues
|
||||||
|
if (values != null) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
values.forEach { arr.put(it) }
|
||||||
|
json.put("list", arr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ValidationType.INTEGER -> json.put("kind", "whole")
|
||||||
|
ValidationType.DECIMAL -> json.put("kind", "decimal")
|
||||||
|
ValidationType.TEXT_LENGTH -> json.put("kind", "textLength")
|
||||||
|
ValidationType.DATE -> json.put("kind", "date")
|
||||||
|
else -> continue
|
||||||
|
}
|
||||||
|
constraint.formula1?.toDoubleOrNull()?.let { json.put("min", it) }
|
||||||
|
constraint.formula2?.toDoubleOrNull()?.let { json.put("max", it) }
|
||||||
|
dv.errorBoxText?.let { json.put("errorMessage", it) }
|
||||||
|
|
||||||
|
for (region in dv.regions.cellRangeAddresses) {
|
||||||
|
for (r in region.firstRow..region.lastRow) {
|
||||||
|
for (c in region.firstColumn..region.lastColumn) {
|
||||||
|
map["$r,$c"] = json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) { /* validations best-effort */ }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------- save ------------------------------- */
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun save(saveRequestJson: String, promise: Promise) {
|
||||||
|
val wb = workbook
|
||||||
|
if (wb == null) {
|
||||||
|
promise.reject("E_NO_WORKBOOK", "No workbook is open.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val request = JSONObject(saveRequestJson)
|
||||||
|
val edits = request.optJSONArray("edits") ?: JSONArray()
|
||||||
|
for (i in 0 until edits.length()) {
|
||||||
|
applyEdit(wb, edits.getJSONObject(i))
|
||||||
|
}
|
||||||
|
// Ask consumers (Excel) to recalculate; HyperFormula already did in-app.
|
||||||
|
wb.setForceFormulaRecalculation(true)
|
||||||
|
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
wb.write(bos)
|
||||||
|
val base64 = Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP)
|
||||||
|
promise.resolve(base64)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
promise.reject("E_SAVE", "Failed to save workbook: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyEdit(wb: Workbook, edit: JSONObject) {
|
||||||
|
val sheetIndex = edit.getInt("sheet")
|
||||||
|
if (sheetIndex < 0 || sheetIndex >= wb.numberOfSheets) return
|
||||||
|
val sheet = wb.getSheetAt(sheetIndex)
|
||||||
|
val r = edit.getInt("r")
|
||||||
|
val c = edit.getInt("c")
|
||||||
|
val row: Row = sheet.getRow(r) ?: sheet.createRow(r)
|
||||||
|
val cell: Cell = row.getCell(c) ?: row.createCell(c)
|
||||||
|
|
||||||
|
when {
|
||||||
|
edit.optBoolean("clear", false) -> cell.setBlank()
|
||||||
|
!edit.isNull("f") -> cell.cellFormula = edit.getString("f")
|
||||||
|
else -> {
|
||||||
|
when (val v = edit.opt("v")) {
|
||||||
|
is Boolean -> cell.setCellValue(v)
|
||||||
|
is Int -> cell.setCellValue(v.toDouble())
|
||||||
|
is Long -> cell.setCellValue(v.toDouble())
|
||||||
|
is Double -> cell.setCellValue(v)
|
||||||
|
is String -> cell.setCellValue(v)
|
||||||
|
null, JSONObject.NULL -> cell.setBlank()
|
||||||
|
else -> cell.setCellValue(v.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------ close ------------------------------- */
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun close(promise: Promise) {
|
||||||
|
close()
|
||||||
|
promise.resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
try {
|
||||||
|
workbook?.close()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
} finally {
|
||||||
|
workbook = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------ helpers ----------------------------- */
|
||||||
|
|
||||||
|
private fun hexFromColor(color: XSSFColor?): String? {
|
||||||
|
if (color == null) return null
|
||||||
|
return try {
|
||||||
|
val argb = color.argbHex ?: return null // "FFRRGGBB"
|
||||||
|
if (argb.length == 8) "#${argb.substring(2)}" else "#$argb"
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeFromNumFmt(fmt: String?): String {
|
||||||
|
if (fmt != null) {
|
||||||
|
val stripped = fmt.replace(Regex("\\[[^\\]]*\\]"), "").replace(Regex("\"[^\"]*\""), "")
|
||||||
|
if (fmt.contains("%")) return "percentage"
|
||||||
|
if (Regex("[dmy]", RegexOption.IGNORE_CASE).containsMatchIn(stripped) && !Regex("[#0]").containsMatchIn(stripped)) return "date"
|
||||||
|
if (Regex("[$€£₹¥]").containsMatchIn(fmt)) return "currency"
|
||||||
|
}
|
||||||
|
return "number"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun charUnitsToPx(width256: Int): Int {
|
||||||
|
val chars = width256 / 256.0
|
||||||
|
return (chars * 7.5 + 8).toInt().coerceIn(48, 420)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pointsToPx(points: Float): Int {
|
||||||
|
return (points * 1.3333).toInt().coerceIn(28, 240)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.procure.excel
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers [ExcelModule] with React Native. Added manually in
|
||||||
|
* MainApplication.getPackages(). Works under both the old and new architecture
|
||||||
|
* (the JS ExcelBridge resolves it via TurboModuleRegistry first, then the
|
||||||
|
* legacy NativeModules registry).
|
||||||
|
*/
|
||||||
|
class ExcelPackage : ReactPackage {
|
||||||
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
||||||
|
listOf(ExcelModule(reactContext))
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Allow ALL cleartext (http://) traffic for every build, including release.
|
<!--
|
||||||
Needed so the app can reach http://eproc-backend.lincpay/ over plain HTTP. -->
|
Cleartext (http://) is DENIED by default. It is permitted only for the
|
||||||
|
specific backend hosts below that are not yet served over HTTPS.
|
||||||
|
TODO(backend): once eproc-backend.lincpay terminates TLS, delete its
|
||||||
|
domain-config so the entire app is HTTPS-only, and add certificate pinning.
|
||||||
|
-->
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<base-config cleartextTrafficPermitted="true" />
|
<base-config cleartextTrafficPermitted="false" />
|
||||||
|
|
||||||
|
<!-- Production REST backend — currently HTTP-only. Remove when HTTPS lands. -->
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">eproc-backend.lincpay</domain>
|
||||||
|
</domain-config>
|
||||||
|
|
||||||
|
<!-- Local development hosts (Metro / LAN backends / emulator loopback). -->
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="false">localhost</domain>
|
||||||
|
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||||
|
<domain includeSubdomains="false">192.168.6.194</domain>
|
||||||
|
<domain includeSubdomains="false">192.168.29.226</domain>
|
||||||
|
</domain-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
buildToolsVersion = "35.0.0"
|
buildToolsVersion = "35.0.0"
|
||||||
minSdkVersion = 24
|
minSdkVersion = 26
|
||||||
compileSdkVersion = 35
|
compileSdkVersion = 35
|
||||||
targetSdkVersion = 35
|
targetSdkVersion = 35
|
||||||
ndkVersion = "27.1.12297006"
|
ndkVersion = "27.1.12297006"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copy to android/keystore.properties (gitignored) and fill in real values.
|
||||||
|
# The release keystore itself must live OUTSIDE the repository or be gitignored.
|
||||||
|
#
|
||||||
|
# IMPORTANT: the previous keystore (android/app/appKeyStore) was committed to
|
||||||
|
# git history — treat it as compromised. Generate a NEW keystore:
|
||||||
|
# keytool -genkeypair -v -storetype PKCS12 -keystore procurelinc-release.keystore \
|
||||||
|
# -alias procurelinc -keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
storeFile=../procurelinc-release.keystore
|
||||||
|
storePassword=CHANGE_ME
|
||||||
|
keyAlias=procurelinc
|
||||||
|
keyPassword=CHANGE_ME
|
||||||
+10
-3
@@ -1,6 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:@react-native/babel-preset'],
|
presets: ['module:@react-native/babel-preset'],
|
||||||
plugins: [
|
plugins: [
|
||||||
'react-native-reanimated/plugin',
|
// Must remain the LAST entry in this list.
|
||||||
],
|
'react-native-reanimated/plugin',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
// Release bundles: strip every console.* so no PII/debug output ships.
|
||||||
|
production: {
|
||||||
|
plugins: ['transform-remove-console'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# ProcureLinc — Architecture Guide (as-built)
|
||||||
|
|
||||||
|
This is the **as-built** companion to [`ARCHITECTURE_AUDIT.md`](./ARCHITECTURE_AUDIT.md).
|
||||||
|
The audit is the blueprint (what was wrong, what to build); this document is the
|
||||||
|
map of what now exists and the rules for working within it. Read this before
|
||||||
|
adding a feature or "fixing" something that looks odd — several things that look
|
||||||
|
like smells are deliberate, documented below under **Deferred / do not touch blindly**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Layering
|
||||||
|
|
||||||
|
```
|
||||||
|
Presentation src/Screens/**, src/components/** (UI only)
|
||||||
|
│
|
||||||
|
Application src/Services/**, feature hooks (use-cases, orchestration)
|
||||||
|
│
|
||||||
|
Domain slices, reducers, pattern configs (business rules, pure)
|
||||||
|
│
|
||||||
|
Infrastructure src/core/** (http, storage, crypto, events)
|
||||||
|
│
|
||||||
|
Platform react-native, native modules
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependencies point **inward**. `src/core/**` must never import from `Screens`,
|
||||||
|
`Services`, or `Redux`. It reaches outward only through injection/events
|
||||||
|
(see §3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The `core/` layer — the single source of infrastructure truth
|
||||||
|
|
||||||
|
| Module | Responsibility | Rule |
|
||||||
|
|--------|----------------|------|
|
||||||
|
| `core/config/env.ts` | All hosts/base URLs, selected by build type | **No hardcoded URLs anywhere else.** |
|
||||||
|
| `core/http/client.ts` | THE axios instance; `request<T>()` | Throws `AppError`; never returns a sentinel. All new network code uses this. |
|
||||||
|
| `core/http/AppError.ts` | Normalized typed error | Catch with `isAppError()`; branch on `.kind`. |
|
||||||
|
| `core/observability/logger.ts` | The only sanctioned log sink | **Never `console.*` in app code.** DEV-only; release-stripped. |
|
||||||
|
| `core/observability/redact.ts` | PII/secret masking | Every log payload is redacted; extend `SENSITIVE_KEY_PATTERN` when adding sensitive fields. |
|
||||||
|
| `core/storage/secureStorage.ts` | Keychain-backed persist storage for the auth session | Token is never in plaintext AsyncStorage. |
|
||||||
|
| `core/events/appEvents.ts` | Typed cross-layer event bus | How infra signals up without importing the store. |
|
||||||
|
| `core/query/queryClient.ts` | Module-singleton React Query client | Never construct a QueryClient in a component. |
|
||||||
|
| `core/errors/ErrorBoundary.tsx` | App-root render-error guard | Wraps the tree in `App.tsx`. |
|
||||||
|
| `core/platform/polyfills.ts` | Buffer / TextEncoder | Imported first in `index.js`; nothing may depend on load order otherwise. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hard rules (enforced by review; some by CI)
|
||||||
|
|
||||||
|
1. **Networking:** use `request()` from `core/http/client`. It injects the JWT,
|
||||||
|
handles 401 (emits `auth/unauthorized`), times out, and throws `AppError`.
|
||||||
|
`src/Axios/Axios.ts` (`apiCall`) is a **legacy compatibility adapter** that
|
||||||
|
preserves the old never-throws `{success:false}` envelope for un-migrated
|
||||||
|
call sites; its contract is pinned by tests. Do not add new `apiCall` sites.
|
||||||
|
2. **Auth token:** read only via `selectAuthJwt(state)`. The transport reads it
|
||||||
|
through a provider registered in `Redux/Store.ts` — never `import { store }`
|
||||||
|
into `core`.
|
||||||
|
3. **Logout:** call `performLogout()` from `Services/Auth/session.ts`. It clears
|
||||||
|
redux + keychain + the query cache in one place. `AuthSessionGuard` (mounted
|
||||||
|
in `App.tsx`) resets navigation to Login whenever the session ends.
|
||||||
|
4. **Post-auth routing:** `Services/Auth/registrationRouting.ts` is the single
|
||||||
|
decision function. Splash and Login both call it. Do not re-implement.
|
||||||
|
5. **Logging:** `logger.<level>(scope, message, data?)`. Never log key material,
|
||||||
|
plaintext bids, OTPs, or full PII (the redactor is a safety net, not a licence).
|
||||||
|
6. **Theme/colors:** use `useTheme()`. `Constant/Color.tsx` is **deprecated**
|
||||||
|
(see §6). There is one brand orange, sourced from `theme/colors`.
|
||||||
|
7. **Components:** one root — `src/components/**`. `src/Component/` no longer exists.
|
||||||
|
8. **Navigation:** route param shapes live in `src/Navigation/types.ts`
|
||||||
|
(`RootStackParamList`). `NavigationString` is the `as const` route-name registry.
|
||||||
|
9. **Forward/Reverse bidding:** every behavioural difference lives in
|
||||||
|
`Screens/Bidding/biddingPattern.ts`. The screen/card contain zero
|
||||||
|
`if (pattern === ...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Feature anatomy (pattern to copy)
|
||||||
|
|
||||||
|
The tender-bidding and registration features show the intended shape — logic is
|
||||||
|
extracted out of the screen into pure, testable units:
|
||||||
|
|
||||||
|
```
|
||||||
|
Screens/Tender/tenderBidding/
|
||||||
|
TenderBidding.tsx presentation + state only
|
||||||
|
envelopeTypes.ts types
|
||||||
|
envelopeCrypto.ts AES/RSA (never logs key material)
|
||||||
|
envelopePayload.ts pure payload build + validation ← unit-tested
|
||||||
|
envelopeSubmission.ts API calls
|
||||||
|
components/* memoized, module-scope sub-components
|
||||||
|
|
||||||
|
Screens/Ragistration/
|
||||||
|
Ragistration.tsx orchestration + render
|
||||||
|
registrationState.ts typed useReducer (was ~20 useStates) ← unit-tested
|
||||||
|
registrationValidation.ts pure per-stage validation ← unit-tested
|
||||||
|
```
|
||||||
|
|
||||||
|
When you build a feature: keep the screen thin; put decisions in pure functions;
|
||||||
|
test the pure functions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Testing & CI
|
||||||
|
|
||||||
|
- `npm test` (jest). 75 tests across 11 suites, concentrated on the
|
||||||
|
money/crypto/auth/storage paths: `envelopePayload`, `biddingPattern`,
|
||||||
|
`registrationState`/`registrationValidation`, `registrationRouting`,
|
||||||
|
`Token` slice, `apiCall` envelope contract, `AppError`, `redact`, `appEvents`,
|
||||||
|
and the `secureStorage` keychain-migration path.
|
||||||
|
- CI (`.github/workflows/ci.yml`) runs **install → typecheck → lint → test**,
|
||||||
|
all **blocking**. `tsc --noEmit` is green; `eslint . --quiet` (errors) is green.
|
||||||
|
Style *warnings* still exist and are being burned down; tighten to
|
||||||
|
`--max-warnings=0` once clean.
|
||||||
|
- New pure logic should ship with tests. UI can follow with RNTL later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Deferred / do not touch blindly
|
||||||
|
|
||||||
|
These look like leftovers but were left deliberately. Read before "fixing".
|
||||||
|
|
||||||
|
- **`Constant/Color.tsx`** is deprecated but still imported by ~20 files for
|
||||||
|
neutral grays. Its brand tokens are now sourced from the theme, but its neutral
|
||||||
|
values differ from `theme/colors` in several places. A blind find/replace to
|
||||||
|
`useTheme()` **will change appearance** — migrate screen-by-screen with visual
|
||||||
|
review.
|
||||||
|
- **`EnvelopeCExcelPreview.tsx`** still uses raw axios (`responseType:
|
||||||
|
'arraybuffer'`) and `react-native-fs` with `'ascii'` write encoding. The RNFS →
|
||||||
|
`react-native-blob-util` dedupe and the axios → `core/http` migration are
|
||||||
|
deferred because this sealed-bid document flow can't be runtime-verified here
|
||||||
|
and the encodings aren't 1:1. Needs a device.
|
||||||
|
- **Sealed-bid crypto** is AES-ECB with the symmetric key echoed to the server
|
||||||
|
(`randomKeyGenerated`). This is a backend-contract weakness (audit §11); fixing
|
||||||
|
it requires a coordinated backend change, not a client edit.
|
||||||
|
- **`ResetPassword`** sends the new password as a URL query param. Migrated to
|
||||||
|
`core/http` (so it's redacted in logs) but the query-param placement is a
|
||||||
|
backend contract — see the `TODO(backend)` at the call site.
|
||||||
|
- **`PurchaseTenderTile` "details" tap** navigates to `'TenderDetail'`, an
|
||||||
|
unregistered route (a pre-existing latent bug). The intended destination is
|
||||||
|
ambiguous (the tile already renders an in-tile modal), so it awaits a product
|
||||||
|
decision rather than a guessed target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Open work (gated on device or backend/ops)
|
||||||
|
|
||||||
|
- **Device/emulator needed:** full Auth/App two-stack navigator split
|
||||||
|
(`AuthSessionGuard` + `registrationRouting` are the groundwork); release
|
||||||
|
minify/shrink + App Bundle + ABI splits; verifying the Envelope C flow.
|
||||||
|
- **Backend/ops needed:** HTTPS for `eproc-backend.lincpay` (then remove its
|
||||||
|
cleartext exemption in `network_security_config.xml` and add pinning); rotate
|
||||||
|
the previously-committed keystore + Sonar token and purge git history.
|
||||||
|
- **Incremental:** finish `Constant/Color` → `useTheme`; per-screen typed
|
||||||
|
navigation adoption (`useNavigation<NativeStackNavigationProp<RootStackParamList>>`);
|
||||||
|
broaden accessibility labels; i18n scaffolding.
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
# ProcureLinc — Principal-Level Architectural Audit & Redesign
|
||||||
|
|
||||||
|
**Subject:** ProcureLinc React Native mobile app (e-procurement: auctions + tenders + eKYC + sealed-bid bidding)
|
||||||
|
**Stack:** React Native 0.79.0, React 19, Redux Toolkit + redux-persist, React Query, STOMP/SockJS, Axios, Formik/Yup, Hermes + New Architecture (Fabric/TurboModules) enabled.
|
||||||
|
**Scope:** Full-project architectural review — no code was modified. Every claim below is anchored to `file:line` evidence.
|
||||||
|
**Verdict:** The app *works*, but it is at **prototype maturity carrying production-grade risk**. It is not currently safe to ship to a real user base handling money and Aadhaar/PAN PII, and it is not structured to scale to a large team or user count without significant rework. This document is the redesign blueprint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
ProcureLinc is a functional e-procurement client with genuinely good *ideas* in places (a clean `AppText` typography primitive, a correct paginated-list template in `LiveTender.tsx`, a working dark-mode theme system, STOMP-based live bidding). But the codebase is in a state common to fast-moving startups: **multiple half-finished architectural attempts layered on top of each other, none fully adopted, with security and correctness debt that compounds as it grows.**
|
||||||
|
|
||||||
|
The five findings that matter most, in priority order:
|
||||||
|
|
||||||
|
| # | Finding | Severity | Where |
|
||||||
|
|---|---------|----------|-------|
|
||||||
|
| 1 | **A binary signing keystore (`appKeyStore`) is committed to git**, and **release builds are signed with the public debug keystore** (`storePassword 'android'`). Anyone with repo access can forge signed ProcureLinc APKs. | 🔴 Critical | `android/app/appKeyStore` (git-tracked), `android/app/build.gradle:103` |
|
||||||
|
| 2 | **A global Axios interceptor logs every request/response — full URLs, JWT headers, login passwords, Aadhaar/PAN/OTP, and sealed-bid amounts — to logcat, ungated by `__DEV__`, in release builds.** | 🔴 Critical | `src/Axios/Axios.ts:47-99` |
|
||||||
|
| 3 | **The data layer is four competing API stacks across three different backend hosts**, and the dominant one (`apiCall`) *swallows every error and returns `{success:false}` cast to the success type* — defeating TypeScript, React Query error/retry/cache, and forcing brittle status-code checks everywhere. | 🔴 Critical (architecture) | `Axios.ts:124-141`, `api/client.ts`, `apis.ts`, `Constant/Variable.tsx` |
|
||||||
|
| 4 | **App-wide cleartext HTTP is permitted in release, with no SSL pinning**; auth JWT + full login response (incl. PII) is persisted **unencrypted** in AsyncStorage; device binding is client-spoofable and DSC "binding" is UI-only. | 🔴 Critical (security) | `network_security_config.xml`, `Redux/Store.js:20-24`, `DeviceBinding.tsx`, `DscBinding.tsx` |
|
||||||
|
| 5 | **God-components with mixed concerns and massive duplication** — `TenderBidding.tsx` is 1205 lines (crypto + REST + upload + validation + UI); `StartBidding`/`StartReverseBidding` and their card components are ~90% copy-paste forks. | 🟠 High (maintainability) | `TenderBidding.tsx`, `Bidding/*` |
|
||||||
|
|
||||||
|
**Bottom line:** before any new feature work, the project needs (a) a security remediation sprint (secrets, transport, logging, storage), and (b) a data-layer consolidation onto a single typed client + properly-configured React Query. Everything else — modularization, design-system unification, testing, CI/CD — follows from those two.
|
||||||
|
|
||||||
|
**On the "AI-powered operating system" vision:** I am challenging the premise directly (see §16). The current codebase contains **zero AI, agent, RAG, voice, or knowledge-graph subsystems**. That is not a criticism — an e-procurement app doesn't need them today. But the audit cannot review AI architecture that doesn't exist; instead §16 lays out what an AI-native evolution would *actually* require and why the current foundation must be fixed first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Current Architecture Overview
|
||||||
|
|
||||||
|
**Shape:** A single-module React Native app. No feature-module boundaries; organized by loose type-folders (`Screens/`, `Component/` **and** `components/`, `Hooks/`, `Services/`, `Redux/`, `socket/`, `theme/` **and** `Constant/Color`, `Styles/`).
|
||||||
|
|
||||||
|
**Runtime composition** (`App.tsx:19-41`): `NavigationContainer → QueryClientProvider → GestureHandlerRootView → AlertNotificationRoot → Redux Provider → PersistGate → Routes`. `LogBox.ignoreAllLogs(true)` is set at module load (`App.tsx:15`).
|
||||||
|
|
||||||
|
**Navigation** (`Navigation/MainStack.jsx`): one flat native-stack with 23 sibling screens; `Home` hosts a custom animated `BottomTab` (4 tabs). No logged-in/out navigator split — auth gating is a one-shot imperative redirect inside `Splash.tsx`.
|
||||||
|
|
||||||
|
**State:** Redux Toolkit, 6 slices (`token`, `auction`, `selectPurchaseAuction`, `selectPurchaseTender`, `tender`, `theme`), only `token` + `theme` persisted. `serializableCheck:false` globally (`Store.js:30`).
|
||||||
|
|
||||||
|
**Data:** Four coexisting mechanisms — `apiCall` wrapper (dominant), a second `apiClient` axios instance, raw `axios` calls in `apis.ts`, and raw `axios` in screens — hitting **three** hardcoded hosts: `eproc.godemo.in` (REST), `procurelinc.in` (sockets), `procurelinc.com` (dead `client.ts`).
|
||||||
|
|
||||||
|
**Real-time:** STOMP-over-SockJS (`socket/Socket.tsx`) for receiving bid updates; bids are *placed* over REST, not STOMP.
|
||||||
|
|
||||||
|
**Crypto:** JS-side AES (ECB mode, SHA-256 key truncated to 128 bits) + RSA (jsencrypt) for sealed-bid envelopes (`Services/Encryption/encrypt.tsx`).
|
||||||
|
|
||||||
|
**Native:** No custom Java/Kotlin — stock RN host. Hermes on, New Architecture on. No env abstraction, no tests, no CI/CD.
|
||||||
|
|
||||||
|
Total: ~26k LOC of TS/JS across 138 source files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architectural Strengths (worth preserving)
|
||||||
|
|
||||||
|
An honest audit names what's good so it isn't lost in a refactor:
|
||||||
|
|
||||||
|
- **`components/AppText.tsx`** — a proper typographic primitive: variant + theme color + Android-safe font-family merging. This is the seed of a real design system. Keep and expand.
|
||||||
|
- **`useTheme` + `ThemeSlice`** — a working light/dark/system theme with a user switcher (`CustomDrawer.tsx`). Solid foundation; the problem is only that half the app bypasses it.
|
||||||
|
- **`LiveTender.tsx`** — the *correct* FlatList pattern: memoized `renderItem`/`renderFooter`, `keyExtractor`, infinite-scroll pagination via React Query `fetchNextPage`/`hasNextPage`, and tuned perf props (`initialNumToRender`, `windowSize`, `removeClippedSubviews`). **This should be the template every list screen follows.**
|
||||||
|
- **`socket/Socket.tsx`** — a reasonable `useReducer`-based STOMP abstraction with a pending-subscription queue and reconnect/heartbeat config. The core is sound; the consumers misuse it.
|
||||||
|
- **Modern baseline** — RN 0.79, Hermes, New Architecture, React Query, Redux Toolkit, Formik/Yup are all present. The *ingredients* of a good architecture exist; they are simply half-wired.
|
||||||
|
- **`android:allowBackup="false"`** is correctly set — limits `adb backup` extraction of the (unfortunately plaintext) token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Architectural Weaknesses
|
||||||
|
|
||||||
|
Grouped by subsystem. Severity: 🔴 Critical, 🟠 High, 🟡 Medium.
|
||||||
|
|
||||||
|
### 4.1 Data / API layer
|
||||||
|
- 🔴 **Four parallel API stacks, three hosts, two auth-header names.** `apiCall` sends `Authentication: Bearer` (note: not `Authorization`) reading `token.token` (`Axios.ts:118`); `apiClient` sends `Authorization` reading `token.token.token` (`client.ts:17`); screens send raw axios with `Authentication: Bearer ${Token.token}` (`BiddingCard.tsx:82`). There is no single place to add retry, refresh, tracing, or error handling.
|
||||||
|
- 🔴 **`apiCall` swallows errors and casts `{success:false}` to `R`** (`Axios.ts:124-141`). This is the single most damaging decision in the codebase — it defeats the type system, makes React Query believe every call succeeded (`isError` never fires, retries never happen, bad responses get cached as valid), and forces defensive `res?.statusCode===200` checks in every consumer. `try/catch` blocks around `apiCall` (e.g. `apis.ts:76-82`) are dead code because it never throws.
|
||||||
|
- 🟠 **React Query is present but mis-configured.** `new QueryClient()` with no defaults (`App.tsx:20`) ⇒ `staleTime:0`, refetch-on-mount/focus ⇒ refetch storms. It's also instantiated *inside the component body*, so it's recreated on every re-render. Broken query keys: `useAucitonItemDetail` uses `queryKey:[]` (all instances collide), `useGetAuciton` uses static `['getAuction']` ignoring the id. Missing `enabled` guards fire `?tenderId=undefined` calls. No `invalidateQueries` anywhere — lists go stale after writes.
|
||||||
|
- 🟡 **Dead/broken config.** `Constant/AllUrls.tsx` builds ~30 URLs from `${Variable.Main_Base}` — but `Main_Base` doesn't exist, so every URL is `undefined/...`. This whole file is leftover from a different (e-commerce) app. The generic hooks (`useFetch/useUpdate/useDelete`) are imported nowhere.
|
||||||
|
|
||||||
|
### 4.2 State management
|
||||||
|
- 🟠 **The token slice lies about its type.** Declared `{token:''}` (`Token.tsx:3`) but at runtime holds a full object (`Login.tsx:212`). No `RootState`/`AppDispatch` types exist; every `useSelector` is `(state:any)`. Zero compile-time safety on auth state.
|
||||||
|
- 🟡 **Two inconsistent logout paths** — `removeToken()` (sets `''`) vs `setToken({})` (leaves a truthy empty object) vs direct `AsyncStorage.removeItem('persist:root')`. These can desync in-memory state from disk and confuse the splash auth check.
|
||||||
|
- 🟡 **`serializableCheck:false` globally** hides genuine non-serializable leaks, and the slices store whole API response objects.
|
||||||
|
|
||||||
|
### 4.3 UI / screens
|
||||||
|
- 🟠 **God-components:** `TenderBidding.tsx` (1205), `ProfileTab.tsx` (1031), `Login.tsx` (792), `Ragistration.tsx` (725, ~25 `useState`), `Emd.tsx` (608). Networking + crypto + validation + sub-components + rendering all in one file.
|
||||||
|
- 🟠 **Copy-paste forks:** `StartBidding`≈`StartReverseBidding` (~90%), `BiddingCard`≈`ReverseBiddingCard` (identical StyleSheets) — diverging subtly (one card is `memo`'d, the other isn't; wrong color import in reverse). Every fix must be made twice, and they *will* drift.
|
||||||
|
- 🟠 **Split-brain design system:** `src/Component/` (old) vs `src/components/` (new); two `MyInput`, two `PrimaryButton`, three side-drawers (one dead), two color palettes with **two different brand oranges** (`Constant/Color:#F97316` vs `theme/colors:#e39546ff`).
|
||||||
|
|
||||||
|
### 4.4 Navigation
|
||||||
|
- 🟠 **No route type safety** (`createNavigationContainerRef<any>()`, `useNavigation<any>()`, `useRoute<any>()` everywhere), **no route guards** (every screen reachable), **no deep linking** (`NavigationContainer` has no `linking`).
|
||||||
|
|
||||||
|
### 4.5 Real-time
|
||||||
|
- 🟠 **`subscribe()` called in the render body** (`StartBidding.tsx:137`, `StartReverseBidding.tsx:171`), re-registering the STOMP callback every render; **no `disconnect()`/`unsubscribe()` on unmount** ⇒ leaked connections that keep reconnecting (5s) and draining battery/data. Bids split across REST *and* STOMP for the same feature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Design Smells
|
||||||
|
|
||||||
|
- **Misspelled, leaked-through names as public API:** folder `Ragistration/`, components `Serach/SeacrhInput`, props `isVarify`/`maxlength`, constant `FORARD_AUCTION_ITEM`, string `'Failed load cities'`. Names are the cheapest documentation; these signal no review gate.
|
||||||
|
- **Duplicate constants pointing at the same endpoint** (`SELECTED_AUCTION_ITEM` == `EMD_CHECK`).
|
||||||
|
- **`useState` used as a value holder**: `const tenderId = useState(route.params?.tenderId)` captures the `[state,setState]` tuple and never uses it (`TenderBidding.tsx:71`).
|
||||||
|
- **Sub-components declared inside a parent's render body** (`InfoRow`, `EnvelopeButton`, `renderEnvelopeItem` in `TenderBidding.tsx:113-309`) — recreated every render, defeating memoization and remounting subtrees.
|
||||||
|
- **A test/debug screen wired into production** (`TestScreen/DecryptionScreen.tsx`) with a hardcoded real AES key copyable to clipboard.
|
||||||
|
- **`LogBox.ignoreAllLogs(true)`** — silencing all warnings is hiding the smoke detector.
|
||||||
|
|
||||||
|
## 6. Anti-Patterns
|
||||||
|
|
||||||
|
- **Error-swallowing service layer** (`apiCall` returning `{success:false} as R`) — the "exceptions considered harmful, return sentinels typed as success" anti-pattern.
|
||||||
|
- **Effect logic in render** (`subscribe()` outside `useEffect`).
|
||||||
|
- **Fat controller / god object** (the 1205-line screen).
|
||||||
|
- **Shotgun surgery by construction** (forward/reverse duplication guarantees every change touches ≥2 files).
|
||||||
|
- **Config-by-comment** (base URLs selected by commenting/uncommenting lines in `ApiUrls.tsx`).
|
||||||
|
- **Boolean-flip refetch** (`setAllBoolean(reload:!prev.reload)` to trigger a parent `useEffect` instead of query invalidation).
|
||||||
|
- **Primitive obsession in state** (~25 discrete boolean `useState`s in `Ragistration.tsx` instead of a reducer/state machine).
|
||||||
|
|
||||||
|
## 7. Bottlenecks
|
||||||
|
|
||||||
|
- **Refetch storms** from `staleTime:0` + refetch-on-focus on dashboards/lists.
|
||||||
|
- **Whole-list re-renders** on every live bid tick (inline `renderItem`, unmemoized reverse card) — worsens as bidder lists grow.
|
||||||
|
- **Per-second `setState` timers** (`CountdownTimer` *and* the parallel `AuctionTimer`) re-rendering timer subtrees every second; two implementations doing it.
|
||||||
|
- **Nested non-virtualized `FlatList scrollEnabled={false}` inside `ScrollView`** (`TenderBidding.tsx:1056,1099`) renders all rows eagerly.
|
||||||
|
- **Synchronous blocking API call on the Splash screen** (registration-state fetch gates app entry for tender users, `Splash.tsx:45-107`).
|
||||||
|
- **Startup polyfills** (`Buffer`, `TextEncoder`) set synchronously; `QueryClient` recreated per render.
|
||||||
|
|
||||||
|
## 8. Tight Coupling
|
||||||
|
|
||||||
|
- **Transport layer knows about UI and navigation:** `apiCall` dispatches `setToken({})` on 401 (`Axios.ts:129`); `Socket.placeBid` calls `showSuccessAlert`/`showErrorAlert` from inside the service (`Socket.tsx:200-207`). Data/transport should emit events, not drive UI.
|
||||||
|
- **Screens coupled directly to the crypto implementation** (`TenderBidding` calls `encrypt`/`encryptWithRSA` inline with hardcoded ECB assumptions).
|
||||||
|
- **Redux token shape coupled across ≥3 readers** with different nesting (`token.token` vs `token.token.token`) — change one, break the others silently.
|
||||||
|
- **Components coupled to two different color modules**, so a rebrand touches both.
|
||||||
|
|
||||||
|
## 9. Hidden Dependencies
|
||||||
|
|
||||||
|
- **`jsencrypt` is in `devDependencies`** (`package.json:91`) but imported by runtime code (`encrypt.tsx`). A production install with `--production` (or a clean CI install) **breaks the sealed-bid crypto path**. This is a latent production outage.
|
||||||
|
- **`global.Buffer` / `global.TextEncoder`** monkey-patched at startup (`index.js:9-10`, `App.tsx:22`) — any code depending on them has an invisible ordering dependency on app bootstrap.
|
||||||
|
- **Socket token captured at hook-init** (`StartBidding.tsx:55`) — if the token refreshes mid-session, the socket keeps the stale one with no re-read.
|
||||||
|
- **`text-encoding`, `react-native-get-random-values`** are crypto polyfills whose absence would silently weaken/break encryption.
|
||||||
|
|
||||||
|
## 10. Single Points of Failure
|
||||||
|
|
||||||
|
- **The one signing key** — committed to git, so its secrecy is already lost; simultaneously the *only* thing standing between the app and forged updates.
|
||||||
|
- **`apiCall` / the default axios instance** — every feature funnels through it; its error-swallowing behavior means one backend contract change produces silent `undefined`s app-wide rather than a visible failure.
|
||||||
|
- **The single flat navigator + imperative Splash gate** — if Splash's registration-state call fails, the recovery path *wipes persisted state and forces re-login* (`Splash.tsx:103`); a backend hiccup logs everyone out.
|
||||||
|
- **`Constant/Variable.tsx` socket host** — hardcoded to a single origin; no failover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Security Risks
|
||||||
|
|
||||||
|
> Full evidence in the security appendix; the highest-priority items:
|
||||||
|
|
||||||
|
| Severity | Risk | Evidence |
|
||||||
|
|----------|------|----------|
|
||||||
|
| 🔴 Critical | **Signing keystore committed to git** (`.gitignore` `*.keystore` missed it — no extension) + **release signed with debug key** (`storePassword 'android'`). Forgeable/ tamperable APKs. | `android/app/appKeyStore` (git-tracked, verified), `build.gradle:88-104` |
|
||||||
|
| 🔴 Critical | **Global request/response logger dumps JWTs, passwords, Aadhaar/PAN/OTP, and sealed-bid values to logcat in release** (no `__DEV__` gate). 378 `console.*` calls across 34 files, none stripped (no `transform-remove-console`). | `Axios/Axios.ts:47-99`, `Login.tsx:213-216`, `encrypt.tsx:30-155`, `TenderBidding.tsx:474,561` |
|
||||||
|
| 🔴 Critical | **Committed SonarQube auth token.** | `sonar-project.properties:13` (verified git-tracked) |
|
||||||
|
| 🟠 High | **App-wide cleartext HTTP permitted in release + no SSL pinning.** A hardcoded `http://` base with attached Bearer token also lives in `client.ts`. | `network_security_config.xml`, `AndroidManifest.xml:19`, `api/client.ts:6` |
|
||||||
|
| 🟠 High | **JWT + full login response persisted unencrypted in AsyncStorage.** Readable on rooted/backed-up devices. No Keychain/Keystore/EncryptedStorage. | `Redux/Store.js:20-24`, `Login.tsx:212` |
|
||||||
|
| 🟠 High | **Device binding is client-spoofable** (`getUniqueId()`/`getIpAddress()`, no Play Integrity attestation) and **silently falls back to fabricated GPS coords** on permission denial. This is the *fraud control* for bidding. | `DeviceBinding.tsx:47-86` |
|
||||||
|
| 🟠 High | **Weak sealed-bid crypto:** AES-**ECB** mode, SHA-256 key **truncated to 128 bits**, and the symmetric key is **echoed back to the server in plaintext alongside the ciphertext** (`randomKeyGenerated`) — adding no real confidentiality. | `encrypt.tsx:9-71`, `TenderBidding.tsx:546` |
|
||||||
|
| 🟡 Medium | **WebView KYC success detected by page-title string match** (spoofable); loads **raw server-supplied HTML** with JS enabled; logs Aadhaar callback URLs. | `KycWebViewScreen.tsx:85-143` |
|
||||||
|
| 🟡 Medium | **No root/tamper/Frida detection, no `FLAG_SECURE`** (sealed bids & Aadhaar OTP are screenshot-/screen-recordable and appear in the app-switcher thumbnail), **no ProGuard/R8** minification. | grep-confirmed absent; `build.gradle:60,104` |
|
||||||
|
| 🟡 Medium | **`loginWithoutDecryption` endpoints** + **`ResetPassword` sends new password as a URL query param** (`?newPassword=...`) — lands in server/proxy logs. | `apis.ts:29-46`, `ResetPassword.tsx:220` |
|
||||||
|
| 🟡 Medium | **Repo-committed bulk-user script with default password `P@ssword123`** targeting the live backend. | `scripts/create-users.js:10` |
|
||||||
|
|
||||||
|
## 12. Privacy Risks
|
||||||
|
|
||||||
|
- **Regulated PII logged in the clear.** Full Aadhaar number, PAN, GST, DOB are `console.log`-ed (`Ragistration.tsx:363`) and the **Aadhaar OTP is displayed in an `Alert` dialog** (`Ragistration.tsx:422`). Under UIDAI/Aadhaar Act and India's DPDP Act, logging full Aadhaar is a compliance exposure, not just a bug.
|
||||||
|
- **PII travels over a transport that permits cleartext** (§11) and through the global logger (§11) on every request.
|
||||||
|
- **No data-minimization or masking layer** anywhere — the app has no concept of "sensitive field."
|
||||||
|
- **Unjustified permissions:** `READ_MEDIA_AUDIO` (no audio feature) and always-on FINE+COARSE location whose only use is a device-binding geo-tag that fabricates coordinates anyway.
|
||||||
|
- **Cached profile PII** (from `getcontractorfulldetails`) may persist unencrypted via React Query + any persisted slice.
|
||||||
|
|
||||||
|
## 13. Scalability Risks
|
||||||
|
|
||||||
|
- **Team scalability** is the nearer-term problem than user scalability: no module boundaries, god-components, and duplicated screens mean two developers *cannot* work on bidding without colliding, and every change risks the forward/reverse drift.
|
||||||
|
- **No env/config abstraction** ⇒ dev/staging/prod require source edits and recompiles; you cannot run a QA build against staging without a code change. This blocks a release pipeline.
|
||||||
|
- **The error-swallowing data layer** means production incidents are invisible (no error surfaced, bad data cached as good) — you scale the user base but lose the ability to see failures.
|
||||||
|
- **No observability** (no crash reporting, no analytics, no structured logging) — at 10k+ users you'd be flying blind.
|
||||||
|
- **Backend coupling by string concatenation** (`${BASE_URL}path?query=`) with no client SDK/contract means backend changes break the app silently.
|
||||||
|
|
||||||
|
## 14. Performance & Battery Risks
|
||||||
|
|
||||||
|
- Refetch storms (§7), per-second timer re-renders (§7), whole-list re-renders on bid ticks (§7).
|
||||||
|
- **Leaked STOMP connections** (no unmount disconnect) keep reconnecting every 5s with 1s heartbeats — a measurable battery/data drain when a user leaves a bidding screen without killing the app.
|
||||||
|
- **No `getItemLayout`** on any list — every list pays layout measurement cost.
|
||||||
|
- **No release minification/shrinkResources** — larger APK, and no ABI splits/App Bundle so users download all architectures.
|
||||||
|
- **Duplicate heavy libs shipped:** `exceljs` **and** `xlsx`; `react-native-fs` **and** `react-native-blob-util`; two icon systems — dead weight in the bundle.
|
||||||
|
|
||||||
|
## 15. Maintainability Risks
|
||||||
|
|
||||||
|
- **~26k LOC, zero tests, no CI, lint/format enforced only by developer discipline** (no husky/lint-staged). There is no automated safety net for any refactor.
|
||||||
|
- **`.jsx`/`.js` files bypass TypeScript** entirely (`Store.js`, `MainStack.jsx`, `Routes.jsx`, `ImagePath.jsx`).
|
||||||
|
- **Duplication tax:** every bidding change is a 2-file (often 4-file) edit; the two color palettes and two component roots triple the surface for any visual change.
|
||||||
|
- **`any` everywhere** (navigation, selectors, socket callbacks, `apiCall` generics) — the compiler cannot help.
|
||||||
|
- **Dead code** (`AllUrls.tsx`, `DrawerStack`, `CustomDrawer`, generic hooks, `useLogin`, `userLogin`) increases cognitive load and hides which path is live.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. AI System Evaluation
|
||||||
|
|
||||||
|
**Finding: there is no AI subsystem in this codebase.** No LLM/model integration, no agent/orchestration layer, no intent recognition, no task planning, no RAG/retrieval, no knowledge graph, no vector/embedding index, no voice pipeline, no local or cloud inference. Grep confirms none of the associated libraries or patterns are present.
|
||||||
|
|
||||||
|
I'm flagging this plainly because the brief framed the review around an "AI-powered operating system," and a Principal Engineer's job is to challenge the premise rather than invent findings. **An e-procurement bidding app does not need any of that to succeed, and bolting an "AI OS" onto the current foundation would be a strategic mistake** — you'd be building a second story on a cracked foundation (committed keys, cleartext transport, error-swallowing data layer).
|
||||||
|
|
||||||
|
If AI capabilities *are* a genuine roadmap goal, here is the honest prerequisite ordering and what each would require:
|
||||||
|
|
||||||
|
1. **Fix the foundation first** (§17–19). An AI feature that calls a cloud LLM over the current cleartext-permitted transport, logging prompts (which would contain tender/bid PII) through the global logger, is a data-leak generator.
|
||||||
|
2. **Realistic, high-value AI features for *this* domain** (in priority order):
|
||||||
|
- **Tender discovery & ranking** — semantic search over live tenders (embeddings + a vector index server-side; the app just calls a ranked-search endpoint). Low risk, high user value.
|
||||||
|
- **Document intelligence** — extract/summarize tender documents and envelope requirements (server-side LLM + RAG over the tender corpus; app renders results).
|
||||||
|
- **Bid-assist / anomaly detection** — flag unusual bid patterns or missing mandatory documents before submission (server-side).
|
||||||
|
- **Conversational support agent** — scoped, tool-using assistant answering "what tenders match my class?" via typed tool calls to existing endpoints.
|
||||||
|
3. **Architectural requirement:** all of the above should be **server-mediated** (model keys never on-device; PII never sent to a third-party model without consent + DPA). The mobile app's role is a thin, typed client to an AI *gateway* service — which is exactly the clean data-layer boundary §17 prescribes. **Build the clean API/domain layer, and AI features become just more endpoints behind it.**
|
||||||
|
|
||||||
|
**Verdict:** AI is a *future* concern gated on foundation work. Do not start it now.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Android System Evaluation
|
||||||
|
|
||||||
|
- **Build config is not release-ready.** No release keystore (debug-signed release), no minify/shrink/ProGuard, no ABI splits or App Bundle, hardcoded `versionCode 1`/`versionName 1.0` with no bump automation.
|
||||||
|
- **New Architecture + Hermes are on** (good, modern), and confirmed in native host code (`MainApplication.kt`). No custom native modules — everything is JS + autolinked libs, which keeps the native surface small but means device-binding/DSC/crypto get *no* native security guarantees (no Keystore-backed keys, no attestation).
|
||||||
|
- **Manifest:** app-wide cleartext, no orientation lock, single exported `MainActivity` with only MAIN/LAUNCHER (no deep links). `allowBackup=false` is correct.
|
||||||
|
- **Cross-platform:** iOS project exists and is set up (`ios/` with Podfile, AppDelegate.swift, PrivacyInfo.xcprivacy) — this is *not* Android-only, so all remediation must be applied per-platform (e.g. ATS on iOS mirrors the network-security-config fix).
|
||||||
|
- **Tooling gaps:** no tests, no CI/CD (`.github` contains only a Java-upgrade tooling artifact), stock ESLint/Prettier with no enforcement, committed Sonar token pointing at `localhost:9000` scanning `node_modules`.
|
||||||
|
|
||||||
|
## 18. Data Architecture Review
|
||||||
|
|
||||||
|
**Current:** string-concatenated endpoints off a hardcoded base, four transport styles, error-swallowing wrapper, mis-configured React Query, plaintext persisted auth, three backend hosts.
|
||||||
|
|
||||||
|
**Target (see §20):** one typed API client → a **domain/repository layer** (feature-scoped: `auth`, `tenders`, `auctions`, `bidding`, `kyc`, `profile`) → React Query hooks with correct keys/`staleTime`/invalidation → screens. Errors *thrown* and normalized once. Secrets in secure storage. Env-driven base URLs. This is the highest-leverage change in the whole document.
|
||||||
|
|
||||||
|
## 19. UX Architecture Review
|
||||||
|
|
||||||
|
- **Strong primitives, inconsistent application.** `AppText` + `useTheme` are good, but per-element inline styles + `color=` overrides dilute the tokens, and half the app uses the *other* palette/components.
|
||||||
|
- **No accessibility** (zero `accessibilityLabel`/`Role`/`Hint` — icon-only touchables and steppers are unusable with TalkBack), **no i18n** (hardcoded English + inline `₹`), **no responsive scaling** (fixed pixels), **no error boundary** (an unhandled render error crashes the app; warnings are globally silenced).
|
||||||
|
- **Inconsistent loading/empty/error states** — no shared wrapper; main lists lack `ListEmptyComponent`; most `catch` blocks only `console.log`, so React Query `isError` never reaches the user.
|
||||||
|
- **Forms are backwards:** the *simplest* screens (Login, SignUp) use Formik+Yup; the *hardest* (multi-step registration, envelope bidding) use hand-rolled `useState` state machines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. Recommended Target Architecture
|
||||||
|
|
||||||
|
**Principle:** feature-modular, layered, with dependencies pointing inward (Clean/Hexagonal). The mobile app is a *thin typed client* over well-defined domain boundaries.
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├─ core/ # cross-cutting, no feature knowledge
|
||||||
|
│ ├─ config/ env.ts # ← react-native-config; NO hardcoded hosts
|
||||||
|
│ ├─ http/ client.ts # ONE axios instance; throws on error
|
||||||
|
│ │ interceptors # auth (single token read), tracing, refresh
|
||||||
|
│ ├─ storage/ secure.ts # react-native-keychain for token; MMKV/AsyncStorage for prefs
|
||||||
|
│ ├─ crypto/ envelope.ts # AES-GCM (not ECB), key never echoed to server
|
||||||
|
│ ├─ realtime/ stomp.ts # subscribe in useEffect; disconnect on unmount
|
||||||
|
│ ├─ errors/ AppError.ts # normalized error type + ErrorBoundary
|
||||||
|
│ ├─ observability/ # crash reporting + structured, PII-scrubbed logging (DEV-gated)
|
||||||
|
│ └─ ui/ # ONE design system: AppText, Button, Input, Modal, List, tokens
|
||||||
|
├─ features/
|
||||||
|
│ ├─ auth/ { api, hooks, screens, state }
|
||||||
|
│ ├─ tenders/ { api, hooks, screens }
|
||||||
|
│ ├─ auctions/ { api, hooks, screens }
|
||||||
|
│ ├─ bidding/ { api, hooks, screens } # forward+reverse UNIFIED via config/props
|
||||||
|
│ ├─ kyc/ { api, hooks, screens }
|
||||||
|
│ └─ profile/ { api, hooks, screens }
|
||||||
|
├─ navigation/ RootNavigator # typed ParamList; Auth vs App stack split; deep linking
|
||||||
|
└─ App.tsx # QueryClient (module-const, tuned defaults) + ErrorBoundary + providers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key decisions & justifications:**
|
||||||
|
|
||||||
|
| Decision | Replaces | Principle |
|
||||||
|
|----------|----------|-----------|
|
||||||
|
| **One `apiClient` that throws typed `AppError`** | 4 stacks + error-swallowing | Single Responsibility; lets React Query do retry/error/cache; restores TypeScript safety |
|
||||||
|
| **`react-native-config` env files** (dev/staging/prod) | Hardcoded/commented hosts | 12-factor config; enables CI + QA builds without code edits |
|
||||||
|
| **`react-native-keychain` for token**, MMKV for prefs | Plaintext AsyncStorage | Defense in depth for auth/PII |
|
||||||
|
| **Typed navigation** (`RootStackParamList`) + **Auth/App stack split** | `any` + imperative Splash gate | Compile-time route safety; declarative auth gating; enables deep links |
|
||||||
|
| **Feature modules** (`api/hooks/screens` per domain) | Type-folders + god-components | High cohesion, low coupling; two devs work without collision |
|
||||||
|
| **Unify forward/reverse bidding** behind one config-driven screen | 90% duplicated forks | DRY; one place to fix bugs |
|
||||||
|
| **One design system** (`core/ui`), delete `src/Component` + one palette | Split-brain components/colors | Single source of visual truth |
|
||||||
|
| **AES-GCM, key wrapped by server RSA public key, never echoed back** | AES-ECB + echoed key | Actual confidentiality for sealed bids |
|
||||||
|
| **`transform-remove-console` + PII-scrubbing logger, DEV-gated** | 378 raw console calls | Privacy/compliance |
|
||||||
|
| **Server-mediated integrity** (Play Integrity / SafetyNet) for device binding | Client-spoofable IDs | Real anti-fraud |
|
||||||
|
| **ErrorBoundary + shared Loading/Empty/Error wrappers** | Per-screen ad-hoc + crashes | Consistent, resilient UX |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 21. Migration Roadmap
|
||||||
|
|
||||||
|
Sequenced so each phase is shippable and de-risks the next. **Phase 0 is non-negotiable and blocks release.**
|
||||||
|
|
||||||
|
### Phase 0 — Security stop-the-bleed (days, do first)
|
||||||
|
1. **Rotate and remove signing keys from git.** Purge `appKeyStore` + `debug.keystore` from history (`git filter-repo`), rotate on Play Console if `appKeyStore` was the release key, move signing to `keystore.properties` (gitignored) / CI secrets. Create a real release signing config.
|
||||||
|
2. **Rotate the leaked Sonar token**, remove it from `sonar-project.properties`, fix `sonar.sources` to exclude `node_modules`.
|
||||||
|
3. **Gate/remove the global logger** behind `__DEV__` + add `babel-plugin-transform-remove-console` for release; scrub PII. Remove `TestScreen/DecryptionScreen` and the hardcoded key.
|
||||||
|
4. **Disable cleartext** (`usesCleartextTraffic=false`, restrictive network-security-config for release; iOS ATS) once backends are HTTPS. Fix/remove the `http://` `client.ts`.
|
||||||
|
5. **Move the token to Keychain**; stop persisting the full login response.
|
||||||
|
6. **Move `jsencrypt` to `dependencies`** (latent prod outage).
|
||||||
|
|
||||||
|
### Phase 1 — Data-layer consolidation (1–2 sprints)
|
||||||
|
7. Introduce `core/config` env + a **single throwing `apiClient`**; migrate feature by feature off `apiCall`/raw axios/`client.ts`. Delete `AllUrls.tsx` and the dead generic hooks.
|
||||||
|
8. Tune `QueryClient` defaults (`staleTime`, `retry`), fix query keys, add `enabled` guards and `invalidateQueries` on mutations. Move `QueryClient` to a module const.
|
||||||
|
9. Add typed `RootState`/`AppDispatch`; fix the token slice type; unify logout on `persistor.purge()` + Keychain clear + socket disconnect + query cache clear.
|
||||||
|
|
||||||
|
### Phase 2 — Structure & correctness (2–3 sprints)
|
||||||
|
10. Typed navigation + **Auth/App stack split**; move Splash gating into declarative navigator logic.
|
||||||
|
11. **Unify forward/reverse bidding** and the two card components into one config-driven module; move `subscribe` into `useEffect` with cleanup; disconnect on unmount.
|
||||||
|
12. Break up `TenderBidding`/`ProfileTab`/`Ragistration`: extract crypto, API, and validation into `core`/feature modules; convert registration to a reducer/state machine (or Formik).
|
||||||
|
13. Fix crypto (AES-GCM, no echoed key); add `FLAG_SECURE` to bidding/KYC; add Play Integrity to device binding.
|
||||||
|
|
||||||
|
### Phase 3 — Consistency & platform (2 sprints)
|
||||||
|
14. Collapse to **one design system + one palette**; delete `src/Component` and `Constant/Color` after migration. Add ErrorBoundary + shared state wrappers.
|
||||||
|
15. Add responsive scaling + accessibility labels; introduce i18n scaffolding (even if only `en` at first).
|
||||||
|
16. Remove duplicate deps (`xlsx` or `exceljs`; `fs` or `blob-util`; one icon lib); enable release minify/shrink + App Bundle + ABI splits; automate version bumps.
|
||||||
|
|
||||||
|
### Phase 4 — Quality gates (ongoing, start in Phase 1)
|
||||||
|
17. CI (GitHub Actions): install → typecheck → lint → test → build. Add husky + lint-staged.
|
||||||
|
18. Testing: unit tests for `core/http`, crypto, and domain hooks; component tests for the design system; a Detox smoke test for login→bid. Add crash reporting + analytics.
|
||||||
|
|
||||||
|
## 22. Prioritized Refactoring Plan (single ranked backlog)
|
||||||
|
|
||||||
|
| Rank | Item | Effort | Risk if skipped |
|
||||||
|
|------|------|--------|-----------------|
|
||||||
|
| 1 | Purge & rotate committed keys; real release signing | S | Critical — forgeable app |
|
||||||
|
| 2 | DEV-gate/strip logging; scrub PII | S | Critical — credential/PII leak |
|
||||||
|
| 3 | Rotate Sonar token | XS | Critical — exposed secret |
|
||||||
|
| 4 | HTTPS-only + kill cleartext + fix `client.ts` | S | High — MITM |
|
||||||
|
| 5 | Token → Keychain; stop persisting login response | S | High — PII at rest |
|
||||||
|
| 6 | `jsencrypt` → dependencies | XS | High — prod crypto outage |
|
||||||
|
| 7 | Single throwing `apiClient` + env config | M | Critical — invisible failures, no envs |
|
||||||
|
| 8 | React Query defaults + keys + invalidation | M | High — stale data, refetch storms |
|
||||||
|
| 9 | Typed navigation + Auth/App split | M | High — no auth enforcement |
|
||||||
|
| 10 | Unify bidding forward/reverse + socket cleanup | M | High — drift + leaks |
|
||||||
|
| 11 | Break up god-components | L | High — unmaintainable |
|
||||||
|
| 12 | Crypto (AES-GCM) + FLAG_SECURE + Play Integrity | M | High — weak sealed bids/fraud |
|
||||||
|
| 13 | One design system + one palette | L | Medium — visual drift |
|
||||||
|
| 14 | ErrorBoundary + shared states + a11y + i18n | M | Medium — UX/compliance |
|
||||||
|
| 15 | Dedup deps + release optimization + CI/CD + tests | M | Medium — quality/velocity |
|
||||||
|
|
||||||
|
## 23. Risk Assessment (residual, if nothing is done)
|
||||||
|
|
||||||
|
- **Compliance/legal:** logging full Aadhaar + storing PII in plaintext over cleartext-capable transport is a DPDP/UIDAI exposure — **the highest business risk**, not merely technical.
|
||||||
|
- **Financial fraud:** spoofable device binding + UI-only DSC + weak sealed-bid crypto undermine the integrity guarantees a tendering platform legally depends on.
|
||||||
|
- **Supply-chain/integrity:** committed + debug signing key means a malicious actor with repo access (or the public debug key) can distribute a trojaned "ProcureLinc."
|
||||||
|
- **Operational:** error-swallowing + no observability means production incidents are silent; you learn about outages from users, not dashboards.
|
||||||
|
- **Velocity:** duplication + god-components + no tests means feature velocity *decreases* as the app grows, and every bidding change risks a regression in the un-fixed twin.
|
||||||
|
|
||||||
|
## 24. Future Vision
|
||||||
|
|
||||||
|
Once the foundation is fixed, ProcureLinc is well-positioned:
|
||||||
|
|
||||||
|
- **Short term (foundation solid):** a secure, observable, typed, feature-modular app where two teams can build tenders and auctions in parallel; QA runs against staging without code edits; releases are automated and signed correctly.
|
||||||
|
- **Medium term:** offline-first for tender/auction browsing (React Query persistence + optimistic bid queueing over a resilient socket), full accessibility + i18n for a national user base, and a real design system that supports white-labeling for multiple procurement authorities.
|
||||||
|
- **Long term (only after the above):** the AI features in §16 — semantic tender discovery, document intelligence, bid-assist — added as *endpoints behind the clean data layer*, with all model access server-mediated and PII-governed. The "AI-powered OS" framing is better realized as **an intelligent procurement platform with a thin, secure mobile client**, not an AI runtime on the device.
|
||||||
|
|
||||||
|
**The single most important sentence in this report:** *Fix the security foundation and consolidate the data layer first; every other improvement — modularity, design system, testing, and eventually AI — compounds cleanly on top of those two, and none of them is safe or durable without them.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Appendix A — Evidence index (key files)
|
||||||
|
- Data/transport: `src/Axios/Axios.ts`, `src/api/client.ts`, `src/Services/ApiCall/{apis.ts,ApiUrls.tsx,Hooks.ts}`, `src/Hooks/use{Fetch,Create,Update,Delete}.ts`, `src/Constant/{Variable.tsx,AllUrls.tsx}`
|
||||||
|
- State: `src/Redux/Store.js`, `src/Redux/Slices/*`
|
||||||
|
- Realtime/crypto: `src/socket/Socket.tsx`, `src/Services/Encryption/encrypt.tsx`
|
||||||
|
- UI: `src/Screens/Tender/tenderBidding/TenderBidding.tsx`, `src/Screens/Bidding/{StartBidding,StartReverseBidding,Emd}.tsx` + `Component/{BiddingCard,ReverseBiddingCard}.tsx`, `src/Screens/Ragistration/Ragistration.tsx`, `src/Screens/Tender/LiveTender.tsx` (good template), `src/components/AppText.tsx`
|
||||||
|
- Navigation: `src/Navigation/{MainStack.jsx,Bottom.tsx,NavigationService.ts}`, `src/Screens/Splash/Splash.tsx`
|
||||||
|
- Android/build: `android/app/build.gradle`, `android/gradle.properties`, `android/app/src/main/{AndroidManifest.xml,res/xml/network_security_config.xml}`, `android/app/appKeyStore` (remove), `sonar-project.properties` (remove token)
|
||||||
|
- Security-sensitive: `src/Screens/Home/KycWebViewScreen.tsx`, `src/Screens/Tender/{DeviceBinding,DscBinding}.tsx`, `src/Screens/TestScreen/DecryptionScreen.tsx` (remove), `scripts/create-users.js`
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Mobile bid encryption contract
|
||||||
|
|
||||||
|
The mobile client uses the server-issued envelope key `K` for all bid
|
||||||
|
submissions. For text values, encrypt with AES-128/ECB/PKCS5 using the first
|
||||||
|
16 bytes of SHA-256(`K`) and send Base64 ciphertext. For the Envelope-C BOQ,
|
||||||
|
encrypt the raw workbook bytes with AES-128/ECB/PKCS5 using the first 16 bytes
|
||||||
|
of SHA-1(`K`) and upload the raw ciphertext bytes as the multipart `file`.
|
||||||
|
|
||||||
|
The same `K` must be sent as `randomKeyGenerated` and RSA-wrapped with every
|
||||||
|
officer/contractor public key in `saveEnvelopesOfficersDetailsResDTO`.
|
||||||
|
|
||||||
|
Envelope-C `requestDto` includes `deviceType: "Mobile"`, `fieldValue`,
|
||||||
|
`fieldValueInWord`, `hashValue` (SHA-256 of the unencrypted workbook bytes),
|
||||||
|
`fileName`, and the wrapped officer keys. The workbook editor preserves the
|
||||||
|
source workbook structure and only applies permitted cell edits.
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import './src/core/platform/polyfills';
|
||||||
|
// Installs the single DEV console switch before the app and its screens load.
|
||||||
|
import './src/core/observability/logger';
|
||||||
import { AppRegistry } from 'react-native';
|
import { AppRegistry } from 'react-native';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { name as appName } from './app.json';
|
import { name as appName } from './app.json';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { Buffer } from 'buffer';
|
|
||||||
global.Buffer = Buffer;
|
|
||||||
const Root = () => (
|
const Root = () => (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'react-native',
|
preset: 'react-native',
|
||||||
|
// The RN preset only transpiles react-native packages; these ESM-only deps
|
||||||
|
// (pulled in by Redux Toolkit) must be transformed too or jest can't parse them.
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(jest-)?react-native|@react-native(-community)?|@react-navigation|@reduxjs/toolkit|immer|redux|redux-persist)/',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ const config = {
|
|||||||
/node_modules\/.*\/android\/build\/.*/,
|
/node_modules\/.*\/android\/build\/.*/,
|
||||||
/node_modules\/.*\/android\/\.gradle\/.*/,
|
/node_modules\/.*\/android\/\.gradle\/.*/,
|
||||||
]
|
]
|
||||||
},
|
|
||||||
watcher: {
|
|
||||||
additionalExclusions: [
|
|
||||||
// Also exclude from the watcher layer for safety
|
|
||||||
"**/android/.cxx/**",
|
|
||||||
"**/android/build/**",
|
|
||||||
"**/android/.gradle/**",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Generated
+247
-82
@@ -19,6 +19,7 @@
|
|||||||
"@react-navigation/native-stack": "^7.17.3",
|
"@react-navigation/native-stack": "^7.17.3",
|
||||||
"@react-navigation/stack": "7.6.13",
|
"@react-navigation/stack": "7.6.13",
|
||||||
"@reduxjs/toolkit": "2.11.2",
|
"@reduxjs/toolkit": "2.11.2",
|
||||||
|
"@shopify/flash-list": "^1.7.6",
|
||||||
"@stomp/stompjs": "7.2.1",
|
"@stomp/stompjs": "7.2.1",
|
||||||
"@tanstack/react-query": "5.90.12",
|
"@tanstack/react-query": "5.90.12",
|
||||||
"axios": "1.13.2",
|
"axios": "1.13.2",
|
||||||
@@ -27,8 +28,10 @@
|
|||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-native": "5.0.0",
|
"eslint-plugin-react-native": "5.0.0",
|
||||||
"exceljs": "4.4.0",
|
"exceljs": "^3.4.0",
|
||||||
"formik": "2.4.9",
|
"formik": "2.4.9",
|
||||||
|
"hyperformula": "^2.7.1",
|
||||||
|
"jsencrypt": "^3.5.4",
|
||||||
"lottie-react-native": "7.3.4",
|
"lottie-react-native": "7.3.4",
|
||||||
"lucide-react-native": "0.562.0",
|
"lucide-react-native": "0.562.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"react-native-get-random-values": "1.11.0",
|
"react-native-get-random-values": "1.11.0",
|
||||||
"react-native-gifted-charts": "1.4.70",
|
"react-native-gifted-charts": "1.4.70",
|
||||||
"react-native-haptic-feedback": "2.3.3",
|
"react-native-haptic-feedback": "2.3.3",
|
||||||
|
"react-native-keychain": "^10.0.0",
|
||||||
"react-native-linear-gradient": "^2.8.3",
|
"react-native-linear-gradient": "^2.8.3",
|
||||||
"react-native-modal": "^14.0.0-rc.1",
|
"react-native-modal": "^14.0.0-rc.1",
|
||||||
"react-native-permissions": "5.4.4",
|
"react-native-permissions": "5.4.4",
|
||||||
@@ -83,9 +87,9 @@
|
|||||||
"@types/react-test-renderer": "19.0.0",
|
"@types/react-test-renderer": "19.0.0",
|
||||||
"@types/sockjs-client": "1.5.4",
|
"@types/sockjs-client": "1.5.4",
|
||||||
"@types/text-encoding": "0.0.40",
|
"@types/text-encoding": "0.0.40",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"jest": "29.6.3",
|
"jest": "29.6.3",
|
||||||
"jsencrypt": "^3.5.4",
|
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"react-native-bundle-visualizer": "3.1.3",
|
"react-native-bundle-visualizer": "3.1.3",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.0.0",
|
||||||
@@ -3820,6 +3824,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@shopify/flash-list": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-vXuj6JyuMjONVOXjEhWFeaONPuWN/53Cl2LeyeM8TZ0JzUcNU+BE6iyga1/yyJeDf0K7YPgAE/PcUX2+DM1LiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"recyclerlistview": "4.2.3",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/runtime": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sideway/address": {
|
"node_modules/@sideway/address": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||||
@@ -4880,21 +4899,21 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/archiver": {
|
"node_modules/archiver": {
|
||||||
"version": "5.3.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-4.0.2.tgz",
|
||||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
"integrity": "sha512-B9IZjlGwaxF33UN4oPbfBkyA4V1SxNLeIhR1qY8sRXSsbdUkEHrrOvwlYFPx+8uQeCe9M+FG6KgO+imDmQ79CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.0",
|
||||||
"buffer-crc32": "^0.2.1",
|
"buffer-crc32": "^0.2.1",
|
||||||
|
"glob": "^7.1.6",
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"readdir-glob": "^1.1.2",
|
"tar-stream": "^2.1.2",
|
||||||
"tar-stream": "^2.2.0",
|
"zip-stream": "^3.0.1"
|
||||||
"zip-stream": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils": {
|
"node_modules/archiver-utils": {
|
||||||
@@ -5299,6 +5318,13 @@
|
|||||||
"@babel/plugin-syntax-flow": "^7.12.1"
|
"@babel/plugin-syntax-flow": "^7.12.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-transform-remove-console": {
|
||||||
|
"version": "6.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz",
|
||||||
|
"integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/babel-preset-current-node-syntax": {
|
"node_modules/babel-preset-current-node-syntax": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
||||||
@@ -5499,6 +5525,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -5806,6 +5833,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chevrotain": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"regexp-to-ast": "0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chrome-launcher": {
|
"node_modules/chrome-launcher": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
|
||||||
@@ -6039,18 +6075,54 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/compress-commons": {
|
"node_modules/compress-commons": {
|
||||||
"version": "4.1.2",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-3.0.0.tgz",
|
||||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
"integrity": "sha512-FyDqr8TKX5/X0qo+aVfaZ+PVmNJHJeckFBlq8jZGSJOgnynhfifoyl24qaqdUdDIBe0EVTHByN6NAkqYvE/2Xg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^3.0.1",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"readable-stream": "^3.6.0"
|
"readable-stream": "^2.3.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compress-commons/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/compressible": {
|
"node_modules/compressible": {
|
||||||
@@ -6201,6 +6273,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crc-32": {
|
"node_modules/crc-32": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
@@ -6213,17 +6294,41 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crc32-stream": {
|
"node_modules/crc/node_modules/buffer": {
|
||||||
"version": "4.0.3",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crc32-stream": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crc": "^3.4.4",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/create-jest": {
|
"node_modules/create-jest": {
|
||||||
@@ -7656,29 +7761,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exceljs": {
|
"node_modules/exceljs": {
|
||||||
"version": "4.4.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/exceljs/-/exceljs-3.10.0.tgz",
|
||||||
"integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==",
|
"integrity": "sha512-c48puhmEvXOBB/FqX0GLkMr7GLOySTHIbEaDSYiSXjSk0UMMAKwWpYT+VNv9IVnRso3d/8gMKF9k0lBbxUdMaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^5.0.0",
|
"archiver": "^4.0.1",
|
||||||
"dayjs": "^1.8.34",
|
"dayjs": "^1.8.26",
|
||||||
"fast-csv": "^4.3.1",
|
"fast-csv": "^4.1.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.4.0",
|
||||||
|
"proxyquire": "^2.1.3",
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"saxes": "^5.0.1",
|
"saxes": "^5.0.1",
|
||||||
"tmp": "^0.2.0",
|
"tmp": "^0.2.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
"uuid": "^8.3.0"
|
"uuid": "^7.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.3.0"
|
"node": ">=8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exceljs/node_modules/uuid": {
|
"node_modules/exceljs/node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
|
||||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -7892,6 +7998,19 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fill-keys": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-object": "~1.0.1",
|
||||||
|
"merge-descriptors": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -8666,6 +8785,16 @@
|
|||||||
"node": ">=10.17.0"
|
"node": ">=10.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyperformula": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-mpVF5zOyNpksZzgTaCQyRAzdC/X43+taz5x1n7zNbs/PUUv0AuLmsy2yfihCr+vihUzN/pk+gXBbOfNpXKOpgA==",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"chevrotain": "^6.5.0",
|
||||||
|
"tiny-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -9115,6 +9244,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-object": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-path-inside": {
|
"node_modules/is-path-inside": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||||
@@ -10371,7 +10509,6 @@
|
|||||||
"version": "3.5.4",
|
"version": "3.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz",
|
||||||
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
|
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
@@ -10678,7 +10815,6 @@
|
|||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
@@ -11067,6 +11203,15 @@
|
|||||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge-options": {
|
"node_modules/merge-options": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||||
@@ -11633,6 +11778,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/module-not-found-error": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
@@ -12426,6 +12577,17 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/proxyquire": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fill-keys": "^1.0.2",
|
||||||
|
"module-not-found-error": "^1.0.1",
|
||||||
|
"resolve": "^1.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -13021,6 +13183,19 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-keychain": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-YzPKSAnSzGEJ12IK6CctNLU79T1W15WDrElRQ+1/FsOazGX9ucFPTQwgYe8Dy8jiSEDJKM4wkVa3g4lD2Z+Pnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"KeychainExample",
|
||||||
|
"website"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-linear-gradient": {
|
"node_modules/react-native-linear-gradient": {
|
||||||
"version": "2.8.3",
|
"version": "2.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz",
|
||||||
@@ -13318,25 +13493,19 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdir-glob": {
|
"node_modules/recyclerlistview": {
|
||||||
"version": "1.1.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.3.tgz",
|
||||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
"integrity": "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"lodash.debounce": "4.0.8",
|
||||||
}
|
"prop-types": "15.8.1",
|
||||||
},
|
"ts-object-utils": "0.0.5"
|
||||||
"node_modules/readdir-glob/node_modules/minimatch": {
|
|
||||||
"version": "5.1.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
|
||||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"peerDependencies": {
|
||||||
"node": ">=10"
|
"react": ">= 15.2.1",
|
||||||
|
"react-native": ">= 0.30.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
@@ -13409,6 +13578,12 @@
|
|||||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/regexp-to-ast": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@@ -13496,7 +13671,6 @@
|
|||||||
"version": "1.22.12",
|
"version": "1.22.12",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -14830,6 +15004,12 @@
|
|||||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -14837,9 +15017,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
@@ -14900,6 +15080,12 @@
|
|||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-object-utils": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -15703,38 +15889,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zip-stream": {
|
"node_modules/zip-stream": {
|
||||||
"version": "4.1.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-3.0.1.tgz",
|
||||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
"integrity": "sha512-r+JdDipt93ttDjsOVPU5zaq5bAyY+3H19bDrThkvuVxC0xMQzU1PJcS6D+KrP3u96gH9XLomcHPb+2skoDjulQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^2.1.0",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^3.0.0",
|
||||||
"readable-stream": "^3.6.0"
|
"readable-stream": "^3.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 8"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zip-stream/node_modules/archiver-utils": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.2.3",
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"lazystream": "^1.0.0",
|
|
||||||
"lodash.defaults": "^4.2.0",
|
|
||||||
"lodash.difference": "^4.5.0",
|
|
||||||
"lodash.flatten": "^4.4.0",
|
|
||||||
"lodash.isplainobject": "^4.0.6",
|
|
||||||
"lodash.union": "^4.6.0",
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"readable-stream": "^3.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -24,14 +24,17 @@
|
|||||||
"@reduxjs/toolkit": "2.11.2",
|
"@reduxjs/toolkit": "2.11.2",
|
||||||
"@stomp/stompjs": "7.2.1",
|
"@stomp/stompjs": "7.2.1",
|
||||||
"@tanstack/react-query": "5.90.12",
|
"@tanstack/react-query": "5.90.12",
|
||||||
|
"@shopify/flash-list": "^1.7.6",
|
||||||
"axios": "1.13.2",
|
"axios": "1.13.2",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"exceljs": "^3.4.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-native": "5.0.0",
|
"eslint-plugin-react-native": "5.0.0",
|
||||||
"exceljs": "4.4.0",
|
|
||||||
"formik": "2.4.9",
|
"formik": "2.4.9",
|
||||||
|
"hyperformula": "^2.7.1",
|
||||||
|
"jsencrypt": "^3.5.4",
|
||||||
"lottie-react-native": "7.3.4",
|
"lottie-react-native": "7.3.4",
|
||||||
"lucide-react-native": "0.562.0",
|
"lucide-react-native": "0.562.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
"react-native-get-random-values": "1.11.0",
|
"react-native-get-random-values": "1.11.0",
|
||||||
"react-native-gifted-charts": "1.4.70",
|
"react-native-gifted-charts": "1.4.70",
|
||||||
"react-native-haptic-feedback": "2.3.3",
|
"react-native-haptic-feedback": "2.3.3",
|
||||||
|
"react-native-keychain": "^10.0.0",
|
||||||
"react-native-linear-gradient": "^2.8.3",
|
"react-native-linear-gradient": "^2.8.3",
|
||||||
"react-native-modal": "^14.0.0-rc.1",
|
"react-native-modal": "^14.0.0-rc.1",
|
||||||
"react-native-permissions": "5.4.4",
|
"react-native-permissions": "5.4.4",
|
||||||
@@ -86,9 +90,9 @@
|
|||||||
"@types/react-test-renderer": "19.0.0",
|
"@types/react-test-renderer": "19.0.0",
|
||||||
"@types/sockjs-client": "1.5.4",
|
"@types/sockjs-client": "1.5.4",
|
||||||
"@types/text-encoding": "0.0.40",
|
"@types/text-encoding": "0.0.40",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"jest": "29.6.3",
|
"jest": "29.6.3",
|
||||||
"jsencrypt": "^3.5.4",
|
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"react-native-bundle-visualizer": "3.1.3",
|
"react-native-bundle-visualizer": "3.1.3",
|
||||||
"react-test-renderer": "19.0.0",
|
"react-test-renderer": "19.0.0",
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ sonar.projectName=ProcureLinc
|
|||||||
sonar.projectVersion=1.0
|
sonar.projectVersion=1.0
|
||||||
|
|
||||||
# Source configuration
|
# Source configuration
|
||||||
sonar.sources=.
|
sonar.sources=src
|
||||||
|
sonar.exclusions=**/node_modules/**,**/android/**,**/ios/**,**/__tests__/**,**/assets/**,**/lottie/**
|
||||||
|
|
||||||
# SonarQube server
|
# SonarQube server
|
||||||
sonar.host.url=http://localhost:9000
|
sonar.host.url=http://localhost:9000
|
||||||
|
|
||||||
# Authentication token
|
# Authentication: NEVER commit a token here.
|
||||||
sonar.login=sqp_da89e447c964b6e91e142777097e25fa2f17a1cc
|
# Pass it via the environment instead: SONAR_TOKEN=... npx sonar-scanner
|
||||||
|
# The previously committed token must be revoked in SonarQube
|
||||||
|
# (Administration > Security > Users > Tokens).
|
||||||
|
|||||||
+35
-120
@@ -1,102 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* LEGACY COMPATIBILITY ADAPTER.
|
||||||
|
*
|
||||||
|
* `apiCall` preserves the historical contract — it never throws, returning a
|
||||||
|
* `{ success:false, message, ...errorBody }` envelope on failure — because
|
||||||
|
* dozens of call sites still branch on `res?.statusCode === 200`.
|
||||||
|
*
|
||||||
|
* All transport concerns (auth header, 401 handling, timeouts, tracing) now
|
||||||
|
* live in `src/core/http/client`. New code must use `request()` from there
|
||||||
|
* directly; migrate call sites off `apiCall` feature by feature, then delete
|
||||||
|
* this file.
|
||||||
|
*/
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { setToken } from '../Redux/Slices/Token';
|
import { request } from '../core/http/client';
|
||||||
import { store } from '../Redux/Store';
|
import { isAppError } from '../core/http/AppError';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import { logger } from '../core/observability/logger';
|
||||||
import { resetAndNavigate } from '../Navigation/NavigationService';
|
import { attachDevHttpLogging } from '../core/observability/httpLogging';
|
||||||
import NavigationString from '../Constant/NavigationString';
|
|
||||||
|
|
||||||
// ============================================================
|
// A few screens still issue raw axios.* calls; keep them traced in DEV.
|
||||||
// GLOBAL API LOGGER
|
attachDevHttpLogging(axios);
|
||||||
// Logs EVERY axios call (apiCall wrapper + direct axios.*),
|
|
||||||
// including full URL, method, payload, params, headers,
|
|
||||||
// response data and full error response.
|
|
||||||
// React-Native friendly (no %c CSS — that doesn't work in Metro/Logcat).
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Build the full URL exactly as it goes on the wire: baseURL + url + ?query
|
|
||||||
const buildFullUrl = (config: any = {}): string => {
|
|
||||||
const base = config.baseURL || '';
|
|
||||||
const path = config.url || '';
|
|
||||||
let full = base ? `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}` : path;
|
|
||||||
|
|
||||||
const params = config.params;
|
|
||||||
if (params && typeof params === 'object') {
|
|
||||||
const qs = Object.keys(params)
|
|
||||||
.filter(k => params[k] !== undefined && params[k] !== null)
|
|
||||||
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
|
|
||||||
.join('&');
|
|
||||||
if (qs) full += (full.includes('?') ? '&' : '?') + qs;
|
|
||||||
}
|
|
||||||
return full;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make any payload safe/readable for logging. console.log already handles
|
|
||||||
// nested objects and circular refs, so we only special-case FormData
|
|
||||||
// (which otherwise prints as an opaque object).
|
|
||||||
const safe = (val: any): any => {
|
|
||||||
if (val == null) return val;
|
|
||||||
if (typeof FormData !== 'undefined' && val instanceof FormData) {
|
|
||||||
const parts = (val as any)?._parts;
|
|
||||||
return Array.isArray(parts)
|
|
||||||
? `[FormData] ${parts.map((p: any) => p?.[0]).join(', ')}`
|
|
||||||
: '[FormData]';
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register interceptors only once (module may be re-imported on fast-refresh)
|
|
||||||
const g: any = globalThis as any;
|
|
||||||
if (!g.__API_LOGGER_INSTALLED__) {
|
|
||||||
g.__API_LOGGER_INSTALLED__ = true;
|
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
|
||||||
config => {
|
|
||||||
const method = (config.method || 'GET').toUpperCase();
|
|
||||||
const fullUrl = buildFullUrl(config);
|
|
||||||
console.log('\n==================== 🚀 API REQUEST ====================');
|
|
||||||
console.log('METHOD →', method);
|
|
||||||
console.log('FULL URL →', fullUrl);
|
|
||||||
console.log('PARAMS →', safe(config.params) ?? {});
|
|
||||||
console.log('PAYLOAD →', safe(config.data) ?? {});
|
|
||||||
console.log('HEADERS →', safe(config.headers) ?? {});
|
|
||||||
console.log('========================================================');
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.log('❌ REQUEST SETUP ERROR →', error?.message, error);
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
response => {
|
|
||||||
const method = (response.config.method || 'GET').toUpperCase();
|
|
||||||
const fullUrl = buildFullUrl(response.config);
|
|
||||||
console.log('\n==================== ✅ API RESPONSE ====================');
|
|
||||||
console.log('METHOD →', method);
|
|
||||||
console.log('FULL URL →', fullUrl);
|
|
||||||
console.log('STATUS →', response.status);
|
|
||||||
console.log('RESPONSE →', safe(response.data));
|
|
||||||
console.log('=========================================================');
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
const cfg = error?.config || {};
|
|
||||||
const method = (cfg.method || 'GET').toUpperCase();
|
|
||||||
const fullUrl = buildFullUrl(cfg);
|
|
||||||
console.log('\n==================== ❌ API ERROR ====================');
|
|
||||||
console.log('METHOD →', method);
|
|
||||||
console.log('FULL URL →', fullUrl);
|
|
||||||
console.log('REQ PAYLOAD→', safe(cfg.data) ?? {});
|
|
||||||
console.log('STATUS →', error?.response?.status ?? 'NO RESPONSE / NETWORK');
|
|
||||||
console.log('MESSAGE →', error?.response?.data?.message || error?.message);
|
|
||||||
console.log('ERROR DATA →', safe(error?.response?.data));
|
|
||||||
console.log('FULL ERROR →', error?.toJSON ? error.toJSON() : safe(error));
|
|
||||||
console.log('======================================================');
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiCall = async <R, D = {}>(
|
export const apiCall = async <R, D = {}>(
|
||||||
method: 'get' | 'post' | 'patch' | 'delete' | 'put',
|
method: 'get' | 'post' | 'patch' | 'delete' | 'put',
|
||||||
@@ -105,38 +26,32 @@ export const apiCall = async <R, D = {}>(
|
|||||||
params?: any,
|
params?: any,
|
||||||
contentType?: string
|
contentType?: string
|
||||||
): Promise<R> => {
|
): Promise<R> => {
|
||||||
const token: any = store.getState().token.token;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Request/response/error are logged globally by the axios interceptors above.
|
return await request<R>({
|
||||||
const response = await axios({
|
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
data,
|
data,
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: { 'Content-Type': contentType || 'application/json' },
|
||||||
Authentication: `Bearer ${token?.token}`,
|
|
||||||
'Content-Type': contentType || 'application/json',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
return response.data;
|
if (isAppError(error)) {
|
||||||
} catch (error: any) {
|
if (error.kind === 'unauthorized') {
|
||||||
// ===== AUTH ERROR =====
|
// Session clearing happens via the auth/unauthorized event → store.
|
||||||
if (error?.response?.status === 401) {
|
logger.warn('auth', 'JWT expired or malformed — session cleared');
|
||||||
console.warn('⚠ JWT ERROR → Token expired or malformed');
|
return {
|
||||||
store.dispatch(setToken({}));
|
success: false,
|
||||||
|
message: 'JWT Error - Token expired or malformed',
|
||||||
|
} as R;
|
||||||
|
}
|
||||||
|
const body =
|
||||||
|
typeof error.data === 'object' && error.data !== null ? (error.data as object) : {};
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'JWT Error - Token expired or malformed',
|
message: (body as { message?: string }).message || 'No Response From Server',
|
||||||
|
...body,
|
||||||
} as R;
|
} as R;
|
||||||
}
|
}
|
||||||
|
return { success: false, message: 'No Response From Server' } as R;
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
error?.response?.data?.message || 'No Response From Server',
|
|
||||||
...(error?.response?.data || {})
|
|
||||||
} as R;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Contract tests for the legacy adapter: until every call site migrates to
|
||||||
|
* core/http, `apiCall` MUST keep the historical never-throws envelope —
|
||||||
|
* screens branch on `res?.statusCode === 200` and on these exact messages.
|
||||||
|
*/
|
||||||
|
import { apiCall } from '../Axios';
|
||||||
|
import { request } from '../../core/http/client';
|
||||||
|
import { AppError } from '../../core/http/AppError';
|
||||||
|
|
||||||
|
jest.mock('../../core/http/client', () => ({
|
||||||
|
request: jest.fn(),
|
||||||
|
setTokenProvider: jest.fn(),
|
||||||
|
httpClient: { interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockRequest = request as jest.MockedFunction<typeof request>;
|
||||||
|
|
||||||
|
describe('apiCall (legacy envelope adapter)', () => {
|
||||||
|
beforeEach(() => mockRequest.mockReset());
|
||||||
|
|
||||||
|
it('returns the response body on success', async () => {
|
||||||
|
mockRequest.mockResolvedValue({ statusCode: 200, data: [1, 2] });
|
||||||
|
const res = await apiCall<any>('get', '/x');
|
||||||
|
expect(res).toEqual({ statusCode: 200, data: [1, 2] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never throws: maps 401 to the historical JWT envelope', async () => {
|
||||||
|
mockRequest.mockRejectedValue(new AppError('unauthorized', 'nope', 401));
|
||||||
|
const res = await apiCall<any>('get', '/x');
|
||||||
|
expect(res).toEqual({
|
||||||
|
success: false,
|
||||||
|
message: 'JWT Error - Token expired or malformed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('spreads the server error body and preserves its message', async () => {
|
||||||
|
mockRequest.mockRejectedValue(
|
||||||
|
new AppError('validation', 'Invalid bid', 400, {
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid bid',
|
||||||
|
field: 'bidAmount',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const res = await apiCall<any>('post', '/x', {});
|
||||||
|
expect(res).toEqual({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid bid',
|
||||||
|
statusCode: 400,
|
||||||
|
field: 'bidAmount',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the historical network-failure message verbatim', async () => {
|
||||||
|
mockRequest.mockRejectedValue(new AppError('network', 'No response from server.'));
|
||||||
|
const res = await apiCall<any>('get', '/x');
|
||||||
|
expect(res).toEqual({ success: false, message: 'No Response From Server' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import React, { FC, useRef, useEffect } from 'react';
|
|
||||||
import { StyleSheet, Text, View, TouchableOpacity, Animated, Platform } from 'react-native';
|
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import { BlurView } from '@react-native-community/blur';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
|
|
||||||
interface CardProps {
|
|
||||||
title: string;
|
|
||||||
leftLabel: string;
|
|
||||||
leftValue: React.ReactNode;
|
|
||||||
rightLabel: string;
|
|
||||||
rightValue: React.ReactNode;
|
|
||||||
buttonText: string;
|
|
||||||
onPress: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Card: FC<CardProps> = ({
|
|
||||||
title,
|
|
||||||
leftLabel,
|
|
||||||
leftValue,
|
|
||||||
rightLabel,
|
|
||||||
rightValue,
|
|
||||||
buttonText,
|
|
||||||
onPress,
|
|
||||||
}) => {
|
|
||||||
const scale = useRef(new Animated.Value(0.95)).current;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Animated.spring(scale, {
|
|
||||||
toValue: 1,
|
|
||||||
friction: 8,
|
|
||||||
tension: 90,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View style={[styles.outer, { transform: [{ scale }] }]}>
|
|
||||||
{/* Glass Base */}
|
|
||||||
{Platform.OS === 'ios' && (
|
|
||||||
<BlurView
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
blurType="light"
|
|
||||||
blurAmount={10}
|
|
||||||
reducedTransparencyFallbackColor="#ffffff"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Card Body */}
|
|
||||||
<LinearGradient
|
|
||||||
colors={[
|
|
||||||
'rgba(255,255,255,0.85)',
|
|
||||||
'rgba(255,255,255,0.65)',
|
|
||||||
]}
|
|
||||||
style={styles.card}
|
|
||||||
>
|
|
||||||
{/* Title */}
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<View style={styles.statsRow}>
|
|
||||||
<View style={styles.statBox}>
|
|
||||||
<Text style={styles.statValue}>{leftValue}</Text>
|
|
||||||
<Text style={styles.statLabel}>{leftLabel}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.verticalDivider} />
|
|
||||||
|
|
||||||
<View style={styles.statBox}>
|
|
||||||
<Text style={styles.statValue}>{rightValue}</Text>
|
|
||||||
<Text style={styles.statLabel}>{rightLabel}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<TouchableOpacity activeOpacity={0.85} onPress={onPress}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.primary, colors.primaryLight]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 1 }}
|
|
||||||
style={styles.ctaButton}
|
|
||||||
>
|
|
||||||
<Text style={styles.ctaText}>{buttonText}</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</LinearGradient>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Card;
|
|
||||||
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
outer: {
|
|
||||||
marginBottom: 20,
|
|
||||||
borderRadius: 20,
|
|
||||||
overflow: 'hidden',
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 10 },
|
|
||||||
shadowOpacity: 0.12,
|
|
||||||
shadowRadius: 18,
|
|
||||||
elevation: 8,
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
padding: 22,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255,255,255,0.6)',
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '800',
|
|
||||||
color: '#0f172a',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
statsRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 18,
|
|
||||||
},
|
|
||||||
|
|
||||||
statBox: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
statValue: {
|
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: '900',
|
|
||||||
color: '#020617',
|
|
||||||
},
|
|
||||||
|
|
||||||
statLabel: {
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.secondary,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
|
|
||||||
verticalDivider: {
|
|
||||||
width: 1,
|
|
||||||
height: 42,
|
|
||||||
backgroundColor: 'rgba(15,23,42,0.15)',
|
|
||||||
},
|
|
||||||
|
|
||||||
ctaButton: {
|
|
||||||
height: 46,
|
|
||||||
borderRadius: 14,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
ctaText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '800',
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { View, Text, Animated, StyleSheet } from 'react-native';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
|
|
||||||
const Counter = ({ toValue = 1000, duration = 2000 }) => {
|
|
||||||
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
||||||
const [displayValue, setDisplayValue] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Animated.timing(animatedValue, {
|
|
||||||
toValue,
|
|
||||||
duration,
|
|
||||||
useNativeDriver: false,
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
const listener = animatedValue.addListener(({ value }) => {
|
|
||||||
setDisplayValue(Math.floor(value));
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
animatedValue.removeListener(listener);
|
|
||||||
};
|
|
||||||
}, [toValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.counterText}>{displayValue}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Counter
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
backgroundColor: '#f2f2f2',
|
|
||||||
padding: 5,
|
|
||||||
borderRadius: 5,
|
|
||||||
},
|
|
||||||
counterText: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: colors.primary,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { View, Text, TouchableOpacity, Image, StyleSheet, Dimensions } from 'react-native';
|
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
import { Menu } from 'lucide-react-native';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
import ImagePath from '../../Constant/ImagePath';
|
|
||||||
import FontsFamily from '../../Constant/FontsFamily';
|
|
||||||
import SideDrawer from '../SideDrawer/SideDrawer';
|
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
interface HeaderProps {
|
|
||||||
title: string;
|
|
||||||
showDrawer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({ title, showDrawer = true }) => {
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.white, 'rgba(255,255,255,0.8)']}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
style={[styles.container, { paddingTop: insets.top + 10 }]}
|
|
||||||
>
|
|
||||||
<View style={styles.row}>
|
|
||||||
<View style={styles.left}>
|
|
||||||
{showDrawer && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setDrawerVisible(true)}
|
|
||||||
style={styles.drawerIcon}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Menu size={28} color={colors.black} strokeWidth={2.5} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Image
|
|
||||||
source={ImagePath.Icon.Logo2}
|
|
||||||
style={styles.logo}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{/* Drawer Modal */}
|
|
||||||
<SideDrawer
|
|
||||||
visible={drawerVisible}
|
|
||||||
onClose={() => setDrawerVisible(false)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
width,
|
|
||||||
paddingBottom: 5,
|
|
||||||
},
|
|
||||||
row: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
|
||||||
left: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
drawerIcon: {
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
color: colors.black,
|
|
||||||
fontFamily: FontsFamily.poppinsSemiBold,
|
|
||||||
},
|
|
||||||
logo: {
|
|
||||||
width: 55,
|
|
||||||
height: 55,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import Header from "./Header/Header";
|
|
||||||
import InsideHeader from "./Header/InsideHeader";
|
|
||||||
import Loader from "./Loader/Loader";
|
|
||||||
import Skelton from "./Skelton/Skelton";
|
|
||||||
|
|
||||||
export { Header , Skelton , Loader , InsideHeader}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
||||||
import LottieView from "lottie-react-native";
|
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
|
||||||
|
|
||||||
const InternetConnectivity = () => {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<View style={styles.card}>
|
|
||||||
<View style={styles.lottieContainer}>
|
|
||||||
<LottieView
|
|
||||||
source={require("../../lottie/NoInternet.json")}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
style={styles.lottie}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.title}>No Internet Connection</Text>
|
|
||||||
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
It looks like you're offline. Check your connection and try again to continue.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.hintContainer}>
|
|
||||||
<View style={styles.hintRow}>
|
|
||||||
<View style={styles.hintDot} />
|
|
||||||
<Text style={styles.hintText}>Check your Wi-Fi or mobile data</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.hintRow}>
|
|
||||||
<View style={styles.hintDot} />
|
|
||||||
<Text style={styles.hintText}>Try turning airplane mode on/off</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.buttonContainer}>
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => {}}
|
|
||||||
style={styles.touchable}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={["#63adde", "#3089bd50"]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={styles.retryBtn}
|
|
||||||
>
|
|
||||||
<Text style={styles.retryText}>Try Again</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InternetConnectivity;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#F8FAFF",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 400,
|
|
||||||
backgroundColor: "#FFF",
|
|
||||||
borderRadius: 24,
|
|
||||||
paddingVertical: 32,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
alignItems: "center",
|
|
||||||
shadowColor: "#667EEA",
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowOffset: { width: 0, height: 10 },
|
|
||||||
shadowRadius: 20,
|
|
||||||
elevation: 8,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
|
||||||
lottieContainer: {
|
|
||||||
width: 180,
|
|
||||||
height: 180,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
lottie: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 26,
|
|
||||||
fontWeight: "800",
|
|
||||||
color: "#1A1A1A",
|
|
||||||
marginTop: 8,
|
|
||||||
letterSpacing: -0.5,
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#666",
|
|
||||||
textAlign: "center",
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 28,
|
|
||||||
lineHeight: 24,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
fontWeight: "400",
|
|
||||||
},
|
|
||||||
hintContainer: {
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: "#F8F9FF",
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: 20,
|
|
||||||
marginBottom: 32,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#EDF2FE",
|
|
||||||
},
|
|
||||||
hintRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
hintDot: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: "#667EEA",
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
hintText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: "#555",
|
|
||||||
fontWeight: "400",
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
width: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
touchable: {
|
|
||||||
width: "100%",
|
|
||||||
shadowColor: "#667EEA",
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 5,
|
|
||||||
},
|
|
||||||
retryBtn: {
|
|
||||||
paddingVertical: 18,
|
|
||||||
paddingHorizontal: 30,
|
|
||||||
borderRadius: 16,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
retryText: {
|
|
||||||
color: "#FFF",
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: "700",
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
secondaryButton: {
|
|
||||||
marginTop: 16,
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 30,
|
|
||||||
borderRadius: 16,
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: "#E2E8F0",
|
|
||||||
backgroundColor: "#FFF",
|
|
||||||
width: "100%",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
secondaryText: {
|
|
||||||
color: "#64748B",
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: "600",
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView } from 'react-native';
|
|
||||||
import LottieView from 'lottie-react-native';
|
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import { AlertCircle, Clock, RefreshCw, Activity } from 'lucide-react-native';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
import FontsFamily from '../../Constant/FontsFamily';
|
|
||||||
|
|
||||||
|
|
||||||
const ServerDown = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ScrollView
|
|
||||||
contentContainerStyle={styles.scrollContainer}
|
|
||||||
showsVerticalScrollIndicator={false}>
|
|
||||||
<View style={styles.container}>
|
|
||||||
<View style={styles.card}>
|
|
||||||
{/* Status Indicator */}
|
|
||||||
<View style={styles.statusIndicator}>
|
|
||||||
<View style={styles.statusPulse} />
|
|
||||||
<View style={styles.statusDot}>
|
|
||||||
<AlertCircle size={24} color="#FFF" />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Animated Illustration */}
|
|
||||||
<View style={styles.animationContainer}>
|
|
||||||
<LottieView
|
|
||||||
source={require('../../lottie/MaintenanceServer.json')}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
style={styles.animation}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.title}>Server Unavailable</Text>
|
|
||||||
|
|
||||||
<View style={styles.statusMessage}>
|
|
||||||
<Clock size={18} color="#FF6B6B" style={styles.statusIcon} />
|
|
||||||
<Text style={styles.statusText}>
|
|
||||||
Temporarily down for maintenance
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.description}>
|
|
||||||
Our servers are currently undergoing maintenance. This usually
|
|
||||||
takes 15-30 minutes. We appreciate your patience while we work
|
|
||||||
to improve your experience.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<View style={styles.buttonGroup}>
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => { }}
|
|
||||||
style={styles.touchable}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.primary, colors.primaryLight]}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={styles.primaryButton}>
|
|
||||||
<RefreshCw
|
|
||||||
size={20}
|
|
||||||
color="#FFF"
|
|
||||||
style={styles.buttonIcon}
|
|
||||||
/>
|
|
||||||
<Text style={styles.primaryButtonText}>Refresh Status</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Live Status */}
|
|
||||||
<View style={styles.liveStatus}>
|
|
||||||
<View style={styles.liveIndicator}>
|
|
||||||
<View style={styles.livePulse} />
|
|
||||||
<View style={styles.liveDot} />
|
|
||||||
</View>
|
|
||||||
<Activity size={14} color="#10B981" style={styles.liveIcon} />
|
|
||||||
<Text style={styles.liveText}>Monitoring server status...</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Background Pattern */}
|
|
||||||
<View style={styles.backgroundPattern} />
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServerDown;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
scrollContainer: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#F8FAFF',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: 20,
|
|
||||||
minHeight: Dimensions.get('window').height,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 420,
|
|
||||||
backgroundColor: '#FFF',
|
|
||||||
borderRadius: 28,
|
|
||||||
paddingVertical: 36,
|
|
||||||
paddingHorizontal: 28,
|
|
||||||
alignItems: 'center',
|
|
||||||
shadowColor: colors.primary,
|
|
||||||
shadowOpacity: 0.08,
|
|
||||||
shadowOffset: { width: 0, height: 12 },
|
|
||||||
shadowRadius: 24,
|
|
||||||
elevation: 10,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
statusIndicator: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: -20,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
statusPulse: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
borderRadius: 30,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
opacity: 0.2,
|
|
||||||
},
|
|
||||||
statusDot: {
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
shadowColor: colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 6,
|
|
||||||
},
|
|
||||||
animationContainer: {
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
marginLeft: -20,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: '800',
|
|
||||||
color: '#1A1A1A',
|
|
||||||
marginTop: 8,
|
|
||||||
letterSpacing: -0.5,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
statusMessage: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#FFF5F5',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderRadius: 12,
|
|
||||||
marginBottom: 24,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#FED7D7',
|
|
||||||
},
|
|
||||||
statusIcon: {
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#C53030',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#666',
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 24,
|
|
||||||
marginBottom: 28,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
fontFamily: FontsFamily.poppinsMedium,
|
|
||||||
},
|
|
||||||
progressContainer: {
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: 32,
|
|
||||||
},
|
|
||||||
progressBar: {
|
|
||||||
width: '100%',
|
|
||||||
height: 8,
|
|
||||||
backgroundColor: '#E2E8F0',
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: 'hidden',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
progressFill: {
|
|
||||||
width: '60%',
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: '#8B5CF6',
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
progressText: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#64748B',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
infoContainer: {
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: '#F8FAFF',
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: 20,
|
|
||||||
marginBottom: 32,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#EDF2FE',
|
|
||||||
},
|
|
||||||
infoRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
paddingBottom: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#EDF2F7',
|
|
||||||
},
|
|
||||||
infoLabel: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#64748B',
|
|
||||||
fontWeight: '500',
|
|
||||||
marginLeft: 12,
|
|
||||||
marginRight: 8,
|
|
||||||
width: 90,
|
|
||||||
},
|
|
||||||
infoValue: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#1A1A1A',
|
|
||||||
fontWeight: '600',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: '#F3E8FF',
|
|
||||||
},
|
|
||||||
statusMaintenance: {
|
|
||||||
backgroundColor: '#F3E8FF',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#D6BCFA',
|
|
||||||
},
|
|
||||||
statusBadgeText: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#8B5CF6',
|
|
||||||
fontWeight: '700',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
buttonGroup: {
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
touchable: {
|
|
||||||
width: '100%',
|
|
||||||
shadowColor: '#FF6B6B',
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 6,
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
primaryButton: {
|
|
||||||
paddingVertical: 18,
|
|
||||||
paddingHorizontal: 30,
|
|
||||||
borderRadius: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
buttonIcon: {
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
primaryButtonText: {
|
|
||||||
color: '#FFF',
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: '700',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
secondaryButtonGroup: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
secondaryButton: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#FFF',
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: '#E2E8F0',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
secondaryButtonText: {
|
|
||||||
color: '#64748B',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
liveStatus: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
liveIndicator: {
|
|
||||||
position: 'relative',
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
livePulse: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#10B981',
|
|
||||||
opacity: 0.4,
|
|
||||||
},
|
|
||||||
liveDot: {
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
borderRadius: 5,
|
|
||||||
backgroundColor: '#10B981',
|
|
||||||
},
|
|
||||||
liveIcon: {
|
|
||||||
marginRight: 6,
|
|
||||||
},
|
|
||||||
liveText: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#64748B',
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
backgroundPattern: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: '#F8FAFF',
|
|
||||||
opacity: 0.3,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, ActivityIndicator } from 'react-native';
|
|
||||||
|
|
||||||
interface LoaderType {
|
|
||||||
size?: 'small' | 'large';
|
|
||||||
color?: string;
|
|
||||||
style?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Loader = ({ size = "large", color = '#000', style }: LoaderType) => {
|
|
||||||
return (
|
|
||||||
<View style={[{ justifyContent: 'center', alignItems: 'center' , flex:1}, style]}>
|
|
||||||
<ActivityIndicator size={size} color={color} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Loader;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { StyleSheet, TextInput, View, TextInputProps, TouchableOpacity } from 'react-native';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
type MyInputProps = {
|
|
||||||
icon?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
value?: string;
|
|
||||||
onChangeText?: (text: string) => void;
|
|
||||||
isVarify?: boolean;
|
|
||||||
maxlength?: number;
|
|
||||||
onFocus?: () => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
focused?: boolean;
|
|
||||||
keyboardType?: TextInputProps['keyboardType'];
|
|
||||||
editable?: boolean;
|
|
||||||
autoCapitalize?: TextInputProps['autoCapitalize'];
|
|
||||||
rightIcon?: string,
|
|
||||||
rightIconPress?: () => void
|
|
||||||
} & TextInputProps;
|
|
||||||
|
|
||||||
const MyInput: React.FC<MyInputProps> = ({
|
|
||||||
icon,
|
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
onChangeText,
|
|
||||||
isVarify,
|
|
||||||
maxlength,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
focused,
|
|
||||||
keyboardType,
|
|
||||||
editable = true,
|
|
||||||
autoCapitalize,
|
|
||||||
rightIcon,
|
|
||||||
rightIconPress,
|
|
||||||
...textInputProps
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.inputWrapper,
|
|
||||||
focused && styles.inputFocused,
|
|
||||||
!editable && styles.inputDisabled,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* <Icon
|
|
||||||
name={icon}
|
|
||||||
size={18}
|
|
||||||
color={focused ? "#0000" : '#aaa'}
|
|
||||||
style={styles.icon}
|
|
||||||
/> */}
|
|
||||||
<TextInput
|
|
||||||
{...textInputProps}
|
|
||||||
placeholder={placeholder}
|
|
||||||
style={[styles.input, !editable && { color: '#999' }]}
|
|
||||||
placeholderTextColor="#aaa"
|
|
||||||
value={value}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
maxLength={maxlength}
|
|
||||||
editable={editable}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
keyboardType={keyboardType}
|
|
||||||
autoCapitalize={autoCapitalize}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
rightIcon ?
|
|
||||||
<TouchableOpacity onPress={rightIconPress}>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
: null}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyInput;
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
inputWrapper: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#e0e0e0',
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
marginVertical: 10,
|
|
||||||
height: 55,
|
|
||||||
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 15,
|
|
||||||
color: '#000',
|
|
||||||
paddingVertical: 0,
|
|
||||||
},
|
|
||||||
inputFocused: {
|
|
||||||
borderColor: '#007BFF',
|
|
||||||
},
|
|
||||||
inputDisabled: {
|
|
||||||
backgroundColor: '#f2f2f2',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
|
||||||
import React from 'react';
|
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
|
|
||||||
|
|
||||||
type PrimaryButtonProps = {
|
|
||||||
title: string;
|
|
||||||
onPress: () => void | any;
|
|
||||||
style?: ViewStyle;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PrimaryButton: React.FC<PrimaryButtonProps> = ({ title, onPress, style, disabled }) => {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
|
|
||||||
style={[styles.primaryButton, style]}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.touchable}
|
|
||||||
onPress={onPress}
|
|
||||||
activeOpacity={0.82}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>{title}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrimaryButton;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
primaryButton: {
|
|
||||||
borderRadius: 14,
|
|
||||||
paddingVertical: 16,
|
|
||||||
paddingHorizontal: 28,
|
|
||||||
marginBottom: 20,
|
|
||||||
minHeight: 54,
|
|
||||||
|
|
||||||
// Orange-themed shadows
|
|
||||||
shadowColor: colors.primary,
|
|
||||||
shadowOffset: { width: 0, height: 6 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 10,
|
|
||||||
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
touchable: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
buttonText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
color: colors.white,
|
|
||||||
fontWeight: '800', // Extra bold for orange contrast
|
|
||||||
fontSize: 16,
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
textShadowColor: 'rgba(0,0,0,0.2)',
|
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
|
||||||
textShadowRadius: 2,
|
|
||||||
includeFontPadding: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, Image } from 'react-native';
|
|
||||||
import Modal from 'react-native-modal';
|
|
||||||
import Animated, { useSharedValue, useAnimatedStyle, withTiming, withDelay } from 'react-native-reanimated';
|
|
||||||
import { Home, Gavel, FileText, User, LogOut, ChevronRight } from 'lucide-react-native';
|
|
||||||
import { CommonActions, useNavigation } from '@react-navigation/native';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { removeToken } from '../../Redux/Slices/Token';
|
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
|
||||||
const DRAWER_WIDTH = width * 0.78;
|
|
||||||
|
|
||||||
interface Props { visible: boolean; onClose: () => void; }
|
|
||||||
|
|
||||||
const SideDrawer: React.FC<Props> = ({ visible, onClose }) => {
|
|
||||||
const navigation: any = useNavigation();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const userData = useSelector((state: any) => state.token?.token);
|
|
||||||
|
|
||||||
const user = { name: userData?.fullName, role: userData?.role, };
|
|
||||||
const translateX = useSharedValue(-DRAWER_WIDTH);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
translateX.value = visible
|
|
||||||
? withTiming(0, { duration: 0 })
|
|
||||||
: withTiming(-DRAWER_WIDTH, { duration: 250 });
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
const drawerStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ translateX: translateX.value }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{
|
|
||||||
label: 'Profile',
|
|
||||||
icon: User,
|
|
||||||
onPress: () => navigation.navigate(NavigationString.Profile),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Home',
|
|
||||||
icon: Home,
|
|
||||||
onPress: () => navigation.navigate(NavigationString.Home),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Auctions',
|
|
||||||
icon: Gavel,
|
|
||||||
onPress: () => navigation.navigate(NavigationString.Auction),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tenders',
|
|
||||||
icon: FileText,
|
|
||||||
onPress: () => navigation.navigate(NavigationString.Tender),
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isVisible={visible}
|
|
||||||
onBackdropPress={onClose}
|
|
||||||
onBackButtonPress={onClose}
|
|
||||||
backdropOpacity={0.45}
|
|
||||||
animationIn="fadeIn"
|
|
||||||
animationOut="fadeOut"
|
|
||||||
style={styles.modal}
|
|
||||||
useNativeDriver
|
|
||||||
coverScreen={false}
|
|
||||||
propagateSwipe
|
|
||||||
>
|
|
||||||
<Animated.View style={[styles.drawer, drawerStyle]}>
|
|
||||||
{/* ================= Header / Profile ================= */}
|
|
||||||
<View style={styles.profileHeader}>
|
|
||||||
{/* <TouchableOpacity style={styles.closeBtn} onPress={() => {
|
|
||||||
}}>
|
|
||||||
<X size={20} color="#111827" />
|
|
||||||
</TouchableOpacity> */}
|
|
||||||
|
|
||||||
<View style={styles.avatar}>
|
|
||||||
<User size={40} color={colors.black} strokeWidth={2} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.userName}>{user.name}</Text>
|
|
||||||
<Text style={styles.userRole}>{user.role}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ================= Menu ================= */}
|
|
||||||
<View style={styles.menuContainer}>
|
|
||||||
{menuItems.map((item, index) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
|
|
||||||
const itemStyle = useAnimatedStyle(() => ({
|
|
||||||
opacity: withDelay(
|
|
||||||
index * 80,
|
|
||||||
withTiming(1, { duration: 300 }),
|
|
||||||
),
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateX: withDelay(
|
|
||||||
index * 80,
|
|
||||||
withTiming(0, { duration: 300 }),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View key={item.label} style={itemStyle}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.menuItem}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => {
|
|
||||||
onClose();
|
|
||||||
item.onPress();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={styles.menuLeft}>
|
|
||||||
<Icon size={20} color="#1F2937" />
|
|
||||||
<Text style={styles.menuText}>{item.label}</Text>
|
|
||||||
</View>
|
|
||||||
<ChevronRight size={18} color="#9CA3AF" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* ================= Logout ================= */}
|
|
||||||
<View style={styles.logoutContainer}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.logoutBtn}
|
|
||||||
onPress={() => {
|
|
||||||
dispatch(removeToken());
|
|
||||||
navigation.dispatch(
|
|
||||||
CommonActions.reset({
|
|
||||||
index: 0,
|
|
||||||
routes: [{ name: NavigationString.Login }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LogOut size={20} color="#EF4444" />
|
|
||||||
<Text style={styles.logoutText}>Logout</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SideDrawer;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
modal: {
|
|
||||||
margin: 0,
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
},
|
|
||||||
|
|
||||||
drawer: {
|
|
||||||
width: DRAWER_WIDTH,
|
|
||||||
height: height - 80,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderTopRightRadius: 24,
|
|
||||||
borderBottomRightRadius: 24,
|
|
||||||
elevation: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* -------- Profile -------- */
|
|
||||||
profileHeader: {
|
|
||||||
backgroundColor: '#F9FAFB',
|
|
||||||
paddingTop: 50,
|
|
||||||
paddingBottom: 24,
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTopRightRadius: 24,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeBtn: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 20,
|
|
||||||
right: 16,
|
|
||||||
padding: 10,
|
|
||||||
zIndex: 20
|
|
||||||
},
|
|
||||||
|
|
||||||
avatar: {
|
|
||||||
justifyContent:'center',
|
|
||||||
alignItems:'center',
|
|
||||||
backgroundColor:colors.primary,
|
|
||||||
padding:25,
|
|
||||||
borderRadius:60,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
|
|
||||||
userName: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#111827',
|
|
||||||
},
|
|
||||||
|
|
||||||
userRole: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#6B7280',
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* -------- Menu -------- */
|
|
||||||
menuContainer: {
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingTop: 20,
|
|
||||||
},
|
|
||||||
|
|
||||||
menuItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderRadius: 14,
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
|
|
||||||
menuLeft: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
menuText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#1F2937',
|
|
||||||
},
|
|
||||||
|
|
||||||
/* -------- Logout -------- */
|
|
||||||
logoutContainer: {
|
|
||||||
marginTop: 'auto',
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingBottom: 24,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: '#E5E7EB',
|
|
||||||
},
|
|
||||||
|
|
||||||
logoutBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
paddingVertical: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
logoutText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#EF4444',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import colors from './Color';
|
|
||||||
import AllUrls from './AllUrls';
|
|
||||||
import FontsFamily from './FontsFamily';
|
|
||||||
import ImagePath from './ImagePath';
|
|
||||||
import NavigationString from './NavigationString';
|
|
||||||
import { showErrorAlert, showSuccessAlert, showToast } from './ShowDailog';
|
|
||||||
import Variable from './Variable';
|
|
||||||
|
|
||||||
export { AllUrls, colors, ImagePath, NavigationString, showErrorAlert, showSuccessAlert, showToast, Variable ,FontsFamily};
|
|
||||||
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import Variable from "./Variable"
|
|
||||||
|
|
||||||
// Customer
|
|
||||||
const Login = `${Variable.Main_Base}/customer/login`
|
|
||||||
const OtpVerify = `${Variable.Main_Base}/customer/verify_otp`
|
|
||||||
const userUpdate = `${Variable.Main_Base}/customer`
|
|
||||||
const userLogout = `${Variable.Main_Base}/customer/logout`
|
|
||||||
const userInfo = `${Variable.Main_Base}/customer/info`
|
|
||||||
|
|
||||||
// Category
|
|
||||||
const categoryApi = `${Variable.Main_Base}/category`
|
|
||||||
|
|
||||||
// Brand
|
|
||||||
const brandsApi = `${Variable.Main_Base}/brand`
|
|
||||||
|
|
||||||
// Brand
|
|
||||||
const bannersApi = `${Variable.Main_Base}/banner/position`
|
|
||||||
|
|
||||||
// Vehicle Segment
|
|
||||||
const segmentApi = `${Variable.Main_Base}/vehicleSegmentType/without-page`
|
|
||||||
const vehicleApi = `${Variable.Main_Base}/vehicle`
|
|
||||||
|
|
||||||
// Products
|
|
||||||
const productApi = `${Variable.Main_Base}/product`
|
|
||||||
const brandProductApi = `${Variable.Main_Base}/product/assigned`
|
|
||||||
const ProductDetailsApi = `${Variable.Main_Base}/product`
|
|
||||||
const ProductWishlistApi = `${Variable.Main_Base}/wish-list`
|
|
||||||
const recentViewedProductApi = `${Variable.Main_Base}/view`
|
|
||||||
const updateAddToCartApi = `${Variable.Main_Base}/add-to-cart`
|
|
||||||
const brandCategoryApi = `${Variable.Main_Base}/category/brand`
|
|
||||||
|
|
||||||
// Rating
|
|
||||||
const ratingApi = `${Variable.Main_Base}/rating`
|
|
||||||
|
|
||||||
// Coupon
|
|
||||||
const couponApi = `${Variable.Main_Base}/coupon`
|
|
||||||
|
|
||||||
// Orders
|
|
||||||
const orderApi = `${Variable.Main_Base}/order`
|
|
||||||
|
|
||||||
// Address
|
|
||||||
const addressApi = `${Variable.Main_Base}/address`
|
|
||||||
const addressStatusApi = `${Variable.Main_Base}/address/status`
|
|
||||||
|
|
||||||
// Recharge
|
|
||||||
const rechrgeWalletApi = `${Variable.Main_Base}/wallets/create-order`
|
|
||||||
const rechrgeWalletVerifyApi = `${Variable.Main_Base}/wallets/verify`
|
|
||||||
const rechrgeWalletHistoryApi = `${Variable.Main_Base}/wallets`
|
|
||||||
|
|
||||||
// Order Payment
|
|
||||||
const orderPaymentApi = `${Variable.Main_Base}/order/init`
|
|
||||||
const orderConfirmApi = `${Variable.Main_Base}/order/callback`
|
|
||||||
|
|
||||||
// Charges
|
|
||||||
const chargesApi = `${Variable.Main_Base}/charges`
|
|
||||||
// FAQs
|
|
||||||
const FAQsApi = `${Variable.Main_Base}/faq`
|
|
||||||
|
|
||||||
// offers
|
|
||||||
const offersApi = `${Variable.Main_Base}/recharge`
|
|
||||||
// PrivacyPolicy
|
|
||||||
const privacyPolicyApi = `${Variable.Main_Base}/documents`
|
|
||||||
|
|
||||||
const AllUrls = {
|
|
||||||
Login, OtpVerify, userUpdate, userLogout, categoryApi, brandsApi, bannersApi, segmentApi, productApi, brandProductApi, ProductDetailsApi, ratingApi,
|
|
||||||
ProductWishlistApi, recentViewedProductApi, updateAddToCartApi, vehicleApi, brandCategoryApi, userInfo, couponApi, orderApi, addressApi, addressStatusApi,
|
|
||||||
rechrgeWalletApi, orderPaymentApi, orderConfirmApi, rechrgeWalletVerifyApi, chargesApi, FAQsApi, rechrgeWalletHistoryApi, offersApi,privacyPolicyApi
|
|
||||||
};
|
|
||||||
export default AllUrls;
|
|
||||||
+17
-7
@@ -1,7 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @deprecated Legacy palette kept only for not-yet-migrated screens.
|
||||||
|
* New code must use `useTheme()` (src/theme) — it is the single source of
|
||||||
|
* visual truth and supports dark mode.
|
||||||
|
*
|
||||||
|
* The BRAND colors below are derived from the theme so the app has exactly
|
||||||
|
* one brand orange (historically this file carried a second, different one).
|
||||||
|
*/
|
||||||
|
import { lightColors } from '../theme/colors';
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
// Primary (Orange-based)
|
// Primary — single-sourced from the design system
|
||||||
primary: '#F97316', // Main orange (buttons, highlights)
|
primary: lightColors.primary,
|
||||||
primaryLight: '#FDBA74', // Soft orange (chips, badges)
|
primaryLight: lightColors.primaryLight,
|
||||||
primaryHighlight: '#f4f1efff', // Very light orange (backgrounds)
|
primaryHighlight: '#f4f1efff', // Very light orange (backgrounds)
|
||||||
|
|
||||||
// Secondary (Warm contrast)
|
// Secondary (Warm contrast)
|
||||||
@@ -25,16 +35,16 @@ const colors = {
|
|||||||
border: '#E5E7EB',
|
border: '#E5E7EB',
|
||||||
lightGray: '#F9FAFB',
|
lightGray: '#F9FAFB',
|
||||||
disabled: '#D1D5DB',
|
disabled: '#D1D5DB',
|
||||||
gray:'#b9babb',
|
gray: '#b9babb',
|
||||||
|
|
||||||
// Additional
|
// Additional
|
||||||
placeholder: '#9CA3AF',
|
placeholder: '#9CA3AF',
|
||||||
backdrop: 'rgba(0,0,0,0.5)',
|
backdrop: 'rgba(0,0,0,0.5)',
|
||||||
shadow: 'rgba(0,0,0,0.12)',
|
shadow: 'rgba(0,0,0,0.12)',
|
||||||
|
|
||||||
// Aliases (optional)
|
// Aliases (single-sourced with primary above)
|
||||||
Orange: '#F97316',
|
Orange: lightColors.primary,
|
||||||
OrangeLight: '#FDBA74',
|
OrangeLight: lightColors.primaryLight,
|
||||||
OrangeSoft: '#FFF3E8',
|
OrangeSoft: '#FFF3E8',
|
||||||
|
|
||||||
darkGray: '#5f6368',
|
darkGray: '#5f6368',
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ const NavigationString = {
|
|||||||
ResetPassword:'ResetPassword',
|
ResetPassword:'ResetPassword',
|
||||||
KycWebViewScreen:'KycWebViewScreen',
|
KycWebViewScreen:'KycWebViewScreen',
|
||||||
StageThreeScreen:'StageThree',
|
StageThreeScreen:'StageThree',
|
||||||
DecryptionScreen:'DecryptionScreen',
|
|
||||||
DeviceBinding: 'DeviceBinding',
|
DeviceBinding: 'DeviceBinding',
|
||||||
DscBinding: 'DscBinding'
|
DscBinding: 'DscBinding',
|
||||||
}
|
EnvelopeCSpreadsheet: 'EnvelopeCSpreadsheet'
|
||||||
export default NavigationString
|
} as const
|
||||||
|
export default NavigationString
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ALERT_TYPE, Dialog, Root, Toast } from 'react-native-alert-notification';
|
import { ALERT_TYPE, Toast } from 'react-native-alert-notification';
|
||||||
|
|
||||||
const showSuccessAlert = (text: string) => {
|
const showSuccessAlert = (text: string) => {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
const webSocketUrl = 'https://procurelinc.in/Auction/liveBid/';
|
/**
|
||||||
const socketUrl = `https://procurelinc.in/Auction/api/`;
|
* @deprecated Compatibility re-export. Import { env } from 'src/core/config/env'
|
||||||
const Variables = { webSocketUrl, socketUrl }
|
* in new code; this file exists only until the socket/bidding consumers migrate.
|
||||||
export default Variables
|
*/
|
||||||
|
import { env } from '../core/config/env';
|
||||||
|
|
||||||
|
const Variables = {
|
||||||
|
webSocketUrl: env.webSocketUrl,
|
||||||
|
socketUrl: env.socketBaseUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Variables;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Platform } from 'react-native';
|
|
||||||
import * as Network from 'expo-network';
|
|
||||||
|
|
||||||
export const getDeviceDetails = async () => {
|
|
||||||
const ipAddress = await Network.getIpAddressAsync();
|
|
||||||
|
|
||||||
const detail = [
|
|
||||||
{ name: 'IMEI', value: '356938035643809' },
|
|
||||||
{ name: 'IP', value: ipAddress },
|
|
||||||
{ name: 'OS', value: Platform.OS },
|
|
||||||
{ name: 'APP', value: 'Globots' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return {detail};
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import apiClient from '../api/client';
|
|
||||||
|
|
||||||
export const useCreate = <T, V>(
|
|
||||||
url: string,
|
|
||||||
options?: UseMutationOptions<T, Error, V>
|
|
||||||
) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation<T, Error, V>({
|
|
||||||
mutationFn: async (variables: V) => {
|
|
||||||
const { data } = await apiClient.post<T>(url, variables);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
// Invalidate queries if needed, handled by component or passed in options
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import apiClient from '../api/client';
|
|
||||||
|
|
||||||
export const useDelete = <T>(
|
|
||||||
url: string,
|
|
||||||
options?: UseMutationOptions<T, Error, void>
|
|
||||||
) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation<T, Error, void>({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const { data } = await apiClient.delete<T>(url);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query';
|
|
||||||
import apiClient from '../api/client';
|
|
||||||
|
|
||||||
export const useFetch = <T>(
|
|
||||||
key: QueryKey,
|
|
||||||
url: string,
|
|
||||||
options?: UseQueryOptions<T, Error>
|
|
||||||
) => {
|
|
||||||
return useQuery<T, Error>({
|
|
||||||
queryKey: key,
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await apiClient.get<T>(url);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import apiClient from '../api/client';
|
|
||||||
|
|
||||||
export const useUpdate = <T, V>(
|
|
||||||
url: string,
|
|
||||||
options?: UseMutationOptions<T, Error, V>
|
|
||||||
) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation<T, Error, V>({
|
|
||||||
mutationFn: async (variables: V) => {
|
|
||||||
const { data } = await apiClient.put<T>(url, variables);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { selectIsAuthenticated } from '../Redux/Slices/Token';
|
||||||
|
import NavigationString from '../Constant/NavigationString';
|
||||||
|
import { resetAndNavigate } from './NavigationService';
|
||||||
|
import { logger } from '../core/observability/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarative auth gate: whenever an existing session ends — a 401 clearing
|
||||||
|
* the token via the auth/unauthorized event, an explicit logout, or a
|
||||||
|
* keychain wipe — reset navigation to Login.
|
||||||
|
*
|
||||||
|
* Historically a mid-session 401 cleared the token but left the user
|
||||||
|
* stranded on an authenticated screen where every request failed silently.
|
||||||
|
* Renders nothing; mount once inside the Redux provider.
|
||||||
|
*/
|
||||||
|
const AuthSessionGuard = () => {
|
||||||
|
const isAuthenticated = useSelector(selectIsAuthenticated);
|
||||||
|
const wasAuthenticated = useRef(isAuthenticated);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasAuthenticated.current && !isAuthenticated) {
|
||||||
|
logger.info('auth', 'session ended — routing to Login');
|
||||||
|
resetAndNavigate(NavigationString.Login);
|
||||||
|
}
|
||||||
|
wasAuthenticated.current = isAuthenticated;
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthSessionGuard;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@@ -24,10 +23,6 @@ import { useTheme } from '../Hooks/useTheme';
|
|||||||
import { typography } from '../theme/typography';
|
import { typography } from '../theme/typography';
|
||||||
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
|
// import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
const TAB_BAR_HEIGHT = 70; // Slightly taller for premium feel
|
|
||||||
const TAB_BUTTON_WIDTH = width / 4;
|
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
const TabButton = ({ route, isFocused, onPress }: any) => {
|
const TabButton = ({ route, isFocused, onPress }: any) => {
|
||||||
@@ -55,9 +50,11 @@ const TabButton = ({ route, isFocused, onPress }: any) => {
|
|||||||
scale.value = withTiming(0, { duration: 200 });
|
scale.value = withTiming(0, { duration: 200 });
|
||||||
translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
|
translateY.value = withSpring(0, { damping: 12, stiffness: 100 });
|
||||||
}
|
}
|
||||||
|
// Intentionally runs only when isFocused changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isFocused]);
|
}, [isFocused]);
|
||||||
|
|
||||||
const animatedPillStyle = useAnimatedStyle(() => {
|
useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
transform: [{ scale: scale.value }],
|
transform: [{ scale: scale.value }],
|
||||||
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
|
opacity: interpolate(scale.value, [0, 1], [0, 1], Extrapolation.CLAMP),
|
||||||
@@ -86,6 +83,9 @@ const TabButton = ({ route, isFocused, onPress }: any) => {
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={styles.tabButton}
|
style={styles.tabButton}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
|
accessibilityRole="tab"
|
||||||
|
accessibilityState={{ selected: isFocused }}
|
||||||
|
accessibilityLabel={`${route.name} tab`}
|
||||||
>
|
>
|
||||||
<View style={styles.tabContent}>
|
<View style={styles.tabContent}>
|
||||||
<Animated.View style={animatedIconStyle}>
|
<Animated.View style={animatedIconStyle}>
|
||||||
@@ -119,8 +119,8 @@ const TabButton = ({ route, isFocused, onPress }: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomTabBar = ({ state, descriptors, navigation }: any) => {
|
const CustomTabBar = ({ state, navigation }: any) => {
|
||||||
const { colors, shadows } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { width: windowWidth } = Dimensions.get('window');
|
const { width: windowWidth } = Dimensions.get('window');
|
||||||
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
|
const TAB_BAR_WIDTH = windowWidth - 40; // 20 margin each side
|
||||||
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
|
const TAB_PADDING_H = 10; // 5 padding left + 5 padding right
|
||||||
@@ -134,6 +134,8 @@ const CustomTabBar = ({ state, descriptors, navigation }: any) => {
|
|||||||
stiffness: 150,
|
stiffness: 150,
|
||||||
mass: 0.5,
|
mass: 0.5,
|
||||||
});
|
});
|
||||||
|
// Intentionally runs only when the selected tab index changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [state.index]);
|
}, [state.index]);
|
||||||
|
|
||||||
const animatedIndicatorStyle = useAnimatedStyle(() => {
|
const animatedIndicatorStyle = useAnimatedStyle(() => {
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, StyleSheet, TouchableOpacity, Switch, Image } from 'react-native';
|
|
||||||
import { DrawerContentScrollView, DrawerItemList } from '@react-navigation/drawer';
|
|
||||||
import { useTheme } from '../Hooks/useTheme';
|
|
||||||
import AppText from '../components/AppText';
|
|
||||||
import { Moon, Sun, Monitor } from 'lucide-react-native';
|
|
||||||
import ImagePath from '../Constant/ImagePath';
|
|
||||||
|
|
||||||
const CustomDrawer = (props: any) => {
|
|
||||||
const { colors, isDarkMode, themeMode, toggleTheme } = useTheme();
|
|
||||||
|
|
||||||
const renderThemeOption = (mode: 'light' | 'dark' | 'system', label: string, Icon: any) => {
|
|
||||||
const isActive = themeMode === mode;
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.themeOption,
|
|
||||||
isActive && { backgroundColor: colors.primary + '20' } // 20% opacity
|
|
||||||
]}
|
|
||||||
onPress={() => toggleTheme(mode)}
|
|
||||||
>
|
|
||||||
<Icon size={20} color={isActive ? colors.primary : colors.textSecondary} />
|
|
||||||
<AppText variant="medium" style={{ marginLeft: 10, color: isActive ? colors.primary : colors.textSecondary }}>
|
|
||||||
{label}
|
|
||||||
</AppText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
|
||||||
<DrawerContentScrollView {...props} contentContainerStyle={{ paddingTop: 0 }}>
|
|
||||||
{/* Header */}
|
|
||||||
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
|
|
||||||
<Image source={ImagePath.Icon.Logo} style={styles.logo} resizeMode="contain" />
|
|
||||||
<AppText variant="bold" size={20} style={{ color: colors.text }}>ProcureLinc</AppText>
|
|
||||||
<AppText variant="regular" size={12} style={{ color: colors.textSecondary }}>Smart Procurement</AppText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ flex: 1, paddingTop: 10 }}>
|
|
||||||
<DrawerItemList {...props} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Theme Switch Section */}
|
|
||||||
<View style={[styles.section, { borderTopColor: colors.border }]}>
|
|
||||||
<AppText variant="bold" style={{ marginLeft: 16, marginBottom: 10, color: colors.text }}>Running Theme</AppText>
|
|
||||||
<View style={styles.themeRow}>
|
|
||||||
{renderThemeOption('light', 'Light', Sun)}
|
|
||||||
{renderThemeOption('dark', 'Dark', Moon)}
|
|
||||||
{renderThemeOption('system', 'System', Monitor)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</DrawerContentScrollView>
|
|
||||||
|
|
||||||
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
||||||
<AppText variant="regular" size={12} style={{ color: colors.textSecondary, textAlign: 'center' }}>
|
|
||||||
Version 1.0.0
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
header: {
|
|
||||||
padding: 20,
|
|
||||||
paddingTop: 50, // safe area approximation
|
|
||||||
alignItems: 'center',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
},
|
|
||||||
logo: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
padding: 10,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
marginTop: 'auto',
|
|
||||||
marginBottom: 20
|
|
||||||
},
|
|
||||||
themeRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-around',
|
|
||||||
},
|
|
||||||
themeOption: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 12,
|
|
||||||
flexDirection: 'row'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
padding: 20,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default CustomDrawer;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { createDrawerNavigator } from '@react-navigation/drawer';
|
|
||||||
import BottomTab from './Bottom';
|
|
||||||
import CustomDrawer from './CustomDrawer';
|
|
||||||
import { useTheme } from '../Hooks/useTheme';
|
|
||||||
import NavigationString from '../Constant/NavigationString';
|
|
||||||
import { Dimensions } from 'react-native';
|
|
||||||
|
|
||||||
const Drawer = createDrawerNavigator();
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
const DrawerStack = () => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer.Navigator
|
|
||||||
drawerContent={(props) => <CustomDrawer {...props} />}
|
|
||||||
screenOptions={{
|
|
||||||
headerShown: false,
|
|
||||||
drawerType: 'slide',
|
|
||||||
drawerStyle: {
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
width: width * 0.75,
|
|
||||||
},
|
|
||||||
drawerActiveTintColor: colors.primary,
|
|
||||||
drawerInactiveTintColor: colors.textSecondary,
|
|
||||||
overlayColor: 'rgba(0,0,0,0.5)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Drawer.Screen
|
|
||||||
name="MainTabs"
|
|
||||||
component={BottomTab}
|
|
||||||
options={{ title: 'Home' }}
|
|
||||||
/>
|
|
||||||
{/* Add other drawer screens here that are NOT in the bottom tab */}
|
|
||||||
</Drawer.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DrawerStack;
|
|
||||||
@@ -5,8 +5,9 @@ import Screens from '../Screens';
|
|||||||
import colors from '../Constant/Color';
|
import colors from '../Constant/Color';
|
||||||
import BottomTab from './Bottom';
|
import BottomTab from './Bottom';
|
||||||
import Login from '../Screens/Login/Login';
|
import Login from '../Screens/Login/Login';
|
||||||
|
import { RootStackParamList } from './types';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|
||||||
export default function MainStack() {
|
export default function MainStack() {
|
||||||
return (
|
return (
|
||||||
@@ -41,9 +42,9 @@ export default function MainStack() {
|
|||||||
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} />
|
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} />
|
||||||
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} />
|
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} />
|
||||||
<Stack.Screen name={NavigationString.StageThreeScreen} component={Screens.StageThreeScreen} />
|
<Stack.Screen name={NavigationString.StageThreeScreen} component={Screens.StageThreeScreen} />
|
||||||
<Stack.Screen name={NavigationString.DecryptionScreen} component={Screens.DecryptionScreen} />
|
|
||||||
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} />
|
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} />
|
||||||
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} />
|
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} />
|
||||||
|
<Stack.Screen name={NavigationString.EnvelopeCSpreadsheet} component={Screens.EnvelopeCSpreadsheet} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import MainStack from './MainStack';
|
|
||||||
export default function Routes() {
|
|
||||||
return <>{MainStack()} </>
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import MainStack from './MainStack';
|
||||||
|
|
||||||
|
export default function Routes() {
|
||||||
|
// Render as an element (not a function call) so React treats MainStack as a
|
||||||
|
// real component — hooks, devtools identity, and memoization all depend on it.
|
||||||
|
return <MainStack />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Typed route registry for the root stack. Param shapes reflect what each
|
||||||
|
* screen actually reads today — tighten them as screens get typed models.
|
||||||
|
*
|
||||||
|
* Usage in screens:
|
||||||
|
* useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||||
|
* useRoute<RouteProp<RootStackParamList, 'TenderBidding'>>()
|
||||||
|
*/
|
||||||
|
export type RootStackParamList = {
|
||||||
|
Splash: undefined;
|
||||||
|
Signup: undefined;
|
||||||
|
Onboarding: undefined;
|
||||||
|
Login: undefined;
|
||||||
|
Home: undefined;
|
||||||
|
PurchaseList: undefined;
|
||||||
|
Emd: undefined;
|
||||||
|
// Both bidding routes render the pattern-driven AuctionBiddingScreen; the
|
||||||
|
// auction itself comes from the redux `auction` slice, not params.
|
||||||
|
StartBidding: undefined;
|
||||||
|
StartReverseBidding: undefined;
|
||||||
|
PurchaseAuctionList: undefined;
|
||||||
|
PurchaseTenderList: undefined;
|
||||||
|
ProceedToPurTender: undefined;
|
||||||
|
TenderBidding: { tenderId: number | string };
|
||||||
|
ProceedToBidDetail: { tender: { tenderId: number | string } & Record<string, unknown> };
|
||||||
|
Registration: Record<string, unknown> | undefined;
|
||||||
|
Partnership: { otherId: unknown; orgType: unknown };
|
||||||
|
/** The whole tender object is currently spread into params. */
|
||||||
|
LiveTenderDetail: { tenderId: number } & Record<string, unknown>;
|
||||||
|
ResetPassword: undefined;
|
||||||
|
/** Server-rendered KYC HTML delivered via params. */
|
||||||
|
KycWebViewScreen: { res: { data: string } } & Record<string, unknown>;
|
||||||
|
StageThree: { otherId: unknown; orgType: unknown };
|
||||||
|
DeviceBinding: { message?: string; showBindButton?: boolean } | undefined;
|
||||||
|
DscBinding: { message?: string } | undefined;
|
||||||
|
EnvelopeCSpreadsheet: { tender: Record<string, unknown>; buttonEnable?: boolean };
|
||||||
|
};
|
||||||
+32
-49
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Centralized permission broker — screens must request permissions through
|
||||||
|
* these helpers, never react-native-permissions directly.
|
||||||
|
*/
|
||||||
import {
|
import {
|
||||||
check,
|
check,
|
||||||
request,
|
request,
|
||||||
@@ -6,93 +10,72 @@ import {
|
|||||||
openSettings,
|
openSettings,
|
||||||
PERMISSIONS,
|
PERMISSIONS,
|
||||||
Permission,
|
Permission,
|
||||||
|
checkNotifications,
|
||||||
|
requestNotifications,
|
||||||
} from 'react-native-permissions';
|
} from 'react-native-permissions';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
import { logger } from '../core/observability/logger';
|
||||||
|
|
||||||
// Normalize permission for platform
|
// Normalize permission for platform
|
||||||
const normalizePermission = (perm: string): string => {
|
const normalizePermission = (perm: string): Permission => {
|
||||||
return Platform.select({
|
return Platform.select({
|
||||||
ios: (PERMISSIONS.IOS as Record<string, string>)[perm] || perm,
|
ios: (PERMISSIONS.IOS as Record<string, string>)[perm] || perm,
|
||||||
android: (PERMISSIONS.ANDROID as Record<string, string>)[perm] || perm,
|
android: (PERMISSIONS.ANDROID as Record<string, string>)[perm] || perm,
|
||||||
default: perm,
|
default: perm,
|
||||||
})!;
|
}) as Permission;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic permission requester
|
// Generic permission requester
|
||||||
export const requestSinglePermission = async (
|
export const requestSinglePermission = async (
|
||||||
permission: string
|
permission: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const normalized = normalizePermission(permission);
|
const normalized = normalizePermission(permission);
|
||||||
|
|
||||||
const status: PermissionStatus = await check(normalized);
|
const status: PermissionStatus = await check(normalized);
|
||||||
|
|
||||||
if (status === RESULTS.GRANTED) {
|
if (status === RESULTS.GRANTED) {
|
||||||
console.log(`✅ ${permission} already granted.`);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === RESULTS.BLOCKED) {
|
if (status === RESULTS.BLOCKED) {
|
||||||
console.log(`🚫 ${permission} is blocked. Prompt user to go to settings.`);
|
logger.info('permissions', `${permission} is blocked — opening settings`);
|
||||||
openSettings();
|
openSettings();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === RESULTS.DENIED || status === RESULTS.LIMITED) {
|
if (status === RESULTS.DENIED || status === RESULTS.LIMITED) {
|
||||||
const result: PermissionStatus = await request(normalized);
|
const result: PermissionStatus = await request(normalized);
|
||||||
if (result === RESULTS.GRANTED) {
|
return result === RESULTS.GRANTED;
|
||||||
console.log(`✅ ${permission} granted after request.`);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.log(`❌ ${permission} denied.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Fallback definition
|
|
||||||
const POST_NOTIFICATIONS =
|
|
||||||
Platform.OS === 'android' ? 'android.permission.POST_NOTIFICATIONS' : '';
|
|
||||||
|
|
||||||
export const requestNotificationPermission = async (): Promise<boolean> => {
|
export const requestNotificationPermission = async (): Promise<boolean> => {
|
||||||
|
if (Platform.OS !== 'android') {
|
||||||
if (Platform.OS !== 'android' || !POST_NOTIFICATIONS) {
|
|
||||||
console.log('Notification permission not needed on this platform.');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status: PermissionStatus = await check(POST_NOTIFICATIONS);
|
const { status } = await checkNotifications();
|
||||||
|
|
||||||
if (status === RESULTS.GRANTED) {
|
if (status === RESULTS.GRANTED) {
|
||||||
console.log('🔔 Notification permission already granted.');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === RESULTS.BLOCKED) {
|
if (status === RESULTS.BLOCKED) {
|
||||||
console.log('🔕 Notification permission is blocked. Prompt user to open settings.');
|
logger.info('permissions', 'notifications blocked — opening settings');
|
||||||
openSettings();
|
openSettings();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === RESULTS.DENIED) {
|
if (status === RESULTS.DENIED) {
|
||||||
const result: PermissionStatus = await request(POST_NOTIFICATIONS);
|
const { status: result } = await requestNotifications(['alert', 'sound']);
|
||||||
if (result === RESULTS.GRANTED) {
|
return result === RESULTS.GRANTED;
|
||||||
console.log('✅ Notification permission granted.');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.log('❌ Notification permission denied.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const isAnyPermissionGranted = async (permissions: Permission[]): Promise<boolean> => {
|
export const isAnyPermissionGranted = async (permissions: Permission[]): Promise<boolean> => {
|
||||||
for (const permission of permissions) {
|
for (const permission of permissions) {
|
||||||
const result = await check(permission);
|
const result = await check(permission);
|
||||||
@@ -105,12 +88,12 @@ export const isAnyPermissionGranted = async (permissions: Permission[]): Promise
|
|||||||
return false; // No permissions granted
|
return false; // No permissions granted
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function checkPermission(permission:any) {
|
export async function checkPermission(permission: Permission) {
|
||||||
try {
|
try {
|
||||||
const result = await check(permission);
|
const result = await check(permission);
|
||||||
return result === RESULTS.GRANTED;
|
return result === RESULTS.GRANTED;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Permission check error:', error);
|
logger.warn('permissions', 'permission check failed', { error: String(error) });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const auctionSlice = createSlice({
|
|||||||
},
|
},
|
||||||
removeAuction: (state) => {
|
removeAuction: (state) => {
|
||||||
state.auction = '';
|
state.auction = '';
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const { setAuction , removeAuction} = auctionSlice.actions;
|
export const { setAuction , removeAuction} = auctionSlice.actions;
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ export const {
|
|||||||
clearPurchaseTender,
|
clearPurchaseTender,
|
||||||
} = selectPurchaseTenderSlice.actions;
|
} = selectPurchaseTenderSlice.actions;
|
||||||
|
|
||||||
export default selectPurchaseTenderSlice.reducer;
|
export default selectPurchaseTenderSlice.reducer;
|
||||||
|
|||||||
+57
-13
@@ -1,21 +1,65 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const initialState = {
|
/**
|
||||||
token: '',
|
* The authenticated session. `token` is the JWT; the remaining fields are the
|
||||||
|
* backend user profile returned at login. Kept intentionally open (`unknown`
|
||||||
|
* index) until a typed User domain model replaces the raw login response.
|
||||||
|
*/
|
||||||
|
export type AuthSession = {
|
||||||
|
token: string;
|
||||||
|
/** 'tender' | 'auction' — kept as string until the backend contract is typed. */
|
||||||
|
userType?: string;
|
||||||
|
firstName?: string;
|
||||||
|
fullName?: string;
|
||||||
|
contractorId?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** `''` means logged out — kept for backward compatibility with persisted state. */
|
||||||
|
export type TokenState = {
|
||||||
|
token: AuthSession | '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: TokenState = {
|
||||||
|
token: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidSession = (payload: unknown): payload is AuthSession =>
|
||||||
|
typeof payload === 'object' && payload !== null && typeof (payload as AuthSession).token === 'string';
|
||||||
|
|
||||||
export const tokenSlice = createSlice({
|
export const tokenSlice = createSlice({
|
||||||
name: 'token',
|
name: 'token',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setToken: (state, { payload }) => {
|
setToken: (state, { payload }: PayloadAction<AuthSession | Record<string, never> | ''>) => {
|
||||||
state.token = payload;
|
// Normalize every "cleared" shape ({} on 401, '' on logout) to ''
|
||||||
},
|
// so there is exactly one logged-out representation.
|
||||||
removeToken: (state) => {
|
state.token = isValidSession(payload) ? payload : '';
|
||||||
state.token = '';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
removeToken: state => {
|
||||||
|
state.token = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setToken, removeToken } = tokenSlice.actions;
|
export const { setToken, removeToken } = tokenSlice.actions;
|
||||||
export const getToken = (state: any) => state.token.token;
|
|
||||||
|
type WithTokenSlice = { token: TokenState };
|
||||||
|
|
||||||
|
/** The session object, or null when logged out. */
|
||||||
|
export const getToken = (state: WithTokenSlice): AuthSession | null => {
|
||||||
|
const session = state.token.token;
|
||||||
|
return isValidSession(session) ? session : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The raw JWT, or null when logged out — the ONLY sanctioned way to read it. */
|
||||||
|
export const selectAuthJwt = (state: WithTokenSlice): string | null => {
|
||||||
|
const session = state.token.token;
|
||||||
|
return isValidSession(session) ? session.token : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** True when a usable session exists. */
|
||||||
|
export const selectIsAuthenticated = (state: WithTokenSlice): boolean =>
|
||||||
|
selectAuthJwt(state) !== null;
|
||||||
|
|
||||||
export default tokenSlice.reducer;
|
export default tokenSlice.reducer;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import reducer, {
|
||||||
|
setToken,
|
||||||
|
removeToken,
|
||||||
|
getToken,
|
||||||
|
selectAuthJwt,
|
||||||
|
selectIsAuthenticated,
|
||||||
|
TokenState,
|
||||||
|
} from '../Token';
|
||||||
|
|
||||||
|
const asRoot = (token: TokenState) => ({ token });
|
||||||
|
|
||||||
|
describe('token slice', () => {
|
||||||
|
const session = { token: 'jwt-abc', userType: 'tender', fullName: 'A User' };
|
||||||
|
|
||||||
|
it('stores a valid session', () => {
|
||||||
|
const state = reducer(undefined, setToken(session));
|
||||||
|
expect(state.token).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes every cleared shape to the single logged-out value', () => {
|
||||||
|
// Historical 401 path dispatched setToken({}); logout dispatched ''.
|
||||||
|
expect(reducer(undefined, setToken({})).token).toBe('');
|
||||||
|
expect(reducer(undefined, setToken('')).token).toBe('');
|
||||||
|
expect(reducer({ token: session }, removeToken()).token).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectors expose the JWT only for valid sessions', () => {
|
||||||
|
expect(selectAuthJwt(asRoot({ token: session }))).toBe('jwt-abc');
|
||||||
|
expect(selectAuthJwt(asRoot({ token: '' }))).toBeNull();
|
||||||
|
expect(selectIsAuthenticated(asRoot({ token: session }))).toBe(true);
|
||||||
|
expect(selectIsAuthenticated(asRoot({ token: '' }))).toBe(false);
|
||||||
|
expect(getToken(asRoot({ token: session }))).toEqual(session);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
|
||||||
import { combineReducers } from 'redux';
|
|
||||||
import { persistStore, persistReducer } from 'redux-persist';
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
import { tokenSlice } from './Slices/Token';
|
|
||||||
import { auctionSlice } from './Slices/SelectedAuction';
|
|
||||||
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction'
|
|
||||||
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
|
|
||||||
import { tenderSlice } from './Slices/SelectedTender';
|
|
||||||
import { themeSlice } from './Slices/ThemeSlice';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
|
||||||
token: tokenSlice.reducer,
|
|
||||||
auction: auctionSlice.reducer,
|
|
||||||
selectPurchaseAuction: selectPurchaseAuctionReducer,
|
|
||||||
selectPurchaseTender: selectPurchaseTenderReducer,
|
|
||||||
tender: tenderSlice.reducer,
|
|
||||||
theme: themeSlice.reducer,
|
|
||||||
});
|
|
||||||
const persistConfig = {
|
|
||||||
key: 'root',
|
|
||||||
storage: AsyncStorage,
|
|
||||||
whitelist: ['token', 'theme'],
|
|
||||||
};
|
|
||||||
const persistReducers = persistReducer(persistConfig, rootReducer);
|
|
||||||
export const store = configureStore({
|
|
||||||
reducer: persistReducers,
|
|
||||||
middleware: getDefaultMiddleware =>
|
|
||||||
getDefaultMiddleware({
|
|
||||||
serializableCheck: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
export const persistor = persistStore(store);
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
persistStore,
|
||||||
|
persistReducer,
|
||||||
|
FLUSH,
|
||||||
|
REHYDRATE,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
} from 'redux-persist';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { removeToken, selectAuthJwt, tokenSlice } from './Slices/Token';
|
||||||
|
import { auctionSlice } from './Slices/SelectedAuction';
|
||||||
|
import selectPurchaseAuctionReducer from './Slices/selectPurchaseAuction';
|
||||||
|
import selectPurchaseTenderReducer from './Slices/SelectedPurchaseTender';
|
||||||
|
import { tenderSlice } from './Slices/SelectedTender';
|
||||||
|
import { themeSlice } from './Slices/ThemeSlice';
|
||||||
|
import { secureStorage } from '../core/storage/secureStorage';
|
||||||
|
import { setTokenProvider } from '../core/http/client';
|
||||||
|
import { appEvents } from '../core/events/appEvents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth session is persisted through the device keychain/keystore
|
||||||
|
* (hardware-backed encryption at rest) — never plaintext AsyncStorage.
|
||||||
|
* Non-sensitive prefs (theme) stay in AsyncStorage via the root persist.
|
||||||
|
*/
|
||||||
|
const tokenPersistConfig = {
|
||||||
|
key: 'token',
|
||||||
|
storage: secureStorage,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
token: persistReducer(tokenPersistConfig, tokenSlice.reducer),
|
||||||
|
auction: auctionSlice.reducer,
|
||||||
|
selectPurchaseAuction: selectPurchaseAuctionReducer,
|
||||||
|
selectPurchaseTender: selectPurchaseTenderReducer,
|
||||||
|
tender: tenderSlice.reducer,
|
||||||
|
theme: themeSlice.reducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootPersistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
storage: AsyncStorage,
|
||||||
|
whitelist: ['theme'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(rootPersistConfig, rootReducer);
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
// redux-persist dispatches non-serializable actions by design; everything
|
||||||
|
// else stays checked so genuine leaks surface in development.
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Composition root wiring: the transport layer never imports the store; it
|
||||||
|
// reads the JWT through this provider and announces auth failures on the
|
||||||
|
// event bus, which we translate into state changes here.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
setTokenProvider(() => selectAuthJwt(store.getState()));
|
||||||
|
|
||||||
|
appEvents.on('auth/unauthorized', () => {
|
||||||
|
if (selectAuthJwt(store.getState()) !== null) {
|
||||||
|
store.dispatch(removeToken());
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import type { AppDispatch, RootState } from './Store';
|
||||||
|
|
||||||
|
/** Typed replacements for the raw react-redux hooks — use these everywhere. */
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
Animated,
|
Animated,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { InsideHeader } from '../../Component/Index';
|
import { InsideHeader } from '../../components';
|
||||||
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
|
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
|
||||||
import LiveAuction from './LiveAuction';
|
import LiveAuction from './LiveAuction';
|
||||||
import PurchaseAuction from './PurchaseAuction';
|
import PurchaseAuction from './PurchaseAuction';
|
||||||
import { ShoppingCart, Lock, Radio, Package } from 'lucide-react-native';
|
import { ShoppingCart, Radio, Package } from 'lucide-react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -28,7 +28,7 @@ const Auction = () => {
|
|||||||
const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
|
const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
|
||||||
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
|
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
|
||||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||||
const reduxToken = useSelector((state: any) => state.token?.token);
|
useSelector((state: any) => state.token?.token);
|
||||||
|
|
||||||
const liveCount = Array.isArray(liveAuctionData?.result)
|
const liveCount = Array.isArray(liveAuctionData?.result)
|
||||||
? liveAuctionData?.pages?.[0]?.totalCount
|
? liveAuctionData?.pages?.[0]?.totalCount
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
|
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
|
||||||
import { RefreshCw } from 'lucide-react-native';
|
|
||||||
import LiveAuctionTile from './LiveAuctionTile';
|
import LiveAuctionTile from './LiveAuctionTile';
|
||||||
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
|
import { useFetchLiveAuction } from '../../Services/ApiCall/Hooks';
|
||||||
import { SearchInput } from '../../Component/Serach/SeacrhInput';
|
import { SearchInput } from '../../components/SearchInput';
|
||||||
import { Skelton } from '../../Component/Index';
|
import { Skeleton } from '../../components';
|
||||||
import SkeltonList from '../../Component/Skelton/SkeltonList';
|
import SkeletonList from '../../components/SkeletonList';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
@@ -20,7 +19,7 @@ import AppText from '../../components/AppText';
|
|||||||
|
|
||||||
const LiveAuction = () => {
|
const LiveAuction = () => {
|
||||||
const Dispatch = useDispatch();
|
const Dispatch = useDispatch();
|
||||||
const { colors, spacing, shadows } = useTheme();
|
const { colors, shadows } = useTheme();
|
||||||
const user = useSelector((state: any) => state.token?.token);
|
const user = useSelector((state: any) => state.token?.token);
|
||||||
const onEndReachedCalledDuringMomentum = useRef(true);
|
const onEndReachedCalledDuringMomentum = useRef(true);
|
||||||
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
|
||||||
@@ -64,6 +63,8 @@ const LiveAuction = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// Intentionally runs only on SelectedPurchaseList changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[SelectedPurchaseList],
|
[SelectedPurchaseList],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ const LiveAuction = () => {
|
|||||||
}, [data, hasNextPage, isFetchingNextPage, isPending]);
|
}, [data, hasNextPage, isFetchingNextPage, isPending]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skelton />;
|
return <Skeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -138,7 +139,7 @@ const LiveAuction = () => {
|
|||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<SkeltonList />
|
<SkeletonList />
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data?.result ?? []}
|
data={data?.result ?? []}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Trash2,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
Layers,
|
Layers,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Clock,
|
|
||||||
IndianRupee,
|
|
||||||
Wrench,
|
|
||||||
Mail,
|
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
|
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
@@ -91,6 +87,8 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// Intentionally runs only on auction number/document changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data.auctionNumber, auctionDocuments]);
|
}, [data.auctionNumber, auctionDocuments]);
|
||||||
|
|
||||||
const isAmended = auctionData?.auctionStatus === 'Amended';
|
const isAmended = auctionData?.auctionStatus === 'Amended';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { memo, useState, useMemo } from 'react';
|
import React, { memo, useState, useMemo } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
|
import { View, StyleSheet, TouchableOpacity, ScrollView, Platform, Modal } from 'react-native';
|
||||||
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
|
import { Package, IndianRupee, FileText, Download, ArrowLeft, Info, } from 'lucide-react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
|
import { useAucitonItemDetail } from '../../../Services/ApiCall/Hooks';
|
||||||
import ReactNativeBlobUtil from 'react-native-blob-util';
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
||||||
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
|
import { AUCTION_ITEM_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
|
||||||
import { Loader } from '../../../Component/Index';
|
import { Loader } from '../../../components';
|
||||||
import { useTheme } from '../../../Hooks/useTheme';
|
import { useTheme } from '../../../Hooks/useTheme';
|
||||||
import AppText from '../../../components/AppText';
|
import AppText from '../../../components/AppText';
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
|||||||
|
|
||||||
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
|
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
|
||||||
|
|
||||||
const response = await ReactNativeBlobUtil.config({
|
await ReactNativeBlobUtil.config({
|
||||||
fileCache: false,
|
fileCache: false,
|
||||||
addAndroidDownloads: {
|
addAndroidDownloads: {
|
||||||
useDownloadManager: true,
|
useDownloadManager: true,
|
||||||
@@ -63,7 +63,7 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
|
|||||||
Accept: 'application/pdf',
|
Accept: 'application/pdf',
|
||||||
});
|
});
|
||||||
|
|
||||||
const stat = await ReactNativeBlobUtil.fs.stat(path);
|
await ReactNativeBlobUtil.fs.stat(path);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log('Error:', error?.message || error);
|
console.log('Error:', error?.message || error);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { useEffect, useState, useCallback, memo, useRef } from 'react';
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, RefreshControl, ActivityIndicator } from 'react-native';
|
import { View, FlatList, RefreshControl, ActivityIndicator } from 'react-native';
|
||||||
import LiveAuctionTile from './LiveAuctionTile';
|
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { useFetchLiveAuctionPage, useFetchPurchaseAuction } from '../../Services/ApiCall/Hooks';
|
import { useFetchLiveAuctionPage } from '../../Services/ApiCall/Hooks';
|
||||||
import PurchaseAuctionTile from './PurchaseAuctionTile';
|
import PurchaseAuctionTile from './PurchaseAuctionTile';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setAuction } from '../../Redux/Slices/SelectedAuction';
|
import { setAuction } from '../../Redux/Slices/SelectedAuction';
|
||||||
import { SearchInput } from '../../Component/Serach/SeacrhInput';
|
import { SearchInput } from '../../components/SearchInput';
|
||||||
import { Skelton } from '../../Component/Index';
|
import { Skeleton } from '../../components';
|
||||||
import { allBooleanProps } from './types/types';
|
import { allBooleanProps } from './types/types';
|
||||||
import FilterModal from './Modal/FilterModal';
|
import FilterModal from './Modal/FilterModal';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
@@ -20,7 +19,7 @@ const PurchaseAuction = () => {
|
|||||||
const Dispatch = useDispatch()
|
const Dispatch = useDispatch()
|
||||||
const onEndReachedCalledDuringMomentum = useRef(true);
|
const onEndReachedCalledDuringMomentum = useRef(true);
|
||||||
const [filter, setFilter] = useState({})
|
const [filter, setFilter] = useState({})
|
||||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuctionPage(filter);
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isRefetching, refetch, isPending }: any = useFetchLiveAuctionPage(filter);
|
||||||
const [allBoolean, setAllBoolean] = useState<allBooleanProps>({ isShowFilter: false })
|
const [allBoolean, setAllBoolean] = useState<allBooleanProps>({ isShowFilter: false })
|
||||||
const format = 'DD-MM-YYYY HH:mm:ss';
|
const format = 'DD-MM-YYYY HH:mm:ss';
|
||||||
|
|
||||||
@@ -65,11 +64,11 @@ const PurchaseAuction = () => {
|
|||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
|
<SearchInput onChange={(val) => { setFilter(prev => ({ ...prev, auctionNumber: val })) }} placeholder='Search....' filterPress={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: !prev.isShowFilter })) }} filter={false} />
|
||||||
{isPending ? <Skelton />
|
{isPending ? <Skeleton />
|
||||||
:
|
:
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data.result ?? []}
|
data={data.result ?? []}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item }) => (
|
||||||
<PurchaseAuctionTile
|
<PurchaseAuctionTile
|
||||||
item={item}
|
item={item}
|
||||||
onPressDetails={() => {
|
onPressDetails={() => {
|
||||||
|
|||||||
@@ -15,17 +15,15 @@ import {
|
|||||||
Receipt,
|
Receipt,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Gavel,
|
Gavel,
|
||||||
Building2,
|
|
||||||
BadgeIndianRupee,
|
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import { InsideHeader } from '../../Component/Index';
|
import { InsideHeader } from '../../components';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
|
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/ApiCall/Hooks';
|
||||||
import { showSuccessAlert } from '../../Constant/ShowDailog';
|
import { showSuccessAlert } from '../../Constant/ShowDailog';
|
||||||
import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
import { clearPurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
|
||||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import Skeleton from '../../Component/Skelton/Skelton';
|
import Skeleton from '../../components/Skeleton';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
|
|
||||||
@@ -115,6 +113,8 @@ const PurchaseAuctionList = () => {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
getPurchaseList();
|
getPurchaseList();
|
||||||
|
// Intentionally runs only on SelectedPurchaseList changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [SelectedPurchaseList]),
|
}, [SelectedPurchaseList]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
|||||||
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
import Animated, { FadeInUp, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
import CountdownTimer from '../../components/CountdownTimer';
|
||||||
import AuctionDetail from './Modal/AuctionDetail';
|
import AuctionDetail from './Modal/AuctionDetail';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
|
|
||||||
const PurchaseAuctionTile = ({
|
const PurchaseAuctionTile = ({
|
||||||
item,
|
item,
|
||||||
onPressDetails,
|
|
||||||
onStartBidding,
|
onStartBidding,
|
||||||
buttonEnable,
|
buttonEnable,
|
||||||
isAuctionEnd,
|
isAuctionEnd,
|
||||||
@@ -38,10 +37,6 @@ const PurchaseAuctionTile = ({
|
|||||||
const biddingEnd = isAmended
|
const biddingEnd = isAmended
|
||||||
? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
|
? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
|
||||||
: item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
|
: item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
|
||||||
const biddingStart = isAmended
|
|
||||||
? item?.amendedkeyDates?.auctionBidding?.startDateTime?.replace(' kk', '')
|
|
||||||
: item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', '');
|
|
||||||
|
|
||||||
const statusConfig = useMemo(() => {
|
const statusConfig = useMemo(() => {
|
||||||
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
|
if (isAuctionEnd) return { label: 'ENDED', color: colors.error, bg: `${colors.error}15`, Icon: TimerOff };
|
||||||
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
|
if (buttonEnable) return { label: 'LIVE', color: colors.success, bg: `${colors.success}15`, Icon: CircleDot };
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
FadeInDown,
|
FadeInDown,
|
||||||
@@ -15,7 +15,7 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
|
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
|
||||||
import { InsideHeader } from '../../Component/Index';
|
import { InsideHeader } from '../../components';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -38,6 +38,8 @@ const PurchaseList = () => {
|
|||||||
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
|
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
|
||||||
Navigation.goBack();
|
Navigation.goBack();
|
||||||
}
|
}
|
||||||
|
// Intentionally runs only on SelectedPurchaseList changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [SelectedPurchaseList]);
|
}, [SelectedPurchaseList]);
|
||||||
|
|
||||||
const styles = makeStyles(colors, spacing, shadows);
|
const styles = makeStyles(colors, spacing, shadows);
|
||||||
|
|||||||
+75
-56
@@ -1,40 +1,52 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
FlatList,
|
FlatList,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Alert,
|
|
||||||
BackHandler,
|
BackHandler,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import LottieView from 'lottie-react-native';
|
import LottieView from 'lottie-react-native';
|
||||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||||
import { InsideHeader } from '../../Component/Index';
|
import { InsideHeader } from '../../components';
|
||||||
import { useWebSocketService } from '../../socket/Socket';
|
import { useWebSocketService } from '../../socket/Socket';
|
||||||
import Variables from '../../Constant/Variable';
|
import Variables from '../../Constant/Variable';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { apiCall } from '../../Axios/Axios';
|
import { apiCall } from '../../Axios/Axios';
|
||||||
import {
|
import {
|
||||||
AUCTION_ONE_TIME_BID,
|
AUCTION_ONE_TIME_BID,
|
||||||
FORWARD_AUCTION_ITEM,
|
|
||||||
GET_AUCTION_DETAILS,
|
GET_AUCTION_DETAILS,
|
||||||
REVERSE_AUCTION_ITEM,
|
|
||||||
SELECTED_AUCTION_ITEM,
|
SELECTED_AUCTION_ITEM,
|
||||||
} from '../../Services/ApiCall/ApiUrls';
|
} from '../../Services/ApiCall/ApiUrls';
|
||||||
import BiddingCard from './Component/BiddingCard';
|
import AuctionBiddingCard from './Component/AuctionBiddingCard';
|
||||||
import ConfirmBidModal from './Component/ConfirmBidModal';
|
import ConfirmBidModal from './Component/ConfirmBidModal';
|
||||||
import GoBackModal from './Component/GoBackModal';
|
import GoBackModal from './Component/GoBackModal';
|
||||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
import CountdownTimer from '../../components/CountdownTimer';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
|
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
|
||||||
|
import { showErrorAlert, showSuccessAlert } from '../../Constant/ShowDailog';
|
||||||
|
import { logger } from '../../core/observability/logger';
|
||||||
|
import { getBiddingConfig } from './biddingPattern';
|
||||||
|
|
||||||
const StartBidding = () => {
|
/**
|
||||||
|
* THE bidding panel — serves both auction patterns. The pattern comes from
|
||||||
|
* the selected auction; every forward/reverse difference lives in
|
||||||
|
* biddingPattern.ts, never in this file.
|
||||||
|
*
|
||||||
|
* Registered under both the StartBidding and StartReverseBidding routes so
|
||||||
|
* existing navigation keeps working unchanged.
|
||||||
|
*/
|
||||||
|
const AuctionBiddingScreen = () => {
|
||||||
const { colors, spacing, shadows } = useTheme();
|
const { colors, spacing, shadows } = useTheme();
|
||||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
||||||
const loginUser = useSelector((state: any) => state.token.token);
|
const loginUser = useSelector((state: any) => state.token.token);
|
||||||
const Navigation: any = useNavigation();
|
const Navigation: any = useNavigation();
|
||||||
|
const config = useMemo(
|
||||||
|
() => getBiddingConfig(SelectedAuction?.auctionPattern),
|
||||||
|
[SelectedAuction?.auctionPattern],
|
||||||
|
);
|
||||||
|
|
||||||
const [items, setItems] = useState<any>([]);
|
const [items, setItems] = useState<any>([]);
|
||||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
||||||
@@ -48,11 +60,10 @@ const StartBidding = () => {
|
|||||||
reload: false,
|
reload: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
const { subscribe, unsubscribe, disconnect, createRoom, placeBid } = useWebSocketService(
|
||||||
Variables.webSocketUrl,
|
Variables.webSocketUrl,
|
||||||
() => console.log('Connected!'),
|
() => logger.info('stomp', 'connected'),
|
||||||
(error: any) => console.log('WebSocket Error:', error),
|
(error: any) => logger.warn('stomp', `connection error: ${error}`),
|
||||||
loginUser.token,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getAuction = async () => {
|
const getAuction = async () => {
|
||||||
@@ -74,10 +85,8 @@ const StartBidding = () => {
|
|||||||
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
||||||
let auctionItem: any[] = [];
|
let auctionItem: any[] = [];
|
||||||
|
|
||||||
if (SelectedAuction.auctionPattern === 'Forward') {
|
const res = await apiCall<any>('get', config.itemsUrl, {}, payload);
|
||||||
const res = await apiCall<any>('get', FORWARD_AUCTION_ITEM, {}, payload);
|
if (res?.statusCode === 200) { auctionItem = res.data || []; }
|
||||||
if (res?.statusCode === 200) auctionItem = res.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
||||||
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
|
const uniqueArray: number[] = Array.from(new Set(selectedRes?.data || []));
|
||||||
@@ -85,18 +94,15 @@ const StartBidding = () => {
|
|||||||
|
|
||||||
if (SelectedAuction.auctionCategories === 'Live') {
|
if (SelectedAuction.auctionCategories === 'Live') {
|
||||||
const result = await createRoom(SelectedAuction.auctionNumber);
|
const result = await createRoom(SelectedAuction.auctionNumber);
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data: any) => {
|
const liveBids = new Map<number, { bidAmount: number; contractorId: number }>();
|
||||||
console.log('🔥 BID UPDATE:', data);
|
|
||||||
});
|
|
||||||
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
|
|
||||||
(result || []).forEach((r: any) => {
|
(result || []).forEach((r: any) => {
|
||||||
bidAmountMap2.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
|
liveBids.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
|
||||||
});
|
});
|
||||||
const updatedFilteredItems = filteredItems.map(item => {
|
const updatedFilteredItems = filteredItems.map(item => {
|
||||||
const bidData = bidAmountMap2.get(item.id);
|
const bidData = liveBids.get(item.id);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
bidAmount: bidData?.bidAmount ?? item.auctionStartValueFigure,
|
bidAmount: bidData?.bidAmount ?? config.startValue(item),
|
||||||
bidAvilable: bidData?.bidAmount ?? 0,
|
bidAvilable: bidData?.bidAmount ?? 0,
|
||||||
me: bidData?.contractorId === loginUser.contractorId,
|
me: bidData?.contractorId === loginUser.contractorId,
|
||||||
};
|
};
|
||||||
@@ -106,14 +112,13 @@ const StartBidding = () => {
|
|||||||
setBidValues(initialBids);
|
setBidValues(initialBids);
|
||||||
setItems(updatedFilteredItems);
|
setItems(updatedFilteredItems);
|
||||||
} else {
|
} else {
|
||||||
if (SelectedAuction.rebidAllowed) setAllBoolean(prev => ({ ...prev, rebid: true }));
|
if (SelectedAuction.rebidAllowed) { setAllBoolean(prev => ({ ...prev, rebid: true })); }
|
||||||
let result: any[] = [];
|
const result: any[] = await getOneTimeBidValue();
|
||||||
if (SelectedAuction.auctionCategories !== 'Live') result = await getOneTimeBidValue();
|
|
||||||
const bidMap = new Map<number, any>();
|
const bidMap = new Map<number, any>();
|
||||||
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
|
result.forEach((bid: any) => bidMap.set(bid.auctionItemId, bid));
|
||||||
const mergedData = auctionItem.map((item: any) => {
|
const mergedData = auctionItem.map((item: any) => {
|
||||||
const bid = bidMap.get(item.id);
|
const bid = bidMap.get(item.id);
|
||||||
if (!bid) return { ...item, bidAmount: item.auctionStartValueFigure, contractorId: item.contractorId };
|
if (!bid) { return { ...item, bidAmount: config.startValue(item), contractorId: item.contractorId }; }
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
bidAmount: bid.bidAmount,
|
bidAmount: bid.bidAmount,
|
||||||
@@ -130,34 +135,41 @@ const StartBidding = () => {
|
|||||||
setItems(mergedData);
|
setItems(mergedData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching auction items:', error);
|
logger.error('bidding', 'fetching auction items failed', { error: String(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message: any) => {
|
// Live bid updates: subscribe once per room from an effect (never the render
|
||||||
const result = message[message.length - 1];
|
// body) and tear the subscription down when the room changes or we unmount.
|
||||||
setItems((prev: any[]) =>
|
useEffect(() => {
|
||||||
prev.map(item =>
|
const destination = `/topic/room/${SelectedAuction.auctionNumber}`;
|
||||||
item.id === result.auctionItemId
|
subscribe(destination, (message: any) => {
|
||||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
|
const result = message[message.length - 1];
|
||||||
: item,
|
setItems((prev: any[]) =>
|
||||||
),
|
prev.map(item =>
|
||||||
);
|
item.id === result.auctionItemId
|
||||||
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
|
||||||
});
|
: item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setBidValues(prev => ({ ...prev, [result.auctionItemId]: result.bidAmount }));
|
||||||
|
});
|
||||||
|
return () => unsubscribe(destination);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [SelectedAuction.auctionNumber]);
|
||||||
|
|
||||||
|
// Fully close the STOMP connection on unmount — a leaked client keeps
|
||||||
|
// reconnecting every 5s and drains battery/data.
|
||||||
|
const disconnectRef = useRef(disconnect);
|
||||||
|
disconnectRef.current = disconnect;
|
||||||
|
useEffect(() => () => disconnectRef.current(), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (SelectedAuction.auctionCategories === 'One-time') getAuction();
|
if (SelectedAuction.auctionCategories === 'One-time') { getAuction(); }
|
||||||
getAllAcutionItems();
|
getAllAcutionItems();
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, () => {});
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [allBoolean.reload]);
|
}, [allBoolean.reload]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initial: Record<number, number> = {};
|
|
||||||
items.forEach((item: any) => { initial[item.id] = item.bidAmount + item.bidVariationValue; });
|
|
||||||
setBidValues(initial);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUpdate = (id: number, val: number) => {
|
const handleUpdate = (id: number, val: number) => {
|
||||||
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
||||||
};
|
};
|
||||||
@@ -177,10 +189,15 @@ const StartBidding = () => {
|
|||||||
const placeBidSubmit = async (data: any) => {
|
const placeBidSubmit = async (data: any) => {
|
||||||
setAllBoolean(prev => ({ ...prev, showDialog: false }));
|
setAllBoolean(prev => ({ ...prev, showDialog: false }));
|
||||||
try {
|
try {
|
||||||
const result: any = await placeBid(data);
|
const result = await placeBid(data);
|
||||||
if (result?.status && result.data) getAllAcutionItems();
|
if (result?.status && result.data) {
|
||||||
|
if (result.message) { showSuccessAlert(result.message); }
|
||||||
|
getAllAcutionItems();
|
||||||
|
} else if (result?.message) {
|
||||||
|
showErrorAlert(result.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error placing bid:', error);
|
logger.error('bidding', 'placing bid failed', { error: String(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +209,7 @@ const StartBidding = () => {
|
|||||||
};
|
};
|
||||||
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||||
return () => sub.remove();
|
return () => sub.remove();
|
||||||
}, [Navigation]),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = makeStyles(colors, spacing, shadows);
|
const styles = makeStyles(colors, spacing, shadows);
|
||||||
@@ -260,15 +277,16 @@ const StartBidding = () => {
|
|||||||
data={items}
|
data={items}
|
||||||
keyExtractor={item => item.id.toString()}
|
keyExtractor={item => item.id.toString()}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<BiddingCard
|
<AuctionBiddingCard
|
||||||
item={{ ...item, index }}
|
item={{ ...item, index }}
|
||||||
bidValue={bidValues[item.id] || 0}
|
bidValue={bidValues[item.id] || 0}
|
||||||
onIncrease={() => handleUpdate(item.id, item.bidVariationValue)}
|
onIncrease={() => handleUpdate(item.id, config.step(item))}
|
||||||
onDecrease={() => handleUpdate(item.id, -item.bidVariationValue)}
|
onDecrease={() => handleUpdate(item.id, -config.step(item))}
|
||||||
onPlaceBid={() => submitBid(item)}
|
onPlaceBid={() => submitBid(item)}
|
||||||
SelectedAuction={SelectedAuction}
|
SelectedAuction={SelectedAuction}
|
||||||
allBoolean={allBoolean}
|
allBoolean={allBoolean}
|
||||||
setAllBoolean={setAllBoolean}
|
setAllBoolean={setAllBoolean}
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
@@ -282,7 +300,7 @@ const StartBidding = () => {
|
|||||||
source={require('../../lottie/Success.json')}
|
source={require('../../lottie/Success.json')}
|
||||||
autoPlay
|
autoPlay
|
||||||
loop={false}
|
loop={false}
|
||||||
style={{ width: 200, height: 200 }}
|
style={styles.successLottie}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -311,7 +329,7 @@ const StartBidding = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StartBidding;
|
export default AuctionBiddingScreen;
|
||||||
|
|
||||||
/* ─── Styles factory ─── */
|
/* ─── Styles factory ─── */
|
||||||
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
||||||
@@ -399,4 +417,5 @@ const makeStyles = (colors: any, spacing: any, shadows: any) =>
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 99,
|
zIndex: 99,
|
||||||
},
|
},
|
||||||
|
successLottie: { width: 200, height: 200 },
|
||||||
});
|
});
|
||||||
+123
-73
@@ -1,25 +1,48 @@
|
|||||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput, Alert } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet, TextInput } from 'react-native';
|
||||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
||||||
import colors_const from '../../../Constant/Color';
|
|
||||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
||||||
import MainStyle from '../../../Styles/MainStyle';
|
import MainStyle from '../../../Styles/MainStyle';
|
||||||
import { useTheme } from '../../../Hooks/useTheme';
|
import { useTheme } from '../../../Hooks/useTheme';
|
||||||
import AppText from '../../../components/AppText';
|
import AppText from '../../../components/AppText';
|
||||||
import { Loader } from '../../../Component/Index';
|
import { Loader } from '../../../components';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import axios from 'axios';
|
import { env } from '../../../core/config/env';
|
||||||
import Variables from '../../../Constant/Variable';
|
import { request } from '../../../core/http/client';
|
||||||
|
import { isAppError } from '../../../core/http/AppError';
|
||||||
|
import { logger } from '../../../core/observability/logger';
|
||||||
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
||||||
|
import { BiddingPatternConfig } from '../biddingPattern';
|
||||||
|
|
||||||
const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
type Props = {
|
||||||
|
item: any;
|
||||||
|
bidValue: number;
|
||||||
|
onIncrease: () => void;
|
||||||
|
onDecrease: () => void;
|
||||||
|
onPlaceBid: () => void;
|
||||||
|
SelectedAuction: any;
|
||||||
|
allBoolean: any;
|
||||||
|
setAllBoolean: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
config: BiddingPatternConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single bidding card for BOTH auction patterns. All forward/reverse
|
||||||
|
* behavior differences come from `config` (see biddingPattern.ts) — never
|
||||||
|
* branch on the pattern inside this component.
|
||||||
|
*/
|
||||||
|
const AuctionBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean, config }: Props) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
||||||
const Token = useSelector((state: any) => state.token.token)
|
const Token = useSelector((state: any) => state.token.token);
|
||||||
const [showDetail, setShowDetail] = useState(false);
|
const [showDetail, setShowDetail] = useState(false);
|
||||||
const [value, setValue] = useState(`${item.bidAmount}`);
|
const [value, setValue] = useState(`${item.bidAmount}`);
|
||||||
const [allState, setAllState] = useState({ isLoading: false })
|
const [allState, setAllState] = useState({ isLoading: false });
|
||||||
const isMin = bidValue <= item.bidAmount;
|
|
||||||
|
const canIncrease = config.canIncrease(bidValue, item);
|
||||||
|
const canDecrease = config.canDecrease(bidValue, item);
|
||||||
|
const bodyFull = config.isBodyFull(allBoolean, item);
|
||||||
|
const submitVisible = config.showSubmit(allBoolean, item);
|
||||||
|
|
||||||
// 🔒 Persist expand state safely
|
// 🔒 Persist expand state safely
|
||||||
const expandedRef = useRef(false);
|
const expandedRef = useRef(false);
|
||||||
@@ -31,7 +54,7 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
|||||||
damping: 15,
|
damping: 15,
|
||||||
stiffness: 140,
|
stiffness: 140,
|
||||||
});
|
});
|
||||||
}, []);
|
}, [animation]);
|
||||||
|
|
||||||
const bodyStyle = useAnimatedStyle(() => ({
|
const bodyStyle = useAnimatedStyle(() => ({
|
||||||
height: interpolate(animation.value, [0, 1], [0, 190]),
|
height: interpolate(animation.value, [0, 1], [0, 190]),
|
||||||
@@ -51,22 +74,26 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
const MAX_VALUE = Number(item.bidAmount);
|
|
||||||
|
|
||||||
const handleChange = (text: string) => {
|
const handleChange = (text: string) => {
|
||||||
let cleaned = text.replace(/[^0-9.]/g, '');
|
const cleaned = text.replace(/[^0-9.]/g, '');
|
||||||
if ((cleaned.match(/\./g) || []).length > 1) return;
|
if ((cleaned.match(/\./g) || []).length > 1) { return; }
|
||||||
|
if (cleaned === '') {
|
||||||
|
setValue('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!config.acceptManualInput(Number(cleaned), item)) { return; }
|
||||||
setValue(cleaned);
|
setValue(cleaned);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const oneTimePlaceBid = async () => {
|
const oneTimePlaceBid = async () => {
|
||||||
|
const validationError = config.validateManualBid(Number(value), item);
|
||||||
|
if (validationError) {
|
||||||
|
showErrorAlert(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (Number(value) < MAX_VALUE) {
|
setAllState((prev: any) => ({ ...prev, isLoading: true }));
|
||||||
showErrorAlert("Enter the greather vlaue to current bid value")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
setAllState((prev: any) => ({ ...prev, isLoading: true }))
|
|
||||||
const payload = {
|
const payload = {
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
bidAmount: Number(value),
|
bidAmount: Number(value),
|
||||||
@@ -75,28 +102,41 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
|||||||
fullName: Token.fullName,
|
fullName: Token.fullName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const res = await request<any>({
|
||||||
|
method: 'post',
|
||||||
|
url: `${env.socketBaseUrl}bidding/placeBidForAuction`,
|
||||||
|
data: [payload],
|
||||||
|
});
|
||||||
|
|
||||||
const res = await axios.post(
|
if (res.statusCode === 200) {
|
||||||
`${Variables.socketUrl}bidding/placeBidForAuction`,
|
showSuccessAlert(res.message);
|
||||||
[payload],
|
|
||||||
{ headers: { Authentication: `Bearer ${Token.token}` } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.statusCode === 200) {
|
|
||||||
showSuccessAlert(res.data.message);
|
|
||||||
}
|
}
|
||||||
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }))
|
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (isAppError(error) && error.statusCode !== undefined) {
|
||||||
showErrorAlert(error.response?.data.message)
|
showErrorAlert(error.message);
|
||||||
} else {
|
} else {
|
||||||
console.log('UNKNOWN ERROR:', error);
|
logger.error('bidding', 'manual bid failed', { error: String(error) });
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
setAllState((prev: any) => ({ ...prev, isLoading: false }))
|
setAllState((prev: any) => ({ ...prev, isLoading: false }));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const manualInput = (allBoolean.oneTimeBid || allBoolean.rebid) && (
|
||||||
|
<View style={[styles.inputBox2, config.inlineManualInput ? styles.inputBox2Inline : styles.inputBox2Block]}>
|
||||||
|
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={value}
|
||||||
|
onChangeText={handleChange}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
placeholder="0.00"
|
||||||
|
style={[styles.input, { color: colors.text }]}
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
@@ -109,19 +149,18 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
|||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={() => { setShowDetail(true) }}
|
onPress={() => { setShowDetail(true); }}
|
||||||
style={styles.cardHeader}
|
style={styles.cardHeader}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={styles.flex1}>
|
||||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
||||||
{/* auctionStartValueFigure */}
|
|
||||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
||||||
Initial Amt : {item?.auctionStartValueFigure}
|
Initial Amt : ₹{config.startValue(item)}
|
||||||
</AppText>
|
</AppText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
||||||
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
|
<View style={[MainStyle.flexBetween, styles.gap10]}>
|
||||||
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
|
<View style={[styles.badge, item.me ? styles.meBadge : styles.liveBadge]}>
|
||||||
<AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
|
<AppText variant="caption" bold style={[styles.badgeText as any, item.me ? styles.meText : styles.liveText]}>
|
||||||
{item.me ? 'LEADING' : 'LIVE'}
|
{item.me ? 'LEADING' : 'LIVE'}
|
||||||
@@ -143,77 +182,83 @@ const BiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, Selec
|
|||||||
<View style={styles.priceRow}>
|
<View style={styles.priceRow}>
|
||||||
<View>
|
<View>
|
||||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
||||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || item.auctionStartValueFigure}</AppText>
|
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item?.bidAmount || config.startValue(item)}</AppText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ alignItems: 'flex-end' }}>
|
<View style={styles.alignEnd}>
|
||||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Increment</AppText>
|
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>{config.stepLabel}</AppText>
|
||||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
||||||
+₹{item.bidVariationValue}
|
{config.stepPrefix}₹{config.step(item)}
|
||||||
</AppText>
|
</AppText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* COLLAPSIBLE BODY */}
|
{/* COLLAPSIBLE BODY */}
|
||||||
<Animated.View style={[styles.collapsibleContainer, (!allBoolean.rebid && item.contractorId == undefined) ? bodyStyle : bodyStyle2]}>
|
<Animated.View style={[styles.collapsibleContainer, bodyFull ? bodyStyle : bodyStyle2]}>
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
|
|
||||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
||||||
|
|
||||||
{!allBoolean.oneTimeBid && <View style={styles.stepperContainer}>
|
{(!allBoolean.oneTimeBid || config.inlineManualInput) && <View style={styles.stepperContainer}>
|
||||||
<TouchableOpacity
|
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||||
onPress={onDecrease}
|
onPress={onDecrease}
|
||||||
disabled={isMin}
|
disabled={!canDecrease}
|
||||||
style={[styles.stepBtn, isMin && styles.disabledBtn]}
|
style={[styles.stepBtn, !canDecrease && styles.disabledBtn]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Decrease bid amount"
|
||||||
|
accessibilityState={{ disabled: !canDecrease }}
|
||||||
>
|
>
|
||||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>}
|
||||||
|
|
||||||
<View style={styles.inputBox}>
|
{!allBoolean.oneTimeBid && <View style={styles.inputBox} accessibilityLabel={`Current bid amount ${bidValue} rupees`}>
|
||||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
||||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
||||||
</View>
|
</View>}
|
||||||
|
|
||||||
<TouchableOpacity onPress={onIncrease} style={styles.stepBtn}>
|
{config.inlineManualInput && manualInput}
|
||||||
|
|
||||||
|
{!allBoolean.oneTimeBid && <TouchableOpacity
|
||||||
|
onPress={onIncrease}
|
||||||
|
disabled={!canIncrease}
|
||||||
|
style={[styles.stepBtn, !canIncrease && styles.disabledBtn]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Increase bid amount"
|
||||||
|
accessibilityState={{ disabled: !canIncrease }}
|
||||||
|
>
|
||||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>}
|
||||||
</View>}
|
</View>}
|
||||||
{
|
|
||||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
|
||||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
|
||||||
|
|
||||||
<TextInput
|
{!config.inlineManualInput && manualInput}
|
||||||
value={value}
|
|
||||||
onChangeText={handleChange}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
placeholder="0.00"
|
|
||||||
style={[styles.input, { color: colors.text }]}
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
{(!allBoolean.rebid && item.contractorId == undefined) ? <TouchableOpacity
|
{submitVisible ? <TouchableOpacity
|
||||||
style={[styles.submitBtn]}
|
style={[styles.submitBtn]}
|
||||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Confirm bid"
|
||||||
|
accessibilityState={{ busy: allState.isLoading }}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
allState.isLoading ? <Loader size="small" color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
||||||
}
|
}
|
||||||
</TouchableOpacity> : null}
|
</TouchableOpacity> : null}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
:
|
:
|
||||||
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false) }} visible={showDetail} />
|
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false); }} visible={showDetail} />
|
||||||
}
|
}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(BiddingCard);
|
export default memo(AuctionBiddingCard);
|
||||||
|
|
||||||
const makeStyles = (colors: any) => StyleSheet.create({
|
const makeStyles = (colors: any) => StyleSheet.create({
|
||||||
|
flex1: { flex: 1 },
|
||||||
|
gap10: { gap: 10 },
|
||||||
|
alignEnd: { alignItems: 'flex-end' },
|
||||||
inputBox2: {
|
inputBox2: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -221,8 +266,13 @@ const makeStyles = (colors: any) => StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
height: 45,
|
height: 45,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
inputBox2Block: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginBottom: 20
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
inputBox2Inline: {
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
currency2: {
|
currency2: {
|
||||||
marginRight: 4,
|
marginRight: 4,
|
||||||
@@ -231,7 +281,7 @@ const makeStyles = (colors: any) => StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
width: '100%'
|
width: '100%',
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import moment from 'moment';
|
|
||||||
import LottieView from 'lottie-react-native';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
|
|
||||||
const AuctionTimer = ({ SelectedAuction, setAllBoolean }: any) => {
|
|
||||||
const [timeLeft, setTimeLeft] = useState('00:00:00');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!SelectedAuction?.keyDates?.auctionBidding?.endDateTime) return;
|
|
||||||
|
|
||||||
const format = 'DD-MM-YYYY HH:mm:ss';
|
|
||||||
const endTime = moment(
|
|
||||||
SelectedAuction.keyDates.auctionBidding.endDateTime,
|
|
||||||
format
|
|
||||||
);
|
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
const now = moment();
|
|
||||||
const diff = moment.duration(endTime.diff(now));
|
|
||||||
|
|
||||||
if (diff.asSeconds() <= 0) {
|
|
||||||
setTimeLeft('00:00:00');
|
|
||||||
clearInterval(timer);
|
|
||||||
|
|
||||||
setAllBoolean((prev: any) => ({
|
|
||||||
...prev,
|
|
||||||
timeEnd: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = String(Math.floor(diff.asHours())).padStart(2, '0');
|
|
||||||
const minutes = String(diff.minutes()).padStart(2, '0');
|
|
||||||
const seconds = String(diff.seconds()).padStart(2, '0');
|
|
||||||
|
|
||||||
setTimeLeft(`${hours}:${minutes}:${seconds}`);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
|
||||||
}, [SelectedAuction]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LottieView
|
|
||||||
source={require('../../lottie/clock.json')}
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
style={{ width: 20, height: 20 }}
|
|
||||||
/>
|
|
||||||
<Text style={{ fontSize: 18, color: colors.black }}>{timeLeft}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AuctionTimer;
|
|
||||||
@@ -19,7 +19,7 @@ const ConfirmBidModal = ({ visible, onClose, onConfirm, bidAmount, isLoading }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.strong }]}>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
|
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose} disabled={isLoading}>
|
||||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const STEPS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
|
const EmdComplete = ({ onNext }: { onNext?: () => void }) => {
|
||||||
const { colors, spacing, shadows } = useTheme();
|
const { colors, shadows } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.root, { backgroundColor: colors.background }]}>
|
<View style={[styles.root, { backgroundColor: colors.background }]}>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const GoBackModal = ({ visible, onClose, onConfirm }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.large }]}>
|
<View style={[styles.modalCard, { backgroundColor: colors.surface, ...shadows.strong }]}>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
|
<TouchableOpacity style={[styles.closeBtn, { backgroundColor: colors.background }]} onPress={onClose}>
|
||||||
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
<X size={18} color={colors.textSecondary} strokeWidth={2.5} />
|
||||||
|
|||||||
@@ -1,358 +0,0 @@
|
|||||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, TextInput } from 'react-native';
|
|
||||||
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
||||||
import colors_const from '../../../Constant/Color';
|
|
||||||
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
|
|
||||||
import MainStyle from '../../../Styles/MainStyle';
|
|
||||||
import { useTheme } from '../../../Hooks/useTheme';
|
|
||||||
import AppText from '../../../components/AppText';
|
|
||||||
import axios, { all } from 'axios';
|
|
||||||
import Variables from '../../../Constant/Variable';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { showErrorAlert, showSuccessAlert } from '../../../Constant/ShowDailog';
|
|
||||||
import { Loader } from '../../../Component/Index';
|
|
||||||
|
|
||||||
const ReverseBiddingCard = ({ item, bidValue, onIncrease, onDecrease, onPlaceBid, SelectedAuction, allBoolean, setAllBoolean }: any) => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
|
||||||
const Token = useSelector((state: any) => state.token.token)
|
|
||||||
const [allState, setAllState] = useState({ isLoading: false })
|
|
||||||
const [showDetail, setShowDetail] = useState(false);
|
|
||||||
const [value, setValue] = useState(`${item.bidAmount}`);
|
|
||||||
const canDecrease = bidValue > item.emdAmount;
|
|
||||||
const canIncrease = bidValue < item.bidAmount;
|
|
||||||
// Animation Part Rohit Start
|
|
||||||
|
|
||||||
const expandedRef = useRef(false);
|
|
||||||
const animation = useSharedValue(0);
|
|
||||||
|
|
||||||
const toggleExpand = useCallback(() => {
|
|
||||||
expandedRef.current = !expandedRef.current;
|
|
||||||
animation.value = withSpring(expandedRef.current ? 1 : 0, {
|
|
||||||
damping: 15,
|
|
||||||
stiffness: 140,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const bodyStyle = useAnimatedStyle(() => ({
|
|
||||||
height: interpolate(animation.value, [0, 1], [0, 190]),
|
|
||||||
opacity: interpolate(animation.value, [0, 0.5, 1], [0, 0, 1]),
|
|
||||||
marginTop: interpolate(animation.value, [0, 1], [0, 15]),
|
|
||||||
}));
|
|
||||||
const bodyStyle2 = useAnimatedStyle(() => ({
|
|
||||||
height: interpolate(animation.value, [0, 1], [0, 100]),
|
|
||||||
opacity: interpolate(animation.value, [0, 0.5, 1], [0, 0, 1]),
|
|
||||||
marginTop: interpolate(animation.value, [0, 1], [0, 15]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const arrowStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
rotate: `${interpolate(animation.value, [0, 1], [0, 180])}deg`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Animation Part Rohit End
|
|
||||||
|
|
||||||
const MAX_VALUE = Number(item.auctionStartValue);
|
|
||||||
|
|
||||||
const handleChange = (text: string) => {
|
|
||||||
let cleaned = text.replace(/[^0-9.]/g, '');
|
|
||||||
if ((cleaned.match(/\./g) || []).length > 1) return;
|
|
||||||
if (cleaned === '') {
|
|
||||||
setValue('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const numericValue = Number(cleaned);
|
|
||||||
if (numericValue < 0) return;
|
|
||||||
if (numericValue > MAX_VALUE) return;
|
|
||||||
setValue(cleaned);
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneTimePlaceBid = async () => {
|
|
||||||
try {
|
|
||||||
setAllState((prev: any) => ({ ...prev, isLoading: true }))
|
|
||||||
const payload = {
|
|
||||||
itemId: item.id,
|
|
||||||
bidAmount: Number(value),
|
|
||||||
auctionId: SelectedAuction.auctionId,
|
|
||||||
contractorId: Token.contractorId,
|
|
||||||
fullName: Token.fullName,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const res = await axios.post(
|
|
||||||
`${Variables.socketUrl}bidding/placeBidForAuction`,
|
|
||||||
[payload],
|
|
||||||
{ headers: { Authentication: `Bearer ${Token.token}` } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data.statusCode === 200) {
|
|
||||||
showSuccessAlert(res.data.message);
|
|
||||||
}
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }))
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
showErrorAlert(error.response?.data.message)
|
|
||||||
} else {
|
|
||||||
console.log('UNKNOWN ERROR:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
setAllState((prev: any) => ({ ...prev, isLoading: false }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{!showDetail ? (
|
|
||||||
<Animated.View
|
|
||||||
layout={Layout.springify()}
|
|
||||||
entering={FadeInUp.delay(100)}
|
|
||||||
style={[
|
|
||||||
styles.card,
|
|
||||||
item.me ? styles.leadingCard : undefined,
|
|
||||||
{
|
|
||||||
borderColor: item.me ? colors.success : colors.error,
|
|
||||||
backgroundColor: item.me ? `${colors.success}10` : `${colors.error}10`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* HEADER */}
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={() => setShowDetail(true)}
|
|
||||||
style={styles.cardHeader}
|
|
||||||
>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<AppText variant="h4" color={colors.text} bold style={styles.productName as any}>{item.productName}</AppText>
|
|
||||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
|
||||||
Initial Amt : ₹{item.auctionStartValue}
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
|
|
||||||
<View style={[MainStyle.flexBetween, { gap: 10 }]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.badge,
|
|
||||||
item.me ? styles.meBadge : styles.liveBadge,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<AppText
|
|
||||||
variant="caption"
|
|
||||||
bold
|
|
||||||
style={[
|
|
||||||
styles.badgeText as any,
|
|
||||||
item.me ? styles.meText : styles.liveText,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{item.me ? 'LEADING' : 'LIVE'}
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Animated.Text style={[styles.arrow, { color: colors.textSecondary }, arrowStyle]}>
|
|
||||||
▼
|
|
||||||
</Animated.Text>
|
|
||||||
</View>
|
|
||||||
<AppText variant="caption" color={colors.textSecondary} style={styles.itemIdText as any}>
|
|
||||||
{SelectedAuction?.auctionNumber}
|
|
||||||
</AppText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* PRICE INFO */}
|
|
||||||
<View style={styles.priceRow}>
|
|
||||||
<View>
|
|
||||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Current Bid</AppText>
|
|
||||||
<AppText variant="h2" color={colors.text} bold style={styles.mainPrice as any}>₹{item.bidAmount || item.auctionStartValue}</AppText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ alignItems: 'flex-end' }}>
|
|
||||||
<AppText variant="caption" color={colors.textSecondary} semiBold style={styles.label as any}>Decrement</AppText>
|
|
||||||
<AppText variant="body" color={colors.primary} bold style={styles.incrementText as any}>
|
|
||||||
-₹{item.emdAmount}
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* COLLAPSIBLE BODY */}
|
|
||||||
{/* <Animated.View style={[styles.collapsibleContainer, (allBoolean.oneTimeBid && allBoolean.rebid) ? bodyStyle : (allBoolean.oneTimeBid && !item.me) ? bodyStyle : bodyStyle2]}> */}
|
|
||||||
<Animated.View style={[styles.collapsibleContainer, bodyStyle]}>
|
|
||||||
<View style={styles.divider} />
|
|
||||||
|
|
||||||
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
|
|
||||||
|
|
||||||
<View style={styles.stepperContainer}>
|
|
||||||
{/* DECREASE */}
|
|
||||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
|
||||||
onPress={onDecrease}
|
|
||||||
disabled={!canDecrease}
|
|
||||||
style={[
|
|
||||||
styles.stepBtn,
|
|
||||||
!canDecrease && styles.disabledBtn,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>−</AppText>
|
|
||||||
</TouchableOpacity>}
|
|
||||||
|
|
||||||
{/* VALUE */}
|
|
||||||
{!allBoolean.oneTimeBid && <View style={styles.inputBox}>
|
|
||||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency as any}>₹</AppText>
|
|
||||||
<AppText variant="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
|
|
||||||
</View>}
|
|
||||||
{
|
|
||||||
(allBoolean.oneTimeBid || allBoolean.rebid) && <View style={styles.inputBox2}>
|
|
||||||
<AppText variant="body" color={colors.textSecondary} bold style={styles.currency2 as any}>₹</AppText>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
value={value}
|
|
||||||
onChangeText={handleChange}
|
|
||||||
keyboardType="decimal-pad"
|
|
||||||
placeholder="0.00"
|
|
||||||
style={[styles.input, { color: colors.text }]}
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* INCREASE */}
|
|
||||||
{!allBoolean.oneTimeBid && <TouchableOpacity
|
|
||||||
onPress={onIncrease}
|
|
||||||
disabled={!canIncrease}
|
|
||||||
style={[
|
|
||||||
styles.stepBtn,
|
|
||||||
!canIncrease && styles.disabledBtn,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<AppText variant="h3" color="#FFF" bold style={styles.stepText as any}>+</AppText>
|
|
||||||
</TouchableOpacity>}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.submitBtn]}
|
|
||||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
allState.isLoading ? <Loader size='small' color="#FFF" /> : <AppText variant="button" color="#FFF" bold style={styles.submitBtnText as any}>Confirm Bid</AppText>
|
|
||||||
}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* {(allBoolean.oneTimeBid && allBoolean.rebid) ?
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.submitBtn]}
|
|
||||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
|
||||||
}
|
|
||||||
</TouchableOpacity> :
|
|
||||||
(allBoolean.oneTimeBid && !item.me) ? <TouchableOpacity
|
|
||||||
style={[styles.submitBtn]}
|
|
||||||
onPress={(allBoolean.oneTimeBid || allBoolean.rebid) ? oneTimePlaceBid : onPlaceBid}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
allState.isLoading ? <Loader size='small' color={colors.white} /> : <Text style={styles.submitBtnText}>Confirm Bid</Text>
|
|
||||||
}
|
|
||||||
</TouchableOpacity> : null
|
|
||||||
} */}
|
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
|
||||||
) : (
|
|
||||||
<AuctionItemDetail
|
|
||||||
data={item}
|
|
||||||
onClose={() => setShowDetail(false)}
|
|
||||||
visible={showDetail}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReverseBiddingCard;
|
|
||||||
|
|
||||||
const makeStyles = (colors: any) => StyleSheet.create({
|
|
||||||
inputBox2: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
borderRadius: 12,
|
|
||||||
height: 45,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
currency2: {
|
|
||||||
marginRight: 4,
|
|
||||||
} as any,
|
|
||||||
input: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '700',
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 20,
|
|
||||||
marginBottom: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
leadingCard: {
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
cardHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
},
|
|
||||||
headerRight: { flexDirection: 'column', alignItems: 'center', gap: 8 },
|
|
||||||
productName: {} as any,
|
|
||||||
itemIdText: { marginTop: 2 } as any,
|
|
||||||
badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8 },
|
|
||||||
liveBadge: { backgroundColor: `${colors.error}20` },
|
|
||||||
meBadge: { backgroundColor: `${colors.success}20` },
|
|
||||||
badgeText: { fontSize: 10 } as any,
|
|
||||||
liveText: { color: colors.error },
|
|
||||||
meText: { color: colors.success },
|
|
||||||
arrow: { fontSize: 10 },
|
|
||||||
priceRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 20 },
|
|
||||||
label: {
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
} as any,
|
|
||||||
mainPrice: {} as any,
|
|
||||||
incrementText: {} as any,
|
|
||||||
collapsibleContainer: { overflow: 'hidden' },
|
|
||||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: 10 },
|
|
||||||
bidLabel: { marginBottom: 10 } as any,
|
|
||||||
stepperContainer: { flexDirection: 'row', gap: 12, marginBottom: 20 },
|
|
||||||
stepBtn: {
|
|
||||||
width: 45,
|
|
||||||
height: 45,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
borderRadius: 12,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
disabledBtn: { backgroundColor: colors.border },
|
|
||||||
stepText: {} as any,
|
|
||||||
inputBox: {
|
|
||||||
flex: 1,
|
|
||||||
height: 45,
|
|
||||||
backgroundColor: colors.background,
|
|
||||||
borderRadius: 12,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
currency: { marginRight: 4 } as any,
|
|
||||||
bidValueText: {} as any,
|
|
||||||
submitBtn: {
|
|
||||||
height: 50,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
borderRadius: 15,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
submitBtnText: {} as any,
|
|
||||||
});
|
|
||||||
@@ -7,7 +7,7 @@ import { useSelector } from 'react-redux'
|
|||||||
import { apiCall } from '../../../Axios/Axios'
|
import { apiCall } from '../../../Axios/Axios'
|
||||||
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
|
import { TECHNICAL_DOCUMENT_SUBMIT, TECHNICAL_VALUE_SUBMIT } from '../../../Services/ApiCall/ApiUrls'
|
||||||
import { Props } from '../Type/BidType'
|
import { Props } from '../Type/BidType'
|
||||||
import { Loader } from '../../../Component/Index'
|
import { Loader } from '../../../components'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import NavigationString from '../../../Constant/NavigationString'
|
import NavigationString from '../../../Constant/NavigationString'
|
||||||
import { useTheme } from '../../../Hooks/useTheme'
|
import { useTheme } from '../../../Hooks/useTheme'
|
||||||
@@ -15,13 +15,16 @@ import AppText from '../../../components/AppText'
|
|||||||
import { Upload, FileText, X } from 'lucide-react-native'
|
import { Upload, FileText, X } from 'lucide-react-native'
|
||||||
|
|
||||||
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
|
const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }) => {
|
||||||
if (!template) return null
|
|
||||||
|
|
||||||
const SelectedAuction = useSelector((state: any) => state.auction.auction)
|
const SelectedAuction = useSelector((state: any) => state.auction.auction)
|
||||||
const Navigation: any = useNavigation()
|
const Navigation: any = useNavigation()
|
||||||
const { colors, spacing, typography } = useTheme()
|
const { colors, spacing, typography } = useTheme()
|
||||||
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
|
const styles = useMemo(() => makeStyles(colors, spacing, typography), [colors, spacing, typography])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// Hooks must all run before this guard — a conditional return above any
|
||||||
|
// hook changes the hook order between renders and crashes React.
|
||||||
|
if (!template) return null
|
||||||
|
|
||||||
const initialValues = template?.labels?.reduce((acc: any, label: any) => {
|
const initialValues = template?.labels?.reduce((acc: any, label: any) => {
|
||||||
acc[label.labelName] = ''
|
acc[label.labelName] = ''
|
||||||
return acc
|
return acc
|
||||||
@@ -254,7 +257,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values, errors, touched, setFieldValue }) => (
|
{({ handleSubmit, values, setFieldValue }) => (
|
||||||
<>
|
<>
|
||||||
{template.labels.map((label: any) =>
|
{template.labels.map((label: any) =>
|
||||||
renderInput(
|
renderInput(
|
||||||
@@ -298,7 +301,7 @@ const TemplateFormModal: React.FC<Props> = ({ visible, template, setAllBoolean }
|
|||||||
|
|
||||||
export default TemplateFormModal
|
export default TemplateFormModal
|
||||||
|
|
||||||
const makeStyles = (colors: any, spacing: any, typography: any) =>
|
const makeStyles = (colors: any, spacing: any, _typography: any) =>
|
||||||
StyleSheet.create({
|
StyleSheet.create({
|
||||||
screen: {
|
screen: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { pick } from '@react-native-documents/picker';
|
import { pick } from '@react-native-documents/picker';
|
||||||
import Animated, { FadeInDown, FadeOutUp, Layout } from 'react-native-reanimated';
|
import Animated, { FadeInDown, FadeOutUp, Layout } from 'react-native-reanimated';
|
||||||
import BouncyCheckbox from 'react-native-bouncy-checkbox';
|
import BouncyCheckbox from 'react-native-bouncy-checkbox';
|
||||||
import { InsideHeader, Skelton } from '../../Component/Index';
|
import { InsideHeader, Skeleton } from '../../components';
|
||||||
import EmdComplete from './Component/EmdComplete';
|
import EmdComplete from './Component/EmdComplete';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
@@ -34,12 +34,9 @@ import AppText from '../../components/AppText';
|
|||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Upload,
|
Upload,
|
||||||
CheckCircle2,
|
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
ChevronRight,
|
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
BadgeIndianRupee,
|
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
|
|
||||||
const Emd = () => {
|
const Emd = () => {
|
||||||
@@ -350,6 +347,9 @@ const Emd = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Run once on mount; AlreadyEmdCheck is redeclared each render, so listing
|
||||||
|
// it as a dependency would re-fire the check on every render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
useEffect(() => { AlreadyEmdCheck(); }, []);
|
useEffect(() => { AlreadyEmdCheck(); }, []);
|
||||||
|
|
||||||
const styles = makeStyles(colors, spacing, shadows, typography);
|
const styles = makeStyles(colors, spacing, shadows, typography);
|
||||||
@@ -410,7 +410,7 @@ const Emd = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(isPending || isInitialLoading) ? (
|
{(isPending || isInitialLoading) ? (
|
||||||
<Skelton />
|
<Skeleton />
|
||||||
) : allBoolean.isEmdComplete ? (
|
) : allBoolean.isEmdComplete ? (
|
||||||
<EmdComplete onNext={EmdContinueHandle} />
|
<EmdComplete onNext={EmdContinueHandle} />
|
||||||
) : allBoolean.emdType ? (
|
) : allBoolean.emdType ? (
|
||||||
|
|||||||
@@ -1,349 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { View, FlatList, StyleSheet, Platform, Alert, BackHandler, Text, ToastAndroid } from 'react-native';
|
|
||||||
import LottieView from 'lottie-react-native';
|
|
||||||
import { InsideHeader } from '../../Component/Index';
|
|
||||||
import colors from '../../Constant/Color';
|
|
||||||
import { useWebSocketService } from '../../socket/Socket';
|
|
||||||
import Variables from '../../Constant/Variable';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { apiCall } from '../../Axios/Axios';
|
|
||||||
import { AUCTION_ONE_TIME_BID, GET_AUCTION_DETAILS, REVERSE_AUCTION_ITEM, SELECTED_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
|
|
||||||
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
|
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
|
||||||
import CountdownTimer from '../../Component/Counter/CountdownTimer';
|
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
|
||||||
import AppText from '../../components/AppText';
|
|
||||||
import ReverseBiddingCard from './Component/ReverseBiddingCard';
|
|
||||||
import ConfirmBidModal from './Component/ConfirmBidModal';
|
|
||||||
import GoBackModal from './Component/GoBackModal';
|
|
||||||
|
|
||||||
const StartReverseBidding = () => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
const styles = React.useMemo(() => makeStyles(colors), [colors]);
|
|
||||||
const SelectedAuction = useSelector((state: any) => state.auction.auction);
|
|
||||||
const loginUser = useSelector((state: any) => state.token.token)
|
|
||||||
const Navigation: any = useNavigation()
|
|
||||||
const [items, setItems] = useState<any>([]);
|
|
||||||
const [bidValues, setBidValues] = useState<Record<number, number>>({});
|
|
||||||
const [allBoolean, setAllBoolean] = useState({ showSuccess: false, showDialog: false, showBackDialog: false, bidPayload: {} as any, oneTimeBid: false, rebid: false, reload: false })
|
|
||||||
const { subscribe, createRoom, placeBid } = useWebSocketService(
|
|
||||||
Variables.webSocketUrl,
|
|
||||||
() => console.log('Connected!'),
|
|
||||||
(error) => console.log('WebSocket Error:', error),
|
|
||||||
loginUser.token,
|
|
||||||
);
|
|
||||||
const getAuction = async () => {
|
|
||||||
const selectedRes = await apiCall<any>('get', GET_AUCTION_DETAILS, {}, { id: SelectedAuction.auctionId });
|
|
||||||
if (selectedRes.statusCode == 200 && selectedRes.data.contractorId.length) {
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOneTimeBidValue = async () => {
|
|
||||||
try {
|
|
||||||
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
|
|
||||||
if (selectedRes.statusCode == 200) {
|
|
||||||
return selectedRes.data
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllAcutionItems = async () => {
|
|
||||||
try {
|
|
||||||
const payload = { auctionNumber: SelectedAuction.auctionNumber };
|
|
||||||
let auctionItem: any[] = [];
|
|
||||||
|
|
||||||
if (SelectedAuction.auctionPattern === 'Reverse') {
|
|
||||||
const res = await apiCall<any>('get', REVERSE_AUCTION_ITEM, {}, payload);
|
|
||||||
if (res?.statusCode === 200) auctionItem = res.data || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedRes = await apiCall<any>('get', SELECTED_AUCTION_ITEM, {}, payload);
|
|
||||||
|
|
||||||
const uniqueArray: number[] = Array.from(
|
|
||||||
new Set(selectedRes?.data || [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredItems = auctionItem.filter(item =>
|
|
||||||
uniqueArray.includes(item.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (SelectedAuction.auctionCategories == 'Live') {
|
|
||||||
const result = await createRoom(SelectedAuction.auctionNumber);
|
|
||||||
|
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, data => {
|
|
||||||
console.log('🔥 BID UPDATE:', data);
|
|
||||||
});
|
|
||||||
const bidAmountMap2 = new Map<number, { bidAmount: number; contractorId: number }>();
|
|
||||||
|
|
||||||
(result || []).forEach((r: any) => {
|
|
||||||
bidAmountMap2.set(r.auctionItemId, {
|
|
||||||
bidAmount: r.bidAmount,
|
|
||||||
contractorId: r.contractorId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Check One Time bid enable butoon
|
|
||||||
if (SelectedAuction.auctionCategories != 'Live') {
|
|
||||||
const myBid = result.find(
|
|
||||||
(bid: any) => bid.contractorId === loginUser.contractorId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (myBid) {
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, oneTimeBid: true }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bidAmountMap = new Map<number, number>();
|
|
||||||
|
|
||||||
(result || []).forEach((r: any) => {
|
|
||||||
bidAmountMap.set(r.auctionItemId, r.bidAmount);
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedFilteredItems = filteredItems.map(item => {
|
|
||||||
const bidData = bidAmountMap2.get(item.id);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
bidAmount: bidData?.bidAmount ?? item.auctionStartValue,
|
|
||||||
bidAvilable: bidData?.bidAmount ?? 0,
|
|
||||||
me: bidData?.contractorId == loginUser.contractorId ? true : false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialBids: Record<number, number> = {};
|
|
||||||
updatedFilteredItems.forEach(item => {
|
|
||||||
initialBids[item.id] = item.bidAmount;
|
|
||||||
});
|
|
||||||
setBidValues(initialBids);
|
|
||||||
setItems(updatedFilteredItems);
|
|
||||||
} else {
|
|
||||||
let result = []
|
|
||||||
if (SelectedAuction.rebidAllowed) {
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, rebid: true }))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SelectedAuction.auctionCategories != 'Live') {
|
|
||||||
result = await getOneTimeBidValue()
|
|
||||||
}
|
|
||||||
const bidMap = new Map<number, any>();
|
|
||||||
|
|
||||||
result.forEach((bid: any) => {
|
|
||||||
bidMap.set(bid.auctionItemId, bid);
|
|
||||||
});
|
|
||||||
const mergedData = auctionItem.map((item: any) => {
|
|
||||||
const bid = bidMap.get(item.id);
|
|
||||||
|
|
||||||
if (!bid) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
bidAmount: item.auctionStartValue,
|
|
||||||
contractorId: item.contractorId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
bidAmount: bid.bidAmount,
|
|
||||||
bidId: bid.bidId,
|
|
||||||
contractorId: bid.contractorId,
|
|
||||||
contractorName: bid.contractorName,
|
|
||||||
me: bid.contractorId === loginUser.contractorId,
|
|
||||||
bidTime: bid.bidTime,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const initialBids: Record<number, number> = {};
|
|
||||||
mergedData.forEach(item => {
|
|
||||||
initialBids[item.id] = item.bidAmount;
|
|
||||||
});
|
|
||||||
setBidValues(initialBids);
|
|
||||||
setItems(mergedData)
|
|
||||||
setBidValues(initialBids);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching auction items:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, async (message) => {
|
|
||||||
|
|
||||||
const result = message[message.length - 1]
|
|
||||||
|
|
||||||
const updatedBidItem = (prev: any[]) =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.id === result.auctionItemId
|
|
||||||
? { ...item, bidAmount: result.bidAmount, me: result.contractorId == loginUser.contractorId ? true : false }
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
|
|
||||||
setItems(updatedBidItem);
|
|
||||||
|
|
||||||
setBidValues(prev => ({
|
|
||||||
...prev,
|
|
||||||
[result.auctionItemId]: result.bidAmount,
|
|
||||||
}));
|
|
||||||
try {
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.log('JSON parse error:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (SelectedAuction.auctionCategories == 'One-time') {
|
|
||||||
getAuction()
|
|
||||||
}
|
|
||||||
getAllAcutionItems();
|
|
||||||
subscribe(`/topic/room/${SelectedAuction.auctionNumber}`, (data) => { });
|
|
||||||
}, [allBoolean.reload]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initial: Record<number, number> = {};
|
|
||||||
items.forEach((item: any) => {
|
|
||||||
initial[item.id] = item.bidAmount + item.bidVariationValue;
|
|
||||||
});
|
|
||||||
setBidValues(initial);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUpdate = (id: number, val: number) => {
|
|
||||||
setBidValues(prev => ({ ...prev, [id]: prev[id] + val }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitBid = async (item: any) => {
|
|
||||||
const payload = {
|
|
||||||
itemId: item.id,
|
|
||||||
bidAmount: bidValues[item.id],
|
|
||||||
auctionId: SelectedAuction.auctionId,
|
|
||||||
contractorId: loginUser.contractorId,
|
|
||||||
fullName: loginUser.fullName,
|
|
||||||
roomId: SelectedAuction.auctionNumber,
|
|
||||||
};
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, bidPayload: payload, showDialog: true }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const placeBidSubmit = async (data: any) => {
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, showDialog: false }));
|
|
||||||
try {
|
|
||||||
const result: any = await placeBid(data);
|
|
||||||
|
|
||||||
if (result?.status && result.data) {
|
|
||||||
const updatedBidItem = (prev: any[]) =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.id === result.data.auctionItemId
|
|
||||||
? { ...item, bidAmount: result.data.bidAmount, me: result.data?.contractorId == loginUser.contractorId ? true : false }
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
|
|
||||||
getAllAcutionItems()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error placing bid:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
const onBackPress = () => {
|
|
||||||
setAllBoolean((prev: any) => ({ ...prev, showBackDialog: true }));
|
|
||||||
return true; // block default back action
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscription = BackHandler.addEventListener(
|
|
||||||
'hardwareBackPress',
|
|
||||||
onBackPress
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => subscription.remove();
|
|
||||||
}, [Navigation])
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<InsideHeader title="Bidding Panel" showArrow />
|
|
||||||
<View style={styles.container2}>
|
|
||||||
<AppText variant="h3" color={colors.text} bold style={styles.title as any}>
|
|
||||||
{SelectedAuction.deptName}
|
|
||||||
</AppText>
|
|
||||||
|
|
||||||
<View style={styles.timerWrapper}>
|
|
||||||
<CountdownTimer
|
|
||||||
endDate={SelectedAuction.keyDates.auctionBidding.endDateTime}
|
|
||||||
myStyle={{ fontSize: 30 }}
|
|
||||||
size={30}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<FlatList
|
|
||||||
data={items}
|
|
||||||
keyExtractor={item => item.id.toString()}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<ReverseBiddingCard
|
|
||||||
item={{ ...item, index }}
|
|
||||||
bidValue={bidValues[item.id] || 0}
|
|
||||||
onIncrease={() => handleUpdate(item.id, item.emdAmount)}
|
|
||||||
onDecrease={() => handleUpdate(item.id, -item.emdAmount)}
|
|
||||||
onPlaceBid={() => { submitBid(item) }}
|
|
||||||
SelectedAuction={SelectedAuction}
|
|
||||||
allBoolean={allBoolean}
|
|
||||||
setAllBoolean={setAllBoolean}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
contentContainerStyle={{ padding: 16 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{allBoolean.showSuccess && (
|
|
||||||
<View style={styles.overlay}>
|
|
||||||
<LottieView
|
|
||||||
source={require('../../lottie/Success.json')}
|
|
||||||
autoPlay
|
|
||||||
loop={false}
|
|
||||||
style={{ width: 200, height: 200 }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ConfirmBidModal
|
|
||||||
visible={allBoolean.showDialog}
|
|
||||||
onClose={() => setAllBoolean(prev => ({ ...prev, showDialog: false }))}
|
|
||||||
onConfirm={() => placeBidSubmit(allBoolean.bidPayload)}
|
|
||||||
bidAmount={allBoolean.bidPayload?.bidAmount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GoBackModal
|
|
||||||
visible={allBoolean.showBackDialog}
|
|
||||||
onClose={() => setAllBoolean(prev => ({ ...prev, showBackDialog: false }))}
|
|
||||||
onConfirm={() => {
|
|
||||||
setAllBoolean(prev => ({ ...prev, showBackDialog: false }));
|
|
||||||
Navigation.dispatch(
|
|
||||||
CommonActions.reset({
|
|
||||||
index: 0,
|
|
||||||
routes: [{ name: NavigationString.Home }],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StartReverseBidding;
|
|
||||||
|
|
||||||
const makeStyles = (colors: any) => StyleSheet.create({
|
|
||||||
container: { flex: 1, backgroundColor: colors.background },
|
|
||||||
container2: {
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingBottom: 8,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
marginBottom: 6,
|
|
||||||
} as any,
|
|
||||||
timerWrapper: {
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: colors.overlay, justifyContent: 'center', alignItems: 'center', zIndex: 99 },
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { getBiddingConfig } from '../biddingPattern';
|
||||||
|
import { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM } from '../../../Services/ApiCall/ApiUrls';
|
||||||
|
|
||||||
|
describe('getBiddingConfig', () => {
|
||||||
|
it('returns the reverse config only for exactly "Reverse"', () => {
|
||||||
|
expect(getBiddingConfig('Reverse').pattern).toBe('Reverse');
|
||||||
|
// Forward is the safe default for anything else (Forward, undefined, junk).
|
||||||
|
expect(getBiddingConfig('Forward').pattern).toBe('Forward');
|
||||||
|
expect(getBiddingConfig(undefined).pattern).toBe('Forward');
|
||||||
|
expect(getBiddingConfig('anything').pattern).toBe('Forward');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forward pattern', () => {
|
||||||
|
const cfg = getBiddingConfig('Forward');
|
||||||
|
const item = { auctionStartValueFigure: 100, bidVariationValue: 10, bidAmount: 150 };
|
||||||
|
|
||||||
|
it('reads the forward start-value and step fields', () => {
|
||||||
|
expect(cfg.itemsUrl).toBe(FORWARD_AUCTION_ITEM);
|
||||||
|
expect(cfg.startValue(item)).toBe(100);
|
||||||
|
expect(cfg.step(item)).toBe(10);
|
||||||
|
expect(cfg.stepLabel).toBe('Increment');
|
||||||
|
expect(cfg.stepPrefix).toBe('+');
|
||||||
|
expect(cfg.inlineManualInput).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows unbounded increases but blocks decreasing below the current bid', () => {
|
||||||
|
expect(cfg.canIncrease(999999, item)).toBe(true);
|
||||||
|
expect(cfg.canDecrease(160, item)).toBe(true); // 160 > 150
|
||||||
|
expect(cfg.canDecrease(150, item)).toBe(false); // not strictly greater
|
||||||
|
expect(cfg.canDecrease(140, item)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a manual bid below the current bid', () => {
|
||||||
|
expect(cfg.validateManualBid(149, item)).toBe('Enter a value greater than the current bid value');
|
||||||
|
expect(cfg.validateManualBid(150, item)).toBeNull();
|
||||||
|
expect(cfg.validateManualBid(200, item)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts any typed input (forward is unbounded)', () => {
|
||||||
|
expect(cfg.acceptManualInput(5, item)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows body/submit only for a fresh (un-bid) item when not rebidding', () => {
|
||||||
|
const fresh = { contractorId: undefined };
|
||||||
|
const alreadyBid = { contractorId: 7 };
|
||||||
|
expect(cfg.isBodyFull({ oneTimeBid: false, rebid: false }, fresh)).toBe(true);
|
||||||
|
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, fresh)).toBe(true);
|
||||||
|
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, alreadyBid)).toBe(false);
|
||||||
|
expect(cfg.showSubmit({ oneTimeBid: false, rebid: true }, fresh)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reverse pattern', () => {
|
||||||
|
const cfg = getBiddingConfig('Reverse');
|
||||||
|
const item = { auctionStartValue: 1000, emdAmount: 50, bidAmount: 800 };
|
||||||
|
|
||||||
|
it('reads the reverse start-value and step fields', () => {
|
||||||
|
expect(cfg.itemsUrl).toBe(REVERSE_AUCTION_ITEM);
|
||||||
|
expect(cfg.startValue(item)).toBe(1000);
|
||||||
|
expect(cfg.step(item)).toBe(50);
|
||||||
|
expect(cfg.stepLabel).toBe('Decrement');
|
||||||
|
expect(cfg.stepPrefix).toBe('-');
|
||||||
|
expect(cfg.inlineManualInput).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caps increases at the current bid and decreases at the EMD floor', () => {
|
||||||
|
expect(cfg.canIncrease(700, item)).toBe(true); // 700 < 800
|
||||||
|
expect(cfg.canIncrease(800, item)).toBe(false);
|
||||||
|
expect(cfg.canDecrease(51, item)).toBe(true); // 51 > 50
|
||||||
|
expect(cfg.canDecrease(50, item)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts typed input only within [0, auctionStartValue]', () => {
|
||||||
|
expect(cfg.acceptManualInput(0, item)).toBe(true);
|
||||||
|
expect(cfg.acceptManualInput(1000, item)).toBe(true);
|
||||||
|
expect(cfg.acceptManualInput(1001, item)).toBe(false);
|
||||||
|
expect(cfg.acceptManualInput(-1, item)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always shows body + submit (reverse has no fresh-item gate)', () => {
|
||||||
|
expect(cfg.isBodyFull({ oneTimeBid: true, rebid: true }, item)).toBe(true);
|
||||||
|
expect(cfg.showSubmit({ oneTimeBid: false, rebid: false }, item)).toBe(true);
|
||||||
|
expect(cfg.validateManualBid(123, item)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Pattern configuration for the unified auction bidding screen/card.
|
||||||
|
*
|
||||||
|
* Forward and Reverse bidding were 90%-duplicated screens that drifted apart.
|
||||||
|
* Every behavioral difference between them is captured HERE, explicitly, so
|
||||||
|
* the screen and card contain zero `if (pattern === ...)` logic and a bidding
|
||||||
|
* fix can never again land in one pattern but not the other.
|
||||||
|
*/
|
||||||
|
import { FORWARD_AUCTION_ITEM, REVERSE_AUCTION_ITEM } from '../../Services/ApiCall/ApiUrls';
|
||||||
|
|
||||||
|
export type AuctionPattern = 'Forward' | 'Reverse';
|
||||||
|
|
||||||
|
export type BiddingFlags = {
|
||||||
|
oneTimeBid: boolean;
|
||||||
|
rebid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BiddingPatternConfig = {
|
||||||
|
pattern: AuctionPattern;
|
||||||
|
/** Endpoint returning this pattern's auction items. */
|
||||||
|
itemsUrl: string;
|
||||||
|
/** Opening amount — the backend uses a different field per pattern. */
|
||||||
|
startValue: (item: any) => number;
|
||||||
|
/** Stepper step size. */
|
||||||
|
step: (item: any) => number;
|
||||||
|
stepLabel: 'Increment' | 'Decrement';
|
||||||
|
stepPrefix: '+' | '-';
|
||||||
|
/** Forward bids may rise without bound; reverse bids may not exceed the current bid. */
|
||||||
|
canIncrease: (bidValue: number, item: any) => boolean;
|
||||||
|
/** Forward bids may not drop below the current bid; reverse not below the EMD amount. */
|
||||||
|
canDecrease: (bidValue: number, item: any) => boolean;
|
||||||
|
/** Gate applied while typing a manual (one-time/rebid) amount. */
|
||||||
|
acceptManualInput: (value: number, item: any) => boolean;
|
||||||
|
/** Submit-time validation for manual bids; returns an error message or null. */
|
||||||
|
validateManualBid: (value: number, item: any) => string | null;
|
||||||
|
/** Reverse renders the manual input inline within the stepper row. */
|
||||||
|
inlineManualInput: boolean;
|
||||||
|
/** Whether the collapsible body opens at full height. */
|
||||||
|
isBodyFull: (flags: BiddingFlags, item: any) => boolean;
|
||||||
|
/** Whether the Confirm Bid button is rendered. */
|
||||||
|
showSubmit: (flags: BiddingFlags, item: any) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const forward: BiddingPatternConfig = {
|
||||||
|
pattern: 'Forward',
|
||||||
|
itemsUrl: FORWARD_AUCTION_ITEM,
|
||||||
|
startValue: item => item?.auctionStartValueFigure,
|
||||||
|
step: item => item?.bidVariationValue,
|
||||||
|
stepLabel: 'Increment',
|
||||||
|
stepPrefix: '+',
|
||||||
|
canIncrease: () => true,
|
||||||
|
canDecrease: (bidValue, item) => bidValue > item.bidAmount,
|
||||||
|
acceptManualInput: () => true,
|
||||||
|
validateManualBid: (value, item) =>
|
||||||
|
value < Number(item.bidAmount) ? 'Enter a value greater than the current bid value' : null,
|
||||||
|
inlineManualInput: false,
|
||||||
|
isBodyFull: (flags, item) => !flags.rebid && item.contractorId == undefined,
|
||||||
|
showSubmit: (flags, item) => !flags.rebid && item.contractorId == undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reverse: BiddingPatternConfig = {
|
||||||
|
pattern: 'Reverse',
|
||||||
|
itemsUrl: REVERSE_AUCTION_ITEM,
|
||||||
|
startValue: item => item?.auctionStartValue,
|
||||||
|
step: item => item?.emdAmount,
|
||||||
|
stepLabel: 'Decrement',
|
||||||
|
stepPrefix: '-',
|
||||||
|
canIncrease: (bidValue, item) => bidValue < item.bidAmount,
|
||||||
|
canDecrease: (bidValue, item) => bidValue > item.emdAmount,
|
||||||
|
acceptManualInput: (value, item) => value >= 0 && value <= Number(item?.auctionStartValue),
|
||||||
|
validateManualBid: () => null,
|
||||||
|
inlineManualInput: true,
|
||||||
|
isBodyFull: () => true,
|
||||||
|
showSubmit: () => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBiddingConfig = (pattern: string | undefined): BiddingPatternConfig =>
|
||||||
|
pattern === 'Reverse' ? reverse : forward;
|
||||||
@@ -24,16 +24,20 @@ const Home = () => {
|
|||||||
const userData = useSelector(getToken);
|
const userData = useSelector(getToken);
|
||||||
|
|
||||||
// Hooks for data
|
// Hooks for data
|
||||||
const { data: dashboardData, refetch: refetchDashboard, isPending: isLoadingDashboard } = useDashboard();
|
const { data: dashboardData, refetch: refetchDashboard, isPending: isLoadingDashboard, isFetching: isFetchingDashboard } = useDashboard();
|
||||||
const { data: tenderData, refetch: refetchTender, isPending: isLoadingTender } = useTenderDashboard();
|
const { data: tenderData, refetch: refetchTender, isPending: isLoadingTender, isFetching: isFetchingTender } = useTenderDashboard();
|
||||||
|
|
||||||
|
// Reload the dashboard data every time the Home tab gains focus.
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
refetchDashboard();
|
refetchDashboard();
|
||||||
refetchTender();
|
refetchTender();
|
||||||
}, [])
|
}, [refetchDashboard, refetchTender])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// True while a background refresh (on tab focus) is in flight and data is already shown.
|
||||||
|
const isRefreshing = (isFetchingDashboard || isFetchingTender) && !(isLoadingDashboard || isLoadingTender);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dashboardData) {
|
if (dashboardData) {
|
||||||
setAuctionStats(dashboardData.data || {});
|
setAuctionStats(dashboardData.data || {});
|
||||||
@@ -84,6 +88,9 @@ const Home = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
|
{isRefreshing && (
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} style={{ marginRight: 12 }} />
|
||||||
|
)}
|
||||||
<TouchableOpacity onPress={() => setIsMenuVisible(true)}>
|
<TouchableOpacity onPress={() => setIsMenuVisible(true)}>
|
||||||
<Menu size={24} color={colors.text} />
|
<Menu size={24} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|||||||
import { WebView } from "react-native-webview";
|
import { WebView } from "react-native-webview";
|
||||||
import { CommonActions } from "@react-navigation/native";
|
import { CommonActions } from "@react-navigation/native";
|
||||||
import colors from "../../Constant/Color";
|
import colors from "../../Constant/Color";
|
||||||
import { BASE_URL } from "../../Services/ApiCall/ApiUrls";
|
|
||||||
|
|
||||||
const SUCCESS_DELAY_MS = 5000; // 5 seconds
|
const SUCCESS_DELAY_MS = 5000; // 5 seconds
|
||||||
|
|
||||||
@@ -25,6 +24,8 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (pollTimerRef.current) {
|
if (pollTimerRef.current) {
|
||||||
|
// Intentionally reads the ref at cleanup; the timer id stored on the ref is exactly what must be cleared.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
clearInterval(pollTimerRef.current);
|
clearInterval(pollTimerRef.current);
|
||||||
}
|
}
|
||||||
if (successTimeoutRef.current) {
|
if (successTimeoutRef.current) {
|
||||||
@@ -66,16 +67,16 @@ export const KycWebViewScreen = ({ route, navigation }: any) => {
|
|||||||
}, SUCCESS_DELAY_MS);
|
}, SUCCESS_DELAY_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadStart = (event: any) => {
|
const handleLoadStart = (_event: any) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setLoadError(false);
|
setLoadError(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadEnd = (event: any) => {
|
const handleLoadEnd = (_event: any) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (event: any) => {
|
const handleError = (_event: any) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setLoadError(true);
|
setLoadError(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
|
|||||||
+20
-134
@@ -1,25 +1,25 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity, Dimensions, StatusBar, TextInput, Image, ActivityIndicator, Keyboard } from 'react-native';
|
import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity, Dimensions, TextInput, Image, ActivityIndicator, Keyboard } from 'react-native';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useNavigation, CommonActions } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react-native';
|
import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react-native';
|
||||||
import Animated, { FadeInDown, FadeInUp, FadeInRight, FadeOutLeft, useSharedValue, useAnimatedStyle, withRepeat, withTiming, Easing, interpolate, interpolateColor } from 'react-native-reanimated';
|
import Animated, { FadeInDown, FadeInUp, FadeInRight, FadeOutLeft, useSharedValue, useAnimatedStyle, withTiming, interpolate, interpolateColor } from 'react-native-reanimated';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import { BlurView } from '@react-native-community/blur';
|
import { BlurView } from '@react-native-community/blur';
|
||||||
|
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import { useCreate } from '../../Hooks/useCreate';
|
|
||||||
import { setToken } from '../../Redux/Slices/Token';
|
import { setToken } from '../../Redux/Slices/Token';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import ImagePath from '../../Constant/ImagePath';
|
import ImagePath from '../../Constant/ImagePath';
|
||||||
import { TENDER_LOGIN } from '../../Services/ApiCall/ApiUrls';
|
|
||||||
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
|
import { showErrorAlert, showToast } from '../../Constant/ShowDailog';
|
||||||
import AppBackground from '../../components/AppBackground';
|
import AppBackground from '../../components/AppBackground';
|
||||||
import FontsFamily from '../../Constant/FontsFamily';
|
import FontsFamily from '../../Constant/FontsFamily';
|
||||||
import { useRagistrationState } from '../../Services/ApiCall/Hooks';
|
import { useLogin, useRagistrationState } from '../../Services/ApiCall/Hooks';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import { performLogout } from '../../Services/Auth/session';
|
||||||
|
import { applyRouteDecision, decideRegistrationRoute } from '../../Services/Auth/registrationRouting';
|
||||||
|
import { logger } from '../../core/observability/logger';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ const CAROUSEL_TEXTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { colors, spacing, shadows, isDarkMode } = useTheme();
|
const { colors, isDarkMode } = useTheme();
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<any>();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -65,155 +65,39 @@ const Login = () => {
|
|||||||
bgTransition.value = withTiming(1, { duration: 1200 });
|
bgTransition.value = withTiming(1, { duration: 1200 });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Animation Shared Values for Background Blobs
|
|
||||||
const blob1Pos = useSharedValue(0);
|
|
||||||
const blob2Pos = useSharedValue(0);
|
|
||||||
const blob3Pos = useSharedValue(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Animate Blobs
|
|
||||||
blob1Pos.value = withRepeat(
|
|
||||||
withTiming(1, { duration: 8000, easing: Easing.inOut(Easing.sin) }),
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
blob2Pos.value = withRepeat(
|
|
||||||
withTiming(1, { duration: 10000, easing: Easing.inOut(Easing.quad) }),
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
blob3Pos.value = withRepeat(
|
|
||||||
withTiming(1, { duration: 12000, easing: Easing.inOut(Easing.cubic) }),
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const blob1Style = useAnimatedStyle(() => ({
|
|
||||||
transform: [
|
|
||||||
{ translateX: interpolate(blob1Pos.value, [0, 1], [0, 50]) },
|
|
||||||
{ translateY: interpolate(blob1Pos.value, [0, 1], [0, 30]) },
|
|
||||||
{ scale: interpolate(blob1Pos.value, [0, 1], [1, 1.2]) },
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const blob2Style = useAnimatedStyle(() => ({
|
|
||||||
transform: [
|
|
||||||
{ translateX: interpolate(blob2Pos.value, [0, 1], [0, -40]) },
|
|
||||||
{ translateY: interpolate(blob2Pos.value, [0, 1], [0, -50]) },
|
|
||||||
{ scale: interpolate(blob2Pos.value, [0, 1], [1, 1.1]) },
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const blob3Style = useAnimatedStyle(() => ({
|
|
||||||
transform: [
|
|
||||||
{ translateX: interpolate(blob3Pos.value, [0, 1], [0, 30]) },
|
|
||||||
{ translateY: interpolate(blob3Pos.value, [0, 1], [0, 40]) },
|
|
||||||
{ scale: interpolate(blob3Pos.value, [0, 1], [1, 1.3]) },
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const animatedHeaderStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ translateY: headerTranslateY.value }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const animatedFormStyle = useAnimatedStyle(() => ({
|
const animatedFormStyle = useAnimatedStyle(() => ({
|
||||||
opacity: formOpacity.value,
|
opacity: formOpacity.value,
|
||||||
transform: [{ translateY: interpolate(formOpacity.value, [0, 1], [20, 0]) }],
|
transform: [{ translateY: interpolate(formOpacity.value, [0, 1], [20, 0]) }],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
// Primary brand color
|
|
||||||
const PRIMARY = '#e8a44c';
|
|
||||||
const ACCENT_1 = '#fbbf24'; // Amber-400
|
|
||||||
const ACCENT_2 = '#f59e0b'; // Amber-500
|
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
const loginMutation = useCreate<any, any>(TENDER_LOGIN, {
|
const loginMutation = useLogin();
|
||||||
onSuccess: (res) => handleSuccess(res),
|
|
||||||
onError: (err) => showErrorAlert(err.message || 'Login failed')
|
|
||||||
});
|
|
||||||
const routeByOrgType = useCallback((params: { otherId: any; orgType: any }) => {
|
|
||||||
const orgType = String(params.orgType ?? '');
|
|
||||||
if (orgType === '4' || orgType === '5' || orgType === '6') {
|
|
||||||
navigation.replace(NavigationString.StageThreeScreen, params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orgType === '7') {
|
|
||||||
navigation.replace(NavigationString.Partnership, params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigation.replace('Dashboard');
|
|
||||||
}, [navigation]);
|
|
||||||
|
|
||||||
const handleGetData = useCallback(async () => {
|
const handleGetData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await registrationStateMutate();
|
const res = await registrationStateMutate();
|
||||||
console.log("splash", res);
|
const decision = decideRegistrationRoute(res);
|
||||||
if (res?.status === false) {
|
if (decision) {
|
||||||
navigation.reset({
|
applyRouteDecision(navigation, decision);
|
||||||
index: 0,
|
|
||||||
routes: [{ name: NavigationString.Login }],
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res?.data?.message === 'No Response From Server' && res?.data?.success === false) {
|
logger.warn('auth', 'unexpected registration-state response', { statusCode: res?.statusCode });
|
||||||
navigation.reset({
|
|
||||||
index: 0,
|
|
||||||
routes: [{ name: NavigationString.Login }],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (res?.statusCode === 404 || res?.statusCode === 401) {
|
|
||||||
navigation.replace('Registration');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res?.statusCode === 200 && res?.data) {
|
|
||||||
const { registrationStatus, contOtherId, contOrganization } = res.data;
|
|
||||||
const params = { otherId: contOtherId, orgType: contOrganization };
|
|
||||||
switch (registrationStatus) {
|
|
||||||
case null:
|
|
||||||
case undefined:
|
|
||||||
navigation.replace(NavigationString.Registration);
|
|
||||||
break;
|
|
||||||
case 'Pending':
|
|
||||||
routeByOrgType(params);
|
|
||||||
break;
|
|
||||||
case 'Complete':
|
|
||||||
case 'Completed':
|
|
||||||
navigation.replace(NavigationString.Home);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
navigation.replace(NavigationString.Login);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error('API error:', res);
|
|
||||||
showToast(res?.message || 'Something went wrong');
|
showToast(res?.message || 'Something went wrong');
|
||||||
navigation.replace('Registration');
|
navigation.replace('Registration');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Registration status error:', error);
|
logger.error('auth', 'registration status resolution failed', { error: String(error) });
|
||||||
showToast(error?.message || 'Something went wrong');
|
showToast(error?.message || 'Something went wrong');
|
||||||
|
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
navigation.replace('Registration');
|
navigation.replace('Registration');
|
||||||
} else {
|
} else {
|
||||||
await AsyncStorage.removeItem('persist:root');
|
performLogout();
|
||||||
navigation.replace(NavigationString.Login);
|
navigation.replace(NavigationString.Login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [navigation, registrationStateMutate, routeByOrgType]);
|
}, [navigation, registrationStateMutate]);
|
||||||
const handleSuccess = (res: any) => {
|
const handleSuccess = (res: any) => {
|
||||||
dispatch(setToken({ ...res.data, token: res.token, userType: 'tender' }));
|
dispatch(setToken({ ...res.data, token: res.token, userType: 'tender' }));
|
||||||
console.log('handleSuccess', res)
|
|
||||||
console.log('==================== 🔑 AUTH TOKEN ====================');
|
|
||||||
console.log('TOKEN →', res?.token);
|
|
||||||
console.log('=======================================================');
|
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
handleGetData()
|
handleGetData()
|
||||||
} else {
|
} else {
|
||||||
@@ -225,8 +109,10 @@ const Login = () => {
|
|||||||
if (isLoading) return; // guard against double submit
|
if (isLoading) return; // guard against double submit
|
||||||
Keyboard.dismiss(); // smooth the navigation transition (no keyboard mid-replace)
|
Keyboard.dismiss(); // smooth the navigation transition (no keyboard mid-replace)
|
||||||
const payload = { emailAdd: values.email, password: values.password, device: "mobileApp" };
|
const payload = { emailAdd: values.email, password: values.password, device: "mobileApp" };
|
||||||
console.log('payload------------', payload)
|
loginMutation.mutate(payload, {
|
||||||
loginMutation.mutate(payload);
|
onSuccess: res => handleSuccess(res),
|
||||||
|
onError: (err: any) => showErrorAlert(err?.message || 'Login failed'),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Covers the WHOLE sign-in flow: login request + registration-state resolve
|
// Covers the WHOLE sign-in flow: login request + registration-state resolve
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import axios from 'axios';
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Eye, EyeOff } from 'lucide-react-native';
|
import { Eye, EyeOff } from 'lucide-react-native';
|
||||||
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
|
import { BASE_URL } from '../../Services/ApiCall/ApiUrls';
|
||||||
|
import { request } from '../../core/http/client';
|
||||||
import colors from '../../Constant/Color';
|
import colors from '../../Constant/Color';
|
||||||
|
|
||||||
interface ResetPasswordProps {
|
interface ResetPasswordProps {
|
||||||
@@ -26,7 +26,7 @@ interface ResetPasswordProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
||||||
const [email, setEmail] = useState<string>(route?.params?.email || '');
|
const [email] = useState<string>(route?.params?.email || '');
|
||||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||||
const [newPassword, setNewPassword] = useState<string>('');
|
const [newPassword, setNewPassword] = useState<string>('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||||
@@ -45,7 +45,7 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
const otpInputs = useRef<Array<TextInput | null>>([]);
|
const otpInputs = useRef<Array<TextInput | null>>([]);
|
||||||
const newPasswordRef = useRef<TextInput>(null);
|
const newPasswordRef = useRef<TextInput>(null);
|
||||||
const confirmPasswordRef = useRef<TextInput>(null);
|
const confirmPasswordRef = useRef<TextInput>(null);
|
||||||
const resendTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const resendTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkResendCooldown();
|
checkResendCooldown();
|
||||||
@@ -107,6 +107,8 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
if (fullOtp.length === 6 && !isOtpVerified) {
|
if (fullOtp.length === 6 && !isOtpVerified) {
|
||||||
handleSubmitOtp();
|
handleSubmitOtp();
|
||||||
}
|
}
|
||||||
|
// Intentionally runs only on otp changes; other deps are stable or recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [otp]);
|
}, [otp]);
|
||||||
|
|
||||||
const handleOtpChange = (value: string, index: number) => {
|
const handleOtpChange = (value: string, index: number) => {
|
||||||
@@ -145,20 +147,13 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
setOtpError('');
|
setOtpError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await request<any>({
|
||||||
`${BASE_URL}eproc/verify-otp?email=${encodeURIComponent(email)}&otp=${fullOtp}`,
|
method: 'post',
|
||||||
null,
|
url: `${BASE_URL}eproc/verify-otp`,
|
||||||
{
|
params: { email, otp: fullOtp },
|
||||||
headers: {
|
});
|
||||||
'accept': '*/*',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('API Response OTP verification:', res.data);
|
if (res?.statusCode === 200) {
|
||||||
|
|
||||||
if (res.data?.statusCode === 200) {
|
|
||||||
ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT);
|
ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT);
|
||||||
setIsOtpVerified(true);
|
setIsOtpVerified(true);
|
||||||
|
|
||||||
@@ -166,16 +161,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
newPasswordRef.current?.focus();
|
newPasswordRef.current?.focus();
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.data?.message || 'OTP verification failed');
|
throw new Error(res?.message || 'OTP verification failed');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Verification Error:', error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'OTP verification failed';
|
||||||
const errorMessage = axios.isAxiosError(error)
|
|
||||||
? error.response?.data?.message || error.message
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'OTP verification failed';
|
|
||||||
|
|
||||||
setOtpError(errorMessage);
|
setOtpError(errorMessage);
|
||||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||||
@@ -216,20 +206,21 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
// TODO(backend): credentials belong in the request body, not the
|
||||||
`${BASE_URL}eproc/reset-password?email=${encodeURIComponent(email)}&otp=${otp.join('')}&newPassword=${encodeURIComponent(newPassword)}&confirmPassword=${encodeURIComponent(confirmPassword)}`,
|
// query string (they land in server/proxy logs) — needs an
|
||||||
null,
|
// endpoint contract change.
|
||||||
{
|
const res = await request<any>({
|
||||||
headers: {
|
method: 'post',
|
||||||
'accept': '*/*',
|
url: `${BASE_URL}eproc/reset-password`,
|
||||||
'Content-Type': 'application/json'
|
params: {
|
||||||
}
|
email,
|
||||||
}
|
otp: otp.join(''),
|
||||||
);
|
newPassword,
|
||||||
|
confirmPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
console.log('API Response password reset:', res.data);
|
if (res?.statusCode === 200) {
|
||||||
|
|
||||||
if (res.data?.statusCode === 200) {
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Success',
|
'Success',
|
||||||
'Your password has been reset successfully.',
|
'Your password has been reset successfully.',
|
||||||
@@ -240,14 +231,11 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.data?.message || 'Password reset failed');
|
throw new Error(res?.message || 'Password reset failed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = axios.isAxiosError(err)
|
const errorMessage =
|
||||||
? err.response?.data?.message || err.message
|
err instanceof Error ? err.message : 'Failed to reset password. Please try again.';
|
||||||
: err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: 'Failed to reset password. Please try again.';
|
|
||||||
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||||
@@ -262,31 +250,24 @@ const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
|||||||
setResendLoading(true);
|
setResendLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await request<any>({
|
||||||
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
|
method: 'post',
|
||||||
null,
|
url: `${BASE_URL}eproc/send-otp-forgot-password`,
|
||||||
{
|
params: { email },
|
||||||
headers: {
|
});
|
||||||
'accept': '*/*',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.data?.statusCode === 200) {
|
if (res?.statusCode === 200) {
|
||||||
ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT);
|
ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT);
|
||||||
setOtp(['', '', '', '', '', '']);
|
setOtp(['', '', '', '', '', '']);
|
||||||
setIsOtpVerified(false);
|
setIsOtpVerified(false);
|
||||||
setOtpError('');
|
setOtpError('');
|
||||||
startResendCooldown();
|
startResendCooldown();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.data?.message || 'Failed to resend OTP');
|
throw new Error(res?.message || 'Failed to resend OTP');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = axios.isAxiosError(error)
|
const errorMessage =
|
||||||
? error.response?.data?.message || error.message
|
error instanceof Error ? error.message : 'Failed to resend OTP';
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'Failed to resend OTP';
|
|
||||||
|
|
||||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Modal,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import { Save, X } from 'lucide-react-native';
|
||||||
|
import { useUpdateUserProfile } from '../../Services/ApiCall/Hooks';
|
||||||
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
|
import AppText from '../../components/AppText';
|
||||||
|
import { ContractorData, UpdateProfilePayload } from './profileTypes';
|
||||||
|
|
||||||
|
interface EditProfileModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
data: ContractorData;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Personal-information edit form; owns its validation and update mutation. */
|
||||||
|
const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
data,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||||
|
const [formData, setFormData] = useState<UpdateProfilePayload>({
|
||||||
|
firstName: data.firstName || '',
|
||||||
|
middleName: data.middleName || '',
|
||||||
|
lastName: data.lastName || '',
|
||||||
|
mobileNo: data.mobileNo || '',
|
||||||
|
emailId: data.emailId || '',
|
||||||
|
fullname: data.fullName || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Partial<UpdateProfilePayload>>({});
|
||||||
|
|
||||||
|
const updateProfileMutation = useUpdateUserProfile();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setFormData({
|
||||||
|
firstName: data.firstName || '',
|
||||||
|
middleName: data.middleName || '',
|
||||||
|
lastName: data.lastName || '',
|
||||||
|
mobileNo: data.mobileNo || '',
|
||||||
|
emailId: data.emailId || '',
|
||||||
|
fullname: data.fullName || '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
}
|
||||||
|
}, [visible, data]);
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Partial<UpdateProfilePayload> = {};
|
||||||
|
|
||||||
|
if (!formData.firstName.trim()) {
|
||||||
|
newErrors.firstName = 'First name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.lastName.trim()) {
|
||||||
|
newErrors.lastName = 'Last name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.emailId.trim()) {
|
||||||
|
newErrors.emailId = 'Email is required';
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailId)) {
|
||||||
|
newErrors.emailId = 'Invalid email format';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.mobileNo.trim()) {
|
||||||
|
newErrors.mobileNo = 'Mobile number is required';
|
||||||
|
} else if (!/^\d{10}$/.test(formData.mobileNo.replace(/\s/g, ''))) {
|
||||||
|
newErrors.mobileNo = 'Invalid mobile number';
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedFullName = `${formData.firstName} ${formData.middleName} ${formData.lastName}`.trim();
|
||||||
|
setFormData(prev => ({ ...prev, fullname: generatedFullName }));
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateProfileMutation.mutateAsync(formData);
|
||||||
|
Alert.alert('Success', 'Profile updated successfully');
|
||||||
|
onSuccess();
|
||||||
|
onClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert('Error', error?.message || 'Failed to update profile');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.modalOverlay}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
|
<View style={[styles.modalContent, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<AppText variant="h3" style={styles.modalTitle}>Edit Personal Information</AppText>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
|
<X size={24} color={colors.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.firstName && styles.inputError]}
|
||||||
|
value={formData.firstName}
|
||||||
|
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
|
||||||
|
placeholder="Enter first name"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
{errors.firstName && (
|
||||||
|
<AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={formData.middleName}
|
||||||
|
onChangeText={(text) => setFormData({ ...formData, middleName: text })}
|
||||||
|
placeholder="Enter middle name"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.lastName && styles.inputError]}
|
||||||
|
value={formData.lastName}
|
||||||
|
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
|
||||||
|
placeholder="Enter last name"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
{errors.lastName && (
|
||||||
|
<AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.emailId && styles.inputError]}
|
||||||
|
value={formData.emailId}
|
||||||
|
onChangeText={(text) => setFormData({ ...formData, emailId: text })}
|
||||||
|
placeholder="Enter email"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
{errors.emailId && (
|
||||||
|
<AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputGroup}>
|
||||||
|
<AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, errors.mobileNo && styles.inputError]}
|
||||||
|
value={formData.mobileNo}
|
||||||
|
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
|
||||||
|
placeholder="Enter mobile number"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
keyboardType="phone-pad"
|
||||||
|
maxLength={10}
|
||||||
|
/>
|
||||||
|
{errors.mobileNo && (
|
||||||
|
<AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<View style={styles.modalFooter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.cancelButton]}
|
||||||
|
onPress={onClose}
|
||||||
|
disabled={updateProfileMutation.isPending}
|
||||||
|
>
|
||||||
|
<AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.saveButton]}
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={updateProfileMutation.isPending}
|
||||||
|
>
|
||||||
|
{updateProfileMutation.isPending ? (
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save size={18} color={colors.buttonText} />
|
||||||
|
<AppText variant="button" style={styles.saveButtonText}>Save</AppText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStyles = (colors: any) => StyleSheet.create({
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
maxHeight: '90%',
|
||||||
|
paddingTop: 20,
|
||||||
|
borderRadius: 30,
|
||||||
|
},
|
||||||
|
modalHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
formContainer: {
|
||||||
|
padding: 20,
|
||||||
|
maxHeight: 500,
|
||||||
|
},
|
||||||
|
inputGroup: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 14,
|
||||||
|
fontSize: 15,
|
||||||
|
color: colors.text,
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
},
|
||||||
|
inputError: {
|
||||||
|
borderColor: colors.error,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.error,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
modalFooter: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 20,
|
||||||
|
gap: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
saveButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.buttonText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EditProfileModal;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -6,19 +6,10 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
|
||||||
Platform,
|
Platform,
|
||||||
ViewStyle,
|
|
||||||
TextStyle,
|
|
||||||
Modal,
|
|
||||||
TextInput,
|
|
||||||
KeyboardAvoidingView,
|
|
||||||
Alert,
|
Alert,
|
||||||
StatusBar,
|
|
||||||
Image,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||||
import Animated, { FadeInDown, FadeInUp, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
@@ -45,70 +36,22 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
List,
|
List,
|
||||||
Factory,
|
Factory,
|
||||||
CheckCircle2,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
Edit,
|
Edit,
|
||||||
X,
|
|
||||||
Save,
|
|
||||||
ChevronRight,
|
|
||||||
LogOut
|
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
import { useContractorProfile, useUpdateUserProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
|
import { useContractorProfile, useESetuAdharVarification } from '../../Services/ApiCall/Hooks';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
|
import ConfirmDialog from '../../Constant/Dialog/ConfirmDialog';
|
||||||
import { generateTransactionId } from '../../Helper/uniqueId';
|
import { generateTransactionId } from '../../Helper/uniqueId';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
import ImagePath from '../../Constant/ImagePath';
|
import EditProfileModal from './EditProfileModal';
|
||||||
|
import { ContractorData, hasSectionData, isValidValue } from './profileTypes';
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
// Type Definitions
|
|
||||||
interface ContractorData {
|
|
||||||
contractorId: number;
|
|
||||||
firstName: string;
|
|
||||||
middleName: string;
|
|
||||||
lastName: string;
|
|
||||||
fullName: string;
|
|
||||||
emailId: string;
|
|
||||||
mobileNo: string;
|
|
||||||
company: string;
|
|
||||||
contractorType: string;
|
|
||||||
dialCode: string;
|
|
||||||
twoFactorAuthEnabled: boolean;
|
|
||||||
colorCodeHSL: string | null;
|
|
||||||
isActive: string;
|
|
||||||
isEnabled: boolean | null;
|
|
||||||
role: string;
|
|
||||||
contractorStatus: boolean;
|
|
||||||
designation: string | null;
|
|
||||||
contOtherId: number;
|
|
||||||
contCompanyType: string | null;
|
|
||||||
contOrganization: string;
|
|
||||||
agentAddress: string;
|
|
||||||
agentPincode: string;
|
|
||||||
agentCity: string;
|
|
||||||
agentState: string | null;
|
|
||||||
agentWebsite: string;
|
|
||||||
agentRegno: string;
|
|
||||||
orgPAN: string;
|
|
||||||
orgPANdob: string;
|
|
||||||
orgPANname: string;
|
|
||||||
gistnNo: string;
|
|
||||||
comAadharNo: string;
|
|
||||||
contractorClass: string;
|
|
||||||
socialCategory: string;
|
|
||||||
wizardStepStatus: string;
|
|
||||||
registrationStatus: string;
|
|
||||||
indianOrgName: string;
|
|
||||||
expiryDate: string;
|
|
||||||
msmeStatus: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProfileHeaderProps {
|
interface ProfileHeaderProps {
|
||||||
data: ContractorData;
|
data: ContractorData;
|
||||||
@@ -144,37 +87,12 @@ interface StatItem {
|
|||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateProfilePayload {
|
|
||||||
firstName: string;
|
|
||||||
middleName: string;
|
|
||||||
lastName: string;
|
|
||||||
mobileNo: string;
|
|
||||||
emailId: string;
|
|
||||||
fullname: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if value is valid
|
|
||||||
const isValidValue = (value: any): boolean => {
|
|
||||||
if (value === null || value === undefined) return false;
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed.length > 0 && trimmed.toLowerCase() !== 'n/a';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to check if section has any valid data
|
|
||||||
const hasSectionData = (values: any[]): boolean => {
|
|
||||||
return values.some(value => isValidValue(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main Component
|
// Main Component
|
||||||
const ProfileTab: React.FC = () => {
|
const ProfileTab: React.FC = () => {
|
||||||
const { colors, spacing, shadows, isDarkMode } = useTheme();
|
const { colors } = useTheme();
|
||||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
const styles = useMemo(() => createStyles(colors), [colors]);
|
||||||
const { data, refetch, isLoading, error } = useContractorProfile();
|
const { data, refetch, isLoading, error } = useContractorProfile();
|
||||||
const user = useSelector((state: any) => state.token?.token);
|
const user = useSelector((state: any) => state.token?.token);
|
||||||
console.log("User: ", user)
|
|
||||||
const Navigation: any = useNavigation();
|
const Navigation: any = useNavigation();
|
||||||
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
|
const { mutateAsync: eSetuAdharVarification, isPending } = useESetuAdharVarification();
|
||||||
|
|
||||||
@@ -190,9 +108,12 @@ const ProfileTab: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useFocusEffect(() => {
|
// useFocusEffect requires a memoized callback, or the effect re-runs on every render.
|
||||||
setIsNavigating(false)
|
useFocusEffect(
|
||||||
})
|
React.useCallback(() => {
|
||||||
|
setIsNavigating(false);
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
const handleProceedVerification = async () => {
|
const handleProceedVerification = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -383,216 +304,6 @@ const ProfileTab: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Edit Profile Modal Component
|
|
||||||
interface EditProfileModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
data: ContractorData;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditProfileModal: React.FC<EditProfileModalProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
data,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const { colors } = useTheme();
|
|
||||||
const styles = useMemo(() => createStyles(colors), [colors]);
|
|
||||||
const [formData, setFormData] = useState<UpdateProfilePayload>({
|
|
||||||
firstName: data.firstName || '',
|
|
||||||
middleName: data.middleName || '',
|
|
||||||
lastName: data.lastName || '',
|
|
||||||
mobileNo: data.mobileNo || '',
|
|
||||||
emailId: data.emailId || '',
|
|
||||||
fullname: data.fullName || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<UpdateProfilePayload>>({});
|
|
||||||
|
|
||||||
const updateProfileMutation = useUpdateUserProfile();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
setFormData({
|
|
||||||
firstName: data.firstName || '',
|
|
||||||
middleName: data.middleName || '',
|
|
||||||
lastName: data.lastName || '',
|
|
||||||
mobileNo: data.mobileNo || '',
|
|
||||||
emailId: data.emailId || '',
|
|
||||||
fullname: data.fullName || '',
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
}
|
|
||||||
}, [visible, data]);
|
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
const newErrors: Partial<UpdateProfilePayload> = {};
|
|
||||||
|
|
||||||
if (!formData.firstName.trim()) {
|
|
||||||
newErrors.firstName = 'First name is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.lastName.trim()) {
|
|
||||||
newErrors.lastName = 'Last name is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.emailId.trim()) {
|
|
||||||
newErrors.emailId = 'Email is required';
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailId)) {
|
|
||||||
newErrors.emailId = 'Invalid email format';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.mobileNo.trim()) {
|
|
||||||
newErrors.mobileNo = 'Mobile number is required';
|
|
||||||
} else if (!/^\d{10}$/.test(formData.mobileNo.replace(/\s/g, ''))) {
|
|
||||||
newErrors.mobileNo = 'Invalid mobile number';
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatedFullName = `${formData.firstName} ${formData.middleName} ${formData.lastName}`.trim();
|
|
||||||
setFormData(prev => ({ ...prev, fullname: generatedFullName }));
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!validateForm()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await updateProfileMutation.mutateAsync(formData);
|
|
||||||
Alert.alert('Success', 'Profile updated successfully');
|
|
||||||
onSuccess();
|
|
||||||
onClose();
|
|
||||||
} catch (error: any) {
|
|
||||||
Alert.alert('Error', error?.message || 'Failed to update profile');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
animationType="slide"
|
|
||||||
transparent={true}
|
|
||||||
onRequestClose={onClose}
|
|
||||||
>
|
|
||||||
<KeyboardAvoidingView
|
|
||||||
style={styles.modalOverlay}
|
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
||||||
>
|
|
||||||
<View style={[styles.modalContent, { backgroundColor: colors.background }]}>
|
|
||||||
<View style={styles.modalHeader}>
|
|
||||||
<AppText variant="h3" style={styles.modalTitle}>Edit Personal Information</AppText>
|
|
||||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
|
||||||
<X size={24} color={colors.text} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView style={styles.formContainer} showsVerticalScrollIndicator={false}>
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<AppText variant="bodySmall" style={styles.inputLabel}>First Name *</AppText>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.input, errors.firstName && styles.inputError]}
|
|
||||||
value={formData.firstName}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, firstName: text })}
|
|
||||||
placeholder="Enter first name"
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
{errors.firstName && (
|
|
||||||
<AppText variant="caption" style={styles.errorText}>{errors.firstName}</AppText>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<AppText variant="bodySmall" style={styles.inputLabel}>Middle Name</AppText>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={formData.middleName}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, middleName: text })}
|
|
||||||
placeholder="Enter middle name"
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<AppText variant="bodySmall" style={styles.inputLabel}>Last Name *</AppText>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.input, errors.lastName && styles.inputError]}
|
|
||||||
value={formData.lastName}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, lastName: text })}
|
|
||||||
placeholder="Enter last name"
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
/>
|
|
||||||
{errors.lastName && (
|
|
||||||
<AppText variant="caption" style={styles.errorText}>{errors.lastName}</AppText>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<AppText variant="bodySmall" style={styles.inputLabel}>Email *</AppText>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.input, errors.emailId && styles.inputError]}
|
|
||||||
value={formData.emailId}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, emailId: text })}
|
|
||||||
placeholder="Enter email"
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
{errors.emailId && (
|
|
||||||
<AppText variant="caption" style={styles.errorText}>{errors.emailId}</AppText>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputGroup}>
|
|
||||||
<AppText variant="bodySmall" style={styles.inputLabel}>Mobile Number *</AppText>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.input, errors.mobileNo && styles.inputError]}
|
|
||||||
value={formData.mobileNo}
|
|
||||||
onChangeText={(text) => setFormData({ ...formData, mobileNo: text })}
|
|
||||||
placeholder="Enter mobile number"
|
|
||||||
placeholderTextColor={colors.textSecondary}
|
|
||||||
keyboardType="phone-pad"
|
|
||||||
maxLength={10}
|
|
||||||
/>
|
|
||||||
{errors.mobileNo && (
|
|
||||||
<AppText variant="caption" style={styles.errorText}>{errors.mobileNo}</AppText>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<View style={styles.modalFooter}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, styles.cancelButton]}
|
|
||||||
onPress={onClose}
|
|
||||||
disabled={updateProfileMutation.isPending}
|
|
||||||
>
|
|
||||||
<AppText variant="button" style={styles.cancelButtonText}>Cancel</AppText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, styles.saveButton]}
|
|
||||||
onPress={handleSave}
|
|
||||||
disabled={updateProfileMutation.isPending}
|
|
||||||
>
|
|
||||||
{updateProfileMutation.isPending ? (
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Save size={18} color={colors.buttonText} />
|
|
||||||
<AppText variant="button" style={styles.saveButtonText}>Save</AppText>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Profile Header Component - OPTIMIZED
|
// Profile Header Component - OPTIMIZED
|
||||||
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
|
const ProfileHeader: React.FC<ProfileHeaderProps> = React.memo(({ data, displayName }) => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -810,7 +521,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
},
|
},
|
||||||
retryButtonText: {
|
retryButtonText: {
|
||||||
color: colors.white,
|
color: colors.buttonText,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -828,11 +539,11 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
backgroundColor: colors.card,
|
backgroundColor: colors.surface,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
borderColor: colors.card,
|
borderColor: colors.surface,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shadowColor: colors.shadow,
|
shadowColor: colors.shadow,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
@@ -852,7 +563,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
borderColor: colors.card, // Updated
|
borderColor: colors.surface, // Updated
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@@ -860,7 +571,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.buttonText,
|
||||||
},
|
},
|
||||||
headerName: {
|
headerName: {
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
@@ -894,7 +605,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
},
|
},
|
||||||
statCard: {
|
statCard: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.card,
|
backgroundColor: colors.surface,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -920,7 +631,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
section: {
|
section: {
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
backgroundColor: colors.card,
|
backgroundColor: colors.surface,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@@ -984,7 +695,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: colors.card, // Updated
|
backgroundColor: colors.surface, // Updated
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
@@ -1007,97 +718,6 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||||||
lineHeight: 21,
|
lineHeight: 21,
|
||||||
},
|
},
|
||||||
|
|
||||||
modalOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
modalContent: {
|
|
||||||
borderTopLeftRadius: 24,
|
|
||||||
borderTopRightRadius: 24,
|
|
||||||
maxHeight: '90%',
|
|
||||||
paddingTop: 20,
|
|
||||||
borderRadius: 30
|
|
||||||
},
|
|
||||||
modalHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingBottom: 16,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.border,
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
padding: 4,
|
|
||||||
},
|
|
||||||
formContainer: {
|
|
||||||
padding: 20,
|
|
||||||
maxHeight: 500,
|
|
||||||
},
|
|
||||||
inputGroup: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
inputLabel: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.text,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.border,
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 14,
|
|
||||||
fontSize: 15,
|
|
||||||
color: colors.text,
|
|
||||||
backgroundColor: colors.card, // Updated
|
|
||||||
},
|
|
||||||
inputError: {
|
|
||||||
borderColor: colors.error,
|
|
||||||
},
|
|
||||||
errorText: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: colors.error,
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
modalFooter: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
padding: 20,
|
|
||||||
gap: 12,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border,
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 14,
|
|
||||||
borderRadius: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
backgroundColor: colors.background, // Updated
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
|
||||||
cancelButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.text,
|
|
||||||
},
|
|
||||||
saveButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.white,
|
|
||||||
},
|
|
||||||
navigationOverlay: {
|
navigationOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/** Shared types and value helpers for the Profile feature. */
|
||||||
|
|
||||||
|
export interface ContractorData {
|
||||||
|
contractorId: number;
|
||||||
|
firstName: string;
|
||||||
|
middleName: string;
|
||||||
|
lastName: string;
|
||||||
|
fullName: string;
|
||||||
|
emailId: string;
|
||||||
|
mobileNo: string;
|
||||||
|
company: string;
|
||||||
|
contractorType: string;
|
||||||
|
dialCode: string;
|
||||||
|
twoFactorAuthEnabled: boolean;
|
||||||
|
colorCodeHSL: string | null;
|
||||||
|
isActive: string;
|
||||||
|
isEnabled: boolean | null;
|
||||||
|
role: string;
|
||||||
|
contractorStatus: boolean;
|
||||||
|
designation: string | null;
|
||||||
|
contOtherId: number;
|
||||||
|
contCompanyType: string | null;
|
||||||
|
contOrganization: string;
|
||||||
|
agentAddress: string;
|
||||||
|
agentPincode: string;
|
||||||
|
agentCity: string;
|
||||||
|
agentState: string | null;
|
||||||
|
agentWebsite: string;
|
||||||
|
agentRegno: string;
|
||||||
|
orgPAN: string;
|
||||||
|
orgPANdob: string;
|
||||||
|
orgPANname: string;
|
||||||
|
gistnNo: string;
|
||||||
|
comAadharNo: string;
|
||||||
|
contractorClass: string;
|
||||||
|
socialCategory: string;
|
||||||
|
wizardStepStatus: string;
|
||||||
|
registrationStatus: string;
|
||||||
|
indianOrgName: string;
|
||||||
|
expiryDate: string;
|
||||||
|
msmeStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProfilePayload {
|
||||||
|
firstName: string;
|
||||||
|
middleName: string;
|
||||||
|
lastName: string;
|
||||||
|
mobileNo: string;
|
||||||
|
emailId: string;
|
||||||
|
fullname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the value is renderable profile data (not empty/null/"N/A"). */
|
||||||
|
export const isValidValue = (value: any): boolean => {
|
||||||
|
if (value === null || value === undefined) { return false; }
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 && trimmed.toLowerCase() !== 'n/a';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** True when at least one of the section's values is renderable. */
|
||||||
|
export const hasSectionData = (values: any[]): boolean => values.some(isValidValue);
|
||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
import PrimaryButton from '../../components/PrimaryButton';
|
||||||
import MyInput from '../../components/MyInput';
|
import MyInput from '../../components/MyInput';
|
||||||
import { showToast } from '../../Constant/ShowDailog';
|
import { showToast } from '../../Constant/ShowDailog';
|
||||||
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
|
import { usePartnerShipSubmit } from '../../Services/ApiCall/Hooks';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { NavigationString } from '../../Constant/AllImports';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
@@ -246,7 +246,7 @@ const Partnership = () => {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
|
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} style={{ marginBottom: 20 }} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -254,7 +254,7 @@ const Partnership = () => {
|
|||||||
|
|
||||||
export default Partnership;
|
export default Partnership;
|
||||||
|
|
||||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
const makeStyles = (colors: any, spacing: any, _shadows: any, _typography: any) => StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: spacing.m,
|
paddingHorizontal: spacing.m,
|
||||||
paddingBottom: spacing.m,
|
paddingBottom: spacing.m,
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useReducer } from 'react';
|
||||||
import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid, Alert } from 'react-native';
|
import { View, TouchableOpacity, StyleSheet, Platform, KeyboardAvoidingView, ScrollView, BackHandler, ToastAndroid } from 'react-native';
|
||||||
import { ArrowLeft } from 'lucide-react-native';
|
import { ArrowLeft } from 'lucide-react-native';
|
||||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import { FormDataType } from './types/types';
|
||||||
import { ApiResponse, DocumentsType, FormDataType, OrgType, UserData } from './types/types';
|
|
||||||
import { pick } from '@react-native-documents/picker';
|
import { pick } from '@react-native-documents/picker';
|
||||||
import axios from 'axios';
|
|
||||||
import StageOne from './ragistrationStages/StageOne';
|
import StageOne from './ragistrationStages/StageOne';
|
||||||
import StageTwo from './ragistrationStages/StageTwo';
|
import StageTwo from './ragistrationStages/StageTwo';
|
||||||
import StageZero from './ragistrationStages/StageZero';
|
import StageZero from './ragistrationStages/StageZero';
|
||||||
import StageThree from './ragistrationStages/StageThree';
|
import StageThree from './ragistrationStages/StageThree';
|
||||||
import StageFour from './ragistrationStages/StageFour';
|
import StageFour from './ragistrationStages/StageFour';
|
||||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
import PrimaryButton from '../../components/PrimaryButton';
|
||||||
import StepProgressBar from '../../Component/StepProgressBar';
|
import StepProgressBar from '../../components/StepProgressBar';
|
||||||
import { formatDateInput } from '../../Helper/formatDateInput';
|
import { formatDateInput } from '../../Helper/formatDateInput';
|
||||||
import { showToast } from '../../Constant/ShowDailog';
|
import { showToast } from '../../Constant/ShowDailog';
|
||||||
import { useCityDropdown, useContractorStageTwoSubmit, usefetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyGstNumber, useVerifyPan } from '../../Services/ApiCall/Hooks';
|
import { useCityDropdown, useContractorStageTwoSubmit, useFetchMasterData, useSendAadhaarOtp, useVerifyAadhaarOtp, useVerifyPan } from '../../Services/ApiCall/Hooks';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
|
import Animated, { FadeInUp, FadeInDown } from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
import NavigationString from '../../Constant/NavigationString';
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
|
import { logger } from '../../core/observability/logger';
|
||||||
|
import { initialRegistrationState, registrationReducer } from './registrationState';
|
||||||
|
import { validatePanFormat, validateStage } from './registrationValidation';
|
||||||
|
|
||||||
type RouteParams = {
|
type RouteParams = {
|
||||||
userData?: {
|
userData?: {
|
||||||
@@ -33,192 +34,96 @@ type RouteParams = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-stage registration wizard. All wizard state lives in
|
||||||
|
* registrationState.ts (one reducer); per-stage validation in
|
||||||
|
* registrationValidation.ts. This component orchestrates API calls and
|
||||||
|
* renders the current stage.
|
||||||
|
*/
|
||||||
const Registration: React.FC = () => {
|
const Registration: React.FC = () => {
|
||||||
const { colors, spacing, typography, shadows } = useTheme();
|
const { colors, spacing, typography, shadows } = useTheme();
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<any>();
|
||||||
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
|
const { params } = useRoute<RouteProp<Record<string, RouteParams>, string>>();
|
||||||
const userData = params as RouteParams;
|
const userData = params as RouteParams;
|
||||||
const [stage, setStage] = useState(0);
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [focusedField, setFocusedField] = useState<null | string>(null);
|
|
||||||
const [selectedOrg, setSelectedOrg] = useState<OrgType>(0);
|
const [state, dispatch] = useReducer(registrationReducer, initialRegistrationState);
|
||||||
const [documents, setDocuments] = useState<DocumentsType>({
|
const { stage, formData, documents, selectedOrg, focusedField } = state;
|
||||||
panCard: null,
|
|
||||||
gstnCertificate: null,
|
|
||||||
registrationCertificate: null,
|
|
||||||
msmefile: null,
|
|
||||||
constractorClass: null,
|
|
||||||
aadharCard: null,
|
|
||||||
});
|
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
|
||||||
firstName: '',
|
|
||||||
middleName: '',
|
|
||||||
lastName: '',
|
|
||||||
email: '',
|
|
||||||
mobileNumber: '',
|
|
||||||
organizationName: '',
|
|
||||||
officialWebsite: '',
|
|
||||||
registrationNumber: '',
|
|
||||||
contractorClass: '',
|
|
||||||
orgType: '',
|
|
||||||
registrationDetail: '',
|
|
||||||
socialCategory: '',
|
|
||||||
orgPanNumber: '',
|
|
||||||
dateOfIncorp: '',
|
|
||||||
orgPanName: '',
|
|
||||||
gstnNo: '',
|
|
||||||
aadhaarNumber: '',
|
|
||||||
aadhaarOtp: '',
|
|
||||||
address: '',
|
|
||||||
state: '',
|
|
||||||
city: '',
|
|
||||||
pinCode: '',
|
|
||||||
agentContact2: '',
|
|
||||||
expiryDate: '',
|
|
||||||
});
|
|
||||||
const [loader, setloader] = useState(false);
|
|
||||||
const [aadhaarVerifying, setAadhaarVerifying] = useState(false);
|
|
||||||
const [aadhaarVerified, setAadhaarVerified] = useState(false);
|
|
||||||
const [panVerifying, setPanVerifying] = useState(false);
|
|
||||||
const [panVerified, setPanVerified] = useState(false);
|
|
||||||
const [contractorClassItems, setContractorClassItems] = useState([]);
|
|
||||||
const [allOrganizations, setAllOrganizations] = useState([]);
|
|
||||||
const [allSocialCategories, setAllSocialCategories] = useState([]);
|
|
||||||
const [allMsmeOrgs, setAllMsmeOrgs] = useState([]);
|
|
||||||
const [adharreference, setadharreference] = useState('');
|
|
||||||
const [adharotpsend, setadharotpsend] = useState(false);
|
|
||||||
const [gstverifying, setgstverifying] = useState(false);
|
|
||||||
const [gstverified, setgstverified] = useState(false);
|
|
||||||
const [states, setstates] = useState([]);
|
|
||||||
const [cities, setCities] = useState([]);
|
|
||||||
const [allMSME, setAllMSME] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const { mutateAsync: verifyPanHandlerRes } = useVerifyPan();
|
const { mutateAsync: verifyPanHandlerRes } = useVerifyPan();
|
||||||
const { mutateAsync: sendAadhaarOtp } = useSendAadhaarOtp();
|
const { mutateAsync: sendAadhaarOtp } = useSendAadhaarOtp();
|
||||||
const { mutateAsync: verifyAadhaarOtp } = useVerifyAadhaarOtp();
|
const { mutateAsync: verifyAadhaarOtp } = useVerifyAadhaarOtp();
|
||||||
const { mutateAsync: verifyGstNumber } = useVerifyGstNumber();
|
|
||||||
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
|
const { mutateAsync: contractorStageTwoSubmit } = useContractorStageTwoSubmit();
|
||||||
const { data: masterData } = usefetchMasterData();
|
const { mutate: cityDropdownMutate } = useCityDropdown();
|
||||||
|
const { data: masterData } = useFetchMasterData();
|
||||||
|
const user = useSelector((s: any) => s.token.token);
|
||||||
|
const steps = ['', '', '', '', ''];
|
||||||
|
|
||||||
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
const styles = useMemo(() => makeStyles(colors, spacing, shadows, typography), [colors, spacing, shadows, typography]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!masterData?.data) return;
|
if (!masterData?.data) { return; }
|
||||||
const contractorClasses = masterData.data.contractorClasses || [];
|
const mapList = (list: any[], label: string, value: string) =>
|
||||||
const msmeOrgs = masterData.data.msmeOrgs || [];
|
(list || []).map((item: any) => ({ label: item[label] || '', value: item[value] }));
|
||||||
const mappedmsmeOrgs = msmeOrgs.map((item: any) => ({
|
|
||||||
label: item.comName,
|
dispatch({
|
||||||
value: item.msmeId,
|
type: 'MASTER_DATA_LOADED',
|
||||||
}));
|
lists: {
|
||||||
const orgs = masterData.data.organizationTypes || [];
|
contractorClasses: mapList(masterData.data.contractorClasses, 'contClassName', 'contClassId'),
|
||||||
const mappedcontractorClasses = contractorClasses.map((item: any) => ({
|
organizations: mapList(masterData.data.organizationTypes, 'organizationType', 'orgId'),
|
||||||
label: item.contClassName || '',
|
socialCategories: mapList(masterData.data.socialCategories, 'categoryName', 'socialCategoryId'),
|
||||||
value: item.contClassId,
|
msmeOrgs: mapList(masterData.data.msmeOrgs, 'comName', 'msmeId'),
|
||||||
}));
|
states: mapList(masterData.data.states, 'stateName', 'stateId'),
|
||||||
const mapped = orgs.map((item: any) => ({
|
},
|
||||||
label: item.organizationType,
|
});
|
||||||
value: item.orgId,
|
|
||||||
}));
|
|
||||||
const socialCategories = masterData.data.socialCategories || [];
|
|
||||||
const mappedsocialCategories = socialCategories.map((item: any) => ({
|
|
||||||
label: item.categoryName,
|
|
||||||
value: item.socialCategoryId,
|
|
||||||
}));
|
|
||||||
const stateList = masterData.data.states || [];
|
|
||||||
const mappedStates = stateList.map((item: any) => ({
|
|
||||||
label: item.stateName,
|
|
||||||
value: item.stateId,
|
|
||||||
}));
|
|
||||||
setAllMsmeOrgs(mappedmsmeOrgs);
|
|
||||||
setAllSocialCategories(mappedsocialCategories);
|
|
||||||
setContractorClassItems(mappedcontractorClasses);
|
|
||||||
setAllOrganizations(mapped);
|
|
||||||
setstates(mappedStates);
|
|
||||||
}, [masterData?.data]);
|
}, [masterData?.data]);
|
||||||
|
|
||||||
|
|
||||||
const { mutate: cityDropdownMutate } = useCityDropdown();
|
|
||||||
const { data: MasterData } = usefetchMasterData();
|
|
||||||
const user = useSelector((state: any) => state.token.token);
|
|
||||||
const steps = ['', '', '', '', ''];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserData = async (user: any) => {
|
if (user || userData?.userData) {
|
||||||
try {
|
dispatch({
|
||||||
if (user) {
|
type: 'PREFILL_CONTACT',
|
||||||
const parsedUser = user;
|
values: {
|
||||||
setFormData(prev => ({
|
firstName: user?.firstName || userData?.userData?.firstName,
|
||||||
...prev,
|
lastName: user?.lastName || userData?.userData?.lastName,
|
||||||
firstName: parsedUser?.firstName || userData?.userData?.firstName,
|
mobileNumber: user?.mobileNo || userData?.userData?.mobileNo,
|
||||||
lastName: parsedUser?.lastName || userData?.userData?.lastName,
|
middleName: user?.middleName || userData?.userData?.middleName,
|
||||||
mobileNumber: parsedUser?.mobileNo || userData?.userData?.mobileNo,
|
email: user?.email || userData?.userData?.email,
|
||||||
middleName: parsedUser?.middleName || userData?.userData?.middleName,
|
},
|
||||||
email: parsedUser?.email || userData?.userData?.email,
|
});
|
||||||
}));
|
}
|
||||||
} else {
|
// Intentionally runs only on mount; user/userData are stable route params.
|
||||||
console.log('No user data found');
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching user data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserData(user);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isPANLinkedToGSTIN = (pan: string, gstin: string): { valid: boolean; message: string } => {
|
|
||||||
pan = pan.toUpperCase();
|
|
||||||
gstin = gstin.toUpperCase();
|
|
||||||
|
|
||||||
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
|
|
||||||
|
|
||||||
if (!panRegex.test(pan)) {
|
|
||||||
return { valid: false, message: "Invalid PAN format. It should be like 'ABCDE1234F'." };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true, message: 'PAN and GSTIN are correctly linked.' };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (field: string, value: string) => {
|
const handleChange = (field: string, value: string) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
dispatch({ type: 'SET_FIELD', field: field as keyof FormDataType, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setFocusedField = (field: string | null) =>
|
||||||
|
dispatch({ type: 'SET_FOCUSED_FIELD', field });
|
||||||
|
|
||||||
const verifyPanHandler = async () => {
|
const verifyPanHandler = async () => {
|
||||||
setPanVerifying(true);
|
dispatch({ type: 'PAN_VERIFY_START' });
|
||||||
const payload = {
|
const payload = {
|
||||||
dob: formData?.dateOfIncorp.trim(),
|
dob: formData?.dateOfIncorp.trim(),
|
||||||
name: formData?.orgPanName.trim(),
|
name: formData?.orgPanName.trim(),
|
||||||
panNumber: formData?.orgPanNumber.trim(),
|
panNumber: formData?.orgPanNumber.trim(),
|
||||||
};
|
};
|
||||||
const result = await verifyPanHandlerRes(payload);
|
const result = await verifyPanHandlerRes(payload);
|
||||||
|
dispatch({ type: 'PAN_VERIFY_DONE', verified: !!result });
|
||||||
if (result) {
|
|
||||||
setPanVerified(true);
|
|
||||||
} else {
|
|
||||||
setPanVerified(false);
|
|
||||||
}
|
|
||||||
setPanVerifying(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendAdharOTPHandler = async (text: string) => {
|
const sendAdharOTPHandler = async (text: string) => {
|
||||||
setAadhaarVerifying(true);
|
dispatch({ type: 'AADHAAR_OTP_REQUEST' });
|
||||||
setadharreference('');
|
|
||||||
|
|
||||||
const payload = {
|
const response = await sendAadhaarOtp({ aadhaarNumber: text.trim() });
|
||||||
aadhaarNumber: text.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await sendAadhaarOtp(payload);
|
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
setadharotpsend(true);
|
dispatch({ type: 'AADHAAR_OTP_DONE', sent: true, refNumber: response });
|
||||||
setadharreference(response);
|
|
||||||
} else {
|
} else {
|
||||||
setadharotpsend(false);
|
dispatch({ type: 'AADHAAR_OTP_DONE', sent: false, refNumber: '' });
|
||||||
ToastAndroid.show('Failed to send Aadhaar OTP', ToastAndroid.SHORT);
|
ToastAndroid.show('Failed to send Aadhaar OTP', ToastAndroid.SHORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAadhaarVerifying(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyAdharOtpHandler = async () => {
|
const verifyAdharOtpHandler = async () => {
|
||||||
@@ -226,33 +131,18 @@ const Registration: React.FC = () => {
|
|||||||
ToastAndroid.show('Please enter a valid 6-digit OTP', ToastAndroid.SHORT);
|
ToastAndroid.show('Please enter a valid 6-digit OTP', ToastAndroid.SHORT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAadhaarVerifying(true);
|
dispatch({ type: 'AADHAAR_VERIFY_START' });
|
||||||
const payload = {
|
const response = await verifyAadhaarOtp({
|
||||||
otp: formData.aadhaarOtp,
|
otp: formData.aadhaarOtp,
|
||||||
refNumber: adharreference,
|
refNumber: state.aadhaarRefNumber,
|
||||||
};
|
});
|
||||||
const response = await verifyAadhaarOtp(payload);
|
|
||||||
if (response) {
|
if (response) {
|
||||||
setAadhaarVerified(true);
|
dispatch({ type: 'AADHAAR_VERIFY_DONE', verified: true });
|
||||||
ToastAndroid.show('Aadhaar Verified', ToastAndroid.SHORT);
|
ToastAndroid.show('Aadhaar Verified', ToastAndroid.SHORT);
|
||||||
} else {
|
} else {
|
||||||
|
dispatch({ type: 'AADHAAR_VERIFY_DONE' });
|
||||||
ToastAndroid.show('Verification failed', ToastAndroid.SHORT);
|
ToastAndroid.show('Verification failed', ToastAndroid.SHORT);
|
||||||
}
|
}
|
||||||
setAadhaarVerifying(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyGSTHandler = async () => {
|
|
||||||
setgstverifying(true);
|
|
||||||
const payload = {
|
|
||||||
gstNumber: formData?.gstnNo.trim(),
|
|
||||||
};
|
|
||||||
const response = await verifyGstNumber(payload);
|
|
||||||
if (response) {
|
|
||||||
setgstverified(true);
|
|
||||||
} else {
|
|
||||||
ToastAndroid.show('GST verification failed', ToastAndroid.SHORT);
|
|
||||||
}
|
|
||||||
setgstverifying(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDocumentPick = async (field: string) => {
|
const handleDocumentPick = async (field: string) => {
|
||||||
@@ -264,19 +154,15 @@ const Registration: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response && response?.length > 0) {
|
if (response && response?.length > 0) {
|
||||||
const selectedFile = response[0];
|
dispatch({ type: 'SET_DOCUMENT', field, file: response[0] });
|
||||||
setDocuments(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: selectedFile,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Document pick error:', err);
|
logger.error('registration', 'document pick failed', { error: String(err) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setLoading(true);
|
dispatch({ type: 'SUBMIT_START' });
|
||||||
|
|
||||||
const dtoData = {
|
const dtoData = {
|
||||||
agentAddress: formData.address,
|
agentAddress: formData.address,
|
||||||
@@ -297,7 +183,8 @@ const Registration: React.FC = () => {
|
|||||||
orgPAN: formData.orgPanNumber,
|
orgPAN: formData.orgPanNumber,
|
||||||
comAadharNo: formData.aadhaarNumber,
|
comAadharNo: formData.aadhaarNumber,
|
||||||
gistnNo: formData.gstnNo,
|
gistnNo: formData.gstnNo,
|
||||||
stateName: MasterData?.data?.states?.find((state: any) => state.stateId == formData.state)?.stateName || '',
|
// eslint-disable-next-line eqeqeq
|
||||||
|
stateName: masterData?.data?.states?.find((s: any) => s.stateId == formData.state)?.stateName || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -311,185 +198,93 @@ const Registration: React.FC = () => {
|
|||||||
fileData.append('regfile', documents.registrationCertificate as any);
|
fileData.append('regfile', documents.registrationCertificate as any);
|
||||||
|
|
||||||
const res = await contractorStageTwoSubmit(fileData);
|
const res = await contractorStageTwoSubmit(fileData);
|
||||||
console.log('Stage Two Res : ', res)
|
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
if (res) {
|
showToast('Form submitted successfully!');
|
||||||
|
|
||||||
showToast('Form submitted successfully!');
|
const orgType = selectedOrg;
|
||||||
|
const routeParams = { otherId: res?.data?.contOtherId, orgType: orgType };
|
||||||
const orgType = selectedOrg;
|
if ([4, 5, 6].includes(orgType as number)) {
|
||||||
const params = { otherId: res?.data?.contOtherId, orgType: orgType };
|
navigation.replace(NavigationString.StageThreeScreen as never, routeParams as never);
|
||||||
console.log("params : ", params)
|
} else if (orgType === 7) {
|
||||||
if ([4, 5, 6].includes(orgType)) {
|
navigation.replace(NavigationString.Partnership as never, { otherId: res?.data?.contOtherId } as never);
|
||||||
navigation.replace(NavigationString.StageThreeScreen as never, params as never);
|
|
||||||
} else if (orgType === 7) {
|
|
||||||
navigation.replace(NavigationString.Partnership as never, { otherId: res?.data?.contOtherId } as never);
|
|
||||||
} else {
|
|
||||||
navigation.replace(NavigationString.Home as never);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showToast('Form submission failed');
|
navigation.replace(NavigationString.Home as never);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
showToast(res.message || 'Form submission failed. Please try again.');
|
showToast(res.message || 'Form submission failed. Please try again.');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("Error in form submission")
|
logger.error('registration', 'stage-two submission failed', { error: String(error) });
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
} else {
|
|
||||||
console.error('Unexpected error', error);
|
|
||||||
}
|
|
||||||
console.error('Form submission failed:', error);
|
|
||||||
showToast(error.message || 'Form submission failed. Please try again.');
|
showToast(error.message || 'Form submission failed. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
dispatch({ type: 'SUBMIT_END' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchCity = (stateId: number) => {
|
||||||
const fetchCity = async (stateId: number) => {
|
|
||||||
cityDropdownMutate(stateId, {
|
cityDropdownMutate(stateId, {
|
||||||
onSuccess: (data) => {
|
onSuccess: data => dispatch({ type: 'CITIES_LOADED', cities: data }),
|
||||||
setCities(data);
|
onError: error => {
|
||||||
|
dispatch({ type: 'CITIES_LOADED', cities: [] });
|
||||||
|
logger.error('registration', 'city fetch failed', { error: String(error) });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
|
||||||
setCities([]);
|
|
||||||
console.error('Error in city fetch:', error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('formdata : ', formData);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData?.state) {
|
if (formData?.state) {
|
||||||
fetchCity(Number(formData.state));
|
fetchCity(Number(formData.state));
|
||||||
}
|
}
|
||||||
|
// Intentionally re-runs only when the selected state changes; fetchCity is recreated each render.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [formData?.state]);
|
}, [formData?.state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||||
if (stage > 1) {
|
if (stage > 1) {
|
||||||
setStage(prev => prev - 1);
|
dispatch({ type: 'PREV_STAGE' });
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => backHandler.remove();
|
return () => backHandler.remove();
|
||||||
}, [stage]);
|
}, [stage]);
|
||||||
|
|
||||||
const validateStage = (stage: number) => {
|
const handleNextPress = () => {
|
||||||
console.log(stage, 'validateStage');
|
const validation = validateStage(stage, { formData, documents, selectedOrg });
|
||||||
switch (stage) {
|
if (!validation.valid) {
|
||||||
case 0:
|
showToast(validation.message || 'Please fill all required fields.');
|
||||||
if (!formData.organizationName?.trim()) {
|
return;
|
||||||
showToast('Please fill all required details.');
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 1:
|
if (stage === 2) {
|
||||||
if (selectedOrg == 8) {
|
if (!state.panVerified) {
|
||||||
if (!formData.orgType || !formData.socialCategory) {
|
showToast('Please verify PAN details.');
|
||||||
showToast('Please enter all valid details.');
|
return;
|
||||||
return false;
|
}
|
||||||
}
|
if (!state.aadhaarVerified) {
|
||||||
} else {
|
showToast('Please verify Aadhar Number.');
|
||||||
if (
|
return;
|
||||||
!formData.registrationNumber ||
|
}
|
||||||
!formData.registrationDetail ||
|
const panCheck = validatePanFormat(formData.orgPanNumber);
|
||||||
!formData.socialCategory ||
|
if (!panCheck.valid) {
|
||||||
!formData.orgType
|
showToast(panCheck.message || 'Invalid PAN.');
|
||||||
) {
|
return;
|
||||||
showToast('Please enter all valid details.');
|
}
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 2:
|
if (stage === 4) {
|
||||||
if (
|
handleSubmit();
|
||||||
!formData.orgPanNumber ||
|
} else {
|
||||||
!formData.dateOfIncorp ||
|
dispatch({ type: 'NEXT_STAGE' });
|
||||||
!formData.orgPanName ||
|
|
||||||
(selectedOrg == 8 && !formData.gstnNo) ||
|
|
||||||
!formData.aadhaarNumber ||
|
|
||||||
!formData.aadhaarOtp
|
|
||||||
) {
|
|
||||||
Alert.alert('aadhar otp : ', formData?.aadhaarOtp);
|
|
||||||
showToast('Please fill all required KYC and Aadhaar details.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (formData.aadhaarNumber.length !== 12) {
|
|
||||||
showToast('Aadhaar number must be 12 digits.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (formData.aadhaarOtp.length !== 6) {
|
|
||||||
showToast('OTP must be 6 digits.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
if (!formData.address || !formData.state || !formData.city || !formData.pinCode) {
|
|
||||||
showToast('Please fill all required address details.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.pinCode.length !== 6) {
|
|
||||||
showToast('Please enter a valid 6-digit pin code.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
if (selectedOrg == 8) {
|
|
||||||
if (!documents.panCard || !documents.aadharCard) {
|
|
||||||
showToast('Please upload all required documents.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (formData?.contractorClass !== 1) {
|
|
||||||
if (!documents.constractorClass) {
|
|
||||||
showToast('Please upload CONTRACTOR CLASS documents.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (formData?.registrationDetail !== 3) {
|
|
||||||
if (!documents.msmefile) {
|
|
||||||
showToast('Please upload MSME FILE.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!documents.gstnCertificate || !documents.registrationCertificate) {
|
|
||||||
showToast('Please upload all required documents.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Stage Titles ──
|
|
||||||
const stageTitles = [
|
|
||||||
'Personal & Contact',
|
|
||||||
'Registration Details',
|
|
||||||
'KYC Verification',
|
|
||||||
'Address Details',
|
|
||||||
'Upload Documents',
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
<View style={[styles.root, { backgroundColor: colors.background }]}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={{ flex: 1 }}
|
style={styles.flex1}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -30}
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -30}
|
||||||
>
|
>
|
||||||
@@ -499,43 +294,25 @@ const Registration: React.FC = () => {
|
|||||||
bounces={false}
|
bounces={false}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={{ paddingBottom: 20, marginTop: insets.top, flex: 1 }}>
|
<View style={[styles.stageArea, { marginTop: insets.top }]}>
|
||||||
{/* ═══ Back Button ═══ */}
|
{/* ═══ Back Button ═══ */}
|
||||||
<View style={styles.headerRow}>
|
<View style={styles.headerRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => dispatch({ type: 'PREV_STAGE' })}
|
||||||
if (stage > 0) {
|
|
||||||
setStage(prev => prev - 1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={styles.backBtn}
|
style={styles.backBtn}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={22} color={colors.text} />
|
<ArrowLeft size={22} color={colors.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<AppText variant="h3" color={colors.text} style={{ fontWeight: '700' }}>
|
<AppText variant="h3" color={colors.text} style={styles.title as any}>
|
||||||
Registration
|
Registration
|
||||||
</AppText>
|
</AppText>
|
||||||
<View style={{ width: 36 }} />
|
<View style={styles.headerSpacer} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ═══ Progress Bar ═══ */}
|
{/* ═══ Progress Bar ═══ */}
|
||||||
<StepProgressBar currentStep={stage} steps={steps} />
|
<StepProgressBar currentStep={stage} steps={steps} />
|
||||||
|
|
||||||
{/* ═══ Stage Title ═══
|
|
||||||
<Animated.View
|
|
||||||
key={`title-${stage}`}
|
|
||||||
entering={FadeInUp.duration(300)}
|
|
||||||
style={styles.stageTitleContainer}
|
|
||||||
>
|
|
||||||
<AppText variant="h3" color={colors.text} style={styles.stageTitle}>
|
|
||||||
{stageTitles[stage]}
|
|
||||||
</AppText>
|
|
||||||
<AppText variant="caption" color={colors.textSecondary}>
|
|
||||||
Step {stage + 1} of {steps.length}
|
|
||||||
</AppText>
|
|
||||||
</Animated.View> */}
|
|
||||||
|
|
||||||
{/* ═══ Stage Content ═══ */}
|
{/* ═══ Stage Content ═══ */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
key={`stage-${stage}`}
|
key={`stage-${stage}`}
|
||||||
@@ -545,9 +322,7 @@ const Registration: React.FC = () => {
|
|||||||
{stage === 0 && (
|
{stage === 0 && (
|
||||||
<StageZero
|
<StageZero
|
||||||
formData={formData}
|
formData={formData}
|
||||||
handleChange={(key: string, value: string) =>
|
handleChange={handleChange}
|
||||||
handleChange(key as keyof FormDataType, value)
|
|
||||||
}
|
|
||||||
focusedField={focusedField}
|
focusedField={focusedField}
|
||||||
setFocusedField={setFocusedField}
|
setFocusedField={setFocusedField}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
@@ -557,18 +332,14 @@ const Registration: React.FC = () => {
|
|||||||
{stage === 1 && (
|
{stage === 1 && (
|
||||||
<StageOne
|
<StageOne
|
||||||
formData={formData}
|
formData={formData}
|
||||||
handleChange={(key: string, value: string) =>
|
handleChange={handleChange}
|
||||||
handleChange(key as keyof FormDataType, value)
|
|
||||||
}
|
|
||||||
focusedField={focusedField}
|
focusedField={focusedField}
|
||||||
setFocusedField={setFocusedField}
|
setFocusedField={setFocusedField}
|
||||||
contractorClassItems={contractorClassItems}
|
contractorClassItems={state.masterLists.contractorClasses}
|
||||||
allOrganizations={allOrganizations}
|
allOrganizations={state.masterLists.organizations}
|
||||||
allMsmeOrgs={allMsmeOrgs}
|
allMsmeOrgs={state.masterLists.msmeOrgs}
|
||||||
allSocialCategories={allSocialCategories}
|
allSocialCategories={state.masterLists.socialCategories}
|
||||||
allMSME={allMSME}
|
setSelectedOrg={(org: any) => dispatch({ type: 'SET_SELECTED_ORG', org })}
|
||||||
dropdownStyle={{}}
|
|
||||||
setSelectedOrg={setSelectedOrg}
|
|
||||||
selectedOrg={selectedOrg}
|
selectedOrg={selectedOrg}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -576,25 +347,20 @@ const Registration: React.FC = () => {
|
|||||||
{stage === 2 && (
|
{stage === 2 && (
|
||||||
<StageTwo
|
<StageTwo
|
||||||
formData={formData}
|
formData={formData}
|
||||||
handleChange={(key: string, value: string) =>
|
handleChange={handleChange}
|
||||||
handleChange(key as keyof FormDataType, value)
|
|
||||||
}
|
|
||||||
focusedField={focusedField}
|
focusedField={focusedField}
|
||||||
setFocusedField={setFocusedField}
|
setFocusedField={setFocusedField}
|
||||||
panVerified={panVerified}
|
panVerified={state.panVerified}
|
||||||
setPanVerified={setPanVerified}
|
setPanVerified={(verified: boolean) => dispatch({ type: 'SET_PAN_VERIFIED', verified })}
|
||||||
verifyPanHandler={verifyPanHandler}
|
verifyPanHandler={verifyPanHandler}
|
||||||
panVerifying={panVerifying}
|
panVerifying={state.panVerifying}
|
||||||
aadhaarVerified={aadhaarVerified}
|
aadhaarVerified={state.aadhaarVerified}
|
||||||
setAadhaarVerified={setAadhaarVerified}
|
setAadhaarVerified={(verified: boolean) => dispatch({ type: 'SET_AADHAAR_VERIFIED', verified })}
|
||||||
sendAdharOTPHandler={sendAdharOTPHandler}
|
sendAdharOTPHandler={sendAdharOTPHandler}
|
||||||
adharotpsend={adharotpsend}
|
adharotpsend={state.aadhaarOtpSent}
|
||||||
setadharotpsend={setadharotpsend}
|
setadharotpsend={(sent: boolean) => dispatch({ type: 'SET_AADHAAR_OTP_SENT', sent })}
|
||||||
verifyAdharOtpHandler={verifyAdharOtpHandler}
|
verifyAdharOtpHandler={verifyAdharOtpHandler}
|
||||||
aadhaarVerifying={aadhaarVerifying}
|
aadhaarVerifying={state.aadhaarVerifying}
|
||||||
verifyGSTHandler={verifyGSTHandler}
|
|
||||||
gstverifying={gstverifying}
|
|
||||||
color={colors}
|
|
||||||
styles={styles}
|
styles={styles}
|
||||||
formatDateInput={formatDateInput}
|
formatDateInput={formatDateInput}
|
||||||
selectedOrg={selectedOrg}
|
selectedOrg={selectedOrg}
|
||||||
@@ -604,16 +370,12 @@ const Registration: React.FC = () => {
|
|||||||
{stage === 3 && (
|
{stage === 3 && (
|
||||||
<StageThree
|
<StageThree
|
||||||
formData={formData}
|
formData={formData}
|
||||||
handleChange={(key: string, value: string) =>
|
handleChange={handleChange}
|
||||||
handleChange(key as keyof FormDataType, value)
|
|
||||||
}
|
|
||||||
setFocusedField={setFocusedField}
|
setFocusedField={setFocusedField}
|
||||||
focusedField={focusedField}
|
focusedField={focusedField}
|
||||||
states={states}
|
states={state.masterLists.states}
|
||||||
cities={cities}
|
cities={state.cities}
|
||||||
fetchCity={() => {
|
fetchCity={() => fetchCity(Number(formData.state))}
|
||||||
fetchCity(Number(formData.state));
|
|
||||||
}}
|
|
||||||
dropdownStyle={{}}
|
dropdownStyle={{}}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
/>
|
/>
|
||||||
@@ -634,51 +396,9 @@ const Registration: React.FC = () => {
|
|||||||
{/* ═══ Next / Submit Button ═══ */}
|
{/* ═══ Next / Submit Button ═══ */}
|
||||||
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
|
<Animated.View entering={FadeInDown.duration(300).delay(200)}>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
title={stage === 4 ? (loading ? 'Loading...' : 'Submit') : 'Next'}
|
title={stage === 4 ? (state.submitting ? 'Loading...' : 'Submit') : 'Next'}
|
||||||
style={{ backgroundColor: colors.primary }}
|
style={{ backgroundColor: colors.primary, marginBottom: 20 }}
|
||||||
onPress={() => {
|
onPress={handleNextPress}
|
||||||
if (validateStage(stage)) {
|
|
||||||
switch (stage) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
setStage(prev => prev + 1);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
if (!panVerified) {
|
|
||||||
showToast('Please verify PAN details.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!aadhaarVerified) {
|
|
||||||
showToast('Please verify Aadhar Number.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { valid, message } = isPANLinkedToGSTIN(
|
|
||||||
formData.orgPanNumber,
|
|
||||||
formData.gstnNo,
|
|
||||||
);
|
|
||||||
if (!valid) {
|
|
||||||
showToast(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStage(prev => prev + 1);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
setStage(prev => prev + 1);
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
handleSubmit();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(stage, 'stage');
|
|
||||||
setStage(prev => prev + 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast('Please fill all required fields.');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -691,6 +411,11 @@ export default Registration;
|
|||||||
|
|
||||||
// ── Styles ────────────────────────────────────────────────────────────
|
// ── Styles ────────────────────────────────────────────────────────────
|
||||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
||||||
|
root: { flex: 1 },
|
||||||
|
flex1: { flex: 1 },
|
||||||
|
stageArea: { paddingBottom: 20, flex: 1 },
|
||||||
|
title: { fontWeight: '700' } as any,
|
||||||
|
headerSpacer: { width: 36 },
|
||||||
scrollContainer: {
|
scrollContainer: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
paddingHorizontal: spacing.m,
|
paddingHorizontal: spacing.m,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import PrimaryButton from '../../Component/PrimaryButton/PrimaryButton';
|
import PrimaryButton from '../../components/PrimaryButton';
|
||||||
import MyInput from '../../components/MyInput';
|
import MyInput from '../../components/MyInput';
|
||||||
import { showToast } from '../../Constant/ShowDailog';
|
import { showToast } from '../../Constant/ShowDailog';
|
||||||
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
|
import { useContractorStageThreeSubmit } from '../../Services/ApiCall/Hooks';
|
||||||
@@ -17,6 +17,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../Hooks/useTheme';
|
import { useTheme } from '../../Hooks/useTheme';
|
||||||
import AppText from '../../components/AppText';
|
import AppText from '../../components/AppText';
|
||||||
|
import NavigationString from '../../Constant/NavigationString';
|
||||||
|
import { logger } from '../../core/observability/logger';
|
||||||
import { Trash2, Building2 } from 'lucide-react-native';
|
import { Trash2, Building2 } from 'lucide-react-native';
|
||||||
|
|
||||||
type Company = {
|
type Company = {
|
||||||
@@ -84,16 +86,17 @@ const StageThreeScreen = () => {
|
|||||||
shPANNo: p.panNumber,
|
shPANNo: p.panNumber,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('stage 3 payload : ', payload);
|
|
||||||
const response = await stageThreeSubmit(payload);
|
const response = await stageThreeSubmit(payload);
|
||||||
console.log('✅ company submitted successfully:', response);
|
|
||||||
|
|
||||||
if (response.statusCode === 200 || response.statusCode === 201) {
|
if (response.statusCode === 200 || response.statusCode === 201) {
|
||||||
navigation.replace('Dashboard');
|
// Was navigation.replace('Dashboard') — a route that never existed, so
|
||||||
|
// the navigation was silently dropped and the user was stranded on the
|
||||||
|
// form. Completed registration lands on Home (matches Splash/Login).
|
||||||
|
navigation.replace(NavigationString.Home);
|
||||||
showToast('Submitted successfully!');
|
showToast('Submitted successfully!');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Submission failed:', error.message);
|
logger.error('registration', 'stage-three submission failed', { error: String(error) });
|
||||||
showToast('Submission failed. Please try again.');
|
showToast('Submission failed. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -241,7 +244,7 @@ const StageThreeScreen = () => {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} />
|
<PrimaryButton title={loading ? 'Loading...' : 'Submit'} onPress={handleSubmit} style={{ marginBottom: 20 }} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -249,7 +252,7 @@ const StageThreeScreen = () => {
|
|||||||
|
|
||||||
export default StageThreeScreen;
|
export default StageThreeScreen;
|
||||||
|
|
||||||
const makeStyles = (colors: any, spacing: any, shadows: any, typography: any) => StyleSheet.create({
|
const makeStyles = (colors: any, spacing: any, _shadows: any, _typography: any) => StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: spacing.m,
|
paddingHorizontal: spacing.m,
|
||||||
paddingBottom: spacing.m,
|
paddingBottom: spacing.m,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { initialRegistrationState, registrationReducer, RegistrationState } from '../registrationState';
|
||||||
|
|
||||||
|
const s = (over: Partial<RegistrationState> = {}): RegistrationState => ({
|
||||||
|
...initialRegistrationState,
|
||||||
|
...over,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registrationReducer', () => {
|
||||||
|
it('navigates stages and never goes below 0', () => {
|
||||||
|
expect(registrationReducer(s(), { type: 'NEXT_STAGE' }).stage).toBe(1);
|
||||||
|
expect(registrationReducer(s({ stage: 2 }), { type: 'PREV_STAGE' }).stage).toBe(1);
|
||||||
|
expect(registrationReducer(s({ stage: 0 }), { type: 'PREV_STAGE' }).stage).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a single form field immutably', () => {
|
||||||
|
const next = registrationReducer(s(), { type: 'SET_FIELD', field: 'orgPanNumber', value: 'ABCDE1234F' });
|
||||||
|
expect(next.formData.orgPanNumber).toBe('ABCDE1234F');
|
||||||
|
expect(next.formData.firstName).toBe('');
|
||||||
|
expect(initialRegistrationState.formData.orgPanNumber).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefills contact fields without clobbering others', () => {
|
||||||
|
const next = registrationReducer(s(), {
|
||||||
|
type: 'PREFILL_CONTACT',
|
||||||
|
values: { firstName: 'A', email: 'a@b.c' },
|
||||||
|
});
|
||||||
|
expect(next.formData.firstName).toBe('A');
|
||||||
|
expect(next.formData.email).toBe('a@b.c');
|
||||||
|
expect(next.formData.lastName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs the PAN verification lifecycle', () => {
|
||||||
|
let state = registrationReducer(s(), { type: 'PAN_VERIFY_START' });
|
||||||
|
expect(state.panVerifying).toBe(true);
|
||||||
|
state = registrationReducer(state, { type: 'PAN_VERIFY_DONE', verified: true });
|
||||||
|
expect(state).toMatchObject({ panVerifying: false, panVerified: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the ref number when requesting a new Aadhaar OTP', () => {
|
||||||
|
let state = s({ aadhaarRefNumber: 'old-ref' });
|
||||||
|
state = registrationReducer(state, { type: 'AADHAAR_OTP_REQUEST' });
|
||||||
|
expect(state).toMatchObject({ aadhaarVerifying: true, aadhaarRefNumber: '' });
|
||||||
|
state = registrationReducer(state, { type: 'AADHAAR_OTP_DONE', sent: true, refNumber: 'ref-1' });
|
||||||
|
expect(state).toMatchObject({ aadhaarVerifying: false, aadhaarOtpSent: true, aadhaarRefNumber: 'ref-1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the verified flag untouched on a failed Aadhaar verify', () => {
|
||||||
|
let state = s({ aadhaarVerified: true, aadhaarVerifying: true });
|
||||||
|
state = registrationReducer(state, { type: 'AADHAAR_VERIFY_DONE' });
|
||||||
|
expect(state).toMatchObject({ aadhaarVerifying: false, aadhaarVerified: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores a picked document under its field', () => {
|
||||||
|
const file = { name: 'pan.pdf' };
|
||||||
|
const next = registrationReducer(s(), { type: 'SET_DOCUMENT', field: 'panCard', file });
|
||||||
|
expect((next.documents as any).panCard).toBe(file);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { initialRegistrationState } from '../registrationState';
|
||||||
|
import { validatePanFormat, validateStage } from '../registrationValidation';
|
||||||
|
|
||||||
|
const base = () => ({
|
||||||
|
formData: { ...initialRegistrationState.formData },
|
||||||
|
documents: { ...initialRegistrationState.documents },
|
||||||
|
selectedOrg: 1 as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateStage', () => {
|
||||||
|
it('stage 0 requires an organization name', () => {
|
||||||
|
expect(validateStage(0, base()).valid).toBe(false);
|
||||||
|
const input = base();
|
||||||
|
input.formData.organizationName = 'Acme';
|
||||||
|
expect(validateStage(0, input).valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stage 1 requires fewer fields for individual (org 8)', () => {
|
||||||
|
const individual = base();
|
||||||
|
individual.selectedOrg = 8;
|
||||||
|
individual.formData.orgType = '2';
|
||||||
|
individual.formData.socialCategory = '1';
|
||||||
|
expect(validateStage(1, individual).valid).toBe(true);
|
||||||
|
|
||||||
|
const company = base();
|
||||||
|
company.formData.orgType = '2';
|
||||||
|
company.formData.socialCategory = '1';
|
||||||
|
expect(validateStage(1, company).valid).toBe(false); // missing regno/detail
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stage 2 enforces Aadhaar and OTP lengths', () => {
|
||||||
|
const input = base();
|
||||||
|
Object.assign(input.formData, {
|
||||||
|
orgPanNumber: 'ABCDE1234F',
|
||||||
|
dateOfIncorp: '01/01/2020',
|
||||||
|
orgPanName: 'Acme',
|
||||||
|
aadhaarNumber: '123456789012',
|
||||||
|
aadhaarOtp: '123456',
|
||||||
|
});
|
||||||
|
expect(validateStage(2, input).valid).toBe(true);
|
||||||
|
|
||||||
|
input.formData.aadhaarNumber = '1234';
|
||||||
|
expect(validateStage(2, input)).toMatchObject({
|
||||||
|
valid: false,
|
||||||
|
message: 'Aadhaar number must be 12 digits.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stage 3 enforces the pin code length', () => {
|
||||||
|
const input = base();
|
||||||
|
Object.assign(input.formData, { address: 'x', state: '1', city: '2', pinCode: '12345' });
|
||||||
|
expect(validateStage(3, input)).toMatchObject({
|
||||||
|
valid: false,
|
||||||
|
message: 'Please enter a valid 6-digit pin code.',
|
||||||
|
});
|
||||||
|
input.formData.pinCode = '123456';
|
||||||
|
expect(validateStage(3, input).valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stage 4 requires only PAN + Aadhaar files for individual (org 8)', () => {
|
||||||
|
const input = base();
|
||||||
|
input.selectedOrg = 8;
|
||||||
|
expect(validateStage(4, input).valid).toBe(false);
|
||||||
|
(input.documents as any).panCard = { name: 'pan.pdf' };
|
||||||
|
(input.documents as any).aadharCard = { name: 'aadhaar.pdf' };
|
||||||
|
expect(validateStage(4, input).valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validatePanFormat', () => {
|
||||||
|
it('accepts a valid PAN in any case', () => {
|
||||||
|
expect(validatePanFormat('abcde1234f').valid).toBe(true);
|
||||||
|
expect(validatePanFormat('ABCDE1234F').valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects malformed PANs', () => {
|
||||||
|
expect(validatePanFormat('1234').valid).toBe(false);
|
||||||
|
expect(validatePanFormat('ABCDE12345').valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
ScrollText,
|
ScrollText,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Upload,
|
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import Animated, { FadeInUp } from 'react-native-reanimated';
|
import Animated, { FadeInUp } from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../../Hooks/useTheme';
|
import { useTheme } from '../../../Hooks/useTheme';
|
||||||
@@ -35,7 +34,7 @@ const StageFour: React.FC<UploadDocumentsSectionProps> = ({
|
|||||||
formData,
|
formData,
|
||||||
selectedOrg,
|
selectedOrg,
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, spacing, shadows } = useTheme();
|
const { colors, spacing } = useTheme();
|
||||||
|
|
||||||
const documentItems: DocumentItem[] = [
|
const documentItems: DocumentItem[] = [
|
||||||
{ key: 'panCard', label: 'PAN Card', icon: 'pan' },
|
{ key: 'panCard', label: 'PAN Card', icon: 'pan' },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { View, TouchableOpacity } from 'react-native';
|
import { View, TouchableOpacity } from 'react-native';
|
||||||
import DatePicker from 'react-native-date-picker';
|
import DatePicker from 'react-native-date-picker';
|
||||||
import MyInput from '../../../components/MyInput';
|
import MyInput from '../../../components/MyInput';
|
||||||
import MyDropdown from '../../../Component/Dropdown/MyDropdown';
|
import MyDropdown from '../../../components/MyDropdown';
|
||||||
import { DropdownOption } from '../types/types';
|
import { DropdownOption } from '../types/types';
|
||||||
import { useTheme } from '../../../Hooks/useTheme';
|
import { useTheme } from '../../../Hooks/useTheme';
|
||||||
import AppText from '../../../components/AppText';
|
import AppText from '../../../components/AppText';
|
||||||
@@ -23,8 +23,6 @@ interface Props {
|
|||||||
allOrganizations: DropdownOption[];
|
allOrganizations: DropdownOption[];
|
||||||
allMsmeOrgs: DropdownOption[];
|
allMsmeOrgs: DropdownOption[];
|
||||||
allSocialCategories: DropdownOption[];
|
allSocialCategories: DropdownOption[];
|
||||||
allMSME: DropdownOption[];
|
|
||||||
dropdownStyle: object;
|
|
||||||
setSelectedOrg: (val: number) => void;
|
setSelectedOrg: (val: number) => void;
|
||||||
selectedOrg: string | number;
|
selectedOrg: string | number;
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,6 @@ const StageOne: React.FC<Props> = ({
|
|||||||
allOrganizations,
|
allOrganizations,
|
||||||
allMsmeOrgs,
|
allMsmeOrgs,
|
||||||
allSocialCategories,
|
allSocialCategories,
|
||||||
dropdownStyle,
|
|
||||||
setSelectedOrg,
|
setSelectedOrg,
|
||||||
selectedOrg,
|
selectedOrg,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user