Compare commits

...
19 Commits
Author SHA1 Message Date
Abin ToppoandClaude Opus 4.8 95d8e0a925 perf: project-wide optimization pass + UI/theme fixes
Optimization (audited across src/, typecheck green):
- Crash guards: PurchaseAuction data?.result + stray text-node, AuctionItemDetail
  length>0, LiveAuction user?.contractorId, KYC params guard
- Hoisted render-defined components: Login MinimalInput (keyboard-focus bug),
  TenderFullDetail Section/InfoBox (subtree remount)
- Memoized hot paths: Spreadsheet ColumnHeaderRow/DataRow, list makeStyles,
  shared infinite-query select (stops per-render flatMap)
- Removed hot-path/in-list console logs; SkeletonList 10->1 shimmer

UI/theme: themed AppToast, redesigned sidebar drawer, list empty states,
KYC WebView cleanup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:14:23 +05:30
Abin Toppo 7853bab639 env update
CI / quality (push) Has been cancelled
2026-07-16 11:21:48 +05:30
Abin ToppoandClaude Fable 5 1d7646d938 feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
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>
2026-07-16 10:20:22 +05:30
Abin ToppoandClaude Opus 4.8 9acec328a7 feat: smooth navigation transitions + config updates
- Migrate MainStack from JS stack to native-stack for native-thread,
  jank-free screen transitions (slide_from_right, fade for auth screens)
- Add shift animation to bottom tab switching
- Install @react-navigation/native-stack
- Update API URLs, Axios, Login and Android network security config

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:19:32 +05:30
Abin ToppoandClaude Opus 4.8 44e84ff6f1 chore: remove non-code files from tracking (IDE, logs, generated config)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:15:19 +05:30
Abin ToppoandClaude Opus 4.8 dd672f39f5 chore: push current project state to new repository
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:09:12 +05:30
Abin Toppo 32049d90bd feat: integrate login, registration steps and splash screen changes 2026-05-19 11:23:16 +05:30
Abin Toppo 66461370a2 updated code with dsc and device binding 2026-05-08 13:39:41 +05:30
Abin Toppo c1ecc97e29 with encrypted data on envolope submit 2026-02-05 16:03:26 +05:30
Abin Toppo 499e680404 Merge branch 'testing' of https://git.zeptic.in/rohit/Procurelinc into dev 2026-01-28 16:11:24 +05:30
Abin Toppo 080f1b57cd commit 2026-01-28 16:08:19 +05:30
Jaanu423 d9cb9952ad correction for envelop sumission 2026-01-28 16:07:22 +05:30
Abin Toppo 78a74472dd commit 2026-01-23 11:02:56 +05:30
Jaanu423 76ba2b66f7 development testing 2026-01-19 17:24:08 +05:30
Jaanu423 feb1ee2707 development testing 2026-01-19 17:23:32 +05:30
Abin Toppo 7c949eb570 commit 2026-01-19 17:16:03 +05:30
Abin Toppo b6c927367d commit 2026-01-19 14:16:38 +05:30
Abin Toppo 96d71d1011 Merge branch 'dev' of https://git.zeptic.in/rohit/Procurelinc into dev 2026-01-19 11:53:51 +05:30
Abin Toppo b4b136e801 committed 2026-01-19 11:50:53 +05:30
258 changed files with 26319 additions and 13883 deletions
+31
View File
@@ -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
+20 -1
View File
@@ -27,6 +27,7 @@ DerivedData
build/ build/
.idea .idea
.gradle .gradle
.settings/
local.properties local.properties
*.iml *.iml
*.hprof *.hprof
@@ -35,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/
@@ -46,7 +55,7 @@ yarn-error.log
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed. # screenshots whenever they are needed.
# For more information about the recommended setup visit: # For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/ # http://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml **/fastlane/report.xml
**/fastlane/Preview.html **/fastlane/Preview.html
@@ -66,6 +75,13 @@ yarn-error.log
# testing # testing
/coverage /coverage
# Editor / IDE
.vscode/
# Generated / diagnostic files
*.log
rn-config-output.json
# Yarn # Yarn
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches
@@ -73,3 +89,6 @@ yarn-error.log
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
# Local archives
scripts.zip
+23 -14
View File
@@ -1,39 +1,48 @@
import React from 'react'; import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { LogBox, StatusBar, 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 { 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';
LogBox.ignoreAllLogs(true); import AppStatusBar from './src/components/AppStatusBar';
import { ApiToastBridge, AppToastHost } from './src/components/AppToast';
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 { applyGuardForCurrentRoute } from './src/core/security/screenGuard';
const App = () => { const App = () => {
const queryClient = new QueryClient(); const insets = useSafeAreaInsets();
const insets = useSafeAreaInsets()
global.TextEncoder = TextEncoder;
return ( return (
<NavigationContainer > <ErrorBoundary>
<NavigationContainer
ref={navigationRef}
onReady={applyGuardForCurrentRoute}
onStateChange={applyGuardForCurrentRoute}
>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
<AlertNotificationRoot>
<Provider store={store}> <Provider store={store}>
<PersistGate persistor={persistor}> <PersistGate persistor={persistor}>
<View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}> <View style={{ flex: 1, backgroundColor: colors.white, paddingBottom: insets.bottom }}>
<StatusBar barStyle="dark-content" /> <AppStatusBar />
<AuthSessionGuard />
<Routes /> <Routes />
<ApiToastBridge />
<AppToastHost />
</View> </View>
</PersistGate> </PersistGate>
</Provider> </Provider>
</AlertNotificationRoot>
</GestureHandlerRootView> </GestureHandlerRootView>
</QueryClientProvider> </QueryClientProvider>
</NavigationContainer> </NavigationContainer>
</ErrorBoundary>
); );
}; };
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org' source 'http://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version # You may use http://rbenv.org/ or http://rvm.io/ to install and use this version
ruby ">= 2.6.10" ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures. # Exclude problematic versions of cocoapods and activesupport that causes build failures.
+117 -65
View File
@@ -1,97 +1,149 @@
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://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](https://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](https://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](https://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](https://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](https://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](https://reactnative.dev/docs/troubleshooting) page.
# Learn More
To learn more about React Native, take a look at the following resources:
- [React Native Website](https://reactnative.dev) - learn more about React Native.
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
@@ -1,13 +0,0 @@
arguments=--init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Lincpay\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.51.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=C\:/Program Files/Microsoft/jdk-17.0.14.7-hotspot
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true
+45 -5
View File
@@ -40,7 +40,7 @@ react {
// entryFile = file("../js/MyApplication.android.js") // entryFile = file("../js/MyApplication.android.js")
// //
// A list of extra flags to pass to the 'bundle' commands. // A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle // See http://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = [] // extraPackagerArgs = []
/* Hermes Commands */ /* Hermes Commands */
@@ -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 https://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 {
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Debug-only override: allow all cleartext so Metro / localhost / LAN dev IPs work.
Release uses the restrictive config in src/main/res/xml. -->
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
+3 -1
View File
@@ -5,7 +5,8 @@
<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_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -14,6 +15,7 @@
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:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@@ -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,15 +9,31 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() { class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// FLAG_SECURE fail-closed baseline: the app displays sealed-bid amounts,
// Aadhaar OTPs and other regulated PII. Release builds start fully secured so
// nothing leaks before JS boots (or if the ScreenGuard bridge fails to load).
// Once running, per-screen protection is driven from JS — the central
// controller (core/security/screenGuard.ts, wired via NavigationContainer)
// CLEARS this flag for non-sensitive screens and re-applies it for the
// protected ones. Debug builds start capturable for QA and are likewise
// toggled per screen by the same controller.
if (!BuildConfig.DEBUG) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
/** /**
* Returns the name of the main component registered from JavaScript. This is used to schedule * Returns the name of the main component registered from JavaScript.
* rendering of the component.
*/ */
override fun getMainComponentName(): String = "procure" override fun getMainComponentName(): String = "procure"
/** /**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] * Returns the instance of the [ReactActivityDelegate].
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/ */
override fun createReactActivityDelegate(): ReactActivityDelegate = override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
@@ -11,6 +11,8 @@ 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
import com.procure.security.ScreenGuardPackage
class MainApplication : Application(), ReactApplication { class MainApplication : Application(), ReactApplication {
@@ -18,8 +20,8 @@ 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()) add(ScreenGuardPackage())
} }
override fun getJSMainModuleName(): String = "index" override fun getJSMainModuleName(): String = "index"
@@ -0,0 +1,436 @@
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))
}
// Recompute every formula so its CACHED <v> reflects the new inputs.
// Excel/LibreOffice would recalc on open, but cache-only consumers
// (the web BOQ viewer reads the stored value and never recalculates)
// otherwise see the template's stale result. Per-cell + try/catch so a
// single POI-unsupported function can't blank the whole workbook.
recalculateFormulas(wb)
// Belt-and-suspenders for recalc-capable consumers.
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)
}
}
/**
* Evaluate all formula cells in-place so their cached results are written to
* the file. Runs per-cell in a try/catch: POI cannot evaluate every Excel
* function, and a NotImplemented failure on one cell must not abort the save
* or clear the other results.
*/
private fun recalculateFormulas(wb: Workbook) {
val evaluator = wb.creationHelper.createFormulaEvaluator()
for (sheet in wb) {
for (row in sheet) {
for (cell in row) {
if (cell.cellType == CellType.FORMULA) {
try {
evaluator.evaluateFormulaCell(cell)
} catch (_: Throwable) {
/* leave this cell's cached value as-is */
}
}
}
}
}
}
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()
}
@@ -0,0 +1,35 @@
package com.procure.security
import android.view.WindowManager
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
/**
* ScreenGuard — runtime FLAG_SECURE toggle.
*
* FLAG_SECURE blocks screenshots, screen recording and the app-switcher
* thumbnail for the whole Activity window. The app is single-Activity, so
* flipping the flag as the user navigates gives per-screen protection. WHICH
* screens are protected lives entirely in JS (see core/security/screenGuard.ts);
* this module only applies the decision. Window flags must be mutated on the UI
* thread.
*/
class ScreenGuardModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "ScreenGuard"
@ReactMethod
fun setSecure(enable: Boolean) {
val activity = currentActivity ?: return
activity.runOnUiThread {
val window = activity.window ?: return@runOnUiThread
if (enable) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
}
@@ -0,0 +1,18 @@
package com.procure.security
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
/**
* Registers [ScreenGuardModule] with React Native. Added manually in
* MainApplication.getPackages(); accessible from JS via NativeModules.ScreenGuard.
*/
class ScreenGuardPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
listOf(ScreenGuardModule(reactContext))
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
}
@@ -22,13 +22,13 @@
<selector> <selector>
<!-- <!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I). This file is a copy of abc_edit_text_material (http://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException. The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)' NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR. For more info, see http://bit.ly/3CdLStv (react-native/pull/29452) and http://bit.ly/3nxOMoR.
--> -->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/> <item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/> <item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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>
<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>
+1 -1
View File
@@ -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"
+3 -3
View File
@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
@@ -19,13 +19,13 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# AndroidX package structure to make it clearer which packages are bundled with the # AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # http://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Use this property to specify which architecture you want to build. # Use this property to specify which architecture you want to build.
# You can also override it from the CLI using # You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64 # ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture. # Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in # This will allow you to use TurboModules and the Fabric render in
+4 -4
View File
@@ -7,7 +7,7 @@
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
# https://www.apache.org/licenses/LICENSE-2.0 # http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software # Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, # distributed under the License is distributed on an "AS IS" BASIS,
@@ -57,10 +57,10 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # http://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at http://github.com/gradle/gradle/.
# #
############################################################################## ##############################################################################
@@ -85,7 +85,7 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (http://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
+1 -1
View File
@@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem http://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
+11
View File
@@ -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
+7
View File
@@ -1,6 +1,13 @@
module.exports = { module.exports = {
presets: ['module:@react-native/babel-preset'], presets: ['module:@react-native/babel-preset'],
plugins: [ plugins: [
// Must remain the LAST entry in this list.
'react-native-reanimated/plugin', 'react-native-reanimated/plugin',
], ],
env: {
// Release bundles: strip every console.* so no PII/debug output ships.
production: {
plugins: ['transform-remove-console'],
},
},
}; };
+154
View File
@@ -0,0 +1,154 @@
# 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** (in `PurchaseTenderAllList`) now
navigates to the registered `LiveTenderDetail` route, passing the flat tender
`item` that screen reads from `route.params` — matching `LiveTenderTile`'s
working pattern. It previously targeted an unregistered `'TenderDetail'` route
(a latent bug that no-op'd with a dev warning).
---
## 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.
+363
View File
@@ -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`
+15
View File
@@ -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.
+4 -2
View File
@@ -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 -1
View File
@@ -24,7 +24,7 @@ target 'procure' do
) )
post_install do |installer| post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 # http://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install( react_native_post_install(
installer, installer,
config[:reactNativePath], config[:reactNativePath],
+1 -1
View File
@@ -32,7 +32,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string/> <string>We need your location to securely bind this device for tender bidding verification.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
+5
View File
@@ -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)/',
],
}; };
+10 -2
View File
@@ -1,3 +1,4 @@
const path = require("path");
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config"); const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const defaultConfig = getDefaultConfig(__dirname); const defaultConfig = getDefaultConfig(__dirname);
@@ -5,7 +6,7 @@ const { assetExts, sourceExts } = defaultConfig.resolver;
/** /**
* Metro configuration * Metro configuration
* https://reactnative.dev/docs/metro * http://reactnative.dev/docs/metro
* *
* @type {import('metro-config').MetroConfig} * @type {import('metro-config').MetroConfig}
*/ */
@@ -17,7 +18,14 @@ const config = {
}, },
resolver: { resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"), assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"] sourceExts: [...sourceExts, "svg"],
blockList: [
// Exclude native Android build artifacts that are created/deleted
// during Gradle builds, which crash Metro's file watcher (ENOENT)
/node_modules\/.*\/android\/\.cxx\/.*/,
/node_modules\/.*\/android\/build\/.*/,
/node_modules\/.*\/android\/\.gradle\/.*/,
]
} }
}; };
+3185 -772
View File
File diff suppressed because it is too large Load Diff
+70 -57
View File
@@ -7,80 +7,93 @@
"ios": "react-native run-ios", "ios": "react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "react-native start",
"test": "jest" "test": "jest",
"create:users": "node scripts/create-users.js"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "4.4.1",
"@react-native-documents/picker": "^11.0.3", "@react-native-documents/picker": "11.0.3",
"@react-native-masked-view/masked-view": "^0.3.2", "@react-native-masked-view/masked-view": "0.2.9",
"@react-native-vector-icons/material-icons": "^12.4.0", "@react-native-vector-icons/material-icons": "12.4.0",
"@react-navigation/bottom-tabs": "^7.9.0", "@react-navigation/bottom-tabs": "7.9.0",
"@react-navigation/native": "^7.1.26", "@react-navigation/drawer": "^7.8.1",
"@react-navigation/stack": "^7.6.13", "@react-navigation/native": "^7.1.28",
"@reduxjs/toolkit": "^2.11.2", "@react-navigation/native-stack": "^7.17.3",
"@stomp/stompjs": "^7.2.1", "@react-navigation/stack": "7.6.13",
"@tanstack/react-query": "^5.90.12", "@reduxjs/toolkit": "2.11.2",
"axios": "^1.13.2", "@shopify/flash-list": "^1.7.6",
"buffer": "^6.0.3", "@stomp/stompjs": "7.2.1",
"eslint-config-prettier": "^10.1.8", "@tanstack/react-query": "5.90.12",
"eslint-plugin-react": "^7.37.5", "axios": "1.13.2",
"eslint-plugin-react-native": "^5.0.0", "buffer": "6.0.3",
"exceljs": "^4.4.0", "crypto-js": "^4.2.0",
"formik": "^2.4.9", "eslint-config-prettier": "10.1.8",
"lottie-react-native": "^7.3.4", "eslint-plugin-react": "7.37.5",
"lucide-react-native": "^0.562.0", "eslint-plugin-react-native": "5.0.0",
"moment": "^2.30.1", "exceljs": "^3.4.0",
"formik": "2.4.9",
"hyperformula": "^2.7.1",
"jsencrypt": "^3.5.4",
"lottie-react-native": "7.3.4",
"lucide-react-native": "0.562.0",
"moment": "2.30.1",
"react": "19.0.0", "react": "19.0.0",
"react-native": "0.79.0", "react-native": "0.79.0",
"react-native-alert-notification": "^0.4.2", "react-native-app-intro-slider": "4.0.4",
"react-native-app-intro-slider": "^4.0.4", "react-native-blob-util": "0.24.6",
"react-native-blob-util": "^0.24.6", "react-native-bouncy-checkbox": "4.1.2",
"react-native-bouncy-checkbox": "^4.1.2", "react-native-date-picker": "5.0.13",
"react-native-date-picker": "^5.0.13", "react-native-device-info": "^15.0.2",
"react-native-element-dropdown": "^2.12.4", "react-native-element-dropdown": "2.12.4",
"react-native-fs": "^2.20.0", "react-native-fs": "2.20.0",
"react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "^2.30.0", "react-native-gesture-handler": "^2.30.0",
"react-native-gifted-charts": "^1.4.70", "react-native-get-random-values": "1.11.0",
"react-native-haptic-feedback": "^2.3.3", "react-native-gifted-charts": "1.4.70",
"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",
"react-native-reanimated": "^3.18.0", "react-native-reanimated": "^3.18.0",
"react-native-responsive-screen": "^1.4.2", "react-native-responsive-screen": "1.4.2",
"react-native-safe-area-context": "^5.6.2", "react-native-safe-area-context": "5.6.2",
"react-native-screens": "^4.12.0", "react-native-screens": "4.12.0",
"react-native-skeleton-placeholder": "^5.2.4", "react-native-skeleton-placeholder": "5.2.4",
"react-native-svg": "^15.12.0", "react-native-svg": "15.12.0",
"react-native-svg-transformer": "^1.5.1", "react-native-svg-transformer": "1.5.1",
"react-native-webview": "^13.16.0", "react-native-webview": "13.16.0",
"react-redux": "^9.2.0", "react-redux": "9.2.0",
"redux-persist": "^6.0.0", "redux-persist": "6.0.0",
"sockjs-client": "^1.6.1", "sockjs-client": "1.6.1",
"text-encoding": "^0.7.0", "text-encoding": "0.7.0",
"uuid": "13.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yup": "^1.7.1" "yup": "1.7.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "7.25.2",
"@babel/preset-env": "^7.25.3", "@babel/preset-env": "7.25.3",
"@babel/runtime": "^7.25.0", "@babel/runtime": "7.25.0",
"@react-native-community/cli": "^18.0.1", "@react-native-community/cli": "18.0.1",
"@react-native-community/cli-platform-android": "18.0.0", "@react-native-community/cli-platform-android": "^18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0", "@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.0", "@react-native/babel-preset": "0.79.0",
"@react-native/eslint-config": "0.79.0", "@react-native/eslint-config": "0.79.0",
"@react-native/metro-config": "0.79.0", "@react-native/metro-config": "0.79.0",
"@react-native/typescript-config": "0.79.0", "@react-native/typescript-config": "0.79.0",
"@types/jest": "^29.5.13", "@types/crypto-js": "^4.2.2",
"@types/react": "^19.0.0", "@types/jest": "29.5.13",
"@types/react-test-renderer": "^19.0.0", "@types/react": "19.0.0",
"@types/sockjs-client": "^1.5.4", "@types/react-test-renderer": "19.0.0",
"@types/text-encoding": "^0.0.40", "@types/sockjs-client": "1.5.4",
"eslint": "^8.19.0", "@types/text-encoding": "0.0.40",
"jest": "^29.6.3", "babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^8.57.0",
"jest": "29.6.3",
"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",
"typescript": "5.0.4" "typescript": "5.0.4"
}, },
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
const axios = require('axios');
const { BASE_URL } = require('../src/Services/ApiCall/ApiUrls');
const REGISTER_URL =
`${BASE_URL}contractor/registerContractorStage1`;
const DESIGNATION_URL =
`${BASE_URL}contractor/getAllDesignationContractor`;
const DEFAULT_PASSWORD = 'P@ssword123';
function getArgValue(name, fallback) {
const prefix = `--${name}=`;
const arg = process.argv.find(item => item.startsWith(prefix));
if (!arg) return fallback;
return arg.slice(prefix.length);
}
function toPositiveInt(value, fallback) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) return fallback;
return parsed;
}
function buildMobile(seed, index) {
// Keep 10 digits and start with 9 to match app-side validation.
const dynamicPart = (seed * 1000 + index) % 999999999;
return String(9000000000 + dynamicPart).padStart(10, '9').slice(0, 10);
}
function toAlphabeticSuffix(index) {
let n = index;
let label = '';
while (n > 0) {
const rem = (n - 1) % 26;
label = String.fromCharCode(65 + rem) + label;
n = Math.floor((n - 1) / 26);
}
return label || 'A';
}
async function getDefaultDesignation() {
try {
const { data } = await axios.get(DESIGNATION_URL, { timeout: 15000 });
const list = data?.data;
if (Array.isArray(list) && list.length > 0) {
const firstId = Number(list[0]?.designationid);
if (Number.isFinite(firstId) && firstId > 0) return firstId;
}
} catch (error) {
const message = error?.response?.data?.message || error?.message || 'Unknown error';
console.warn(`Failed to fetch designation list, using fallback id=1. ${message}`);
}
return 1;
}
async function registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
}) {
const serial = seed * 1000 + index;
const emailAdd = `${emailPrefix}${serial}@example.com`;
const alphaSuffix = toAlphabeticSuffix(index);
const payload = {
firstName: `${firstNamePrefix}${alphaSuffix}`,
middleName: '',
lastName: 'Auto',
dialCode: '+91',
mobNo: buildMobile(seed, index),
emailAdd,
contractorType: 'Indian',
designation,
password,
otherDesignation: null,
conPassword: password,
device: 'mobileApp',
};
try {
const { data } = await axios.post(REGISTER_URL, payload, { timeout: 20000 });
const status = String(data?.status ?? data?.statusCode ?? '');
const ok = status === '201' || status === '200' || status === 'SUCCESS';
return {
ok,
email: emailAdd,
mobile: payload.mobNo,
message: data?.message || (ok ? 'Success' : 'Unexpected response'),
rawStatus: status || 'unknown',
details: data,
};
} catch (error) {
const responseData = error?.response?.data;
const apiMessage =
responseData?.message ||
responseData?.error ||
error?.message ||
'Request failed';
return {
ok: false,
email: emailAdd,
mobile: payload.mobNo,
message: apiMessage,
rawStatus: String(error?.response?.status || 'network_error'),
details: responseData,
};
}
}
async function main() {
const count = toPositiveInt(getArgValue('count', '100'), 100);
const concurrency = toPositiveInt(getArgValue('concurrency', '5'), 5);
const startIndex = toPositiveInt(getArgValue('start', '1'), 1);
const password = getArgValue('password', DEFAULT_PASSWORD);
const emailPrefix = getArgValue('emailPrefix', 'bulkuser');
const firstNamePrefix = getArgValue('firstNamePrefix', 'BulkUser');
const seed = Number(String(Date.now()).slice(-6));
const designation = await getDefaultDesignation();
console.log(
`Creating ${count} users (start=${startIndex}, concurrency=${concurrency}, designation=${designation})...`,
);
const indexes = Array.from({ length: count }, (_, i) => i + startIndex);
let cursor = 0;
const results = [];
async function worker() {
while (true) {
const current = cursor;
cursor += 1;
if (current >= indexes.length) break;
const index = indexes[current];
const result = await registerUser({
designation,
emailPrefix,
firstNamePrefix,
seed,
index,
password,
});
results.push(result);
const marker = result.ok ? 'OK' : 'FAIL';
console.log(`[${marker}] #${index} ${result.email} -> ${result.message}`);
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()));
const success = results.filter(item => item.ok).length;
const failed = results.length - success;
console.log('\nSummary');
console.log(`Total: ${results.length}`);
console.log(`Success: ${success}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
console.log('\nFailed entries (up to 20):');
results
.filter(item => !item.ok)
.slice(0, 20)
.forEach(item => {
console.log(`- ${item.email} (${item.mobile}) [${item.rawStatus}] ${item.message}`);
if (item.details) {
console.log(` details: ${JSON.stringify(item.details)}`);
}
});
process.exitCode = 1;
}
}
main().catch(error => {
console.error('Bulk user creation crashed:', error?.message || error);
process.exit(1);
});
+6 -3
View File
@@ -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).
+33 -24
View File
@@ -1,6 +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 { logger } from '../core/observability/logger';
import { attachDevHttpLogging } from '../core/observability/httpLogging';
// A few screens still issue raw axios.* calls; keep them traced in DEV.
attachDevHttpLogging(axios);
export const apiCall = async <R, D = {}>( export const apiCall = async <R, D = {}>(
method: 'get' | 'post' | 'patch' | 'delete' | 'put', method: 'get' | 'post' | 'patch' | 'delete' | 'put',
@@ -10,39 +27,31 @@ export const apiCall = async <R, D = {}>(
contentType?: string contentType?: string
): Promise<R> => { ): Promise<R> => {
try { try {
const token: any = store.getState().token.token; 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') {
const errorMessage = error?.response?.data?.error; // Session clearing happens via the auth/unauthorized event → store.
// console.log(error?.response?.data.error, '---------------------------api error message'); logger.warn('auth', 'JWT expired or malformed — session cleared');
if (error?.response?.statusCode === 401) {
store.dispatch(setToken({}));
return { return {
success: false, success: false,
message: 'JWT Error - Token expired or malformed', message: 'JWT Error - Token expired or malformed',
} as R; } as R;
} }
const data = { const body =
data: { typeof error.data === 'object' && error.data !== null ? (error.data as object) : {};
return {
success: false, success: false,
message: error?.response?.data?.message || 'No Response From Server', message: (body as { message?: string }).message || 'No Response From Server',
...body,
} as R;
} }
return { success: false, message: 'No Response From Server' } as R;
}
return data as any
} }
}; };
+58
View File
@@ -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' });
});
});
-163
View File
@@ -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,
},
});
-45
View File
@@ -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,
},
});
-137
View File
@@ -1,137 +0,0 @@
import React from 'react';
import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
import { Shield } from 'lucide-react-native';
const colors = {
primary: '#F97316',
primaryLight: '#FDBA74',
primaryHighlight: '#f4f1efff',
secondary: '#1B4B66',
secondaryLight: '#639599',
success: '#22C55E',
error: '#EF4444',
warning: '#F59E0B',
info: '#3B82F6',
golden: '#FFD700',
white: '#FFFFFF',
black: '#000000',
darkText: '#2B2B2B',
grayText: '#6B7280',
lightText: '#9CA3AF',
border: '#E5E7EB',
lightGray: '#F9FAFB',
disabled: '#D1D5DB',
placeholder: '#9CA3AF',
backdrop: 'rgba(0,0,0,0.5)',
shadow: 'rgba(0,0,0,0.12)',
Orange: '#F97316',
OrangeLight: '#FDBA74',
OrangeSoft: '#FFF3E8',
darkGray: '#5f6368',
lightestGray: '#f1f3f4',
successLight: '#e6f4ea',
};
type DropdownItem = {
[key: string]: any;
};
type MyDropdownProps = {
label?: string;
data: DropdownItem[];
placeholder?: string;
labelField: string;
valueField: string;
value: any;
onChange: (item: DropdownItem) => void;
style?: StyleProp<ViewStyle>;
containerStyle?: StyleProp<ViewStyle>;
placeholderStyle?: StyleProp<TextStyle>;
selectedTextStyle?: StyleProp<TextStyle>;
focusedField?: boolean;
onFocus?: () => void;
onBlur?: () => void;
dropdownStyle?: StyleProp<ViewStyle>;
[key: string]: any;
};
const MyDropdown: React.FC<MyDropdownProps> = ({
label,
data,
placeholder = 'Select an option',
labelField,
valueField,
value,
onChange,
style,
containerStyle,
placeholderStyle,
selectedTextStyle,
focusedField,
onFocus,
onBlur,
dropdownStyle,
...rest
}) => {
return (
<View style={[{ zIndex: 4000 }, containerStyle]}>
{label && <Text style={styles.label}>{label}</Text>}
<Dropdown
data={data}
labelField={labelField}
valueField={valueField}
placeholder={placeholder}
value={value}
onChange={onChange}
style={[
styles.dropdown,
style,
{ borderColor: focusedField ? colors.primary : '#e0e0e0' },
]}
containerStyle={[styles.dropdownContainer, dropdownStyle]}
selectedTextStyle={[styles.selectedText, selectedTextStyle]}
placeholderStyle={[styles.placeholderText, placeholderStyle]}
onFocus={onFocus}
onBlur={onBlur}
search
searchPlaceholder="Search Class"
renderLeftIcon={() => (
<Shield
style={{ marginRight: 10 }}
color={focusedField ? colors.primary : '#aaa'}
size={20}
/>
)}
{...rest}
/>
</View>
);
};
export default MyDropdown;
const styles = StyleSheet.create({
label: {
marginVertical: 10,
color: colors.darkText,
},
dropdown: {
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 12,
borderColor: colors.border,
backgroundColor: colors.white,
},
dropdownContainer: {
borderRadius: 12,
borderColor: colors.border,
},
selectedText: {
color: colors.darkText,
},
placeholderText: {
color: colors.placeholder,
},
});
-90
View File
@@ -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,
},
});
-6
View File
@@ -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,380 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, ScrollView, StatusBar } 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 (
<>
<StatusBar barStyle="dark-content" />
<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,
},
});
-106
View File
@@ -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,75 +0,0 @@
import { StyleSheet, Text, TouchableOpacity, 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 (
<LinearGradient
colors={[
colors.primary, // #F97316 (Orange)
colors.primaryLight, // #FDBA74 (OrangeLight)
colors.OrangeLight, // #FDBA74 (backup)
colors.OrangeSoft // #FFF3E8 (super soft orange end)
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
locations={[0.0, 0.4, 0.7, 1.0]}
style={[styles.primaryButton, style]}
>
<TouchableOpacity
style={styles.touchable}
onPress={onPress}
activeOpacity={0.82}
disabled={disabled}
>
<Text style={styles.buttonText}>{title}</Text>
</TouchableOpacity>
</LinearGradient>
);
};
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,
},
});
-257
View File
@@ -1,257 +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={() => {
console.log('sdfadsfdsf');
}}>
<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',
},
});
-34
View File
@@ -1,34 +0,0 @@
import { View, Dimensions } from 'react-native';
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
const Skeleton = () => {
const { width } = Dimensions.get('screen');
const cardWidth = width - 30; // full width with some padding
return (
<View style={{ padding: 10 }}>
{[...Array(6)].map((_, index) => (
<SkeletonPlaceholder key={index}>
<SkeletonPlaceholder.Item
width={cardWidth}
marginBottom={20}
borderRadius={12}
overflow="hidden"
>
{/* Image */}
<SkeletonPlaceholder.Item width={cardWidth} height={150} borderRadius={12} />
{/* Title */}
<SkeletonPlaceholder.Item marginTop={10} width={cardWidth * 0.7} height={18} borderRadius={6} />
{/* Subtitle / Price */}
<SkeletonPlaceholder.Item marginTop={6} width={cardWidth * 0.5} height={14} borderRadius={6} />
</SkeletonPlaceholder.Item>
</SkeletonPlaceholder>
))}
</View>
);
};
export default Skeleton;
-19
View File
@@ -1,19 +0,0 @@
import { View, Dimensions } from 'react-native';
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
const SkeltonList = () => {
const { width, height } = Dimensions.get('screen');
return (
<View style={{ width: width - 30, height }}>
{[...Array(10)].map((_, index) => (
<SkeletonPlaceholder key={index} borderRadius={5}>
<SkeletonPlaceholder.Item width={"100%"} height={50} marginBottom={12} />
</SkeletonPlaceholder>
))}
</View>
);
};
export default SkeltonList;
-109
View File
@@ -1,109 +0,0 @@
import React, { useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Animated,
} from 'react-native';
import colors from '../Constant/Color';
type StepProgressBarProps = {
currentStep: number;
steps: string[];
onStepPress?: (stepIndex: number) => void;
};
const StepProgressBar: React.FC<StepProgressBarProps> = ({
currentStep,
steps,
onStepPress = () => {},
}) => {
const animations = useRef<Animated.Value[]>(
steps.map(() => new Animated.Value(1))
).current;
useEffect(() => {
animations.forEach((anim, index) => {
Animated.timing(anim, {
toValue: index === currentStep ? 1.2 : 1,
duration: 300,
useNativeDriver: true,
}).start();
});
}, [currentStep]);
return (
<View style={styles.container}>
{steps.map((label, index) => {
const isActive = index === currentStep;
const isCompleted = index < currentStep;
return (
<TouchableOpacity
key={index}
onPress={() => onStepPress(index)}
style={styles.stepContainer}
activeOpacity={0.8}
>
<Animated.View
style={[
styles.circle,
isCompleted && styles.completedCircle,
isActive && styles.activeCircle,
{
transform: [{ scale: animations[index] }],
},
]}
/>
<Text
style={[
styles.label,
isCompleted && { color: '#4caf50' },
isActive && { color: '#2196f3' },
]}
>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
};
export default StepProgressBar;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 20,
flexWrap: 'wrap',
justifyContent: 'center',
},
stepContainer: {
alignItems: 'center',
width: 70,
},
circle: {
width: 50,
height: 5,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.primary,
},
completedCircle: {
backgroundColor: '#4caf50',
},
activeCircle: {
backgroundColor: '#2196f3',
},
label: {
// marginTop: 6,
// fontSize: 12,
textAlign: 'center',
color: '#555',
},
});
@@ -1,201 +0,0 @@
import React, { ReactNode } from 'react';
import { View, Text, Modal, ScrollView, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import { CheckCircle, X } from 'lucide-react-native'; // Updated icons
import colors from '../../Constant/Color';
type ModalProps = {
visible: boolean;
onClose: () => void;
onChecked: () => void
}
const TermsAndConditionsModal: React.FC<ModalProps> = ({ visible, onClose, onChecked }) => {
return (
<Modal
animationType="slide"
transparent={false}
visible={visible}
onRequestClose={onClose}
>
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.header}>
<Text style={styles.title}>Terms & Conditions</Text>
<Text style={styles.subtitle}>Last updated: {new Date().toLocaleDateString()}</Text>
</View>
<View style={styles.content}>
<Section title="1. Acceptance of Terms">
<Text style={styles.paragraph}>
By accessing or using our services, you agree to be bound by these Terms and Conditions.
If you do not agree with any part of these terms, you must not use our services.
</Text>
</Section>
<Section title="2. User Responsibilities">
<Text style={styles.paragraph}>
You are responsible for maintaining the confidentiality of your account information and
for all activities that occur under your account. You agree to notify us immediately
of any unauthorized use of your account.
</Text>
</Section>
<Section title="3. Intellectual Property">
<Text style={styles.paragraph}>
All content included on our platform, such as text, graphics, logos, and software, is
the property of our company and protected by intellectual property laws. You may not
reproduce, distribute, or create derivative works without our express permission.
</Text>
</Section>
<Section title="4. Limitation of Liability">
<Text style={styles.paragraph}>
Our company shall not be liable for any indirect, incidental, special, or consequential
damages resulting from the use or inability to use our services, even if we have been
advised of the possibility of such damages.
</Text>
</Section>
<Section title="5. Privacy Policy">
<Text style={styles.paragraph}>
Your use of our services is also governed by our Privacy Policy, which explains how we
collect, use, and protect your personal information. Please review our Privacy Policy
to understand our practices.
</Text>
</Section>
<Section title="6. Changes to Terms">
<Text style={styles.paragraph}>
We reserve the right to modify these terms at any time. We will notify you of any changes
by posting the new Terms and Conditions on this page. Your continued use of the service
after such modifications constitutes your acceptance of the new terms.
</Text>
</Section>
<Section title="7. Governing Law">
<Text style={styles.paragraph}>
These Terms shall be governed by and construed in accordance with the laws of the
jurisdiction in which our company is registered, without regard to its conflict of law provisions.
</Text>
</Section>
<Text style={styles.footerText}>
If you have any questions about these Terms, please contact us at legal@company.com.
</Text>
</View>
{/* Accept Button */}
<TouchableOpacity
style={styles.acceptButton}
onPress={() => {
onClose();
onChecked();
}}
>
<CheckCircle color={colors.white} size={28} />
<Text style={styles.acceptButtonText}>I Accept</Text>
</TouchableOpacity>
</ScrollView>
</View>
</Modal>
);
};
// Reusable Section component
type SectionProps = {
title: string;
children: ReactNode;
};
const Section: React.FC<SectionProps> = ({ title, children }) => (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
{children}
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.lightGray, // Updated with your colors
},
scrollContainer: {
paddingBottom: 120, // Space for accept button
flexGrow: 1,
},
header: {
backgroundColor: colors.primary, // Orange header
paddingVertical: 32,
paddingHorizontal: 24,
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
marginBottom: 24,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
},
title: {
fontSize: 28,
fontWeight: '800',
color: colors.white,
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: 'rgba(255,255,255,0.85)',
},
content: {
paddingHorizontal: 24,
paddingBottom: 40,
},
section: {
marginBottom: 28,
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: colors.darkText,
marginBottom: 12,
},
paragraph: {
fontSize: 16,
lineHeight: 25,
color: colors.grayText,
},
footerText: {
fontSize: 15,
fontStyle: 'italic',
color: colors.lightText,
marginTop: 24,
textAlign: 'center',
lineHeight: 22,
},
acceptButton: {
position: 'absolute',
bottom: 40,
left: 24,
right: 24,
backgroundColor: colors.primary,
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 18,
paddingHorizontal: 20,
shadowColor: colors.shadow,
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 16,
elevation: 12,
},
acceptButtonText: {
color: colors.white,
fontSize: 18,
fontWeight: '700',
marginLeft: 12,
letterSpacing: 0.5,
},
});
export default TermsAndConditionsModal;
-10
View File
@@ -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};
-69
View File
@@ -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
View File
@@ -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',
+7 -1
View File
@@ -6,6 +6,7 @@ import {
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Pressable, Pressable,
ActivityIndicator,
} from 'react-native'; } from 'react-native';
import colors from '../Color'; import colors from '../Color';
@@ -17,6 +18,7 @@ type ConfirmDialogProps = {
cancelText?: string; cancelText?: string;
onConfirm: () => void; onConfirm: () => void;
onClose: () => void; onClose: () => void;
loading:boolean
}; };
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
@@ -27,6 +29,7 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
cancelText = 'Cancel', cancelText = 'Cancel',
onConfirm, onConfirm,
onClose, onClose,
loading,
}) => { }) => {
return ( return (
<Modal <Modal
@@ -59,7 +62,10 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
onClose(); onClose();
}} }}
> >
<Text style={styles.confirmText}>{confirmText}</Text> { !loading ?<Text style={styles.confirmText}>{confirmText}</Text>:<ActivityIndicator
color={colors.primary}
size={30}
/>}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Pressable> </Pressable>
+12 -3
View File
@@ -5,9 +5,12 @@ import Diamond from '../Images/diamond.svg'
import Hill from '../Images/Svg/Hill.svg' import Hill from '../Images/Svg/Hill.svg'
const Logo = require('../Images/logo.png') const Logo = require('../Images/logo.png')
const Logo2 = require('../Images/logo2.png') const Logo2 = require('../Images/logo2.png')
const Icon = { const Icon = {
Logo, Logo,
Logo2 Logo2
} }
const ImagePath = { const ImagePath = {
Icon, Icon,
@@ -15,7 +18,13 @@ const ImagePath = {
OnBoard2, OnBoard2,
OnBoard3, OnBoard3,
Diamond, Diamond,
Hill Hill,
DarkBg: require('../Images/Backgrounds/darkBackground.png'),
LightBg: require('../Images/Backgrounds/lightBackground.png'),
DarkBg2: require('../Images/Backgrounds/darkBackground2.png'),
LightBg2: require('../Images/Backgrounds/lightBackground2.png'),
// Home screen backdrop (cream node-constellation art). Light theme only.
HomeBg: require('../Images/Backgrounds/homeBackground.png'),
} }
export default ImagePath; export default ImagePath;
+7 -3
View File
@@ -21,7 +21,11 @@ const NavigationString = {
LiveTenderDetail:'LiveTenderDetail', LiveTenderDetail:'LiveTenderDetail',
AuctionDetail: 'AuctionDetail', AuctionDetail: 'AuctionDetail',
Profile:'Profile', Profile:'Profile',
ViewTenderDetail:'ViewTenderDetail', ResetPassword:'ResetPassword',
ResetPassword:'ResetPassword' KycWebViewScreen:'KycWebViewScreen',
} StageThreeScreen:'StageThree',
DeviceBinding: 'DeviceBinding',
DscBinding: 'DscBinding',
EnvelopeCSpreadsheet: 'EnvelopeCSpreadsheet'
} as const
export default NavigationString export default NavigationString
+6 -19
View File
@@ -1,26 +1,13 @@
import { ALERT_TYPE, Dialog, Root, Toast } from 'react-native-alert-notification'; import { showAppToast } from '../components/AppToast';
const showSuccessAlert = (text: string) => { const showSuccessAlert = (text: string) => {
Toast.show({ showAppToast({ type: 'success', message: text });
type: ALERT_TYPE.SUCCESS,
title: 'Success',
textBody: text,
});
}; };
const showErrorAlert = (text: string) => { const showErrorAlert = (text: string) => {
Toast.show({ showAppToast({ type: 'error', message: text });
type: ALERT_TYPE.DANGER,
title: 'Error',
textBody: text,
});
}; };
const showToast = (text: string) => { const showToast = showSuccessAlert;
Toast.show({
type: ALERT_TYPE.SUCCESS, export { showErrorAlert, showSuccessAlert, showToast };
title: 'Success',
textBody: text,
});
}
export { showErrorAlert, showSuccessAlert, showToast }
+12 -4
View File
@@ -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;
-9
View File
@@ -1,9 +0,0 @@
import { Platform, ToastAndroid } from "react-native";
export const showToast = (message: string) => {
if (Platform.OS === 'android') {
ToastAndroid.show(message, ToastAndroid.SHORT);
} else {
console.log(message); // Fallback for iOS or other platforms
}
};
+15
View File
@@ -0,0 +1,15 @@
export const generateTransactionId = () => {
const now = new Date();
const dateTime =
now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
const randomPart = Math.floor(1000 + Math.random() * 9000); // 4-digit random
return `TNX${dateTime}${randomPart}`;
};
-15
View File
@@ -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};
};
+32
View File
@@ -0,0 +1,32 @@
import { useColorScheme } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { lightColors, darkColors, spacing, typography, shadows } from '../theme';
import { selectThemeMode, setThemeMode } from '../Redux/Slices/ThemeSlice';
export const useTheme = () => {
const systemScheme = useColorScheme();
const themeMode = useSelector(selectThemeMode);
const dispatch = useDispatch();
const isDarkMode =
themeMode === 'system'
? systemScheme === 'dark'
: themeMode === 'dark';
const themeColors = isDarkMode ? darkColors : lightColors;
const toggleTheme = (mode: 'light' | 'dark' | 'system') => {
dispatch(setThemeMode(mode));
};
return {
colors: themeColors,
isDarkMode,
themeMode,
toggleTheme,
spacing,
typography,
shadows,
};
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+32
View File
@@ -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;
+82 -121
View File
@@ -1,156 +1,103 @@
import React, { useEffect, useRef } from 'react'; import React from 'react';
import { import { View, TouchableOpacity, StyleSheet, Text } from 'react-native';
View,
Text,
TouchableOpacity,
StyleSheet,
Dimensions,
Animated,
} from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import * as LucideIcons from 'lucide-react-native'; import * as LucideIcons from 'lucide-react-native';
import Animated, { ZoomIn } from 'react-native-reanimated';
import Screens from '../Screens'; import Screens from '../Screens';
import colors from '../Constant/Color';
import NavigationString from '../Constant/NavigationString';
import EditableExcel from '../Screens/Tender/Component/EnvelopeCExcelPreview';
const { width } = Dimensions.get('window'); import NavigationString from '../Constant/NavigationString';
const TAB_BAR_HEIGHT = 60; import { useTheme } from '../Hooks/useTheme';
const TAB_BUTTON_WIDTH = width / 4; import { typography } from '../theme/typography';
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
const TabButton = ({ route, isFocused, onPress }: any) => { const ICON_BY_ROUTE: Record<string, keyof typeof LucideIcons> = {
const icons: Record<string, { outline: any; filled: any }> = { Home: 'House',
Home: { outline: "Home", filled: "Home" }, Auction: 'Gavel',
Auction: { outline: "Gavel", filled: "Gavel" }, Tender: 'FileText',
Tender: { outline: "FileText", filled: "FileText" }, Profile: 'User',
Profile: { outline: "User", filled: "User" }, };
};
const IconName = LucideIcons[isFocused ? icons[route.name].filled : icons[route.name].outline];
const scaleAnim = useRef(new Animated.Value(1)).current;
const translateYAnim = useRef(new Animated.Value(0)).current;
const opacityAnim = useRef(new Animated.Value(1)).current;
useEffect(() => { const TabButton = ({ route, isFocused, onPress }: any) => {
if (isFocused) { const { colors } = useTheme();
Animated.parallel([ const IconName = (LucideIcons[ICON_BY_ROUTE[route.name] || 'HelpCircle'] as React.ElementType);
Animated.sequence([
Animated.timing(translateYAnim, {
toValue: -8,
duration: 120,
useNativeDriver: true,
}),
Animated.spring(translateYAnim, {
toValue: 0,
friction: 3,
tension: 80,
useNativeDriver: true,
}),
]),
Animated.spring(scaleAnim, {
toValue: 1.3,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.spring(scaleAnim, {
toValue: 1,
friction: 5,
tension: 150,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.6,
duration: 150,
useNativeDriver: true,
}),
]).start();
}
}, [isFocused]);
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
style={[styles.tabButton, { width: TAB_BUTTON_WIDTH }]} style={styles.tabButton}
activeOpacity={0.8} activeOpacity={0.85}
accessibilityRole="tab"
accessibilityState={{ selected: isFocused }}
accessibilityLabel={`${route.name} tab`}
> >
{isFocused ? (
// Active: a solid brand pill that pops in with icon + label.
<Animated.View <Animated.View
style={[ entering={ZoomIn.duration(220)}
styles.iconContainer, style={[styles.activePill, { backgroundColor: colors.primary }]}
{
transform: [{ scale: scaleAnim }, { translateY: translateYAnim }],
opacity: opacityAnim,
},
]}
> >
{IconName && ( {IconName && <IconName size={20} color="#FFFFFF" strokeWidth={2.4} />}
<IconName
size={20}
color={isFocused ? colors.primary : '#888'}
strokeWidth={2}
/>
)}
</Animated.View>
<Text <Text
style={[ style={[styles.activeLabel, { fontFamily: typography.fonts.medium }]}
styles.label, numberOfLines={1}
{
color: isFocused ? colors.primary : '#888',
fontWeight: isFocused ? '600' : '400',
},
]}
> >
{route.name === 'BrandDay' ? 'Coupons' : route.name} {route.name}
</Text> </Text>
</Animated.View>
) : (
// Inactive: muted icon only.
IconName && (
<IconName size={24} color={colors.textSecondary} strokeWidth={2} />
)
)}
</TouchableOpacity> </TouchableOpacity>
); );
}; };
const CustomTabBar = ({ state, descriptors, navigation }: any) => { const CustomTabBar = ({ state, navigation }: any) => {
return ( const { colors } = useTheme();
<View style={styles.tabBar}>
{state.routes.map((route: any, index: number) => {
const isFocused = state.index === index;
return ( return (
<View
style={[
styles.tabBar,
{
backgroundColor: colors.surface,
borderColor: colors.border,
shadowColor: colors.shadow,
},
]}
>
{state.routes.map((route: any, index: number) => (
<TabButton <TabButton
key={route.key} key={route.key}
route={route} route={route}
isFocused={isFocused} isFocused={state.index === index}
onPress={() => navigation.navigate(route.name)} onPress={() => navigation.navigate(route.name)}
/> />
); ))}
})}
</View> </View>
); );
}; };
export default function BottomTab() { export default function BottomTab() {
const { colors } = useTheme();
return ( return (
<View style={styles.container}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<Tab.Navigator <Tab.Navigator
tabBar={(props) => <CustomTabBar {...props} />} tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{ screenOptions={{
headerShown: false, headerShown: false,
tabBarHideOnKeyboard: true, tabBarHideOnKeyboard: true,
// Smooth cross-fade when switching tabs instead of an instant cut.
animation: 'shift',
}} }}
> >
<Tab.Screen name="Home" component={Screens.Home} /> <Tab.Screen name="Home" component={Screens.Home} />
<Tab.Screen name={NavigationString.Auction} component={Screens.Auction} /> <Tab.Screen name={NavigationString.Auction} component={Screens.Auction} />
<Tab.Screen name={NavigationString.Tender} component={Screens.Tender} /> <Tab.Screen name={NavigationString.Tender} component={Screens.Tender} />
{/* <Tab.Screen name={'EditableExcel'} component={EditableExcel} /> */}
<Tab.Screen name={NavigationString.Profile} component={Screens.ProfileTab} /> <Tab.Screen name={NavigationString.Profile} component={Screens.ProfileTab} />
</Tab.Navigator> </Tab.Navigator>
</View> </View>
); );
@@ -159,29 +106,43 @@ export default function BottomTab() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#fff',
}, },
tabBar: { tabBar: {
position: 'absolute',
bottom: 24,
left: 20,
right: 20,
flexDirection: 'row', flexDirection: 'row',
height: TAB_BAR_HEIGHT, height: 66,
backgroundColor: '#fff', borderRadius: 33,
borderTopWidth: 1, borderWidth: StyleSheet.hairlineWidth,
borderTopColor: '#eee', alignItems: 'center',
elevation: 5, justifyContent: 'space-around',
paddingHorizontal: 8,
// Soft floating shadow (iOS) + elevation (Android).
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.15,
shadowRadius: 16,
elevation: 12,
zIndex: 9999,
}, },
tabButton: { tabButton: {
justifyContent: 'center', flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
height: '100%', height: '100%',
}, },
iconContainer: { activePill: {
marginBottom: 4, flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
gap: 6,
paddingHorizontal: 16,
paddingVertical: 9,
borderRadius: 22,
}, },
label: { activeLabel: {
fontSize: 11, color: '#FFFFFF',
textAlign: 'center', fontSize: 13,
includeFontPadding: false, letterSpacing: 0.2,
}, },
}); });
-52
View File
@@ -1,52 +0,0 @@
import * as React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import NavigationString from '../Constant/NavigationString';
import Screens from '../Screens';
import colors from '../Constant/Color';
import BottomTab from './Bottom';
export default function MainStack() {
const Stack = createStackNavigator();
const option = { headerShown: false };
const fadeTransition = {
gestureDirection: 'horizontal',
transitionSpec: {
open: { animation: 'timing', config: { duration: 300 } },
close: { animation: 'timing', config: { duration: 300 } },
},
cardStyleInterpolator: ({ current }) => ({
cardStyle: {
opacity: current.progress,
},
}),
};
return (
<Stack.Navigator
screenOptions={{
cardStyle: { backgroundColor: colors.white },
...fadeTransition
}}
>
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={option} />
<Stack.Screen name={NavigationString.Signup} component={Screens.Signup} options={option} />
<Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} options={option} />
<Stack.Screen name={NavigationString.Login} component={Screens.Login} options={option} />
<Stack.Screen name={NavigationString.Home} component={BottomTab} options={option} />
<Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} options={option} />
<Stack.Screen name={NavigationString.Emd} component={Screens.Emd} options={option} />
<Stack.Screen name={NavigationString.StartBidding} component={Screens.StartBidding} options={option} />
<Stack.Screen name={NavigationString.PurchaseAuctionList} component={Screens.PurchaseAuctionList} options={option} />
<Stack.Screen name={NavigationString.PurchaseTenderList} component={Screens.PurchaseTenderList} options={option} />
<Stack.Screen name={NavigationString.ProceedToPurTender} component={Screens.ProceedToPurTender} options={option} />
<Stack.Screen name={NavigationString.TenderBidding} component={Screens.TenderBidding} options={option} />
<Stack.Screen name={NavigationString.ProceedToBidDetail} component={Screens.ProceedToBidDetail} options={option} />
<Stack.Screen name={NavigationString.StartReverseBidding} component={Screens.StartReverseBidding} options={option} />
<Stack.Screen name={NavigationString.Registration} component={Screens.Registration} options={option} />
<Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} options={option} />
<Stack.Screen name={NavigationString.LiveTenderDetail} component={Screens.LiveTenderDetail} options={option} />
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} options={option} />
</Stack.Navigator>
);
}
+50
View File
@@ -0,0 +1,50 @@
import * as React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import NavigationString from '../Constant/NavigationString';
import Screens from '../Screens';
import colors from '../Constant/Color';
import BottomTab from './Bottom';
import Login from '../Screens/Login/Login';
import { RootStackParamList } from './types';
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function MainStack() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
// Native-thread driven transitions for smooth, jank-free navigation.
animation: 'slide_from_right',
animationDuration: 250,
gestureEnabled: true,
// Avoids a white/black flash between screens during the transition.
contentStyle: { backgroundColor: colors.white },
}}
>
<Stack.Screen name={NavigationString.Splash} component={Screens.Splash} options={{ animation: 'fade' }} />
<Stack.Screen name={NavigationString.Signup} component={Screens.Signup} />
<Stack.Screen name={NavigationString.Onboarding} component={Screens.Onboarding} />
<Stack.Screen name={NavigationString.Login} component={Login} options={{ animation: 'fade' }} />
<Stack.Screen name={NavigationString.Home} component={BottomTab} options={{ animation: 'fade', gestureEnabled: false }} />
<Stack.Screen name={NavigationString.PurchaseList} component={Screens.PurchaseList} />
<Stack.Screen name={NavigationString.Emd} component={Screens.Emd} />
<Stack.Screen name={NavigationString.StartBidding} component={Screens.StartBidding} />
<Stack.Screen name={NavigationString.PurchaseAuctionList} component={Screens.PurchaseAuctionList} />
<Stack.Screen name={NavigationString.PurchaseTenderList} component={Screens.PurchaseTenderList} />
<Stack.Screen name={NavigationString.ProceedToPurTender} component={Screens.ProceedToPurTender} />
<Stack.Screen name={NavigationString.TenderBidding} component={Screens.TenderBidding} />
<Stack.Screen name={NavigationString.ProceedToBidDetail} component={Screens.ProceedToBidDetail} />
<Stack.Screen name={NavigationString.StartReverseBidding} component={Screens.StartReverseBidding} />
<Stack.Screen name={NavigationString.Registration} component={Screens.Registration} />
<Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} />
<Stack.Screen name={NavigationString.LiveTenderDetail} component={Screens.LiveTenderDetail} />
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} />
<Stack.Screen name={NavigationString.KycWebViewScreen} component={Screens.KycWebViewScreen} />
<Stack.Screen name={NavigationString.StageThreeScreen} component={Screens.StageThreeScreen} />
<Stack.Screen name={NavigationString.DeviceBinding} component={Screens.DeviceBinding} />
<Stack.Screen name={NavigationString.DscBinding} component={Screens.DscBinding} />
<Stack.Screen name={NavigationString.EnvelopeCSpreadsheet} component={Screens.EnvelopeCSpreadsheet} />
</Stack.Navigator>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { createNavigationContainerRef, CommonActions } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef<any>();
export function navigate(name: string, params?: any) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
export function resetAndNavigate(name: string) {
if (navigationRef.isReady()) {
navigationRef.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name }],
})
);
}
}
-5
View File
@@ -1,5 +0,0 @@
import * as React from 'react';
import MainStack from './MainStack';
export default function Routes() {
return <>{MainStack()} </>
}
+8
View File
@@ -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 />;
}
+37
View File
@@ -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 };
};
+18 -35
View File
@@ -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,16 +10,19 @@ 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
@@ -27,72 +34,48 @@ export const requestSinglePermission = async (
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;
} }
} }
+1 -1
View File
@@ -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;
+27
View File
@@ -0,0 +1,27 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type ThemeMode = 'light' | 'dark' | 'system';
interface ThemeState {
mode: ThemeMode;
}
const initialState: ThemeState = {
mode: 'system',
};
export const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
setThemeMode: (state, action: PayloadAction<ThemeMode>) => {
state.mode = action.payload;
},
},
});
export const { setThemeMode } = themeSlice.actions;
export const selectThemeMode = (state: any) => state.theme.mode;
export default themeSlice.reducer;
+52 -7
View File
@@ -1,20 +1,65 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
const initialState = { /**
* 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: '', 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.
state.token = isValidSession(payload) ? payload : '';
}, },
removeToken: (state) => { removeToken: state => {
state.token = ''; state.token = '';
} },
}, },
}); });
export const { setToken , removeToken} = tokenSlice.actions;
export const { setToken, removeToken } = tokenSlice.actions;
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;
+34
View File
@@ -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);
});
});
-31
View File
@@ -1,31 +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';
const rootReducer = combineReducers({
token: tokenSlice.reducer,
auction: auctionSlice.reducer,
selectPurchaseAuction: selectPurchaseAuctionReducer,
selectPurchaseTender: selectPurchaseTenderReducer,
tender: tenderSlice.reducer,
});
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['token'],
};
const persistReducers = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistReducers,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export const persistor = persistStore(store);
+78
View File
@@ -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());
}
});
+6
View File
@@ -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;
+202 -89
View File
@@ -1,22 +1,29 @@
import React, { useState, useRef, useMemo } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions } from 'react-native'; import {
import { InsideHeader } from '../../Component/Index'; View,
import { useFetchLiveAuction, useFetchPurchaseAuction } from '../../Services/BBPS/Hooks'; StyleSheet,
import colors from '../../Constant/Color'; TouchableOpacity,
Animated,
Dimensions,
} from 'react-native';
import { InsideHeader } from '../../components';
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 } 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 FontsFamily from '../../Constant/FontsFamily';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const HORIZONTAL_PADDING = 32; const HORIZONTAL_PADDING = 32;
const SWITCH_INSETS = 2; const SWITCH_INSETS = 3;
const TAB_HEIGHT = 44; const TAB_HEIGHT = 48;
const Auction = () => { const Auction = () => {
const { colors, spacing, shadows } = useTheme();
const Navigation = useNavigation() as any; const Navigation = useNavigation() as any;
const { data: liveAuctionData = {} }: any = useFetchLiveAuction(); const { data: liveAuctionData = {} }: any = useFetchLiveAuction();
const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction(); const { data: purchaseAuctionData = {} } = useFetchPurchaseAuction();
@@ -30,15 +37,14 @@ const Auction = () => {
? purchaseAuctionData.data.length ? purchaseAuctionData.data.length
: 0; : 0;
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('live');
const translateX = useRef(new Animated.Value(0)).current;
const TAB_WIDTH = useMemo(() => { const TAB_WIDTH = useMemo(() => {
const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2; const availableWidth = SCREEN_WIDTH - HORIZONTAL_PADDING - SWITCH_INSETS * 2;
return availableWidth / 2.4; return availableWidth / 2;
}, []); }, []);
const [selectedTab, setSelectedTab] = useState<'live' | 'purchase'>('purchase');
const translateX = useRef(new Animated.Value(TAB_WIDTH)).current;
const onTabPress = (tab: 'live' | 'purchase', index: number) => { const onTabPress = (tab: 'live' | 'purchase', index: number) => {
setSelectedTab(tab); setSelectedTab(tab);
Animated.spring(translateX, { Animated.spring(translateX, {
@@ -48,147 +54,254 @@ const Auction = () => {
}).start(); }).start();
}; };
const cartCount = SelectedPurchaseList.length;
const styles = useMemo(
() => makeStyles(colors, spacing, shadows, TAB_WIDTH, TAB_HEIGHT),
[colors, spacing, shadows, TAB_WIDTH],
);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<InsideHeader title="Auction" showArrow /> <InsideHeader title="Auction" showArrow />
{/* Tab Row */}
<View style={styles.topRow}> <View style={styles.topRow}>
{/* Segmented Control */}
<View <View
style={[ style={[
styles.switchWrapper, styles.switchWrapper,
{ { width: TAB_WIDTH * 2 + SWITCH_INSETS * 2 },
width: TAB_WIDTH * 2 + SWITCH_INSETS * 2,
},
]} ]}
> >
{/* Sliding pill */}
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"
style={[ style={[
styles.activeTab, styles.activeTab,
{ { width: TAB_WIDTH, transform: [{ translateX }] },
width: TAB_WIDTH,
transform: [{ translateX }],
},
]} ]}
/> />
{/* Live Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('live', 0)} onPress={() => onTabPress('live', 0)}
> >
<Text <Radio
size={13}
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'live' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'live' && styles.activeText, selectedTab === 'live' && (styles.activeTabText as any),
]} ]}
> >
Live ({liveCount}) Live {liveCount ? `(${liveCount})` : ''}
</Text> </AppText>
</TouchableOpacity> </TouchableOpacity>
{/* Purchase Tab */}
<TouchableOpacity <TouchableOpacity
style={[styles.tab, { width: TAB_WIDTH }]} style={[styles.tab, { width: TAB_WIDTH }]}
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => onTabPress('purchase', 1)} onPress={() => onTabPress('purchase', 1)}
> >
<Text <Package
size={13}
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
strokeWidth={2}
/>
<AppText
variant="caption"
color={selectedTab === 'purchase' ? colors.primary : colors.textSecondary}
style={[ style={[
styles.tabText, styles.tabText,
selectedTab === 'purchase' && styles.activeText, selectedTab === 'purchase' && (styles.activeTabText as any),
]} ]}
> >
Purchased ({purchaseCount}) Purchased {purchaseCount ? `(${purchaseCount})` : ''}
</Text> </AppText>
</TouchableOpacity>
</View>
<View>
{SelectedPurchaseList.length ? (
<View
style={{
borderWidth: 2,
borderColor: colors.white,
backgroundColor: colors.primaryLight,
position: 'absolute',
borderRadius: 50,
zIndex: 20,
right: 0,
top: -10,
paddingHorizontal: 2,
}}
>
<Text style={{ fontSize: 12, color: colors.black }}>
{SelectedPurchaseList.length}
</Text>
</View>
) : null}
<TouchableOpacity
style={{
padding: 10,
backgroundColor: colors.primaryLight,
borderRadius: 20,
}}
onPress={() => {
Navigation.navigate(NavigationString.PurchaseList);
}}
>
<ShoppingCart size={22} color={colors.black} strokeWidth={2} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Content */}
{selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />} {selectedTab === 'live' ? <LiveAuction /> : <PurchaseAuction />}
{/* Cart Floating Button */}
<TouchableOpacity
style={styles.cartFloatingButton}
activeOpacity={0.9}
onPress={() => Navigation.navigate(NavigationString.PurchaseList)}
>
<ShoppingCart size={24} color={colors.buttonText} strokeWidth={2} />
{cartCount > 0 && (
<View style={styles.cartBadge}>
<AppText variant="caption" color={colors.primary} style={styles.cartBadgeText as any}>
{cartCount}
</AppText>
</View>
)}
</TouchableOpacity>
</View> </View>
); );
}; };
export default Auction; export default Auction;
const styles = StyleSheet.create({ /* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any, TAB_WIDTH: number, TAB_HEIGHT: number) =>
StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.white, backgroundColor: colors.background,
}, },
/* ── Tab Row ── */
topRow: { topRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 16, justifyContent: 'center',
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
}, },
switchWrapper: { switchWrapper: {
flexDirection: 'row', flexDirection: 'row',
marginVertical: 14, backgroundColor: colors.surface,
backgroundColor: '#f1f5f9', borderRadius: 16,
borderRadius: 18, padding: SWITCH_INSETS,
padding: 2,
height: TAB_HEIGHT, height: TAB_HEIGHT,
...shadows.light,
}, },
tab: { tab: {
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 5,
zIndex: 2, zIndex: 2,
}, },
tabText: { tabText: {
fontSize: 13, fontWeight: '600',
color: '#64748b', letterSpacing: 0.3,
lineHeight: 18, } as any,
fontFamily: FontsFamily.poppinsSemiBold, activeTabText: {
}, fontWeight: '700',
activeText: { } as any,
color: colors.primary,
},
activeTab: { activeTab: {
position: 'absolute', position: 'absolute',
top: 2, top: SWITCH_INSETS,
left: 2, left: SWITCH_INSETS,
height: TAB_HEIGHT - 4, height: TAB_HEIGHT - SWITCH_INSETS * 2,
backgroundColor: '#fff', backgroundColor: colors.background,
borderRadius: 16, borderRadius: 13,
shadowColor: '#000', ...shadows.medium,
shadowOpacity: 0.1,
shadowRadius: 6,
elevation: 3,
}, },
});
/* ── Cart Floating Button ── */
cartFloatingButton: {
position: 'absolute',
bottom: spacing.xl + 80,
right: spacing.l,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
...shadows.strong,
zIndex: 999,
},
cartBadge: {
position: 'absolute',
top: 0,
right: 0,
minWidth: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.background,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 4,
borderWidth: 1.5,
borderColor: colors.primary,
},
cartBadgeText: {
fontSize: 10,
fontWeight: '800',
lineHeight: 13,
} as any,
/* ── Restricted Access ── */
restrictedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.l,
backgroundColor: colors.background,
},
restrictedCard: {
backgroundColor: colors.surface,
borderRadius: 28,
padding: spacing.l,
alignItems: 'center',
width: '100%',
maxWidth: 400,
...shadows.strong,
},
lockRing: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${colors.primary}12`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.l,
borderWidth: 1.5,
borderColor: `${colors.primary}25`,
},
lockInner: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: `${colors.primary}18`,
alignItems: 'center',
justifyContent: 'center',
},
restrictedTitle: {
marginBottom: spacing.s + 4,
} as any,
restrictedMessage: {
marginBottom: spacing.s,
lineHeight: 24,
} as any,
hintBox: {
backgroundColor: colors.overlay,
borderRadius: 14,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
marginBottom: spacing.l,
width: '100%',
},
loginButton: {
backgroundColor: colors.buttonPrimary,
paddingHorizontal: spacing.l,
paddingVertical: 14,
borderRadius: 16,
width: '100%',
alignItems: 'center',
...shadows.medium,
},
loginBtnText: {
letterSpacing: 1.2,
} as any,
});
+117 -96
View File
@@ -1,69 +1,75 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect, useRef } from 'react';
import { View, FlatList, ActivityIndicator, RefreshControl, Text } from 'react-native'; import {
View,
FlatList,
ActivityIndicator,
RefreshControl,
StyleSheet,
} from 'react-native';
import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated'; import Animated, { FadeInDown, FadeOutUp } from 'react-native-reanimated';
import LiveAuctionTile from './LiveAuctionTile'; import LiveAuctionTile from './LiveAuctionTile';
import { useFetchLiveAuction } from '../../Services/BBPS/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, EmptyState } from '../../components';
import colors from '../../Constant/Color'; 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 { apiCall } from '../../Axios/Axios'; import { useTheme } from '../../Hooks/useTheme';
import { GET_MY_AUCTION } from '../../Services/BBPS/ApiUrls'; import AppText from '../../components/AppText';
import SkeltonList from '../../Component/Skelton/SkeltonList';
import Skeleton from '../../Component/Skelton/Skelton';
const LiveAuction = () => { const LiveAuction = () => {
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, shadows } = useTheme();
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);
const [showRefreshBanner, setShowRefreshBanner] = useState(false); const [showRefreshBanner, setShowRefreshBanner] = useState(false);
const [allBoolean, setAllBoolean] = useState({ liveAuctionData: [], isLoading: false })
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isRefetching, refetch, isPending }: any = useFetchLiveAuction();
// My Methods const {
data,
const getMyAuction = async () => { fetchNextPage,
try { hasNextPage,
setAllBoolean(prev => ({ ...prev, isLoading: true })) isFetchingNextPage,
const res = await apiCall<any>('get', `${GET_MY_AUCTION}`,); isLoading,
if (res?.statusCode === 200) { isRefetching,
const purchasedIds = new Set(res.data.map((item: any) => item.auctionId)); refetch,
const filteredLive = data?.result.map((item: any) => ({ ...item, active: !purchasedIds.has(item.auctionId) })); isPending,
setAllBoolean(prev => ({ ...prev, liveAuctionData: filteredLive })); }: any = useFetchLiveAuction();
}
} catch (error) {
console.error('getMyAuction error', error);
} finally {
setAllBoolean(prev => ({ ...prev, isLoading: false }));
}
};
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: any) => { ({ item, index }: any) => {
const isPurchase = SelectedPurchaseList.findIndex((prev: any) => prev.auctionNumber === item.auctionNumber) const isPurchase = SelectedPurchaseList.findIndex(
return <LiveAuctionTile (prev: any) => prev.auctionNumber === item.auctionNumber,
);
return (
<LiveAuctionTile
data={item} data={item}
index={index} index={index}
isPurchasedByContractor={item.contractorId?.includes(
String(user?.contractorId),
)}
auctionId={item.auctionId} auctionId={item.auctionId}
auctionDispatchDate={item.auctionDispatchDate} auctionDispatchDate={item.auctionDispatchDate}
isPurchased={isPurchase !== -1 ? true : false} isPurchased={isPurchase !== -1}
purchaseEnd={item.purchaseEnd} purchaseEnd={item.purchaseEnd}
auctionOfWork={item.auctionOfWork} auctionOfWork={item.auctionOfWork}
auctionFees={item.auctionFees} auctionFees={item.auctionFees}
auctionNumber={item.auctionNumber} auctionNumber={item.auctionNumber}
onPurchasePress={() => { Dispatch(togglePurchaseAuction(item)) }} onPurchasePress={() => {
onDetailsPress={() => { }} Dispatch(togglePurchaseAuction(item));
}}
onDetailsPress={() => {}}
/> />
);
}, },
[SelectedPurchaseList] // Intentionally runs only on SelectedPurchaseList changes; other deps are stable or recreated each render.
// eslint-disable-next-line react-hooks/exhaustive-deps
[SelectedPurchaseList],
); );
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (onEndReachedCalledDuringMomentum.current) return; if (onEndReachedCalledDuringMomentum.current) return;
if (!hasNextPage || isFetchingNextPage) return; if (!hasNextPage || isFetchingNextPage) return;
fetchNextPage(); fetchNextPage();
onEndReachedCalledDuringMomentum.current = true; onEndReachedCalledDuringMomentum.current = true;
}, [hasNextPage, isFetchingNextPage, fetchNextPage]); }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
@@ -71,91 +77,75 @@ const LiveAuction = () => {
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {
if (!isFetchingNextPage) return null; if (!isFetchingNextPage) return null;
return ( return (
<View style={{ paddingVertical: 12 }}> <View style={styles.footerLoader}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<AppText variant="caption" color={colors.textSecondary}>
Loading more…
</AppText>
</View> </View>
); );
}, [isFetchingNextPage]); }, [isFetchingNextPage, colors]);
useEffect(() => { useEffect(() => {
if (isRefetching) { if (isRefetching) {
setShowRefreshBanner(true); setShowRefreshBanner(true);
} else { } else {
const timer = setTimeout(() => { const timer = setTimeout(() => setShowRefreshBanner(false), 1000);
setShowRefreshBanner(false);
}, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isRefetching]); }, [isRefetching]);
useEffect(() => {
if (data?.result?.length > 0) {
getMyAuction()
}
}, [data])
if (isLoading) { if (isLoading) {
return <Skelton />; return <Skeleton />;
} }
return ( return (
<View style={{ flex: 1 }}> <View style={styles.root}>
<SearchInput onChange={() => { }} placeholder="Search...." filter={false} /> {/* Search */}
<SearchInput onChange={() => {}} placeholder="Search auctions…" filter={false} />
{/* Refresh Banner */}
{showRefreshBanner && ( {showRefreshBanner && (
<Animated.View <Animated.View
entering={FadeInDown.duration(200)} entering={FadeInDown.duration(200)}
exiting={FadeOutUp.duration(500)} exiting={FadeOutUp.duration(400)}
style={{ style={[
alignSelf: 'center', styles.refreshBanner,
flexDirection: 'row', {
alignItems: 'center', backgroundColor: colors.surface,
gap: 8, ...shadows.medium,
paddingVertical: 8, },
paddingHorizontal: 14, ]}
marginTop: 6,
borderRadius: 20,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 8,
shadowOffset: { width: 0, height: 3 },
elevation: 4,
zIndex: 10, // Ensure it stays on top
position: 'absolute',
top: 60, // Adjust based on your SearchInput height
}}
> >
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<Text <AppText variant="caption" color={colors.text} style={styles.refreshText as any}>
style={{ Refreshing auctions…
fontSize: 13, </AppText>
color: '#374151',
fontWeight: '500',
}}
>
Refreshing auctions
</Text>
</Animated.View> </Animated.View>
)} )}
{
isPending || allBoolean.isLoading ? <Skeleton /> :
<FlatList
data={allBoolean.liveAuctionData ?? []}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }}
{/* List */}
{isPending ? (
<SkeletonList />
) : (
<FlatList
data={data?.result ?? []}
renderItem={renderItem}
keyExtractor={(item: any) => item?.auctionId?.toString()}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
onEndReached={loadMore} onEndReached={loadMore}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
onMomentumScrollBegin={() => { onMomentumScrollBegin={() => {
onEndReachedCalledDuringMomentum.current = false; onEndReachedCalledDuringMomentum.current = false;
}} }}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
ListEmptyComponent={
<EmptyState
title="No live auctions found"
subtitle="New live auctions will appear here as they open."
/>
}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={isRefetching} refreshing={isRefetching}
@@ -164,18 +154,49 @@ const LiveAuction = () => {
colors={[colors.primary]} colors={[colors.primary]}
/> />
} }
initialNumToRender={6} initialNumToRender={6}
maxToRenderPerBatch={6} maxToRenderPerBatch={6}
windowSize={7} windowSize={7}
removeClippedSubviews removeClippedSubviews
/> />
} )}
</View> </View>
); );
}; };
export default LiveAuction; export default LiveAuction;
const styles = StyleSheet.create({
root: {
flex: 1,
},
listContent: {
flexGrow: 1,
paddingHorizontal: 16,
paddingBottom: 60,
paddingTop: 10,
},
refreshBanner: {
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 6,
borderRadius: 20,
zIndex: 10,
position: 'absolute',
top: 60,
},
refreshText: {
fontWeight: '500',
} as any,
footerLoader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 16,
},
});
+231 -197
View File
@@ -1,13 +1,25 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import moment from 'moment'; import moment from 'moment';
import colors from '../../Constant/Color';
import { LiveAuctionTileProps } from './types/types'; import { LiveAuctionTileProps } from './types/types';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import { Calendar, Tag, Info, ArrowRight } from 'lucide-react-native'; import {
Tag,
Calendar,
Clock,
ShoppingCart,
CheckCircle2,
XCircle,
} from 'lucide-react-native';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import NavigationString from '../../Constant/NavigationString';
import { useNavigation } from '@react-navigation/native';
const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({ const LiveAuctionTile: React.FC<LiveAuctionTileProps & { index?: number }> = ({
data, data,
index = 0,
isPurchased = false, isPurchased = false,
onPurchasePress, onPurchasePress,
auctionDispatchDate, auctionDispatchDate,
@@ -15,230 +27,252 @@ const LiveAuctionTile: React.FC<LiveAuctionTileProps> = ({
purchaseEnd, purchaseEnd,
auctionFees, auctionFees,
auctionNumber, auctionNumber,
isPurchasedByContractor,
}) => { }) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss')); const purchaseEnded = moment().isAfter(moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss'));
const deadlineFormatted = moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY');
const Navigation = useNavigation() as any;
const getStatus = () => {
if (isPurchasedByContractor) return 'purchased';
if (purchaseEnded) return 'expired';
if (isPurchased) return 'in_cart';
return 'active';
};
const status = getStatus();
const styles = makeStyles(colors, spacing, shadows);
if (showDetail) {
return ( return (
<View style={styles.cardWrapper}> <View style={styles.card}>
{ <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
!showDetail ?
<>
<View
style={[
styles.statusTag,
{ backgroundColor: isPurchased ? colors.error : purchaseEnded ? '#94a3b8' : '#4ade80' },
]}
/>
<View
style={styles.container}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity style={styles.badge} onPress={() => setShowDetail(true)}>
<Tag size={14} color={colors.primary} />
<Text style={styles.badgeText}>#{auctionNumber}</Text>
</TouchableOpacity>
<Text style={styles.priceText}>₹{auctionFees}</Text>
</View> </View>
);
{/* Work Title */}
<Text style={styles.workTitle} numberOfLines={2}>
{auctionOfWork ?? 'General Auction Work'}
</Text>
{/* Info Grid */}
<View style={styles.infoGrid}>
<View style={styles.infoItem}>
<Calendar size={16} color="#64748b" />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Start</Text>
<Text style={styles.infoValue}>{auctionDispatchDate ?? '-'}</Text>
</View>
</View>
<View style={styles.infoItem}>
<Calendar size={16} color={purchaseEnded ? "#ef4444" : "#22c55e"} />
<View style={styles.infoTextGroup}>
<Text style={styles.infoLabel}>Deadline</Text>
<Text style={[styles.infoValue, purchaseEnded && { color: '#ef4444' }]}>
{moment(purchaseEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, YY')}
</Text>
</View>
</View>
</View>
{/* Action Bar */}
<View style={styles.actionBar}>
<TouchableOpacity style={styles.iconBtn} onPress={() => setShowDetail(true)}>
<Info size={18} color={colors.primary} />
<Text style={styles.iconBtnText}>Details</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.mainButton,
purchaseEnded
? styles.btnDisabled
: !data.active ? { backgroundColor: 'lightgray' } : isPurchased
? styles.btnPurchased
: styles.btnActive,
]}
onPress={onPurchasePress}
disabled={purchaseEnded ? true : data.active ? false : true}
>
<Text
style={[
styles.mainButtonText,
isPurchased && !purchaseEnded ? { color: colors.white } : { color: '#fff' },
]}
>
{purchaseEnded ? 'Expired' : !data.active ? 'Purchased' : isPurchased ? 'Remove' : 'Add to Cart'}
</Text>
{!purchaseEnded && (
<ArrowRight size={16} color={isPurchased ? colors.white : '#fff'} />
)}
</TouchableOpacity>
</View>
{/* Auction Detail Modal */}
</View>
</>
: <AuctionDetail visible={showDetail} data={data} onClose={() => setShowDetail(false)} />
} }
return (
<Animated.View
entering={FadeInDown.delay(index * 60).springify()}
style={styles.card}
>
{/* ── Row 1: Number + Fee ── */}
<View style={styles.topRow}>
<View style={styles.numberRow}>
<Tag size={14} color={colors.primary} strokeWidth={2} />
<AppText variant="bodySmall" color={colors.primary} style={styles.auctionNo as any}>
#{auctionNumber}
</AppText>
</View> </View>
<AppText variant="h4" color={colors.primary} style={styles.fee as any}>
₹{auctionFees}
</AppText>
</View>
{/* ── Row 2: Title ── */}
<AppText variant="h4" color={colors.text} numberOfLines={2} style={styles.title as any}>
{auctionOfWork ?? 'General Auction Work'}
</AppText>
{/* ── Row 3: Date strip ── */}
<View style={[styles.dateStrip, { backgroundColor: colors.overlay }]}>
{/* Start */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Calendar size={13} color={colors.textSecondary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Start Date
</AppText>
</View>
<AppText variant="caption" color={colors.text} style={styles.dateValue as any}>
{auctionDispatchDate ?? '—'}
</AppText>
</View>
{/* Separator */}
<View style={[styles.separator, { backgroundColor: colors.border }]} />
{/* Deadline */}
<View style={styles.dateCol}>
<View style={styles.dateHeader}>
<Clock
size={13}
color={purchaseEnded ? colors.error : colors.success}
strokeWidth={2}
/>
<AppText variant="caption" color={colors.textSecondary} style={styles.dateLabel as any}>
Deadline
</AppText>
</View>
<AppText
variant="caption"
color={purchaseEnded ? colors.error : colors.text}
style={styles.dateValue as any}
>
{deadlineFormatted}
</AppText>
</View>
</View>
{/* ── Row 4: Actions ── */}
<View style={styles.actionRow}>
{/* Ghost Details button */}
<TouchableOpacity
style={[styles.detailsBtn, { borderColor: colors.border }]}
onPress={() => setShowDetail(true)}
activeOpacity={0.7}
>
<AppText variant="bodySmall" color={colors.textSecondary} style={styles.detailsText as any}>
View Details
</AppText>
</TouchableOpacity>
{/* CTA */}
{status === 'active' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.primary }]}
onPress={onPurchasePress}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
ADD TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'in_cart' && (
<TouchableOpacity
style={[styles.ctaBtn, { backgroundColor: colors.success }]}
onPress={() => { Navigation.navigate(NavigationString.PurchaseList) }}
activeOpacity={0.85}
>
<ShoppingCart size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
GO TO CART
</AppText>
</TouchableOpacity>
)}
{status === 'purchased' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<CheckCircle2 size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
PURCHASED
</AppText>
</View>
)}
{status === 'expired' && (
<View style={[styles.ctaBtn, { backgroundColor: colors.secondary }]}>
<XCircle size={14} color="#fff" strokeWidth={2.5} />
<AppText variant="caption" color="#fff" style={styles.ctaText as any}>
EXPIRED
</AppText>
</View>
)}
</View>
</Animated.View>
); );
}; };
export default memo(LiveAuctionTile); export default memo(LiveAuctionTile);
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any, shadows: any) =>
cardWrapper: { StyleSheet.create({
marginVertical: 10, card: {
marginHorizontal: 16, backgroundColor: colors.surface,
backgroundColor: '#fff', borderRadius: 18,
borderRadius: 24, borderWidth: 1,
flexDirection: 'row', borderColor: colors.border,
overflow: 'hidden', marginVertical: 7,
padding: spacing.m,
...shadows.medium,
},
// Modern shadow /* Top row */
shadowColor: '#000', topRow: {
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.08,
shadowRadius: 20,
elevation: 8,
},
statusTag: {
width: 6,
height: '100%',
},
container: {
flex: 1,
padding: 20,
},
header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
badge: { numberRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primary + '25', gap: 5,
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 12,
gap: 6,
}, },
badgeText: { auctionNo: {
fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: colors.primary, } as any,
}, fee: {
priceText: {
fontSize: 20,
fontWeight: '800', fontWeight: '800',
color: '#0f172a', } as any,
},
workTitle: { /* Title */
fontSize: 16, title: {
fontWeight: '600', fontWeight: '700',
color: '#334155',
lineHeight: 24, lineHeight: 24,
marginBottom: 16, marginBottom: spacing.m,
}, } as any,
infoGrid: {
flexDirection: 'row', /* Date strip */
justifyContent: 'space-between', dateStrip: {
backgroundColor: '#f0f4f8',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 16,
marginBottom: 18,
},
infoItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 10, borderRadius: 12,
paddingVertical: spacing.s + 2,
paddingHorizontal: spacing.m,
marginBottom: spacing.m,
}, },
infoTextGroup: { dateCol: {
flexDirection: 'column',
},
infoLabel: {
fontSize: 10,
color: '#94a3b8',
textTransform: 'uppercase',
fontWeight: '700',
},
infoValue: {
fontSize: 13,
fontWeight: '700',
color: '#1e293b',
},
actionBar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
iconBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 10,
backgroundColor: '#e2e8f0',
},
iconBtnText: {
fontSize: 14,
fontWeight: '700',
color: colors.primary,
},
mainButton: {
flex: 1, flex: 1,
height: 46, },
borderRadius: 16, dateHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: 3,
},
dateLabel: {
fontWeight: '500',
} as any,
dateValue: {
fontWeight: '700',
} as any,
separator: {
width: StyleSheet.hairlineWidth,
height: 36,
marginHorizontal: spacing.m,
},
/* Actions */
actionRow: {
flexDirection: 'row',
gap: spacing.s,
alignItems: 'center',
},
detailsBtn: {
flex: 1,
paddingVertical: 10,
borderWidth: 1,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
detailsText: {
fontWeight: '600',
} as any,
ctaBtn: {
flex: 2,
height: 44,
borderRadius: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 6,
}, },
btnActive: { ctaText: {
backgroundColor: '#4ade80', // light green
},
btnPurchased: {
backgroundColor: colors.error,
borderWidth: 2,
borderColor: colors.error,
},
btnDisabled: {
backgroundColor: '#e2e8f0',
},
mainButtonText: {
fontSize: 14,
fontWeight: '800', fontWeight: '800',
}, letterSpacing: 0.8,
}); } as any,
});
+328 -278
View File
@@ -1,63 +1,64 @@
import React, { memo, useEffect, useState } from 'react'; import React, { memo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform, StatusBar, Modal } from 'react-native'; import {
import { Briefcase, Calendar, FileText, Download, Eye, Layers, ArrowLeft, Clock } from 'lucide-react-native'; View,
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/BBPS/Hooks'; StyleSheet,
TouchableOpacity,
ScrollView,
Modal,
ActivityIndicator,
} from 'react-native';
import {
Briefcase,
Calendar,
FileText,
Download,
Eye,
Layers,
ArrowLeft,
} from 'lucide-react-native';
import { useAuctionItem, useGetAuciton, useGetAucitonDocument } from '../../../Services/ApiCall/Hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/BBPS/ApiUrls'; import { AUCTION_DOCUMENT_DOWNLOAD } from '../../../Services/ApiCall/ApiUrls';
import ReactNativeBlobUtil from 'react-native-blob-util'; import ReactNativeBlobUtil from 'react-native-blob-util';
import AuctionItemDetail from './AuctionItemDetail'; import AuctionItemDetail from './AuctionItemDetail';
import { Loader } from '../../../Component/Index'; import { useTheme } from '../../../Hooks/useTheme';
import colors from '../../../Constant/Color'; import AppText from '../../../components/AppText';
import { showToast } from '../../../Constant/ShowDailog';
import { logger } from '../../../core/observability/logger';
import moment from 'moment'; import moment from 'moment';
interface Props { visible: boolean; onClose: () => void; data?: any } interface Props { visible: boolean; onClose: () => void; data?: any }
const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets() const inset = useSafeAreaInsets();
const { data: auctionData, isLoading } = useGetAuciton({ auctionId: data?.auctionId }) const { colors, spacing, shadows } = useTheme();
const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData)
const [allBoolean, setAllBoolean] = useState({ showItemDetail: false, selectedItem: {}, fileLoading: false }); const { data: auctionData } = useGetAuciton({ auctionId: data?.auctionId });
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId }) const [aucitonDetail, setAuciotnDetail] = useState<any>(auctionData);
const { mutate } = useAuctionItem() const [allBoolean, setAllBoolean] = useState({
showItemDetail: false,
selectedItem: {},
});
// Tracks WHICH document is downloading, so only that row shows a spinner.
// A shared boolean lit up every row's spinner at once, making a single
// per-document download look like it was downloading all of them.
const [downloadingId, setDownloadingId] = useState<number | null>(null);
const { data: auctionDocuments } = useGetAucitonDocument({ auctionId: data?.auctionId });
const { mutate } = useAuctionItem();
const formatDate = (date?: string) => { const formatDate = (date?: string) => {
if (!date) return '-'; if (!date) return '—';
return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A'); return moment(date, 'DD-MM-YYYY HH:mm:ss').format('DD MMM YYYY, hh:mm A');
}; };
const isLive = data?.purchaseEnd ? moment().isBefore(moment(data.purchaseEnd, 'DD-MM-YYYY HH:mm:ss')) : false;
const Section = ({ title, icon: Icon, children }: any) => (
<View style={styles.sectionContainer}>
<View style={styles.sectionHeader}>
<Icon size={20} color={colors.primary} />
<Text style={styles.sectionTitle}>{title}</Text>
</View>
{children}
</View>
);
const InfoBox = ({ label, value, full = false }: any) => (
<View style={[styles.infoBox, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value || '-'}</Text>
</View>
);
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
if (downloadingId !== null) { return; } // one download at a time
try { try {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: true })); setDownloadingId(id);
const { dirs } = ReactNativeBlobUtil.fs; const { dirs } = ReactNativeBlobUtil.fs;
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`; const downloadUrl = `${AUCTION_DOCUMENT_DOWNLOAD}?documentId=${id}&isAmendment=false`;
// console.log('Download URL:', downloadUrl); await ReactNativeBlobUtil.config({
const response = await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
addAndroidDownloads: { addAndroidDownloads: {
useDownloadManager: true, useDownloadManager: true,
@@ -68,292 +69,341 @@ const AuctionFullDetail: React.FC<Props> = ({ visible, onClose, data }) => {
description: 'Downloading document', description: 'Downloading document',
mediaScannable: true, mediaScannable: true,
}, },
}).fetch('GET', downloadUrl, { }).fetch('GET', downloadUrl, { Accept: 'application/pdf' });
Accept: 'application/pdf', showToast('Download complete');
});
const stat = await ReactNativeBlobUtil.fs.stat(path);
console.log('File size:', stat.size);
} catch (error: any) { } catch (error: any) {
console.log('Error:', error?.message || error); logger.error('auction', 'document download failed', { error: String(error?.message || error) });
showToast('Download failed. Please try again.');
} finally { } finally {
setAllBoolean((prev: any) => ({ ...prev, fileLoading: false })); setDownloadingId(null);
} }
}; };
useEffect(() => { useEffect(() => {
mutate({ auctionNumber: data?.auctionNumber, status: data?.auctionPattern }, { mutate(
onSuccess: (res) => { { auctionNumber: data?.auctionNumber, status: data?.auctionPattern },
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments })) {
}, onError: (err) => { onSuccess: (res: any) => {
setAuciotnDetail((prev: any) => ({ ...prev, auctionItems: res.data, auctionDocuments }));
},
onError: (err: any) => {
console.log(err, '-----auction item error'); console.log(err, '-----auction item error');
} },
}); },
);
// 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 kd = (key: any, amended: any) => isAmended ? amended : key;
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<Modal <Modal
visible={visible} visible={visible}
transparent={false} transparent={false}
animationType="none" animationType="slide"
statusBarTranslucent statusBarTranslucent
onRequestClose={onClose} onRequestClose={onClose}
> >
<View style={styles.safeArea}> <View style={styles.root}>
<StatusBar barStyle="dark-content" /> {/* ── Header ── */}
<View style={[styles.header, { paddingTop: inset.top + 6 }]}>
{/* Full Header */} <TouchableOpacity onPress={onClose} style={styles.backBtn} activeOpacity={0.7}>
<View style={[styles.header, { paddingTop: inset.top }]}> <ArrowLeft size={22} color={colors.text} strokeWidth={2.5} />
<TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" />
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitleText}>Auction Overview</Text> <View style={styles.headerCenter}>
<Text style={styles.headerIdText}>Ref: {data?.auctionNumber || 'N/A'}</Text> <AppText variant="h4" color={colors.text} style={styles.headerTitle as any}>
</View> Auction Overview
<View style={styles.liveIndicator}> </AppText>
<View style={[styles.dot, { backgroundColor: isLive ? '#22c55e' : '#94a3b8' }]} /> <AppText variant="caption" color={colors.textSecondary}>
<Text style={styles.liveText}>{isLive ? 'LIVE' : 'CLOSED'}</Text> Ref: {data?.auctionNumber ?? 'N/A'}
</View> </AppText>
</View> </View>
{/* Scrollable Body */} </View>
{/* ── Body ── */}
<ScrollView <ScrollView
style={styles.scrollView} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true} style={{ backgroundColor: colors.background }}
> >
{/* 1. Basic Info Grid */}
<Section title="Details" icon={Layers}>
<View style={styles.grid}>
<InfoBox label="Auction No." value={data?.auctionNumber} />
<InfoBox label="Pattern" value={data?.auctionPattern || 'Forward'} />
<InfoBox label="Mode" value={data?.auctionMode || 'Open'} />
<InfoBox label="Category" value={data?.categoryOfAuction || 'General'} />
<InfoBox label="EMD Type" value={data?.emd || 'Online'} />
<InfoBox label="EMD Figure" value={data?.emdFigure || 'Item Wise'} />
</View>
</Section>
{/* 2. Work Description */} {/* BASIC DETAILS */}
<Section title="Auction Details" icon={Briefcase}> <SectionCard title="Basic Details" icon={Layers} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.grid}> <InfoRow label="Auction No." value={data?.auctionNumber} colors={colors} />
<InfoBox label="Auction Name" value={auctionData?.auctionOfWork} /> <InfoRow label="Pattern" value={data?.auctionPattern || 'Forward'} colors={colors} />
<InfoBox label="Category Of Auction " value={auctionData?.auctionCategories} /> <InfoRow label="Mode" value={data?.auctionMode || 'Open'} colors={colors} />
<InfoBox label="Auction Fees (In Figure) " value={auctionData?.auctionFees} /> <InfoRow label="Category" value={data?.categoryOfAuction || 'General'} colors={colors} />
<InfoBox label="Auction Fees (In Words)" value={auctionData?.auctionFeesInWord} /> <InfoRow label="EMD Type" value={data?.emd || 'Online'} colors={colors} last />
<InfoBox label="Auction Keyword" value={auctionData?.auctionKeywordName} /> </SectionCard>
</View>
</Section>
{/* 3. Timeline Section */} {/* AUCTION DETAILS */}
<Section title="Key Dates" icon={Calendar}> <SectionCard title="Auction Details" icon={Briefcase} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.timelineContainer}> <InfoRow label="Name" value={auctionData?.auctionOfWork} colors={colors} multiline />
<View style={styles.timelineRow}> <InfoRow label="Category" value={auctionData?.auctionCategories} colors={colors} />
<View style={styles.iconCircle}><Clock size={16} color="#64748b" /></View> <InfoRow label="Fees" value={auctionData?.auctionFees ? `₹ ${auctionData.auctionFees}` : '—'} colors={colors} valueColor={colors.primary} />
<View> <InfoRow label="Fees (Words)" value={auctionData?.auctionFeesInWord} colors={colors} multiline />
<Text style={styles.label}>Auction Purchase (Start)</Text> <InfoRow label="Keyword" value={auctionData?.auctionKeywordName} colors={colors} last />
<Text style={styles.value}>{formatDate(data?.auctionDispatchDate)}</Text> </SectionCard>
</View>
</View>
<View style={styles.timelineConnector} />
<View style={styles.timelineRow}>
<View style={[styles.iconCircle, { backgroundColor: '#fee2e2' }]}><Clock size={16} color="#ef4444" /></View>
<View>
<Text style={styles.label}>Purchase Deadline (End)</Text>
<Text style={styles.value}>{formatDate(data?.purchaseEnd)}</Text>
</View>
</View>
</View>
</Section>
{/* 4. Item Table Section */} {/* KEY DATES */}
<Section title="Auction Items" icon={FileText}> <SectionCard title="Key Dates" icon={Calendar} colors={colors} spacing={spacing} shadows={shadows}>
<View style={styles.tableWrapper}> <InfoRow label="Purchase Start" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.startDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.startDateTime))} colors={colors} />
<View style={styles.tableHead}> <InfoRow label="Purchase End" value={formatDate(kd(auctionData?.keyDates?.auctionPurchase?.endDateTime, auctionData?.amendedkeyDates?.auctionPurchase?.endDateTime))} colors={colors} valueColor={colors.error} />
<Text style={[styles.tableLabel, { flex: 2 }]}>Item</Text> <InfoRow label="Bidding Start" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.startDateTime, auctionData?.amendedkeyDates?.auctionBidding?.startDateTime))} colors={colors} />
<Text style={styles.tableLabel}>Time</Text> <InfoRow label="Bidding End" value={formatDate(kd(auctionData?.keyDates?.auctionBidding?.endDateTime, auctionData?.amendedkeyDates?.auctionBidding?.endDateTime))} colors={colors} valueColor={colors.error} last />
<Text style={[styles.tableLabel, { textAlign: 'right' }]}>View</Text> </SectionCard>
{/* AUCTION ITEMS */}
{aucitonDetail?.auctionItems?.length > 0 && (
<SectionCard title="Auction Items" icon={FileText} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionItems.map((item: any, index: number) => (
<View
key={index}
style={[
styles.itemRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionItems.length - 1 && { borderBottomWidth: 0 },
]}
>
<View style={styles.itemLeft}>
<AppText variant="bodySmall" color={colors.text} style={styles.itemName as any}>
{item.productName}
</AppText>
<AppText variant="caption" color={colors.textSecondary}>
{item?.categoryName} · {item.timeDurationType}
</AppText>
</View> </View>
{/* Dummy Item Row */} <TouchableOpacity
{aucitonDetail?.auctionItems?.map((item: any, index: number) => ( style={[styles.eyeBtn, { backgroundColor: `${colors.primary}15` }]}
<View style={styles.tableRow}> onPress={() => setAllBoolean(prev => ({ ...prev, showItemDetail: true, selectedItem: item }))}
<View style={{ flex: 2 }}> >
<Text style={styles.itemMain}>{item.productName}</Text> <Eye size={16} color={colors.primary} strokeWidth={2} />
<Text style={styles.itemSub}>Category: {item?.categoryName}</Text>
</View>
<Text style={styles.itemQty}>{item.timeDurationType}</Text>
<TouchableOpacity style={styles.actionIcon} onPress={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: true, selectedItem: item })) }}>
<Eye size={18} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
))} ))}
</View> </SectionCard>
</Section>
{/* 5. Downloads */}
<Section title="Downloads" icon={Download}>
{aucitonDetail?.auctionDocuments?.map((doc: any, index: number) => (
<TouchableOpacity style={styles.fileCard} onPress={() => {
handleDownload(
doc.auctionDocumentId,
doc.fileName.replace('.pdf', '')
)
}}>
<View style={styles.fileIcon}><FileText size={20} color="#ef4444" /></View>
<View style={{ flex: 1 }}>
<Text style={styles.fileName}>{doc.fileName}</Text>
</View>
{allBoolean.fileLoading ? (
<View >
<Loader size='small' color='#94a3b8' />
</View>
) : (
<Download size={20} color="#94a3b8" />
)} )}
{/* DOCUMENTS */}
{aucitonDetail?.auctionDocuments?.length > 0 && (
<SectionCard title="Documents" icon={Download} colors={colors} spacing={spacing} shadows={shadows}>
{aucitonDetail.auctionDocuments.map((doc: any, index: number) => (
<TouchableOpacity
key={index}
style={[
styles.docRow,
{ borderBottomColor: colors.border },
index === aucitonDetail.auctionDocuments.length - 1 && { borderBottomWidth: 0 },
]}
activeOpacity={0.7}
disabled={downloadingId !== null}
onPress={() => handleDownload(doc.auctionDocumentId, doc.fileName.replace('.pdf', ''))}
accessibilityRole="button"
accessibilityLabel={`Download ${doc.fileName}`}
>
<View style={[styles.docIcon, { backgroundColor: `${colors.error}15` }]}>
<FileText size={16} color={colors.error} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} style={styles.docName as any} numberOfLines={1}>
{doc.fileName}
</AppText>
{downloadingId === doc.auctionDocumentId
? <ActivityIndicator size="small" color={colors.primary} />
: <Download size={18} color={colors.textSecondary} strokeWidth={2} />
}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</Section> </SectionCard>
)}
</ScrollView> </ScrollView>
{allBoolean.showItemDetail && <AuctionItemDetail data={allBoolean.selectedItem} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, showItemDetail: false })) }} visible={allBoolean.showItemDetail} />} {allBoolean.showItemDetail && (
<AuctionItemDetail
data={allBoolean.selectedItem}
onClose={() => setAllBoolean(prev => ({ ...prev, showItemDetail: false }))}
visible={allBoolean.showItemDetail}
/>
)}
</View> </View>
</Modal > </Modal>
); );
}; };
const styles = StyleSheet.create({ export default memo(AuctionFullDetail);
fullModal: {
margin: 0, /* ─── Sub-components ─── */
backgroundColor: '#fff',
const SectionCard = ({ title, icon: Icon, children, colors, spacing, shadows }: any) => {
const styles = makeStyles(colors, spacing, shadows);
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Icon size={18} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.text} style={styles.cardTitle as any}>
{title.toUpperCase()}
</AppText>
</View>
<View style={[styles.cardDivider, { backgroundColor: colors.border }]} />
{children}
</View>
);
};
const InfoRow = ({ label, value, colors, last = false, multiline = false, valueColor }: any) => (
<View style={[
{
flexDirection: 'row',
alignItems: multiline ? 'flex-start' : 'center',
justifyContent: 'space-between',
paddingVertical: 13,
borderBottomWidth: last ? 0 : StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0,0,0,0.06)',
gap: 12,
}, },
safeArea: { ]}>
<AppText variant="bodySmall" color={colors.textSecondary} style={{ flex: 1 } as any}>
{label}
</AppText>
<AppText
variant="bodySmall"
color={valueColor || colors.text}
style={{ flex: 1, textAlign: 'right', fontWeight: '600' } as any}
numberOfLines={multiline ? 4 : 1}
>
{value || '—'}
</AppText>
</View>
);
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1, flex: 1,
backgroundColor: '#fff', backgroundColor: colors.background,
}, },
/* Header */
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, paddingHorizontal: spacing.m,
borderBottomWidth: 1, paddingBottom: spacing.m,
borderBottomColor: '#f1f5f9', backgroundColor: colors.surface,
backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
}, },
backBtn: { backBtn: {
padding: 8, width: 36,
marginRight: 8, height: 36,
}, borderRadius: 10,
headerTitleContainer: {
flex: 1,
},
headerTitleText: {
fontSize: 18,
fontWeight: '800',
color: '#0f172a',
},
headerIdText: {
fontSize: 13,
color: '#64748b',
},
liveIndicator: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 6,
borderWidth: 1,
borderColor: '#e2e8f0',
},
dot: { width: 8, height: 8, borderRadius: 4 },
liveText: { fontSize: 11, fontWeight: '800', color: '#475569' },
scrollView: { flex: 1, backgroundColor: '#f8fafc' },
scrollContent: { padding: 16, paddingBottom: 30 },
sectionContainer: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
marginBottom: 16,
borderWidth: 1,
borderColor: '#eef2f6',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 20,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#f8fafc',
},
sectionTitle: {
fontSize: 14,
fontWeight: '800',
color: '#1e293b',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
infoBox: { marginBottom: 16 },
label: { fontSize: 12, color: '#94a3b8', fontWeight: '600', marginBottom: 4 },
value: { fontSize: 15, fontWeight: '700', color: '#334155' },
spacer: { height: 12 },
timelineContainer: { paddingLeft: 8 },
timelineRow: { flexDirection: 'row', alignItems: 'center', gap: 15 },
iconCircle: { width: 34, height: 34, borderRadius: 10, backgroundColor: '#f1f5f9', alignItems: 'center', justifyContent: 'center' },
timelineConnector: { width: 2, height: 20, backgroundColor: '#f1f5f9', marginLeft: 16 },
tableWrapper: { backgroundColor: '#f8fafc', borderRadius: 12, padding: 12 },
tableHead: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#e2e8f0', paddingBottom: 8, marginBottom: 12 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 },
tableRow: { flexDirection: 'row', alignItems: 'center' },
itemMain: { fontSize: 14, fontWeight: '700', color: '#1e293b' },
itemSub: { fontSize: 12, color: '#64748b' },
itemQty: { flex: 1, fontSize: 14, fontWeight: '700', color: '#334155' },
actionIcon: { padding: 6 },
fileCard: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 },
fileIcon: { width: 40, height: 40, borderRadius: 8, backgroundColor: '#fee2e2', alignItems: 'center', justifyContent: 'center' },
fileName: { fontSize: 14, fontWeight: '700', color: '#334155' },
fileSize: { fontSize: 12, color: '#94a3b8' },
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
},
mainBtn: {
backgroundColor: colors.primary,
height: 56,
borderRadius: 16,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
}, },
btnDisabled: { backgroundColor: '#cbd5e1' }, headerCenter: {
mainBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' }, flex: 1,
}); },
headerTitle: {
fontWeight: '800',
} as any,
statusChip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.s + 4,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1.2,
gap: 5,
},
statusDot: {
width: 7,
height: 7,
borderRadius: 4,
},
statusText: {
fontWeight: '800',
letterSpacing: 0.5,
} as any,
export default memo(AuctionFullDetail); scrollContent: {
padding: spacing.m,
paddingBottom: 50,
gap: spacing.m,
},
/* Card */
card: {
backgroundColor: colors.surface,
borderRadius: 18,
paddingHorizontal: spacing.m,
paddingTop: spacing.m,
paddingBottom: spacing.s,
marginBottom: spacing.m,
...shadows.light,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
marginBottom: spacing.s + 2,
},
cardTitle: {
fontWeight: '800',
letterSpacing: 1.2,
} as any,
cardDivider: {
height: StyleSheet.hairlineWidth,
marginBottom: spacing.s,
},
/* Items */
itemRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
itemLeft: {
flex: 1,
},
itemName: {
fontWeight: '700',
marginBottom: 2,
} as any,
eyeBtn: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
/* Docs */
docRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.s + 2,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: spacing.s,
},
docIcon: {
width: 32,
height: 32,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
docName: {
flex: 1,
fontWeight: '600',
} as any,
});
+73 -76
View File
@@ -1,38 +1,43 @@
import React, { memo, useState } 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 colors from '../../../Constant/Color';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAucitonItemDetail } from '../../../Services/BBPS/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/BBPS/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 AppText from '../../../components/AppText';
interface Props { visible: boolean; onClose: () => void; data?: any; } interface Props { visible: boolean; onClose: () => void; data?: any; }
const Section = ({ title, icon: Icon, children }: any) => ( const Section = ({ title, icon: Icon, children, styles, colors }: any) => (
<View style={styles.sectionCard}> <View style={styles.sectionCard}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<View style={styles.iconCircle}> <View style={styles.iconCircle}>
<Icon size={18} color={colors.primary} strokeWidth={2.5} /> <Icon size={18} color={colors.primary} strokeWidth={2.5} />
</View> </View>
<Text style={styles.sectionTitle}>{title}</Text> <AppText variant="caption" bold style={styles.sectionTitle}>{title}</AppText>
</View> </View>
{children} {children}
</View> </View>
); );
const PriceTile = ({ label, fig, words, full = false }: any) => ( const PriceTile = ({ label, fig, words, full = false, styles }: any) => (
<View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}> <View style={[styles.priceTile, full ? { width: '100%' } : { width: '48%' }]}>
<Text style={styles.label}>{label}</Text> <AppText variant="caption" color={styles.label.color} bold style={styles.label}>{label}</AppText>
<Text style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</Text> <AppText variant="h3" bold style={styles.priceFig}>₹{Number(fig).toLocaleString('en-IN')}</AppText>
<Text style={styles.priceWords}>{words}</Text> <AppText variant="caption" style={styles.priceWords}>{words}</AppText>
</View> </View>
); );
const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => { const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const inset = useSafeAreaInsets() const inset = useSafeAreaInsets()
const { colors, typography } = useTheme();
const styles = useMemo(() => createStyles(colors, typography), [colors, typography]);
const [allBoolean, setAllBoolean] = useState({ fileLoading: false }); const [allBoolean, setAllBoolean] = useState({ fileLoading: false });
const { data: itemData } = useAucitonItemDetail({ auctionItemId: data.id }); const { data: itemData } = useAucitonItemDetail({ auctionItemId: data?.id });
const handleDownload = async (id: number, fileName: string) => { const handleDownload = async (id: number, fileName: string) => {
try { try {
@@ -41,12 +46,9 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
const path = `${dirs.DownloadDir}/${fileName}.pdf`; const path = `${dirs.DownloadDir}/${fileName}.pdf`;
// console.log('Saving to:', path);
const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`; const downloadUrl = `${AUCTION_ITEM_DOCUMENT_DOWNLOAD}?auctionItemId=${id}`;
// console.log('Download URL:', downloadUrl);
const response = await ReactNativeBlobUtil.config({ await ReactNativeBlobUtil.config({
fileCache: false, fileCache: false,
addAndroidDownloads: { addAndroidDownloads: {
useDownloadManager: true, useDownloadManager: true,
@@ -61,8 +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);
console.log(' File size:', stat.size);
} catch (error: any) { } catch (error: any) {
console.log('Error:', error?.message || error); console.log('Error:', error?.message || error);
@@ -84,11 +85,11 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
{/* Navbar */} {/* Navbar */}
<View style={[styles.header, { paddingTop: inset.top }]}> <View style={[styles.header, { paddingTop: inset.top }]}>
<TouchableOpacity onPress={onClose} style={styles.backBtn}> <TouchableOpacity onPress={onClose} style={styles.backBtn}>
<ArrowLeft size={24} color="#1e293b" /> <ArrowLeft size={24} color={colors.text} />
</TouchableOpacity> </TouchableOpacity>
<View> <View>
<Text style={styles.headerTitle}>Item Information</Text> <AppText variant="body" bold style={styles.headerTitle}>Item Information</AppText>
<Text style={styles.headerSub}>Detail View</Text> <AppText variant="caption" style={styles.headerSub}>Detail View</AppText>
</View> </View>
</View> </View>
@@ -97,69 +98,66 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<Section title="Product Specification" icon={Package}> <Section title="Product Specification" icon={Package} styles={styles} colors={colors}>
<View style={styles.identityHeader}> <View style={styles.identityHeader}>
<Text style={styles.productNameText}>{data.productName.toUpperCase()}</Text> <AppText variant="h4" bold style={styles.productNameText}>{data?.productName?.toUpperCase()}</AppText>
<View style={styles.categoryBadge}> <View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{data.categoryName}</Text> <AppText variant="caption" bold style={styles.categoryText}>{data?.categoryName}</AppText>
</View> </View>
</View> </View>
<View style={styles.specGrid}> <View style={styles.specGrid}>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Sub Category</Text> <AppText variant="caption" bold style={styles.label}>Sub Category</AppText>
<Text style={styles.specValue}>{data.subCategoryName}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.subCategoryName}</AppText>
</View> </View>
<View style={styles.specItem}> <View style={styles.specItem}>
<Text style={styles.label}>Duration</Text> <AppText variant="caption" bold style={styles.label}>Duration</AppText>
<Text style={styles.specValue}>{data.timeDurationType}</Text> <AppText variant="bodySmall" bold style={styles.specValue}>{data?.timeDurationType}</AppText>
</View> </View>
</View> </View>
</Section> </Section>
<Section title="Reserve & Bid Values" icon={IndianRupee}> <Section title="Reserve & Bid Values" icon={IndianRupee} styles={styles} colors={colors}>
<PriceTile <PriceTile
label="Bidding Price" label="Bidding Price"
fig={data.reservePrice || data.emdAmount} fig={data?.reservePrice || data?.emdAmount}
words={data.reservePriceInWords || data.emdAmountInWords} words={data?.reservePriceInWords || data?.emdAmountInWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
<PriceTile <PriceTile
label="Auction Start Value" label="Auction Start Value"
fig={data.reservePrice || data.auctionStartValueFigure} fig={data?.reservePrice || data?.auctionStartValueFigure}
words={data.reservePriceInWords || data.auctionStartValueWords} words={data?.reservePriceInWords || data?.auctionStartValueWords}
full full
styles={styles}
/> />
<View style={styles.hDivider} /> <View style={styles.hDivider} />
{/* <View style={styles.row}>
<PriceTile label="Bid Variation" fig={data.bidVariationFig} words={data.bidVariationWords} />
<PriceTile label="Auction Start Value" fig={data.auctionStartValue} words={data.auctionStartValueInWords
} />
</View> */}
</Section> </Section>
<Section title="Brief Description" icon={Info}> <Section title="Brief Description" icon={Info} styles={styles} colors={colors}>
<Text style={styles.descContent}>{data.briefDescription}</Text> <AppText variant="bodySmall" style={styles.descContent}>{data?.briefDescription}</AppText>
</Section> </Section>
{itemData?.length && <Section title="View Documents" icon={FileText}> {itemData?.length > 0 && <Section title="View Documents" icon={FileText} styles={styles} colors={colors}>
<View style={styles.docTableHead}> <View style={styles.docTableHead}>
<Text style={styles.tableLabel}>Sr No.</Text> <AppText variant="caption" bold style={styles.tableLabel}>Sr No.</AppText>
<Text style={[styles.tableLabel, { flex: 2 }]}>Document Name</Text> <AppText variant="caption" bold style={[styles.tableLabel, { flex: 2 }]}>Document Name</AppText>
<Text style={styles.tableLabel}>Action</Text> <AppText variant="caption" bold style={styles.tableLabel}>Action</AppText>
</View> </View>
{itemData?.map((doc: any, index: any) => ( {itemData?.map((doc: any, index: any) => (
<TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}> <TouchableOpacity style={styles.docRow} activeOpacity={0.7} onPress={() => { handleDownload(doc.assetId, doc.fileName) }} key={doc.assetId}>
<Text style={styles.docIndex}>{index + 1}</Text> <AppText variant="bodySmall" bold style={styles.docIndex}>{index + 1}</AppText>
<View style={{ flex: 2 }}> <View style={{ flex: 2 }}>
<Text style={styles.docTitle} numberOfLines={1}>{doc.fileName}</Text> <AppText variant="bodySmall" bold style={styles.docTitle} numberOfLines={1}>{doc.fileName}</AppText>
</View> </View>
<View style={styles.downloadIconBox}> <View style={styles.downloadIconBox}>
{allBoolean.fileLoading ? ( {allBoolean.fileLoading ? (
<View > <View >
<Loader size='small' color='#94a3b8' /> <Loader size='small' color={colors.textSecondary} />
</View> </View>
) : ( ) : (
<Download size={20} color={colors.primary} /> <Download size={20} color={colors.primary} />
@@ -174,26 +172,26 @@ const AuctionItemDetail: React.FC<Props> = ({ visible, onClose, data }) => {
); );
}; };
const styles = StyleSheet.create({ const createStyles = (colors: any, typography: any) => StyleSheet.create({
fullModal: { margin: 0, backgroundColor: '#f8fafc' }, fullModal: { margin: 0, backgroundColor: colors.background },
safeArea: { flex: 1 }, safeArea: { flex: 1, backgroundColor: colors.background },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 16, padding: 16,
backgroundColor: '#fff', backgroundColor: colors.surface,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#f1f5f9', borderBottomColor: colors.border,
}, },
backBtn: { padding: 8, marginRight: 8 }, backBtn: { padding: 8, marginRight: 8 },
headerTitle: { fontSize: 18, fontWeight: '800', color: '#0f172a' }, headerTitle: { fontSize: 18, color: colors.text },
headerSub: { fontSize: 12, color: '#94a3b8', fontWeight: '600', textTransform: 'uppercase' }, headerSub: { fontSize: 12, color: colors.textSecondary, textTransform: 'uppercase' },
scrollView: { flex: 1 }, scrollView: { flex: 1 },
scrollContent: { padding: 16, paddingBottom: 50 }, scrollContent: { padding: 16, paddingBottom: 50 },
sectionCard: { sectionCard: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 24, borderRadius: 24,
padding: 20, padding: 20,
marginBottom: 16, marginBottom: 16,
@@ -218,55 +216,54 @@ const styles = StyleSheet.create({
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: 14,
fontWeight: '800', color: colors.text,
color: '#334155',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
// Product Identity // Product Identity
identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }, identityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 },
productNameText: { fontSize: 20, fontWeight: '900', color: '#1e293b', flex: 1 }, productNameText: { fontSize: 20, color: colors.text, flex: 1 },
categoryBadge: { backgroundColor: '#f1f5f9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 }, categoryBadge: { backgroundColor: colors.border, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
categoryText: { fontSize: 11, fontWeight: '800', color: '#64748b', textTransform: 'uppercase' }, categoryText: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase' },
specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 }, specGrid: { flexDirection: 'row', gap: 24, marginTop: 5 },
specItem: { flex: 1 }, specItem: { flex: 1 },
specValue: { fontSize: 15, fontWeight: '700', color: '#334155', marginTop: 2 }, specValue: { fontSize: 15, color: colors.text, marginTop: 2 },
// Pricing // Pricing
priceTile: { marginBottom: 16 }, priceTile: { marginBottom: 16 },
label: { fontSize: 11, color: '#94a3b8', fontWeight: '700', textTransform: 'uppercase', marginBottom: 4 }, label: { fontSize: 11, color: colors.textSecondary, textTransform: 'uppercase', marginBottom: 4 },
priceFig: { fontSize: 22, fontWeight: '900', color: colors.black }, priceFig: { fontSize: 22, color: colors.text },
priceWords: { fontSize: 13, color: '#64748b', fontStyle: 'italic', marginTop: 2 }, priceWords: { fontSize: 13, color: colors.textSecondary, fontStyle: 'italic', marginTop: 2 },
hDivider: { height: 1, backgroundColor: '#f1f5f9', marginBottom: 16 }, hDivider: { height: 1, backgroundColor: colors.border, marginBottom: 16 },
row: { flexDirection: 'row', justifyContent: 'space-between' }, row: { flexDirection: 'row', justifyContent: 'space-between' },
// Description // Description
descContent: { fontSize: 15, color: '#475569', lineHeight: 22, fontWeight: '500' }, descContent: { fontSize: 15, color: colors.textSecondary, lineHeight: 22, fontFamily: typography.fonts.medium },
// Table // Table
docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f1f5f9', marginBottom: 10 }, docTableHead: { flexDirection: 'row', paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: colors.border, marginBottom: 10 },
tableLabel: { fontSize: 11, fontWeight: '800', color: '#94a3b8', flex: 1 }, tableLabel: { fontSize: 11, color: colors.textSecondary, flex: 1 },
docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#f8fafc', padding: 12, borderRadius: 12 }, docRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background, padding: 12, borderRadius: 12 },
docIndex: { flex: 1, fontSize: 14, fontWeight: '700', color: '#64748b' }, docIndex: { flex: 1, fontSize: 14, color: colors.textSecondary },
docTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b' }, docTitle: { fontSize: 14, color: colors.text },
downloadIconBox: { backgroundColor: '#fff', padding: 8, borderRadius: 10, borderWidth: 1, borderColor: '#e2e8f0' }, downloadIconBox: { backgroundColor: colors.surface, padding: 8, borderRadius: 10, borderWidth: 1, borderColor: colors.border },
// Footer // Footer
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 0, width: '100%', bottom: 0, width: '100%',
backgroundColor: '#fff', backgroundColor: colors.surface,
padding: 20, padding: 20,
paddingBottom: Platform.OS === 'ios' ? 34 : 20, paddingBottom: Platform.OS === 'ios' ? 34 : 20,
borderTopWidth: 1, borderTopColor: '#f1f5f9' borderTopWidth: 1, borderTopColor: colors.border
}, },
primaryBtn: { primaryBtn: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
height: 56, borderRadius: 18, height: 56, borderRadius: 18,
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10 flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10
}, },
primaryBtnText: { color: '#fff', fontSize: 16, fontWeight: '800' } primaryBtnText: { color: colors.buttonText, fontSize: 16, fontFamily: typography.fonts.bold }
}); });
export default memo(AuctionItemDetail); export default memo(AuctionItemDetail);
@@ -25,10 +25,8 @@ const FilterModal: React.FC<Props> = ({ visible, onClose, setFilter, filter }) =
if (pickerType === 'FROM') { if (pickerType === 'FROM') {
setFromDate(date); setFromDate(date);
console.log('FROM date set');
} else { } else {
setToDate(date); setToDate(date);
console.log('TO date set');
} }
setOpenPicker(false); setOpenPicker(false);
+24 -16
View File
@@ -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/BBPS/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, EmptyState } 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,14 +19,17 @@ 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';
function isAuctionActive(item: any): boolean { function isAuctionActive(item: any): boolean {
const now = moment(); const now = moment();
const start = moment(item?.keyDates?.auctionBidding?.startDateTime, format); const bidding = item?.auctionStatus === 'Amended'
const end = moment(item?.keyDates?.auctionBidding?.endDateTime, format); ? item?.amendedkeyDates?.auctionBidding
: item?.keyDates?.auctionBidding;
const start = moment(bidding?.startDateTime, format);
const end = moment(bidding?.endDateTime, format);
if (!start.isValid() || !end.isValid()) return false; if (!start.isValid() || !end.isValid()) return false;
@@ -36,7 +38,7 @@ const PurchaseAuction = () => {
function isAuctionEnd(item: any): boolean { function isAuctionEnd(item: any): boolean {
const now = moment(); const now = moment();
const end = moment(item?.keyDates?.auctionBidding?.endDateTime, format); const end = moment(item.auctionStatus === "Amended" ? item?.amendedkeyDates?.auctionBidding?.endDateTime : item?.keyDates?.auctionBidding?.endDateTime, format);
if (!end.isValid()) return false; if (!end.isValid()) return false;
@@ -64,12 +66,12 @@ 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 })) }} /> <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={() => {
@@ -83,7 +85,7 @@ const PurchaseAuction = () => {
/> />
)} )}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 50, paddingTop: 10 }} contentContainerStyle={{ flexGrow: 1, paddingBottom: 50, paddingTop: 10 }}
onEndReached={loadMore} onEndReached={loadMore}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
@@ -93,6 +95,12 @@ const PurchaseAuction = () => {
}} }}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
ListEmptyComponent={
<EmptyState
title="No purchased auctions found"
subtitle="Auctions you purchase will appear here."
/>
}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
@@ -111,7 +119,7 @@ const PurchaseAuction = () => {
} }
{allBoolean.isShowFilter && <View style={{ flex: 1 }}> {allBoolean.isShowFilter && <View style={{ flex: 1 }}>
<FilterModal visible={allBoolean.isShowFilter} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: false })) }} setFilter={setFilter} filter={filter} /> <FilterModal visible={allBoolean.isShowFilter} onClose={() => { setAllBoolean((prev: any) => ({ ...prev, isShowFilter: false })) }} setFilter={setFilter} filter={filter} />
</View>}. </View>}
</View> </View>
); );
+398 -173
View File
@@ -1,51 +1,56 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import {
import { InsideHeader, Loader } from '../../Component/Index'; View,
import colors from '../../Constant/Color'; FlatList,
import { CreditCard, Calendar, User, DollarSign, ArrowRight } from 'lucide-react-native'; StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import Animated, { FadeInDown, FadeIn } from 'react-native-reanimated';
import {
Calendar,
Hash,
Wifi,
WifiOff,
Receipt,
CheckCircle2,
Gavel,
} from 'lucide-react-native';
import { InsideHeader } from '../../components';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useOfflineAuctionPurchase, usePurchaseAuctionList } from '../../Services/BBPS/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';
const dummyAuctionList = [ import AppText from '../../components/AppText';
{
auctionId: 1,
auctionNumber: 'AUC-2025-001',
auctionPattern: 'Single Stage',
auctionDispatchDate: '25 Feb 2025',
auctionDispatcherNumber: 'DISP-001',
auctionFees: 2500,
gst: 18,
gstAmount: 450,
portalCharge: 100,
payableAmount: 3050,
},
];
const PurchaseAuctionList = () => { const PurchaseAuctionList = () => {
const Navigation = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [list, setList] = useState([]); const [list, setList] = useState<any[]>([]);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const LoginUser = useSelector((state: any) => state.token.token); const LoginUser = useSelector((state: any) => state.token.token);
const { mutate, isPending } = useOfflineAuctionPurchase()
const { mutate, isPending } = useOfflineAuctionPurchase();
const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList(); const { mutate: purchaseListMutation, isPending: purchaseListPending } = usePurchaseAuctionList();
const calculateTotalAmount = (item: any) => { const calculateTotalAmount = (item: any) => {
const gstAmount = const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) * (Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100); (Number(item.gst) / 100);
return Number(item.auctionFees) + Number(item.portalCharge) + gstAmount;
};
const payableAmount = const grandTotal = list.reduce(
Number(item.auctionFees) + Number(item.portalCharge) + gstAmount; (sum: number, item: any) => sum + calculateTotalAmount(item),
return payableAmount 0,
} );
const handleOfflinePurchase = async () => { const handleOfflinePurchase = async () => {
const body = { const body = {
@@ -59,233 +64,453 @@ const PurchaseAuctionList = () => {
gst: b?.gst, gst: b?.gst,
gstAmount: b?.gstAmount, gstAmount: b?.gstAmount,
payableAmount: calculateTotalAmount(b), payableAmount: calculateTotalAmount(b),
portalCharge: b?.portalCharge portalCharge: b?.portalCharge,
})), })),
orderNo: '' orderNo: '',
}; };
try { try {
mutate(body, { mutate(body, {
onSuccess: (res) => { onSuccess: res => {
if (res.statusCode == 200) { if (res.statusCode === 200) {
showSuccessAlert(res.message) showSuccessAlert(res.message);
Dispatch(clearPurchaseAuction()) Dispatch(clearPurchaseAuction());
Navigation.dispatch( Navigation.dispatch(
CommonActions.reset({ CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
index: 0,
routes: [{ name: NavigationString.Home }],
})
); );
} }
}, },
onError: (error) => { onError: error => console.log(error, 'purchase Error'),
console.log(error, "================purchse Error"); });
}
})
} catch (error) { } catch (error) {
console.log("purchase offline error", error); console.log('purchase offline error', error);
}
} }
};
const handlePurchase = (type: 'online' | 'offline') => { const handlePurchase = (type: 'online' | 'offline') => {
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
type === 'online' ? Alert.alert('Online Purchase Successful!') : handleOfflinePurchase(); type === 'online' ? undefined : handleOfflinePurchase();
}, 1200); }, 1200);
}; };
const renderItem = ({ item }: { item: typeof dummyAuctionList[0] }) => ( const goHome = () => {
<View style={styles.card}> Dispatch(clearPurchaseAuction());
<View style={styles.cardHeader}> Navigation.dispatch(
<Text style={styles.title}>{item.auctionNumber}</Text> CommonActions.reset({ index: 0, routes: [{ name: NavigationString.Home }] }),
<Text style={styles.pattern}>{item.auctionPattern}</Text>
</View>
<View style={styles.cardRow}>
<Calendar size={18} color={colors.primary} />
<Text style={styles.text}>Dispatch Date: {item.auctionDispatchDate}</Text>
</View>
<View style={styles.cardRow}>
<User size={18} color={colors.primary} />
<Text style={styles.text}>Dispatcher No: {item.auctionDispatcherNumber}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Auction Fees: ₹{item.auctionFees}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>GST ({item.gst}%): ₹{item.gstAmount}</Text>
</View>
<View style={styles.cardRow}>
<DollarSign size={18} color={colors.primary} />
<Text style={styles.text}>Portal Charge: ₹{item.portalCharge}</Text>
</View>
<View style={styles.totalRow}>
<CreditCard size={20} color={colors.white} />
<Text style={styles.total}>Total: ₹{item.payableAmount}</Text>
</View>
</View>
); );
};
// The auction was already purchased — proceedToPurchase then has no cart to
// show, so bounce back to Home instead of leaving a blank list. (The backend
// message is surfaced by the global API toast.)
const isAlreadyPurchased = (res: any): boolean => {
const message = String(res?.message ?? '').toLowerCase();
return res?.statusCode === 409 || message.includes('already');
};
const getPurchaseList = () => { const getPurchaseList = () => {
const auctionNumbers: string[] = SelectedPurchaseList.map( const auctionNumbers: string[] = SelectedPurchaseList.map(
(item: { auctionNumber: string }) => item.auctionNumber (item: { auctionNumber: string }) => item.auctionNumber,
); );
purchaseListMutation({ auctionNumbers, contractorId: LoginUser.contractorId }, { purchaseListMutation(
onSuccess: (res) => { { auctionNumbers, contractorId: LoginUser.contractorId },
if (res.statusCode == 200) { {
setList(res.data) onSuccess: res => {
if (res.statusCode === 200) {
setList(res.data);
} else if (isAlreadyPurchased(res)) {
goHome();
} }
}, },
onError: (error) => { onError: (error: any) => {
console.log(error, "=================purchase list error"); if (isAlreadyPurchased(error?.response?.data)) {
} goHome();
}) } else {
console.log(error, 'purchase list error');
} }
},
},
);
};
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
getPurchaseList() getPurchaseList();
}, [SelectedPurchaseList]) // Intentionally runs only on SelectedPurchaseList changes; other deps are stable or recreated each render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [SelectedPurchaseList]),
); );
const styles = makeStyles(colors, spacing, shadows);
/* ── Render Item ── */
const renderItem = ({ item, index }: { item: any; index: number }) => {
const total = calculateTotalAmount(item);
const gstAmount =
(Number(item.auctionFees) + Number(item.portalCharge)) *
(Number(item.gst) / 100);
return (
<Animated.View
entering={FadeInDown.delay(index * 90).springify()}
style={styles.card}
>
{/* Top accent stripe */}
<View style={styles.cardAccent} />
<View style={styles.cardBody}>
{/* Header */}
<View style={styles.cardHeader}>
<View style={[styles.indexBadge, { backgroundColor: colors.primary }]}>
<AppText variant="caption" color={colors.buttonText} style={{ fontWeight: '800' } as any}>
{String(index + 1).padStart(2, '0')}
</AppText>
</View>
<View style={{ flex: 1, marginLeft: spacing.s }}>
<AppText variant="h4" color={colors.text} numberOfLines={2}>
{item.auctionNumber}
</AppText>
{item.auctionPattern ? (
<View style={[styles.patternBadge, { backgroundColor: `${colors.primary}15` }]}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={{ fontWeight: '600' } as any}>
{item.auctionPattern}
</AppText>
</View>
) : null}
</View>
</View>
<View style={styles.divider} />
{/* Info Rows */}
<InfoRow
icon={<Hash size={15} color={colors.primary} />}
label="Dispatcher No"
value={item.auctionDispatcherNumber ?? '—'}
colors={colors}
/>
<InfoRow
icon={<Calendar size={15} color={colors.primary} />}
label="Dispatch Date"
value={item.auctionDispatchDate ?? '—'}
colors={colors}
/>
<View style={[styles.divider, { marginVertical: spacing.s }]} />
{/* Fee Breakdown */}
<View style={[styles.feeSection, { backgroundColor: colors.overlay }]}>
<AppText variant="caption" color={colors.textSecondary} style={styles.feeHeader as any}>
FEE BREAKDOWN
</AppText>
<FeeRow label="Auction Fees" value={`₹ ${Number(item.auctionFees).toFixed(2)}`} colors={colors} />
<FeeRow label={`GST (${item.gst}%)`} value={`₹ ${gstAmount.toFixed(2)}`} colors={colors} />
<FeeRow label="Portal Charge" value={`₹ ${Number(item.portalCharge).toFixed(2)}`} colors={colors} />
</View>
{/* Total Banner */}
<View style={[styles.totalBanner, { backgroundColor: colors.primary }]}>
<View style={styles.totalLeft}>
<Receipt size={18} color={colors.buttonText} />
<AppText variant="bodySmall" color={colors.buttonText} style={{ fontWeight: '600' } as any}>
Total Payable
</AppText>
</View>
<AppText variant="h4" color={colors.buttonText}>
₹ {total.toFixed(2)}
</AppText>
</View>
</View>
</Animated.View>
);
};
return ( return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<InsideHeader title="Purchase List" showArrow />
<View style={styles.screen}> <View style={styles.screen}>
{purchaseListPending ? <Skeleton /> : <FlatList <InsideHeader title="Auction Purchase" showArrow />
{/* Grand Total Strip */}
<Animated.View entering={FadeInDown.springify()} style={styles.grandTotalStrip}>
<View>
<AppText variant="caption" color={colors.textSecondary}>
{list.length} auction{list.length !== 1 ? 's' : ''}
</AppText>
<AppText variant="h3" color={colors.text}>
₹ {grandTotal.toFixed(2)}
</AppText>
</View>
<View style={[styles.grandTotalRight, { backgroundColor: `${colors.success}15` }]}>
<CheckCircle2 size={18} color={colors.success} />
<AppText variant="caption" color={colors.success} style={{ fontWeight: '600' } as any}>
Ready to pay
</AppText>
</View>
</Animated.View>
{/* List */}
{purchaseListPending ? (
<Skeleton />
) : (
<FlatList
data={list} data={list}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={(item) => item.auctionId.toString()} keyExtractor={item => item.auctionId?.toString()}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
/>} showsVerticalScrollIndicator={false}
</View> />
)}
<View style={styles.bottomButtonContainer}> {/* Bottom Buttons */}
<View style={styles.footer}>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginRight: 8 }]} style={[styles.button, styles.onlineButton, { borderColor: colors.primary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('online')} onPress={() => handlePurchase('online')}
> >
<Text style={styles.buttonText}>Purchase Online</Text> <Wifi size={18} color={colors.primary} />
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <AppText variant="button" color={colors.primary} style={styles.btnText as any}>
Online
</AppText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginLeft: 8 }]} style={[styles.button, styles.offlineButton, { backgroundColor: colors.buttonPrimary }]}
activeOpacity={0.85}
onPress={() => handlePurchase('offline')} onPress={() => handlePurchase('offline')}
> >
{ {isPending ? (
isPending ? <Loader size='small' color={colors.white} /> : <> <ActivityIndicator size="small" color="#fff" />
<Text style={styles.buttonText}>Purchase Offline</Text> ) : (
<ArrowRight size={18} color="#fff" style={{ marginLeft: 6 }} /> <>
<WifiOff size={18} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText} style={styles.btnText as any}>
Offline
</AppText>
</> </>
} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Loading Overlay */}
{(loading || isPending) && (
<Animated.View entering={FadeIn.duration(200)} style={styles.loadingOverlay}>
<View style={[styles.loadingCard, { backgroundColor: colors.surface }]}>
<ActivityIndicator size="large" color={colors.primary} />
<AppText variant="bodySmall" color={colors.textSecondary} style={{ marginTop: spacing.s } as any}>
Processing purchase…
</AppText>
</View>
</Animated.View>
)}
</View> </View>
); );
}; };
export default PurchaseAuctionList; export default PurchaseAuctionList;
const styles = StyleSheet.create({ /* ─── Sub-components ─── */
const InfoRow = ({
icon,
label,
value,
colors,
}: {
icon: React.ReactNode;
label: string;
value: string;
colors: any;
}) => (
<View style={infoRowStyles.row}>
<View style={infoRowStyles.iconWrap}>{icon}</View>
<AppText variant="caption" color={colors.textSecondary} style={infoRowStyles.label as any}>
{label}
</AppText>
<AppText variant="bodySmall" color={colors.text} style={infoRowStyles.value as any}>
{value}
</AppText>
</View>
);
const infoRowStyles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 5, gap: 6 },
iconWrap: { width: 22, alignItems: 'center' },
label: { flex: 1, fontWeight: '500' } as any,
value: { fontWeight: '600' } as any,
});
const FeeRow = ({
label,
value,
colors,
}: {
label: string;
value: string;
colors: any;
}) => (
<View style={feeRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary}>{label}</AppText>
<AppText variant="caption" color={colors.text} style={{ fontWeight: '600' } as any}>{value}</AppText>
</View>
);
const feeRowStyles = StyleSheet.create({
row: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
});
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: { screen: {
flex: 1, flex: 1,
backgroundColor: colors.background,
}, },
/* Grand total strip */
grandTotalStrip: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderRadius: 18,
...shadows.medium,
},
grandTotalRight: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: spacing.s + 2,
paddingVertical: spacing.xs + 2,
borderRadius: 20,
},
container: { container: {
padding: 16, paddingHorizontal: spacing.m,
paddingTop: spacing.s,
paddingBottom: 120, paddingBottom: 120,
}, },
/* Card */
card: { card: {
backgroundColor: '#fff', backgroundColor: colors.surface,
borderRadius: 16, borderRadius: 20,
padding: 16, marginBottom: spacing.m,
marginBottom: 16, overflow: 'hidden',
elevation: 5, ...shadows.medium,
shadowColor: '#000', },
shadowOpacity: 0.12, cardAccent: {
shadowOffset: { width: 0, height: 3 }, height: 4,
shadowRadius: 8, backgroundColor: colors.primary,
},
cardBody: {
padding: spacing.m,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'flex-start',
marginBottom: 12, marginBottom: spacing.s,
}, },
title: { indexBadge: {
fontSize: 16, width: 32,
fontWeight: '700', height: 32,
color: '#0F172A', borderRadius: 10,
},
pattern: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
},
cardRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 6,
marginBottom: 4,
},
text: {
fontSize: 14,
color: '#555',
},
totalRow: {
marginTop: 10,
backgroundColor: colors.primary,
borderRadius: 12,
paddingVertical: 10,
flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
marginTop: 2,
},
patternBadge: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 4,
alignSelf: 'flex-start',
paddingHorizontal: spacing.s,
paddingVertical: 2,
borderRadius: 8,
marginTop: 4,
}, },
total: { divider: {
fontSize: 15, height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginVertical: spacing.s,
},
feeSection: {
borderRadius: 12,
padding: spacing.s + 2,
marginBottom: spacing.s,
},
feeHeader: {
letterSpacing: 1.2,
marginBottom: spacing.xs + 2,
fontWeight: '700', fontWeight: '700',
color: '#fff', } as any,
totalBanner: {
borderRadius: 14,
paddingVertical: 12,
paddingHorizontal: spacing.m,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}, },
bottomButtonContainer: { totalLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
/* Footer */
footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0,
flexDirection: 'row', flexDirection: 'row',
backgroundColor: colors.white, paddingHorizontal: spacing.m,
width: '100%', paddingTop: spacing.s + 4,
paddingVertical: 20, paddingBottom: spacing.m + 4,
paddingHorizontal: 10, backgroundColor: colors.surface,
elevation: 5 gap: spacing.s,
...shadows.strong,
}, },
button: { button: {
flex: 1, flex: 1,
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14,
borderRadius: 16,
gap: spacing.s,
}, },
buttonText: { onlineButton: {
color: '#fff', backgroundColor: colors.surface,
fontWeight: '700', borderWidth: 1.5,
fontSize: 14,
}, },
offlineButton: {
...shadows.medium,
},
btnText: {
letterSpacing: 1.2,
} as any,
/* Loading overlay */
loadingOverlay: { loadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.2)', backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
zIndex: 10, zIndex: 10,
}, },
}); loadingCard: {
borderRadius: 20,
padding: spacing.l,
alignItems: 'center',
...shadows.strong,
},
});
+217 -187
View File
@@ -1,263 +1,293 @@
import React from 'react'; import React, { useMemo } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } 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 colors from '../../Constant/Color';
import AuctionDetail from './Modal/AuctionDetail'; import AuctionDetail from './Modal/AuctionDetail';
import AppText from '../../components/AppText';
import { useTheme } from '../../Hooks/useTheme';
import {
Tag,
Calendar,
IndianRupee,
ArrowRight,
CircleDot,
Clock,
TimerOff,
} from 'lucide-react-native';
const PurchaseAuctionTile = ({ item, onPressDetails, onStartBidding, buttonEnable, isAuctionEnd }: any) => { const PurchaseAuctionTile = ({
item,
onStartBidding,
buttonEnable,
isAuctionEnd,
}: any) => {
const { colors, spacing, shadows } = useTheme();
const [showDetail, setShowDetail] = React.useState(false); const [showDetail, setShowDetail] = React.useState(false);
const scale = useSharedValue(1); const scale = useSharedValue(1);
const elevation = useSharedValue(8);
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => ({
return {
transform: [{ scale: scale.value }], transform: [{ scale: scale.value }],
elevation: elevation.value, // Android-safe }));
};
}, []);
const handlePressIn = () => { const handlePressIn = () => scale.value = withTiming(0.98, { duration: 140 });
scale.value = withTiming(0.97, { duration: 120 }); const handlePressOut = () => scale.value = withTiming(1, { duration: 160 });
elevation.value = withTiming(3, { duration: 120 });
};
const handlePressOut = () => { const isAmended = item?.auctionStatus === 'Amended';
scale.value = withTiming(1, { duration: 160 }); const biddingEnd = isAmended
elevation.value = withTiming(8, { duration: 160 }); ? item?.amendedkeyDates?.auctionBidding?.endDateTime?.replace(' kk', '')
}; : item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', '');
const statusConfig = useMemo(() => {
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 };
return { label: 'PENDING', color: colors.textSecondary, bg: `${colors.textSecondary}12`, Icon: Clock };
}, [isAuctionEnd, buttonEnable, colors]);
const statusColor = isAuctionEnd ? '#EF4444' : buttonEnable ? colors.primary : '#94A3B8'; const styles = makeStyles(colors, spacing);
const lightStatusBg = isAuctionEnd ? '#FEF2F2' : buttonEnable ? '#EFF6FF' : '#F8FAFC'; if (showDetail) {
return <AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />;
}
return ( return (
<View style={{ flex: 1 }}> <Animated.View
{!showDetail ? <Animated.View entering={FadeInUp.duration(450).springify()}
entering={FadeInUp.duration(400)} style={[styles.container, shadows.medium, animatedStyle]}
style={[styles.shadowContainer, animatedStyle]}
> >
<TouchableOpacity <TouchableOpacity
activeOpacity={1} activeOpacity={1}
onPress={() => setShowDetail(true)} onPress={() => setShowDetail(true)}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={styles.cardInternal} style={styles.card}
> >
<View style={[styles.statusAccent, { backgroundColor: statusColor }]} /> {/* ── Header ── */}
<View style={styles.header}>
<View style={styles.headerTop}>
{/* Status pill */}
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bg }]}>
<statusConfig.Icon size={11} color={statusConfig.color} strokeWidth={3} />
<AppText variant="caption" color={statusConfig.color} style={styles.statusText as any}>
{statusConfig.label}
</AppText>
</View>
{/* Auction number */}
<View style={styles.numberRow}>
<Tag size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.textSecondary} style={styles.numberText as any}>
{item?.auctionNumber}
</AppText>
</View>
</View>
<View style={styles.mainInfo}> {/* Work title */}
{/* Header */} <AppText variant="bodySmall" color={colors.text} numberOfLines={2} style={styles.workTitle as any}>
<View style={styles.headerRow}> {item?.auctionOfWork ?? item?.auctionNumber ?? 'Auction Details'}
</AppText>
</View>
<View style={styles.divider} />
{/* ── Info Grid ── */}
<View style={styles.infoGrid}>
{/* Fee */}
<View style={styles.infoRow}>
<View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<IndianRupee size={17} color={colors.primary} strokeWidth={2} />
</View>
<View> <View>
<View style={[styles.statusBadge, { backgroundColor: lightStatusBg }]}> <AppText variant="caption" color={colors.textSecondary}>Auction Fee</AppText>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} /> <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
<Text style={[styles.statusText, { color: statusColor }]}> ₹{Number(item?.auctionFees ?? 0).toLocaleString('en-IN')}
{isAuctionEnd ? 'ENDED' : buttonEnable ? 'LIVE' : 'PENDING'} </AppText>
</Text>
</View>
<Text style={styles.title}>{item.auctionNumber}</Text>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amount}>
₹{item.auctionFees?.toLocaleString()}
</Text>
<Text style={styles.feeLabel}>Auction Fee</Text>
</View> </View>
</View> </View>
{/* Date */} {/* Bidding End */}
<View style={styles.dateRow}> <View style={styles.infoRow}>
<Text style={styles.dateText}> <View style={[styles.iconBox, { backgroundColor: `${colors.primary}15` }]}>
<Text style={styles.dateLabel}>Bidding Start: </Text> <Calendar size={17} color={colors.primary} strokeWidth={2} />
{moment( </View>
item?.keyDates?.auctionBidding?.startDateTime?.replace(' kk', ''), <View>
'DD-MM-YYYY HH:mm:ss' <AppText variant="caption" color={colors.textSecondary}>Bidding Ends</AppText>
).format('DD MMM YYYY, hh:mm A')} <AppText variant="caption" color={colors.text} style={styles.infoValue as any}>
</Text> {biddingEnd
<Text style={styles.dateText}> ? moment(biddingEnd, 'DD-MM-YYYY HH:mm:ss').format('DD MMM, hh:mm A')
<Text style={styles.dateLabel}>Bidding Ends: </Text> : '—'}
{moment( </AppText>
item?.keyDates?.auctionBidding?.endDateTime?.replace(' kk', ''), </View>
'DD-MM-YYYY HH:mm:ss' </View>
).format('DD MMM YYYY, hh:mm A')}
</Text>
</View> </View>
{/* Footer */} {/* ── Footer ── */}
<View style={styles.footer}> <View style={styles.footer}>
<View style={styles.footerTop}> <AppText variant="caption" color={colors.textSecondary} style={{ marginBottom: 4, alignSelf: 'flex-start' } as any}>
<Text style={styles.footerLabel}>Remaining Time</Text> Time Remaining
</AppText>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
{/* Timer */}
<View style={styles.timerSection}>
{buttonEnable && !isAuctionEnd ? ( {buttonEnable && !isAuctionEnd ? (
<CountdownTimer <CountdownTimer endDate={biddingEnd} />
endDate={item.keyDates.auctionBidding.endDateTime}
/>
) : ( ) : (
<Text style={styles.endedText}>--:--:--</Text> <AppText variant="bodySmall" color={colors.textSecondary} style={{ fontWeight: '700' } as any}>
-- : -- : --
</AppText>
)} )}
</View> </View>
{/* CTA Button */}
<TouchableOpacity <TouchableOpacity
disabled={!buttonEnable} disabled={!buttonEnable || isAuctionEnd}
onPress={onStartBidding} onPress={onStartBidding}
activeOpacity={0.85} activeOpacity={0.85}
style={[ style={[
styles.actionButton, styles.ctaBtn,
{ backgroundColor: statusColor }, {
!buttonEnable && styles.disabledButton, backgroundColor:
isAuctionEnd
? colors.error
: buttonEnable
? colors.primary
: colors.border,
opacity: (!buttonEnable || isAuctionEnd) ? 0.8 : 1,
},
]} ]}
> >
<Text style={styles.addText}> <AppText
{isAuctionEnd variant="button"
? 'Bidding Closed' color={colors.buttonText}
: !buttonEnable style={styles.ctaText as any}
? 'Pending Bid' >
: 'Proceed to Start Bidding'} {isAuctionEnd ? 'Ended' : !buttonEnable ? 'Pending' : 'Bid Now'}
</Text> </AppText>
{buttonEnable && !isAuctionEnd && (
<ArrowRight size={15} color={colors.buttonText} strokeWidth={2.5} />
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> : </Animated.View>
<AuctionDetail visible={showDetail} data={item} onClose={() => setShowDetail(false)} />
}
</View>
); );
}; };
export default PurchaseAuctionTile; export default PurchaseAuctionTile;
const styles = StyleSheet.create({ const makeStyles = (colors: any, spacing: any) =>
shadowContainer: { StyleSheet.create({
marginHorizontal: 5, container: {
marginTop: 14, marginHorizontal: spacing.s,
marginBottom: 6, marginBottom: spacing.m,
borderRadius: 16, borderRadius: 16,
backgroundColor: '#fff', borderWidth: 1,
elevation: 8, borderColor: colors.border,
backgroundColor: colors.surface,
}, },
cardInternal: { card: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', backgroundColor: colors.surface,
flexDirection: 'row', padding: spacing.m,
backgroundColor: '#fff', borderWidth: 1,
borderColor: 'rgba(0,0,0,0.03)',
}, },
statusAccent: {
width: 5, /* Header */
height: '100%', header: {
marginBottom: spacing.m,
}, },
mainInfo: { headerTop: {
flex: 1,
padding: 16,
},
headerRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'center',
marginBottom: 12, marginBottom: spacing.s,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 8, paddingHorizontal: 10,
paddingVertical: 3, paddingVertical: 5,
borderRadius: 6, borderRadius: 20,
marginBottom: 6, gap: 5,
alignSelf: 'flex-start',
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 5,
}, },
statusText: { statusText: {
fontSize: 10,
fontWeight: '800', fontWeight: '800',
fontSize: 10,
textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
} as any,
numberRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
}, },
title: { numberText: {
fontWeight: '800', letterSpacing: 0.4,
fontSize: 16,
color: '#1E293B',
},
amountContainer: {
alignItems: 'flex-end',
},
amount: {
fontWeight: '900',
fontSize: 18,
color: '#0F172A',
},
feeLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
dateRow: {
backgroundColor: '#F8FAFC',
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 10,
marginBottom: 16,
borderWidth: 1,
borderColor: '#F1F5F9',
},
dateText: {
fontSize: 12,
color: '#334155',
fontWeight: '600', fontWeight: '600',
} as any,
workTitle: {
fontWeight: '600',
lineHeight: 20,
} as any,
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.border,
marginBottom: spacing.m,
opacity: 0.6,
}, },
dateLabel: {
fontWeight: '400', /* Info grid */
color: '#64748B', infoGrid: {
},
footer: {
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
paddingTop: 12,
},
footerTop: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: spacing.l,
gap: spacing.s,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, gap: 10,
flex: 1,
}, },
footerLabel: { iconBox: {
fontSize: 11, width: 36,
fontWeight: '600', height: 36,
color: '#94A3B8', borderRadius: 10,
textTransform: 'uppercase', alignItems: 'center',
},
endedText: {
fontSize: 13,
fontWeight: '700',
color: '#CBD5E1',
},
actionButton: {
width: '100%',
height: 48,
borderRadius: 12,
justifyContent: 'center', justifyContent: 'center',
},
infoValue: {
fontWeight: '700',
} as any,
/* Footer */
footer: {
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
...Platform.select({ backgroundColor: colors.background,
ios: { marginHorizontal: -spacing.m,
shadowColor: '#000', marginBottom: -spacing.m,
shadowOffset: { width: 0, height: 2 }, padding: spacing.m,
shadowOpacity: 0.1, borderBottomLeftRadius: 16,
shadowRadius: 4, borderBottomRightRadius: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.border,
}, },
}), timerSection: {
flex: 1,
}, },
disabledButton: { ctaBtn: {
backgroundColor: '#E2E8F0', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
paddingHorizontal: 18,
borderRadius: 12,
gap: 5,
}, },
addText: { ctaText: {
color: '#fff', letterSpacing: 0.8,
fontSize: 14, } as any,
fontWeight: '800', });
letterSpacing: 0.2,
},
});
+329 -160
View File
@@ -1,220 +1,389 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import {
import Animated, { FadeIn, FadeOut, Layout, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; View,
import { ChevronDown, Trash2, Info } from 'lucide-react-native'; StyleSheet,
import { InsideHeader } from '../../Component/Index'; ScrollView,
import colors from '../../Constant/Color'; TouchableOpacity,
} from 'react-native';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
Layout,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { ChevronDown, ChevronRight, Trash2, ShoppingBag, Gavel, Tag } from 'lucide-react-native';
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';
import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction'; import { togglePurchaseAuction } from '../../Redux/Slices/selectPurchaseAuction';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
const PurchaseList = () => { const PurchaseList = () => {
const Navigation: any = useNavigation() const Navigation: any = useNavigation();
const Dispatch = useDispatch() const Dispatch = useDispatch();
const { colors, spacing, shadows } = useTheme();
const [expandedIndex, setExpandedIndex] = useState<number | null>(null); const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction); const SelectedPurchaseList = useSelector((state: any) => state.selectPurchaseAuction);
const toggleExpand = (index: number) => { const toggleExpand = (index: number) => {
setExpandedIndex(prev => (prev === index ? null : index)); setExpandedIndex(prev => (prev === index ? null : index));
}; };
useEffect(() => {
if (!SelectedPurchaseList || SelectedPurchaseList.length < 1) {
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]);
const styles = makeStyles(colors, spacing, shadows);
return ( return (
<View style={styles.screen}> <View style={styles.screen}>
<InsideHeader title="Purchase" showArrow /> <InsideHeader title="Purchase Auctions" showArrow />
<ScrollView contentContainerStyle={styles.container}> {/* Summary Strip */}
{/* Header */} <View style={styles.summaryStrip}>
<View style={styles.header}> <View style={styles.summaryIcon}>
<Text style={styles.heading}>Purchase List</Text> <ShoppingBag size={16} color={colors.primary} />
{/* <Info size={20} color="#0F172A" /> */} </View>
<AppText variant="caption" color={colors.textSecondary}>
<AppText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' } as any}>
{SelectedPurchaseList.length}
</AppText>
{' '}auction{SelectedPurchaseList.length !== 1 ? 's' : ''} selected for purchase
</AppText>
</View> </View>
{/* Cards */} <ScrollView
{SelectedPurchaseList.map((item: any, index: any) => { contentContainerStyle={styles.container}
const expanded = expandedIndex === index; showsVerticalScrollIndicator={false}
return ( >
<Animated.View {SelectedPurchaseList.map((item: any, index: number) => (
<PurchaseListItem
key={item.auctionId} key={item.auctionId}
layout={Layout.springify()} item={item}
style={styles.card} index={index}
> expandedIndex={expandedIndex}
<TouchableOpacity toggleExpand={toggleExpand}
activeOpacity={0.9} colors={colors}
onPress={() => toggleExpand(index)} styles={styles}
> Dispatch={Dispatch}
<CardHeader
title={item.auctionOfWork}
expanded={expanded}
/> />
))}
<Text style={styles.subTitle}> <View style={{ height: 90 }} />
Auction No: {item.auctionNumber}
</Text>
<Text style={styles.subTitle}>
Status:{' '}
<Text
style={{
color: item.status ? '#16A34A' : '#DC2626',
fontWeight: '700',
}}
>
{item.status ? 'Open' : 'Closed'}
</Text>
</Text>
</TouchableOpacity>
{/* Expanded Content */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(180)}
layout={Layout.springify()}
style={styles.details}
>
{renderRow('Auction Mode', item.auctionMode)}
{renderRow('Pattern', item.auctionPattern)}
{renderRow('Fees', `₹ ${item.auctionFees}`)}
{renderRow('Department', item.deptName)}
{renderRow('Purchase End', item.purchaseEnd)}
<TouchableOpacity style={styles.removeButton} onPress={() => { Dispatch(togglePurchaseAuction(item)) }}>
<Trash2 size={18} color="#fff" />
<Text style={styles.removeText}>Remove</Text>
</TouchableOpacity>
</Animated.View>
)}
</Animated.View>
);
})}
</ScrollView> </ScrollView>
{/* Footer */} {/* Floating Proceed Button */}
<TouchableOpacity disabled={Boolean(!SelectedPurchaseList.length)} style={[styles.proceedButton, { backgroundColor: Boolean(!SelectedPurchaseList.length) ? colors.lightText : colors.primary }]} onPress={() => { Navigation.navigate(NavigationString.PurchaseAuctionList) }}> <View style={styles.footerContainer}>
<Text style={styles.proceedText}>Proceed to Purchase</Text> <TouchableOpacity
activeOpacity={0.9}
disabled={!SelectedPurchaseList.length}
style={[
styles.proceedButton,
{
backgroundColor: SelectedPurchaseList.length
? colors.buttonPrimary
: colors.secondary,
},
]}
onPress={() => Navigation.navigate(NavigationString.PurchaseAuctionList)}
>
<AppText variant="button" color={colors.buttonText} style={styles.proceedText as any}>
Proceed to Purchase
</AppText>
<View style={styles.countBadge}>
<AppText variant="caption" color={colors.buttonPrimary} style={{ fontWeight: '800' } as any}>
{SelectedPurchaseList.length}
</AppText>
</View>
<ChevronRight size={20} color={colors.buttonText} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View>
); );
}; };
export default PurchaseList; export default PurchaseList;
const CardHeader = ({ title, expanded }: { /* ─── Sub-components ─── */
title: string;
expanded: boolean; const PurchaseListItem = ({ item, index, expandedIndex, toggleExpand, colors, styles, Dispatch }: any) => {
}) => { const expanded = expandedIndex === index;
const rotation = useSharedValue(expanded ? 1 : 0); const isActive = item.status;
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 }); const chevronStyle = useChevronStyle(expanded);
const arrowStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
return ( return (
<View style={styles.cardHeader}> <Animated.View
<Text style={styles.title}>{title}</Text> entering={FadeInDown.delay(index * 80).springify()}
<Animated.View style={arrowStyle}> layout={Layout.springify()}
<ChevronDown size={22} color={colors.black} /> style={[styles.card, expanded && styles.cardExpanded]}
>
{/* Left Accent Bar */}
<View style={[styles.accentBar, { backgroundColor: isActive ? colors.success : colors.primary }]} />
<View style={styles.cardInner}>
<TouchableOpacity
activeOpacity={0.85}
onPress={() => toggleExpand(index)}
>
{/* Badge Row */}
<View style={styles.badgeRow}>
<View style={styles.auctionBadge}>
<Gavel size={11} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} style={styles.badgeText as any}>
{item.auctionNumber}
</AppText>
</View>
</View>
{/* Title + Chevron */}
<View style={styles.titleRow}>
<AppText variant="h4" style={styles.title as any} numberOfLines={2}>
{item.auctionOfWork ?? item.auctionNumber}
</AppText>
<Animated.View style={chevronStyle}>
<ChevronDown size={20} color={colors.textSecondary} />
</Animated.View> </Animated.View>
</View> </View>
{/* Dept Tag */}
<View style={styles.deptRow}>
<Tag size={12} color={colors.textSecondary} />
<AppText variant="caption" color={colors.textSecondary} style={styles.deptText as any}>
{item.deptName || 'Department'}
</AppText>
</View>
</TouchableOpacity>
{/* Expanded Section */}
{expanded && (
<Animated.View
entering={FadeIn.duration(220)}
exiting={FadeOut.duration(160)}
layout={Layout.springify()}
style={styles.detailsContainer}
>
<View style={styles.divider} />
<DetailRow label="Auction Mode" value={item.auctionMode} colors={colors} />
<DetailRow label="Pattern" value={item.auctionPattern} colors={colors} />
<DetailRow label="Auction Fees" value={`₹ ${item.auctionFees}`} colors={colors} valueColor={colors.primary} />
<DetailRow label="Department" value={item.deptName} colors={colors} />
<DetailRow label="Purchase End" value={item.purchaseEnd} colors={colors} />
{/* Remove Button */}
<TouchableOpacity
style={styles.removeButton}
activeOpacity={0.85}
onPress={() => Dispatch(togglePurchaseAuction(item))}
>
<Trash2 size={16} color="#fff" />
<AppText variant="button" color="#fff" style={styles.removeText as any}>
Remove
</AppText>
</TouchableOpacity>
</Animated.View>
)}
</View>
</Animated.View>
); );
}; };
const renderRow = (label: string, value: string) => ( const DetailRow = ({
<View style={styles.row}> label,
<Text style={styles.label}>{label}</Text> value,
<Text style={styles.value}>{value}</Text> colors,
valueColor,
}: {
label: string;
value: string;
colors: any;
valueColor?: string;
}) => (
<View style={detailRowStyles.row}>
<AppText variant="caption" color={colors.textSecondary} style={detailRowStyles.label as any}>
{label}
</AppText>
<AppText variant="caption" color={valueColor || colors.text} style={detailRowStyles.value as any}>
{value ?? '—'}
</AppText>
</View> </View>
); );
const detailRowStyles = StyleSheet.create({
const styles = StyleSheet.create({ row: {
screen: {
flex: 1,
backgroundColor: '#F8FAFC',
},
container: {
padding: 16,
paddingBottom: 110,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 14,
},
heading: {
fontSize: 22,
fontWeight: '800',
color: colors.black,
},
card: {
backgroundColor: '#fff',
borderRadius: 18,
padding: 16,
marginBottom: 14,
borderWidth: 1,
borderColor: '#E5E7EB',
},
cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, paddingVertical: 8,
title: { borderBottomWidth: StyleSheet.hairlineWidth,
fontSize: 16, borderBottomColor: 'rgba(0,0,0,0.06)',
fontWeight: '700',
color: '#0F172A',
flex: 1,
marginRight: 12,
},
subTitle: {
fontSize: 14,
color: '#64748B',
marginTop: 6,
},
details: {
marginTop: 16,
paddingTop: 14,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
row: {
marginVertical: 6,
}, },
label: { label: {
fontSize: 13, flex: 1,
color: '#64748B', fontWeight: '500',
}, } as any,
value: { value: {
fontSize: 15, flex: 1,
textAlign: 'right',
fontWeight: '600', fontWeight: '600',
color: '#0F172A', } as any,
});
/* ─── Chevron animated style hook ─── */
function useChevronStyle(expanded: boolean) {
const rotation = useSharedValue(expanded ? 1 : 0);
rotation.value = withTiming(expanded ? 1 : 0, { duration: 220 });
return useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value * 180}deg` }],
}));
}
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
screen: {
flex: 1,
backgroundColor: colors.background,
},
summaryStrip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
marginHorizontal: spacing.m,
marginTop: spacing.m,
marginBottom: spacing.s,
paddingHorizontal: spacing.m,
paddingVertical: spacing.s + 2,
borderRadius: 14,
gap: spacing.s,
...shadows.light,
},
summaryIcon: {
width: 28,
height: 28,
borderRadius: 8,
backgroundColor: `${colors.primary}15`,
alignItems: 'center',
justifyContent: 'center',
},
container: {
paddingHorizontal: spacing.m,
paddingTop: spacing.s,
},
card: {
backgroundColor: colors.surface,
borderRadius: 20,
marginBottom: spacing.m,
flexDirection: 'row',
overflow: 'hidden',
...shadows.medium,
},
cardExpanded: {
...shadows.strong,
},
accentBar: {
width: 5,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
cardInner: {
flex: 1,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
},
badgeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.s,
},
auctionBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary}15`,
paddingHorizontal: spacing.s,
paddingVertical: 3,
borderRadius: 8,
gap: 4,
},
badgeText: {
fontWeight: '600',
letterSpacing: 0.2,
} as any,
titleRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: spacing.s,
},
title: {
flex: 1,
fontWeight: '700',
} as any,
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginTop: spacing.xs + 2,
},
deptText: {
letterSpacing: 0.3,
} as any,
detailsContainer: {
marginTop: spacing.s,
},
divider: {
height: 1,
backgroundColor: colors.border,
marginBottom: spacing.s,
}, },
removeButton: { removeButton: {
marginTop: 18, marginTop: spacing.m,
backgroundColor: colors.primary, backgroundColor: colors.error,
borderRadius: 14, borderRadius: 14,
paddingVertical: 12, paddingVertical: 12,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 8, alignItems: 'center',
gap: spacing.s,
}, },
removeText: { removeText: {
color: '#fff', letterSpacing: 1,
fontWeight: '700', } as any,
footerContainer: {
position: 'absolute',
bottom: 20,
left: spacing.m,
right: spacing.m,
}, },
proceedButton: { proceedButton: {
position: 'absolute', flexDirection: 'row',
bottom: 16,
left: 16,
right: 16,
backgroundColor: colors.primary,
paddingVertical: 16,
borderRadius: 18,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
borderRadius: 20,
gap: spacing.s,
...shadows.strong,
}, },
proceedText: { proceedText: {
color: '#fff', letterSpacing: 1.5,
fontSize: 16, } as any,
fontWeight: '800', countBadge: {
backgroundColor: '#fff',
borderRadius: 12,
minWidth: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
}, },
}); });
+1
View File
@@ -10,6 +10,7 @@ export interface LiveAuctionTileProps {
isPurchased?: boolean; isPurchased?: boolean;
auctionId?: any; auctionId?: any;
auctionNumber?: any; auctionNumber?: any;
isPurchasedByContractor?: boolean
} }
export interface allBooleanProps { export interface allBooleanProps {
@@ -0,0 +1,435 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
View,
FlatList,
StyleSheet,
BackHandler,
} from 'react-native';
import LottieView from 'lottie-react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { InsideHeader } from '../../components';
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,
SELECTED_AUCTION_ITEM,
} from '../../Services/ApiCall/ApiUrls';
import AuctionBiddingCard from './Component/AuctionBiddingCard';
import ConfirmBidModal from './Component/ConfirmBidModal';
import GoBackModal from './Component/GoBackModal';
import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
import NavigationString from '../../Constant/NavigationString';
import CountdownTimer from '../../components/CountdownTimer';
import { useTheme } from '../../Hooks/useTheme';
import AppText from '../../components/AppText';
import { Gavel, Building2, Clock4 } from 'lucide-react-native';
import { showErrorAlert, showSuccessAlert } from '../../Constant/ShowDailog';
import { logger } from '../../core/observability/logger';
import { getBiddingConfig } from './biddingPattern';
/**
* 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 SelectedAuction = useSelector((state: any) => state.auction.auction);
const loginUser = useSelector((state: any) => state.token.token);
// The login session stores the contractor id under `id` (see ProfileTab /
// LiveTender); the older `contractorId` key is absent, which is why bid
// payloads and "me"/LEADING detection were getting undefined.
const contractorId = loginUser?.contractorId ?? loginUser?.id;
const Navigation: any = useNavigation();
const config = useMemo(
() => getBiddingConfig(SelectedAuction?.auctionPattern),
[SelectedAuction?.auctionPattern],
);
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, unsubscribe, disconnect, createRoom, placeBid } = useWebSocketService(
Variables.webSocketUrl,
() => logger.info('stomp', 'connected'),
(error: any) => logger.warn('stomp', `connection error: ${error}`),
);
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 => ({ ...prev, oneTimeBid: true }));
}
};
const getOneTimeBidValue = async () => {
try {
const selectedRes = await apiCall<any>('get', AUCTION_ONE_TIME_BID, {}, { auctionId: SelectedAuction.auctionId });
return selectedRes.statusCode === 200 ? selectedRes.data : [];
} catch { return []; }
};
const getAllAcutionItems = async () => {
try {
const payload = { auctionNumber: SelectedAuction.auctionNumber };
let auctionItem: any[] = [];
const res = await apiCall<any>('get', config.itemsUrl, {}, 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);
const liveBids = new Map<number, { bidAmount: number; contractorId: number }>();
(result || []).forEach((r: any) => {
liveBids.set(r.auctionItemId, { bidAmount: r.bidAmount, contractorId: r.contractorId });
});
const updatedFilteredItems = filteredItems.map(item => {
const bidData = liveBids.get(item.id);
return {
...item,
bidAmount: bidData?.bidAmount ?? config.startValue(item),
bidAvilable: bidData?.bidAmount ?? 0,
me: bidData?.contractorId === contractorId,
};
});
const initialBids: Record<number, number> = {};
updatedFilteredItems.forEach(item => { initialBids[item.id] = Number(item.bidAmount); });
setBidValues(initialBids);
setItems(updatedFilteredItems);
} else {
if (SelectedAuction.rebidAllowed) { setAllBoolean(prev => ({ ...prev, rebid: true })); }
const result: any[] = 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: config.startValue(item), contractorId: item.contractorId }; }
return {
...item,
bidAmount: bid.bidAmount,
bidId: bid.bidId,
contractorId: bid.contractorId,
contractorName: bid.contractorName,
me: bid.contractorId === contractorId,
bidTime: bid.bidTime,
};
});
const initialBids: Record<number, number> = {};
mergedData.forEach(item => { initialBids[item.id] = Number(item.bidAmount); });
setBidValues(initialBids);
setItems(mergedData);
}
} catch (error) {
logger.error('bidding', 'fetching auction items failed', { error: String(error) });
}
};
// Live bid updates: subscribe once per room from an effect (never the render
// body) and tear the subscription down when the room changes or we unmount.
useEffect(() => {
const destination = `/topic/room/${SelectedAuction.auctionNumber}`;
subscribe(destination, (message: any) => {
const result = message[message.length - 1];
setItems((prev: any[]) =>
prev.map(item =>
item.id === result.auctionItemId
? { ...item, bidAmount: result.bidAmount, me: result.contractorId === loginUser.contractorId }
: item,
),
);
setBidValues(prev => ({ ...prev, [result.auctionItemId]: Number(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(() => {
if (SelectedAuction.auctionCategories === 'One-time') { getAuction(); }
getAllAcutionItems();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allBoolean.reload]);
// Move a bid one step in the given direction. The next value is computed from
// the LIVE `prev` state (no stale closure on rapid taps) and clamped to the
// pattern's [floor, ceil] by the config, so the button always moves the value
// by a real amount and can never overshoot a bound.
const stepBid = (item: any, direction: 'up' | 'down') => {
setBidValues(prev => {
let current = Number(prev[item.id]);
if (!Number.isFinite(current)) { current = config.startValue(item); }
const next = direction === 'up'
? config.stepUp(current, item)
: config.stepDown(current, item);
return { ...prev, [item.id]: next };
});
};
const submitBid = async (item: any) => {
const payload = {
itemId: item.id,
bidAmount: bidValues[item.id],
auctionId: SelectedAuction.auctionId,
contractorId,
fullName: loginUser.fullName,
roomId: SelectedAuction.auctionNumber,
};
setAllBoolean(prev => ({ ...prev, bidPayload: payload, showDialog: true }));
};
const placeBidSubmit = async (data: any) => {
setAllBoolean(prev => ({ ...prev, showDialog: false }));
try {
const result = await placeBid(data);
if (result?.status && result.data) {
if (result.message) { showSuccessAlert(result.message); }
getAllAcutionItems();
} else if (result?.message) {
showErrorAlert(result.message);
}
} catch (error) {
logger.error('bidding', 'placing bid failed', { error: String(error) });
}
};
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
setAllBoolean(prev => ({ ...prev, showBackDialog: true }));
return true;
};
const sub = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => sub.remove();
}, []),
);
const styles = useMemo(() => makeStyles(colors, spacing, shadows), [colors, spacing, shadows]);
const biddingEndDate =
SelectedAuction?.auctionStatus === 'Amended'
? SelectedAuction?.amendedkeyDates?.auctionBidding?.endDateTime
: SelectedAuction?.keyDates?.auctionBidding?.endDateTime;
return (
<View style={styles.root}>
<InsideHeader title="Bidding Panel" showArrow />
{/* ── Top Info Band ── */}
<Animated.View entering={FadeInDown.springify()} style={styles.infoBand}>
{/* Department row */}
<View style={styles.deptRow}>
<View style={[styles.deptIcon, { backgroundColor: `${colors.primary}15` }]}>
<Building2 size={16} color={colors.primary} strokeWidth={2} />
</View>
<AppText variant="bodySmall" color={colors.text} bold numberOfLines={1} style={styles.deptName as any}>
{SelectedAuction.deptName ?? 'Auction Department'}
</AppText>
</View>
{/* Timer card */}
<View style={[styles.timerCard, { backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }]}>
<View style={styles.timerLabelRow}>
<Clock4 size={13} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.timerLabel as any}>
BIDDING ENDS IN
</AppText>
</View>
<CountdownTimer
endDate={biddingEndDate}
myStyle={{ fontSize: 28, fontWeight: '900', color: colors.primary, letterSpacing: 1 }}
size={28}
/>
</View>
{/* Auction chip */}
<View style={styles.chipRow}>
<View style={[styles.chip, { backgroundColor: `${colors.primary}12` }]}>
<Gavel size={12} color={colors.primary} strokeWidth={2} />
<AppText variant="caption" color={colors.primary} bold style={styles.chipText as any}>
{SelectedAuction.auctionNumber}
</AppText>
</View>
<View style={[styles.chip, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? `${colors.success}15` : `${colors.textSecondary}12` }]}>
<View style={[styles.liveDot, { backgroundColor: SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary }]} />
<AppText
variant="caption"
color={SelectedAuction.auctionCategories === 'Live' ? colors.success : colors.textSecondary}
bold
style={styles.chipText as any}
>
{SelectedAuction.auctionCategories ?? 'Auction'}
</AppText>
</View>
</View>
</Animated.View>
{/* ── Items List ── */}
<FlatList
data={items}
keyExtractor={item => item.id.toString()}
renderItem={({ item, index }) => (
<AuctionBiddingCard
item={{ ...item, index }}
bidValue={bidValues[item.id] || 0}
onIncrease={() => stepBid(item, 'up')}
onDecrease={() => stepBid(item, 'down')}
onPlaceBid={() => submitBid(item)}
SelectedAuction={SelectedAuction}
allBoolean={allBoolean}
setAllBoolean={setAllBoolean}
config={config}
/>
)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
{/* ── Success overlay ── */}
{allBoolean.showSuccess && (
<View style={styles.overlay}>
<LottieView
source={require('../../lottie/Success.json')}
autoPlay
loop={false}
style={styles.successLottie}
/>
</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 AuctionBiddingScreen;
/* ─── Styles factory ─── */
const makeStyles = (colors: any, spacing: any, shadows: any) =>
StyleSheet.create({
root: {
flex: 1,
backgroundColor: colors.background,
},
/* Info band */
infoBand: {
backgroundColor: colors.surface,
paddingHorizontal: spacing.m,
paddingVertical: spacing.m,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
gap: spacing.s,
...shadows.light,
},
deptRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.s,
},
deptIcon: {
width: 30,
height: 30,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
},
deptName: {
flex: 1,
} as any,
/* Timer */
timerCard: {
borderRadius: 16,
borderWidth: 1.2,
paddingVertical: spacing.m,
paddingHorizontal: spacing.m,
alignItems: 'center',
gap: 4,
},
timerLabelRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
timerLabel: {
letterSpacing: 1.2,
} as any,
/* Chips */
chipRow: {
flexDirection: 'row',
gap: spacing.s,
},
chip: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: spacing.s + 4,
paddingVertical: 5,
borderRadius: 20,
},
chipText: {
letterSpacing: 0.3,
} as any,
liveDot: {
width: 6,
height: 6,
borderRadius: 3,
},
listContent: {
padding: spacing.m,
paddingBottom: 120,
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: colors.overlay,
justifyContent: 'center',
alignItems: 'center',
zIndex: 99,
},
successLottie: { width: 200, height: 200 },
});
@@ -0,0 +1,350 @@
import React, { memo, useCallback, useRef, useState } from 'react';
import { View, TouchableOpacity, StyleSheet, TextInput } from 'react-native';
import Animated, { FadeInUp, Layout, interpolate, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import AuctionItemDetail from '../../Auction/Modal/AuctionItemDetail';
import MainStyle from '../../../Styles/MainStyle';
import { useTheme } from '../../../Hooks/useTheme';
import AppText from '../../../components/AppText';
import { Loader } from '../../../components';
import { useSelector } from 'react-redux';
import { env } from '../../../core/config/env';
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 { BiddingPatternConfig } from '../biddingPattern';
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 styles = React.useMemo(() => makeStyles(colors), [colors]);
const Token = useSelector((state: any) => state.token.token);
const [showDetail, setShowDetail] = useState(false);
const [value, setValue] = useState(`${item.bidAmount}`);
const [allState, setAllState] = useState({ isLoading: false });
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
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,
});
}, [animation]);
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`,
},
],
}));
const handleChange = (text: string) => {
const cleaned = text.replace(/[^0-9.]/g, '');
if ((cleaned.match(/\./g) || []).length > 1) { return; }
if (cleaned === '') {
setValue('');
return;
}
if (!config.acceptManualInput(Number(cleaned), item)) { return; }
setValue(cleaned);
};
const oneTimePlaceBid = async () => {
const validationError = config.validateManualBid(Number(value), item);
if (validationError) {
showErrorAlert(validationError);
return;
}
try {
setAllState((prev: any) => ({ ...prev, isLoading: true }));
const payload = {
itemId: item.id,
bidAmount: Number(value),
auctionId: SelectedAuction.auctionId,
// Session stores the contractor id as `id` (contractorId is absent).
contractorId: Token?.contractorId ?? Token?.id,
fullName: Token.fullName,
};
const res = await request<any>({
method: 'post',
url: `${env.socketBaseUrl}bidding/placeBidForAuction`,
data: [payload],
});
if (res.statusCode === 200) {
showSuccessAlert(res.message);
}
setAllBoolean((prev: any) => ({ ...prev, reload: !prev.reload }));
} catch (error) {
if (isAppError(error) && error.statusCode !== undefined) {
showErrorAlert(error.message);
} else {
logger.error('bidding', 'manual bid failed', { error: String(error) });
}
} finally {
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 (
<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={styles.flex1}>
<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 : ₹{config.startValue(item)}
</AppText>
</View>
<TouchableOpacity style={styles.headerRight} onPress={toggleExpand}>
<View style={[MainStyle.flexBetween, styles.gap10]}>
<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 || config.startValue(item)}</AppText>
</View>
<View style={styles.alignEnd}>
<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}>
{config.stepPrefix}₹{config.step(item)}
</AppText>
</View>
</View>
{/* COLLAPSIBLE BODY */}
<Animated.View style={[styles.collapsibleContainer, bodyFull ? bodyStyle : bodyStyle2]}>
<View style={styles.divider} />
<AppText variant="bodySmall" color={colors.textSecondary} semiBold style={styles.bidLabel as any}>Set Your Bid</AppText>
{(!allBoolean.oneTimeBid || config.inlineManualInput) && <View style={styles.stepperContainer}>
{!allBoolean.oneTimeBid && <TouchableOpacity
onPress={onDecrease}
disabled={!canDecrease}
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>
</TouchableOpacity>}
{!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="h4" color={colors.text} bold style={styles.bidValueText as any}>{bidValue}</AppText>
</View>}
{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>
</TouchableOpacity>}
</View>}
{!config.inlineManualInput && manualInput}
{submitVisible ? <TouchableOpacity
style={[styles.submitBtn]}
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>
}
</TouchableOpacity> : null}
</Animated.View>
</Animated.View>
:
<AuctionItemDetail data={item} onClose={() => { setShowDetail(false); }} visible={showDetail} />
}
</View>
);
};
export default memo(AuctionBiddingCard);
const makeStyles = (colors: any) => StyleSheet.create({
flex1: { flex: 1 },
gap10: { gap: 10 },
alignEnd: { alignItems: 'flex-end' },
inputBox2: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background,
borderRadius: 12,
height: 45,
paddingHorizontal: 12,
},
inputBox2Block: {
width: '100%',
marginBottom: 20,
},
inputBox2Inline: {
flex: 1,
},
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,
});

Some files were not shown because too many files have changed in this diff Show More