Compare commits

..

No commits in common. "feature/app-setup" and "dev" have entirely different histories.

164 changed files with 321 additions and 14015 deletions

View File

@ -1,65 +0,0 @@
# Node modules
node_modules/
# macOS system files
.DS_Store
# Environment files
.env
.env.\*
!.env.example
# Expo
.expo/
.expo-shared/
dist/
web-build/
# Android build artifacts
android/.gradle/
android/build/
android/app/build/
android/app/.cxx/
android/.idea/
android/local.properties
# iOS build artifacts
ios/build/
ios/Pods/
ios/Podfile.lock
ios/.xcworkspace/
ios/.idea/
ios/DerivedData/
# Miscellaneous
_.jks
_.keystore
_.p8
_.p12
_.key
_.mobileprovision
_.tsbuildinfo
_.log
_.pem
_.orig.\*
_.csv
_.bak
# Metro bundler cache
.metro-cache/
.cache/
# Package manager logs
npm-debug.log
yarn-debug.log
yarn-error.log
pnpm-debug.log

6
.gitignore vendored
View File

@ -35,9 +35,3 @@ yarn-error.*
# typescript
*.tsbuildinfo
android/.gradle/
android/.idea/
android/local.properties
android/app/build/
android/build/

View File

16
android/.gitignore vendored
View File

@ -1,16 +0,0 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

View File

@ -1,177 +0,0 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// 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
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace 'com.vinayjangra01.BatteryAsAService'
defaultConfig {
applicationId 'com.vinayjangra01.BatteryAsAService'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
}
}
packagingOptions {
jniLibs {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
}
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
}
}
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}

Binary file not shown.

View File

@ -1,14 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@ -1,7 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@ -1,35 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCJ4IkKM0ybTOwvdylLVx9BxL2ZV1PEvRk"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="batteryasaservice"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,65 +0,0 @@
package com.vinayjangra01.BatteryAsAService
import expo.modules.splashscreen.SplashScreenManager
import android.os.Build
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
// setTheme(R.style.AppTheme);
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
SplashScreenManager.registerOnActivity(this)
// @generated end expo-splashscreen
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
}

View File

@ -1,57 +0,0 @@
package com.vinayjangra01.BatteryAsAService
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(MyReactNativePackage())
return packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,6 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
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)'
<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.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1 +0,0 @@
<resources/>

View File

@ -1,6 +0,0 @@
<resources>
<color name="splashscreen_background">#252A34</color>
<color name="iconBackground">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#252A34</color>
</resources>

View File

@ -1,6 +0,0 @@
<resources>
<string name="app_name">Driver Saathi</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
</resources>

View File

@ -1,12 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.EdgeToEdge">
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#252A34</item>
</style>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>

View File

@ -1,37 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
}
}
def reactNativeAndroidDir = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('react-native/package.json')")
}.standardOutput.asText.get().trim(),
"../android"
)
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(reactNativeAndroidDir)
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"

View File

@ -1,59 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# 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
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
expo.edgeToEdgeEnabled=true

Binary file not shown.

View File

@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored
View File

@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (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
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
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.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored
View File

@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (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
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@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 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,39 +0,0 @@
pluginManagement {
def reactNativeGradlePlugin = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim()
).getParentFile().absolutePath
includeBuild(reactNativeGradlePlugin)
def expoPluginsPath = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim(),
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
}
plugins {
id("com.facebook.react.settings")
id("expo-autolinking-settings")
}
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
expoAutolinking.useExpoModules()
rootProject.name = 'BatteryAsAService'
expoAutolinking.useExpoVersionCatalog()
include ':app'
includeBuild(expoAutolinking.reactNativeGradlePlugin)

View File

@ -6,12 +6,12 @@
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "batteryasaservice",
"userInterfaceStyle": "light",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#252A34"
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
@ -21,8 +21,7 @@
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"package": "com.vinayjangra01.BatteryAsAService"
"edgeToEdgeEnabled": true
},
"web": {
"bundler": "metro",
@ -30,24 +29,10 @@
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"backgroundColor": "#252A34",
"image": "./assets/images/splash-icon.png",
"imageWidth": 200
}
]
"expo-router"
],
"experiments": {
"typedRoutes": true
},
"extra": {
"router": {},
"eas": {
"projectId": "d91c064f-5483-45be-b301-d8f036e7df09"
}
}
}
}

View File

@ -1,84 +1,59 @@
import React, { useEffect } from "react";
import { Tabs } from "expo-router";
import { useTabConfig } from "@/constants/config";
import { useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/store";
import { initSocket } from "@/services/socket";
import { useSnackbar } from "@/contexts/Snackbar";
import NetInfo from "@react-native-community/netinfo";
import { getPaymentSummary, getUserDetails } from "@/store/userSlice";
import { useDispatch } from "react-redux";
import { logout } from "@/store/authSlice";
import React from 'react';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { Link, Tabs } from 'expo-router';
import { Pressable } from 'react-native';
import Colors from '@/constants/Colors';
import { useColorScheme } from '@/components/useColorScheme';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
}
export default function TabLayout() {
const { isLoggedIn } = useSelector((state: RootState) => state.auth);
if (!isLoggedIn) return null;
const TAB_CONFIG = useTabConfig();
const dispatch = useDispatch<AppDispatch>();
const { showSnackbar } = useSnackbar();
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
const isConnected = state.isConnected;
const colorScheme = useColorScheme();
if (isConnected === false) {
console.log("No internet connection");
showSnackbar("No internet connection", "error");
}
});
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
const fetchAndInit = async () => {
try {
const result = await dispatch(getUserDetails()).unwrap();
await dispatch(getPaymentSummary());
console.log("User details fetched", result);
initSocket();
} catch (error) {
console.error("Failed to fetch user details", error);
showSnackbar("Failed to fetch user details", "error");
}
};
if (isLoggedIn) {
fetchAndInit();
}
}, [isLoggedIn]);
return (
<Tabs
screenOptions={{
headerShadowVisible: false,
tabBarStyle: {
backgroundColor: "#252A34",
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
position: "absolute",
overflow: "hidden",
},
tabBarActiveTintColor: "#fff",
tabBarInactiveTintColor: "#aaa",
headerStyle: {
backgroundColor: "#F3F5F8",
},
}}
>
{TAB_CONFIG.map(({ name, title, Icon, IconFilled }) => (
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true),
}}>
<Tabs.Screen
key={name}
name={name}
name="index"
options={{
title,
tabBarIcon: ({ color, focused }) => {
const IconComponent = focused ? IconFilled : Icon;
return <IconComponent />;
},
title: 'Tab One',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
headerRight: () => (
<Link href="/modal" asChild>
<Pressable>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme ?? 'light'].text}
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="two"
options={{
title: 'Tab Two',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
))}
</Tabs>
);
}

View File

@ -1,515 +1,31 @@
import {
Animated,
Easing,
Pressable,
ScrollView,
StyleSheet,
TouchableOpacity,
} from "react-native";
import { useTranslation } from "react-i18next";
import SemiCircleProgress from "../../components/home/SemiCircleProgress";
import { Text, View } from "react-native";
import { useNavigation } from "expo-router";
import { useEffect, useLayoutEffect, useState } from "react";
import { StatusBar } from "expo-status-bar";
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
import ServiceReminderCard from "@/components/home/ServiceReminderCard";
import MetricCard from "@/components/home/MetricCard";
import PaymentDueCard from "@/components/home/PaymentDueCard";
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
import BatteryWarrantyCard from "@/components/home/BatteryWarrantyCars";
import CustomerSupportModal from "@/components/home/CustomerSupportModal";
import { useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/store";
import LocationOff from "@/assets/icons/location_off.svg";
import { Linking } from "react-native";
import { useRouter } from "expo-router";
import ProfileImage from "@/components/home/Profile";
import { calculateBearing, calculateDistance } from "@/utils/Map";
import { useDispatch } from "react-redux";
import RefreshIcon from "@/assets/icons/refresh.svg";
import { StyleSheet } from 'react-native';
import { payments } from "@/constants/config";
import {
clearUser,
getPaymentSummary,
getUserDetails,
} from "@/store/userSlice";
import api from "@/services/axiosClient";
import { setDueAmount } from "@/store/paymentSlice";
import { EmiResponse } from "./payments";
import { Image } from "expo-image";
import EMINotification from "@/components/Payments/EmiNotification";
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
const { SoC, SoH, chargingState, lat, lon, loading, error, totalDistance } =
useSelector((state: RootState) => state.telemetry);
const [refreshing, setRefreshing] = useState(false);
const spinValue = useState(new Animated.Value(0))[0];
const { data, paymentSummary } = useSelector(
(state: RootState) => state.user
);
const [prevPosition, setPrevPosition] = useState<{
lat: number;
lon: number;
} | null>(null);
const [bearing, setBearing] = useState<number>(0);
const dispatch = useDispatch<AppDispatch>();
const vehicle =
Array.isArray(data?.vehicles) && data.vehicles.length > 0
? data.vehicles[0]
: null;
const battery =
Array.isArray(data?.batteries) && data.batteries.length > 0
? data.batteries[0]
: null;
const model = vehicle?.model ?? "---";
const chasisNumber = vehicle?.chasis_number ?? "---";
const warrantyStartDate = data?.batteries[0]?.warranty_start_date || null;
const warrantyEndDate = data?.batteries[0]?.warranty_end_date || null;
useEffect(() => {
if (lat && lon) {
if (prevPosition) {
// Calculate bearing between prev and current position
const newBearing = calculateBearing(
prevPosition.lat,
prevPosition.lon,
lat,
lon
);
setBearing(newBearing);
}
setPrevPosition({ lat, lon }); // Update previous position
}
}, [lat, lon]);
useEffect(() => {
if (lat && lon) {
if (prevPosition) {
const distance = calculateDistance(
prevPosition.lat,
prevPosition.lon,
lat,
lon
);
if (distance > 5) {
// Only update bearing if moved >5 meters
const newBearing = calculateBearing(
prevPosition.lat,
prevPosition.lon,
lat,
lon
);
setBearing(newBearing);
}
}
setPrevPosition({ lat, lon });
}
}, [lat, lon]);
const router = useRouter();
useLayoutEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: "#F3F5F8",
},
headerTitle: () => (
<View style={styles.headerTitleContainer}>
<Text style={styles.title}>{model}</Text>
<Text style={styles.subtitle}>{chasisNumber}</Text>
</View>
),
headerRight: () => (
<View style={styles.rightContainer}>
<Pressable onPress={handleManualRefresh} disabled={refreshing}>
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<RefreshIcon
width={50}
height={50}
opacity={refreshing ? 0.5 : 1}
/>
</Animated.View>
</Pressable>
<Pressable
style={styles.supportButton}
onPress={() => {
setIsSupportModalVisible(true);
console.log("hkdlfjaldf");
}}
>
<CustomerCareIcon width={50} height={50} />
</Pressable>
<View>
<ProfileImage
username={data?.name || "Vec"}
textSize={20}
boxSize={40}
onClick={() => {
router.push("/user/profile");
}}
/>
</View>
</View>
),
});
}, [navigation, data, model, chasisNumber, refreshing]);
const openInGoogleMaps = () => {
const url = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
Linking.openURL(url).catch((err) =>
console.error("Failed to open Google Maps:", err)
);
};
const startSpin = () => {
spinValue.setValue(0);
Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
};
const stopSpin = () => {
spinValue.stopAnimation();
spinValue.setValue(0);
};
const spin = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"],
});
const handleManualRefresh = async () => {
try {
setRefreshing(true);
startSpin();
await dispatch(clearUser());
await dispatch(getUserDetails()).unwrap();
await dispatch(getPaymentSummary()).unwrap();
console.log("Manual refresh complete");
} catch (error) {
console.error("Manual refresh failed", error);
} finally {
stopSpin();
setRefreshing(false);
}
};
const {
daysLeftToPayEmi,
daysLeftToPayEmiText,
regularServiceDueInDaysText,
due_amount,
} = paymentSummary || {};
useEffect(() => {
if (paymentSummary?.due_amount != null) {
dispatch(setDueAmount(paymentSummary.due_amount));
}
}, [paymentSummary, dispatch]);
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function TabOneScreen() {
return (
<View style={{ flex: 1, backgroundColor: "#F3F5F8" }}>
<StatusBar style="dark" />
<ScrollView contentContainerStyle={styles.container} style={{ flex: 1 }}>
{daysLeftToPayEmi && daysLeftToPayEmiText && daysLeftToPayEmi > 0 && (
<ServiceReminderCard
type={
daysLeftToPayEmi >= payments.EMI_WARNING_DAYS_THRESHOLD
? "warning"
: "danger"
}
message={`${daysLeftToPayEmi} ${t("home.days-left-to-pay-emi")}`}
subMessage={t("home.pay-now")}
redirectPath="/(tabs)/payments"
/>
)}
{regularServiceDueInDaysText && (
<ServiceReminderCard
type="info"
message={regularServiceDueInDaysText}
subMessage="Service Reminder"
redirectPath="/(tabs)/service"
/>
)}
<View style={styles.iconContainer}>
<SemiCircleProgress progress={SoC} status={chargingState} />
</View>
<View style={styles.metrics}>
<MetricCard heading={t("home.battery-health")} value={SoH} unit="%" />
<MetricCard
heading={t("home.total-distance")}
value={totalDistance}
unit={t("home.km")}
/>
</View>
{due_amount && due_amount > 0 ? (
<PaymentDueCard
label={t("home.payment-due")}
amount={due_amount}
onPress={() => {
router.push("/payments/selectAmount");
}}
/>
) : due_amount == 0 ? (
<EMINotification
message={`${t("home.payment-complete")}`}
actionText={`${t("home.view-details")}`}
onActionPress={() => router.push("/(tabs)/payments")}
/>
) : null}
<View style={styles.map}>
{loading ? (
<View style={styles.errorContainer}>
<LocationOff />
<Text style={styles.errorText}>
{t("home.fetching-location")}
</Text>
</View>
) : lat != null && lon != null && !(lat == 0 && lon == 0) ? (
<>
<View style={styles.mapContainer}>
<MapView
provider={PROVIDER_GOOGLE}
style={styles.mapStyle}
region={{
latitude: lat,
longitude: lon,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
// customMapStyle={mapStyle}
userInterfaceStyle="light"
>
<Marker
draggable
coordinate={{
latitude: lat,
longitude: lon,
}}
rotation={bearing}
anchor={{ x: 0.5, y: 0.5 }}
tracksViewChanges={false}
>
<Image
source={require("../../assets/images/marker.png")}
style={{ height: 35, width: 35 }}
/>
</Marker>
</MapView>
</View>
<TouchableOpacity>
<Text
style={styles.viewLocationText}
onPress={openInGoogleMaps}
>
{t("home.view-vehicle-location")}
</Text>
</TouchableOpacity>
</>
) : (
<View style={styles.errorContainer}>
<LocationOff />
<Text style={styles.errorText}>{t("home.error-location")}</Text>
</View>
)}
</View>
{warrantyEndDate && warrantyStartDate && (
<TouchableOpacity onPress={() => router.push("/(tabs)/my-battery")}>
<BatteryWarrantyCard
warrantyStartDate={warrantyStartDate}
warrantyEndDate={warrantyEndDate}
/>
</TouchableOpacity>
)}
</ScrollView>
<CustomerSupportModal
visible={isSupportModalVisible}
onClose={() => setIsSupportModalVisible(false)}
/>
<View style={styles.container}>
<Text style={styles.title}>Tab One</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/index.tsx" />
</View>
);
}
const mapStyle = [
{ elementType: "geometry", stylers: [{ color: "#242f3e" }] },
{ elementType: "labels.text.fill", stylers: [{ color: "#746855" }] },
{ elementType: "labels.text.stroke", stylers: [{ color: "#242f3e" }] },
{
featureType: "administrative.locality",
elementType: "labels.text.fill",
stylers: [{ color: "#d59563" }],
},
{
featureType: "poi",
elementType: "labels.text.fill",
stylers: [{ color: "#d59563" }],
},
{
featureType: "poi.park",
elementType: "geometry",
stylers: [{ color: "#263c3f" }],
},
{
featureType: "poi.park",
elementType: "labels.text.fill",
stylers: [{ color: "#6b9a76" }],
},
{
featureType: "road",
elementType: "geometry",
stylers: [{ color: "#38414e" }],
},
{
featureType: "road",
elementType: "geometry.stroke",
stylers: [{ color: "#212a37" }],
},
{
featureType: "road",
elementType: "labels.text.fill",
stylers: [{ color: "#9ca5b3" }],
},
{
featureType: "road.highway",
elementType: "geometry",
stylers: [{ color: "#746855" }],
},
{
featureType: "road.highway",
elementType: "geometry.stroke",
stylers: [{ color: "#1f2835" }],
},
{
featureType: "road.highway",
elementType: "labels.text.fill",
stylers: [{ color: "#f3d19c" }],
},
{
featureType: "transit",
elementType: "geometry",
stylers: [{ color: "#2f3948" }],
},
{
featureType: "transit.station",
elementType: "labels.text.fill",
stylers: [{ color: "#d59563" }],
},
{
featureType: "water",
elementType: "geometry",
stylers: [{ color: "#17263c" }],
},
{
featureType: "water",
elementType: "labels.text.fill",
stylers: [{ color: "#515c6d" }],
},
{
featureType: "water",
elementType: "labels.text.stroke",
stylers: [{ color: "#17263c" }],
},
];
const styles = StyleSheet.create({
errorText: {
color: "#565F70",
fontWeight: "400",
fontSize: 16,
textAlign: "center",
},
errorContainer: {
height: 255,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#FCFCFC",
gap: 8,
},
viewLocationText: {
color: "#388E3C",
fontWeight: "600",
fontSize: 16,
textAlign: "center",
marginTop: 8,
},
map: {
padding: 12,
borderRadius: 10,
overflow: "hidden",
backgroundColor: "#fff",
},
mapContainer: {
height: 255,
},
mapStyle: {
width: "100%",
height: "100%",
borderRadius: 10,
},
metrics: {
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: "#F3F5F8",
},
container: {
padding: 16,
gap: 16,
paddingBottom: 110,
},
iconContainer: {
backgroundColor: "#FCFCFC",
},
supportButton: {
backgroundColor: "#F3F5F8",
},
headerTitleContainer: {
flexDirection: "column",
backgroundColor: "#F3F5F8",
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 14,
color: "#6B7280",
fontWeight: "500",
fontSize: 20,
fontWeight: 'bold',
},
subtitle: {
fontSize: 18,
color: "#111827",
fontWeight: "700",
},
rightContainer: {
flexDirection: "row",
alignItems: "center",
paddingRight: 16,
gap: 4,
backgroundColor: "#F3F5F8",
},
badge: {
backgroundColor: "#FEE2E2",
borderRadius: 20,
width: 30,
height: 30,
justifyContent: "center",
alignItems: "center",
},
badgeText: {
color: "#DC2626",
fontWeight: "700",
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

View File

@ -1,239 +0,0 @@
import { RootState } from "@/store";
import React from "react";
import { useTranslation } from "react-i18next";
import { View, Text, StyleSheet, ScrollView } from "react-native";
import { useSelector } from "react-redux";
const BatteryDetails = () => {
const { data } = useSelector((state: RootState) => state.user);
const { t } = useTranslation();
const model = data?.batteries[0]?.battery_model ?? "---";
const batteryId = data?.batteries[0]?.battery_id ?? "---";
const bmsId = data?.batteries[0]?.bms_id ?? "---";
const warrantyStartDate = data?.batteries[0]?.warranty_start_date ?? "---";
const warrantyEndDate = data?.batteries[0]?.warranty_end_date ?? "---";
const vimId = data?.batteries[0]?.vim_id ?? "---";
const serialNumber = data?.batteries[0]?.serial_no ?? "---";
const chargerUid = data?.batteries[0]?.charger_uid ?? "---";
const battery = data?.batteries?.[0];
const start = battery?.warranty_start_date
? new Date(battery.warranty_start_date)
: null;
const end = battery?.warranty_end_date
? new Date(battery.warranty_end_date)
: null;
const now = new Date();
let progress = 0;
let durationText = "---";
if (
start &&
end &&
!isNaN(start.getTime()) &&
!isNaN(end.getTime()) &&
now < end
) {
const totalDuration = end.getTime() - start.getTime();
const elapsed = now.getTime() - start.getTime();
const remaining = Math.max(end.getTime() - now.getTime(), 0);
progress = Math.min(elapsed / totalDuration, 1);
const yearsLeft = Math.floor(remaining / (365.25 * 24 * 60 * 60 * 1000));
const monthsLeft = Math.floor(
(remaining % (365.25 * 24 * 60 * 60 * 1000)) /
(30.44 * 24 * 60 * 60 * 1000)
);
const daysLeft = Math.floor(
(remaining % (30.44 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)
);
durationText = (() => {
const parts: string[] = [];
if (yearsLeft > 0) {
parts.push(`${yearsLeft} ${t("home.year")}`);
}
if (monthsLeft > 0) {
parts.push(`${monthsLeft} ${t("home.month")}`);
}
if (daysLeft > 0 || parts.length === 0) {
parts.push(`${daysLeft} ${t("home.day")}`);
}
return parts.join(", ");
})();
}
const formatDate = (date?: Date | null): string =>
date && !isNaN(date.getTime())
? date.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
})
: "---";
const isInWarranty =
start &&
end &&
now >= start &&
now <= end &&
!isNaN(start.getTime()) &&
!isNaN(end.getTime());
return (
<ScrollView contentContainerStyle={styles.container}>
{/* Battery Section */}
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{t("battery.battery")}</Text>
{isInWarranty ? (
<View style={styles.badge}>
<Text style={styles.badgeText}>{t("battery.in-warranty")}</Text>
</View>
) : (
<View style={styles.expiredBadge}>
<Text style={styles.expiredBadgeText}>
{t("battery.warranty-expired")}
</Text>
</View>
)}
</View>
<View style={styles.divider} />
<InfoRow label={t("battery.model")} value={model} />
<InfoRow label={t("battery.battery-id")} value={batteryId} />
<InfoRow label={t("battery.bms-id")} value={bmsId} />
</View>
{/* Battery Warranty Details Section */}
<View style={styles.card}>
<Text style={styles.cardTitle}>
{t("battery.battery-warranty-details")}
</Text>
<View style={styles.divider} />
<InfoRow label={t("battery.start-date")} value={formatDate(start)} />
<InfoRow label={t("battery.end-date")} value={formatDate(end)} />
<InfoRow label={t("battery.duration-left")} value={durationText} />
{start && end && !isNaN(start.getTime()) && !isNaN(end.getTime()) && (
<View style={styles.progressBarBackground}>
<View
style={[
styles.progressBarFill,
{ width: `${(1 - progress) * 100}%` },
]}
/>
</View>
)}
</View>
{/* VIM Details Section */}
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("battery.vim-details")}</Text>
<View style={styles.divider} />
<InfoRow label={t("battery.vim-id")} value={vimId} />
<InfoRow label={t("battery.serial-number")} value={serialNumber} />
</View>
{/* Charger Details Section */}
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("battery.charger-details")}</Text>
<View style={styles.divider} />
<InfoRow label={t("battery.uid")} value={chargerUid} />
</View>
</ScrollView>
);
};
type InfoRowProps = {
label: string;
value: string;
};
const InfoRow: React.FC<InfoRowProps> = ({ label, value }) => (
<View style={styles.infoRow}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
</View>
);
export default BatteryDetails;
const styles = StyleSheet.create({
expiredBadgeText: {
fontSize: 12,
fontWeight: "500",
color: "#D51D10",
},
container: {
backgroundColor: "#F3F5F8",
paddingHorizontal: 16,
},
card: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 12,
marginBottom: 16,
},
cardHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
cardTitle: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
expiredBadge: {
backgroundColor: "#FDE9E7",
paddingVertical: 2,
paddingHorizontal: 8,
borderRadius: 4,
},
badge: {
backgroundColor: "#DAF5ED",
paddingVertical: 2,
paddingHorizontal: 8,
borderRadius: 4,
},
badgeText: {
fontSize: 12,
fontWeight: "500",
color: "#006C4D",
},
divider: {
height: 1,
backgroundColor: "#e5e9f0",
marginVertical: 12,
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 4,
},
label: {
fontSize: 14,
color: "#252A34",
},
value: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
progressBarBackground: {
height: 8,
backgroundColor: "#E5E9F0",
borderRadius: 10,
overflow: "hidden",
marginTop: 16,
},
progressBarFill: {
height: 8,
backgroundColor: "#00825B",
borderRadius: 10,
},
});

View File

@ -1,875 +0,0 @@
import {
useLayoutEffect,
useMemo,
useState,
useEffect,
useCallback,
} from "react";
import { useNavigation, useRouter } from "expo-router";
import {
Animated,
Easing,
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
ScrollView,
StyleSheet,
TouchableOpacity,
} from "react-native";
import { Text, View } from "react-native";
import { useSelector } from "react-redux";
import { RootState } from "@/store";
import ProfileImage from "@/components/home/Profile";
import { Overlay } from "@/components/common/Overlay";
import { useSnackbar } from "@/contexts/Snackbar";
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
import api from "@/services/axiosClient";
import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard";
import { BASE_URL } from "@/constants/config";
import { useDispatch } from "react-redux";
import {
setAdvanceBalance,
setDueAmount,
setMyPlan,
} from "@/store/paymentSlice";
import { ActivityIndicator } from "react-native-paper";
import { useFocusEffect } from "@react-navigation/native";
import { displayValue } from "@/utils/Common";
import RefreshIcon from "@/assets/icons/refresh.svg";
import CustomerSupport from "@/components/home/CustomerSupportModal";
import { useTranslation } from "react-i18next";
export interface MyPlan {
no_of_emi: number;
total_amount: number;
down_payment: number;
emi_amount: number;
total_emi: number;
installment_paid: number;
current_amount: number;
}
interface EmiDetails {
due_amount: number;
total_amount_paid_in_current_cycle: number;
due_date: string;
status: string;
advance_balance: number;
pending_cycles: number;
total_pending_installments: number;
myPlain: MyPlan;
}
export interface EmiResponse {
success: boolean;
data: EmiDetails[];
}
interface PaymentHistoryItem {
id: number;
amount: string;
status: string;
created_at: string;
payment_mode: string[];
payment_date: string;
}
interface PaymentHistoryPagination {
total_records: number;
page_number: number;
page_size: number;
}
interface PaymentHistoryResponse {
success: boolean;
data: {
payments: PaymentHistoryItem[];
pagination: PaymentHistoryPagination;
};
}
function formatPaymentDate(input: string): string {
const date = new Date(input);
const options: Intl.DateTimeFormatOptions = {
day: "2-digit",
month: "short",
year: "numeric",
};
return date.toLocaleDateString("en-GB", options);
}
// Format time for payment history
function formatPaymentTime(input: string): string {
if (!input) return "--";
const date = new Date(input);
const timeStr = date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
const dayStr = date.toLocaleDateString("en-US", { weekday: "long" });
return `${timeStr} | ${dayStr}`;
}
export default function PaymentsTabScreen() {
const navigation = useNavigation();
const { data } = useSelector((state: RootState) => state.user);
const router = useRouter();
const { showSnackbar } = useSnackbar();
const [emiDetails, setEmiDetails] = useState<EmiDetails | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
const [isEndReached, setIsEndReached] = useState(false);
const dispatch = useDispatch();
//payment history states
const [paymentHistory, setPaymentHistory] = useState<PaymentHistoryItem[]>(
[]
);
const [refreshing, setRefreshing] = useState(false);
const spinValue = useState(new Animated.Value(0))[0];
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
const [showFullHistory, setShowFullHistory] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [hasMorePages, setHasMorePages] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const vehicle =
Array.isArray(data?.vehicles) && data.vehicles.length > 0
? data.vehicles[0]
: null;
const battery =
Array.isArray(data?.batteries) && data.batteries.length > 0
? data.batteries[0]
: null;
const model = vehicle?.model ?? "---";
const chasisNumber = vehicle?.chasis_number ?? "---";
const fetchEmiDetails = async () => {
try {
setIsLoading(true);
setEmiDetails(null);
setAdvanceBalance(null);
const response = await api.get(`/api/v1/emi-details`);
const result: EmiResponse = response.data;
if (result.success && result.data.length > 0) {
const details = result.data[0];
setEmiDetails(details);
dispatch(setDueAmount(details.due_amount));
dispatch(setMyPlan(details.myPlain));
dispatch(setAdvanceBalance(details.advance_balance));
} else {
showSnackbar("No EMI details found", "error");
}
} catch (err) {
console.error("Error fetching EMI details:", err);
const errorMessage =
err instanceof Error
? err.message
: `${t("service.something-went-wrong")}`;
showSnackbar(errorMessage, "error");
} finally {
setIsLoading(false);
}
};
const handleRefresh = async () => {
try {
setShowFullHistory(false);
setRefreshing(true);
startSpin();
await Promise.all([fetchEmiDetails(), fetchPaymentHistory(1, false)]);
console.log("Manual refresh complete");
} catch (error) {
console.error("Manual refresh failed", error);
} finally {
stopSpin();
setRefreshing(false);
}
};
const startSpin = () => {
spinValue.setValue(0);
Animated.loop(
Animated.timing(spinValue, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
};
const stopSpin = () => {
spinValue.stopAnimation();
spinValue.setValue(0);
};
const spin = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"],
});
useFocusEffect(
useCallback(() => {
setShowFullHistory(false);
fetchEmiDetails();
fetchPaymentHistory(1, false);
}, [])
);
useLayoutEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: "#F3F5F8",
},
headerTitle: () => (
<View style={styles.headerTitleContainer}>
<Text style={styles.title}>{model}</Text>
<Text style={styles.subtitle}>{chasisNumber}</Text>
</View>
),
headerRight: () => (
<View style={styles.rightContainer}>
<Pressable onPress={() => handleRefresh()} disabled={refreshing}>
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<RefreshIcon
height={50}
width={50}
style={{ opacity: refreshing ? 0.5 : 1 }}
/>
</Animated.View>
</Pressable>
<Pressable
style={styles.supportButton}
onPress={() => {
console.log("Support Pressed");
setIsSupportModalVisible(true);
}}
>
<CustomerCareIcon height={50} width={50} />
</Pressable>
<View>
<ProfileImage
username={data?.name || "--"}
onClick={() => router.push("/user/profile")}
textSize={20}
boxSize={40}
/>
</View>
</View>
),
});
}, [navigation, data, model, chasisNumber, refreshing]);
// Format currency
const formatCurrency = (amount: number) => {
console.log(amount, "amount format current");
return `${amount.toLocaleString()}`;
};
const handleViewAll = () => {
setShowFullHistory(true);
};
// Format date
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleDateString("en-IN", {
day: "2-digit",
month: "short",
year: "numeric",
});
} catch {
return dateString;
}
};
// Get current month/year for header
const getCurrentMonthYear = () => {
const date = new Date();
return date.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
};
const fetchPaymentHistory = async (
pageNumber: number = 1,
isLoadMore: boolean = false
) => {
try {
if (isLoadMore) {
setIsLoadingMore(true);
} else {
setIsHistoryLoading(true);
}
const response = await api.get(
`${BASE_URL}/api/v1/payment-history?page_number=${pageNumber}&page_size=10`
);
const result: PaymentHistoryResponse = response.data;
console.log("Payment History Response:", result);
if (result.success) {
const newPayments = result.data.payments;
if (isLoadMore) {
setPaymentHistory((prev) => [...prev, ...newPayments]);
} else {
setPaymentHistory(newPayments);
}
// Check if there are more pages
const totalPages = Math.ceil(
result.data.pagination.total_records /
result.data.pagination.page_size
);
setHasMorePages(pageNumber < totalPages);
setCurrentPage(pageNumber);
} else {
showSnackbar("No payment history found", "error");
}
} catch (err) {
console.error("Error fetching payment history:", err);
const errorMessage =
err instanceof Error
? err.message
: `${t("service.something-went-wrong")}`;
showSnackbar(errorMessage, "error");
} finally {
if (isLoadMore) {
setIsLoadingMore(false);
} else {
setIsHistoryLoading(false);
}
}
};
const handleLoadMore = () => {
if (hasMorePages && !isLoadingMore) {
fetchPaymentHistory(currentPage + 1, true);
}
};
useEffect(() => {
if (isEndReached && showFullHistory && hasMorePages && !isLoadingMore) {
handleLoadMore();
}
}, [isEndReached]);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
const paddingToBottom = 20;
const isAtBottom =
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
setIsEndReached(isAtBottom);
};
const { t } = useTranslation();
return (
<>
<ScrollView
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 125 }}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={16}
>
{/* Last EMI Details Card */}
<View style={styles.emiCard}>
{/* Header */}
<View style={styles.cardHeader}>
<Text style={styles.headerTitle}>
{t("payment.last-emi-details")}
</Text>
<Text style={styles.headerDate}>{getCurrentMonthYear()}</Text>
</View>
{/* Divider */}
<View style={styles.divider} />
{/* EMI Details Content */}
<View style={styles.cardContent}>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{t("payment.amount-due")}</Text>
<Text style={styles.detailValue}>
{displayValue(emiDetails?.due_amount, formatCurrency)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{t("payment.amount-paid")}</Text>
<Text style={styles.detailValue}>
{displayValue(
emiDetails?.total_amount_paid_in_current_cycle,
formatCurrency
)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{t("payment.due-date")}</Text>
<Text style={styles.detailValue}>
{displayValue(emiDetails?.due_date)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>
{t("payment.payment-status")}
</Text>
{emiDetails?.status ? (
<StatusBadge
label={emiDetails.status}
type={emiDetails.status.toLowerCase()}
/>
) : (
<Text style={styles.detailValue}>--</Text>
)}
</View>
</View>
<View style={styles.divider} />
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.primaryButton,
(isLoading || !emiDetails) && { opacity: 0.5 }, // dim if disabled
]}
onPress={() => router.push("/payments/selectAmount")}
disabled={isLoading || !emiDetails}
>
<Text style={styles.primaryButtonText}>
{t("payment.pay-emi")}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tertiaryButton,
(isLoading || !emiDetails) && { opacity: 0.5 },
]}
onPress={() => router.push("/payments/myPlan")}
disabled={isLoading || !emiDetails}
>
<Text style={styles.tertiaryButtonText}>
{t("payment.view-plan")}
</Text>
</TouchableOpacity>
</View>
</View>
<View>
<Text style={styles.sectionTitle}>
{t("payment.payment-history")}
</Text>
<View style={styles.paymentHistoryContainer}>
{isHistoryLoading ? (
<View style={styles.historyLoadingContainer}>
<ActivityIndicator color="#00BE88" />
</View>
) : (
<>
{/* Show initial payments or all payments based on showFullHistory */}
{(showFullHistory
? paymentHistory
: paymentHistory.slice(0, 3)
).map((payment) => (
<PaymentHistoryCard
id={payment.id}
key={payment.id}
date={formatPaymentDate(payment.payment_date)}
amount={formatCurrency(parseFloat(payment.amount))}
time={formatPaymentTime(payment.payment_date)}
method={payment.payment_mode.join(", ")}
status={payment.status}
/>
))}
{!showFullHistory && paymentHistory.length > 2 && (
<TouchableOpacity
style={styles.viewAllButton}
onPress={handleViewAll}
>
<Text style={styles.viewAllText}>
{t("payment.view-all")}
</Text>
<Text style={styles.chevron}></Text>
</TouchableOpacity>
)}
{isLoadingMore && (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>
<ActivityIndicator color="#00BE88" />
</Text>
</View>
)}
{showFullHistory &&
!hasMorePages &&
paymentHistory.length > 0 && (
<View style={styles.noMoreDataContainer}>
<Text style={styles.noMoreDataText}>
No more payments to show
</Text>
</View>
)}
{/* Empty state */}
{paymentHistory.length === 0 && !isHistoryLoading && (
<View style={styles.noDataContainer}>
<Text style={styles.noDataText}>
No payment history found
</Text>
</View>
)}
</>
)}
</View>
</View>
</ScrollView>
<CustomerSupport
visible={isSupportModalVisible}
onClose={() => setIsSupportModalVisible(false)}
/>
</>
);
}
const StatusBadge = ({
label,
type,
}: {
label: string | undefined;
type: string | undefined;
}) => {
if (!label || !type) return "--";
const getBadgeStyle = (type: string) => {
switch (type) {
case "pending":
return {
backgroundColor: "#FFF0E3",
color: "#8E4400",
};
case "completed":
return {
backgroundColor: "#E8F5E8",
color: "#2D7D32",
};
default:
return {
backgroundColor: "#E8F5E8",
color: "#2D7D32",
};
}
};
const badgeStyle = getBadgeStyle(type);
return (
<View
style={[styles.badge, { backgroundColor: badgeStyle.backgroundColor }]}
>
<Text style={[styles.badgeText, { color: badgeStyle.color }]}>
{label.charAt(0).toUpperCase() + label.slice(1)}
</Text>
</View>
);
};
const styles = StyleSheet.create({
loadingContainer: {
padding: 16,
alignItems: "center",
justifyContent: "center",
},
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 14,
color: "#6B7280",
fontWeight: "500",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
headerTitleContainer: {
flexDirection: "column",
backgroundColor: "#F3F5F8",
},
subtitle: {
fontSize: 18,
color: "#111827",
fontWeight: "700",
},
rightContainer: {
flexDirection: "row",
alignItems: "center",
paddingRight: 16,
gap: 8,
backgroundColor: "#F3F5F8",
},
supportButton: {
backgroundColor: "#F3F5F8",
},
scrollContainer: {
flex: 1,
paddingHorizontal: 16,
paddingBottom: 16,
width: "100%",
},
// EMI Card Styles
emiCard: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 12,
marginBottom: 36,
},
cardHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-end",
marginBottom: 12,
},
headerTitle: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
headerDate: {
fontSize: 14,
fontWeight: "500",
color: "#565C70",
},
divider: {
height: 1,
backgroundColor: "#E5E9F0",
marginBottom: 12,
},
cardContent: {
gap: 8,
marginBottom: 12,
},
detailRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
minHeight: 20,
},
detailLabel: {
fontSize: 14,
color: "#252A34",
flex: 1,
},
detailValue: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
// Badge Styles
badge: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
},
badgeText: {
fontSize: 12,
fontWeight: "500",
},
// Button Styles
buttonContainer: {
gap: 8,
},
primaryButton: {
backgroundColor: "#008761",
borderRadius: 4,
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: "center",
minHeight: 40,
justifyContent: "center",
},
primaryButtonText: {
color: "#FCFCFC",
fontSize: 14,
fontWeight: "500",
},
tertiaryButton: {
borderRadius: 4,
paddingVertical: 8,
alignItems: "center",
minHeight: 36,
justifyContent: "center",
},
tertiaryButtonText: {
color: "#006C4D",
fontSize: 14,
fontWeight: "500",
},
// Plan Details Styles
planDetailsSection: {
gap: 8,
marginBottom: 20,
},
planCard: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 12,
},
sectionTitle: {
fontSize: 14,
lineHeight: 20,
color: "#252A34", // rgb(37,42,51) from design
marginBottom: 8,
fontWeight: "600",
},
paymentHistoryContainer: {
flexDirection: "column",
gap: 16,
},
historyLoadingContainer: {
height: 312,
justifyContent: "center",
alignItems: "center",
},
loadingText: {
fontFamily: "Inter-Regular",
fontSize: 14,
color: "#252A33",
},
noDataContainer: {
height: 312,
justifyContent: "center",
alignItems: "center",
},
noDataText: {
fontFamily: "Inter-Regular",
fontSize: 14,
color: "#252A33",
},
noMoreDataContainer: {
marginTop: 16,
justifyContent: "center",
alignItems: "center",
},
noMoreDataText: {
fontFamily: "Inter-Regular",
fontSize: 12,
color: "#5B6478", // rgb(91,100,120) muted text
},
paymentCard: {
width: 328,
height: 76,
backgroundColor: "#FCFCFC", // #FCFCFC or #FBFBFB as in design (near white)
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 12,
marginBottom: 16,
justifyContent: "space-between",
// flexDirection: "column" by default
},
paymentCardTopRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
height: 20,
},
paymentDate: {
fontFamily: "Inter-Regular",
fontSize: 14,
color: "#252A33",
},
paymentAmount: {
fontFamily: "Inter-SemiBold",
fontSize: 14,
color: "#252A33",
},
paymentCardBottomRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
height: 20,
},
paymentTimeMethod: {
fontFamily: "Inter-Regular",
fontSize: 12,
color: "#56607A", // rgb(86,96,122)
},
paymentStatusBadge: {
borderRadius: 4,
paddingVertical: 2,
paddingHorizontal: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
paymentStatusLabel: {
fontFamily: "Inter-Medium",
fontSize: 12,
textAlign: "center",
},
// Status colors — you can override backgroundColor and color dynamically based on status
statusPending: {
backgroundColor: "#FFF0E5", // approx. (1, 0.941, 0.89)
color: "#803F0C", // approx. (0.5, 0.27, 0.047)
},
statusFailed: {
backgroundColor: "#FDDDD7", // approx. (0.99, 0.91, 0.9)
color: "#D6290A", // approx. (0.83, 0.11, 0.06)
},
statusSuccess: {
backgroundColor: "#E6F4EA", // light green example
color: "#0B8235",
},
viewAllButton: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 4,
},
viewAllText: {
fontFamily: "Inter-Medium",
fontSize: 14,
color: "#007958", // greenish text color
},
chevron: {
fontSize: 18,
color: "#007958",
marginLeft: 4,
},
loadMoreButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 4,
backgroundColor: "#007958",
alignSelf: "center",
},
loadMoreText: {
fontFamily: "Inter-Medium",
fontSize: 14,
color: "#FFFFFF",
},
});

View File

@ -1,559 +0,0 @@
import React, { JSX, useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
Image,
StyleSheet,
GestureResponderEvent,
ScrollView,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { Dropdown } from "react-native-element-dropdown";
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
import * as ImagePicker from "expo-image-picker";
import { Formik, FormikHelpers } from "formik";
import * as Yup from "yup";
import ChevronRight from "../../assets/icons/chevron_rightside.svg";
import AddPhoto from "../../assets/icons/add_photo_alternate.svg";
import IssueSelectorModal from "@/components/service/IssueSelectorModal";
import { uploadImage } from "@/utils/User";
import api from "@/services/axiosClient";
import { useSnackbar } from "@/contexts/Snackbar";
import { BASE_URL } from "@/constants/config";
import { Overlay } from "@/components/common/Overlay";
import CrossIcon from "@/assets/icons/close_white.svg";
import { useTranslation } from "react-i18next";
import CalendarIcon from "@/assets/icons/calendar.svg";
interface FormValues {
serviceType: string | null;
issues: string[];
comments: string | null;
date: Date | null;
photos: string[];
}
const validationSchema = Yup.object().shape({
serviceType: Yup.string().required("Service Type is required"),
issues: Yup.array().min(1, "At least one issue is required"),
date: Yup.date().required("Date and Time is required"),
photos: Yup.array().min(1, "At least one photo is required"),
comments: Yup.string(),
});
export default function ServiceFormScreen(): JSX.Element {
const [isFocus, setIsFocus] = useState<boolean>(false);
const [isIssueSelectorVisible, setIssueSelectorVisible] = useState(false);
const { showSnackbar } = useSnackbar();
function toggleIssueSelector() {
setIssueSelectorVisible(!isIssueSelectorVisible);
}
const initialValues: FormValues = {
serviceType: null,
issues: [],
comments: "",
date: null,
photos: [],
};
const handlePhotoPick = async (
setFieldValue: (field: string, value: any) => void,
currentPhotos: string[]
) => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.5,
allowsMultipleSelection: true,
});
if (!result.canceled) {
const newPhotos = result.assets.map((asset) => asset.uri);
const allPhotos = [...currentPhotos, ...newPhotos];
setFieldValue("photos", allPhotos);
}
};
const showPicker = (
currentDate: Date | null,
setFieldValue: (field: string, value: any) => void
) => {
const now = currentDate || new Date();
// First, show the date picker
DateTimePickerAndroid.open({
value: now,
mode: "date",
is24Hour: false,
display: "default",
minimumDate: now,
onChange: (event, selectedDate) => {
if (event.type === "set" && selectedDate) {
// When date is selected, show time picker next
DateTimePickerAndroid.open({
value: selectedDate,
mode: "time",
is24Hour: false,
display: "default",
onChange: (timeEvent, selectedTime) => {
if (timeEvent.type === "set" && selectedTime) {
// Combine date and time into one Date object
const combinedDate = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth(),
selectedDate.getDate(),
selectedTime.getHours(),
selectedTime.getMinutes()
);
if (combinedDate < now) {
showSnackbar(`${t("service.select-valid-time")}`, "error");
return;
}
setFieldValue("date", combinedDate);
}
},
});
}
},
});
};
const { t } = useTranslation();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 0}
>
<ScrollView
style={styles.screen}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (
values: FormValues,
actions: FormikHelpers<FormValues>
) => {
try {
const uploadedPhotoUrls: string[] = [];
for (const uri of values.photos) {
const uploadedUrl = await uploadImage(uri);
uploadedPhotoUrls.push(uploadedUrl);
}
console.log("IMAGES UPLOADED");
const payload = {
service_type: values.serviceType,
issue_types: values.issues,
scheduled_time: values.date?.toISOString(),
photos: uploadedPhotoUrls,
comments: values.comments,
};
const response = await api.post(
`${BASE_URL}/api/v1/schedule-maintenance`,
payload
);
if (!response.data.success) {
console.log(response.data?.message || "Submission failed");
throw new Error(response.data?.message || "Submission failed");
}
console.log("Submission successful:", response.data);
showSnackbar(
`${t("service.service-request-success")}`,
"success"
);
actions.resetForm();
} catch (error: any) {
console.error("Error during submission:", error);
showSnackbar(`${t("service.something-went-wrong")}`, "error");
} finally {
actions.setSubmitting(false);
}
}}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
setFieldValue,
errors,
touched,
isSubmitting,
}) => (
<View style={styles.formContainer}>
<View style={styles.inputContainer}>
<Text style={styles.label}>
{t("service.service-type")}{" "}
<Text style={styles.required}>*</Text>
</Text>
<Dropdown
style={[
styles.dropdown,
isFocus && { borderColor: "#00be88" },
]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
data={[
{ label: "Regular", value: "Regular" },
{ label: "On-demand", value: "On-demand" },
]}
maxHeight={200}
labelField="label"
valueField="value"
placeholder={`${t("service.select")}`}
value={values.serviceType}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onChange={(item) => {
setFieldValue("serviceType", item.value);
setIsFocus(false);
}}
renderLeftIcon={() => (
<View style={{ marginRight: 10 }}>
{/* Add your icon component here if needed */}
</View>
)}
/>
{touched.serviceType && errors.serviceType && (
<Text style={styles.error}>{errors.serviceType}</Text>
)}
</View>
<View style={{ marginTop: 8 }}>
<Text style={styles.label}>
{t("service.issue")} <Text style={styles.required}>*</Text>
</Text>
<TouchableOpacity
style={styles.inputBox}
onPress={toggleIssueSelector}
>
<Text style={styles.issueText}>
{values.issues.length > 0
? values.issues.length + " issues selected"
: `${t("service.select-issue")}`}
</Text>
<ChevronRight />
</TouchableOpacity>
{touched.issues && errors.issues && (
<Text style={styles.error}>{errors.issues}</Text>
)}
</View>
<View style={{ marginTop: 8 }}>
<Text style={styles.label}>
{t("service.select-datetime")}{" "}
<Text style={styles.required}>*</Text>
</Text>
<TouchableOpacity
onPress={() => showPicker(values.date, setFieldValue)}
style={styles.inputBoxDate}
>
<Text style={styles.dateText}>
{values.date
? values.date.toLocaleString()
: `${t("service.select")}`}
</Text>
<CalendarIcon width={20} height={20} />
</TouchableOpacity>
{touched.date && errors.date && (
<Text style={styles.error}>{`${errors.date}`}</Text>
)}
</View>
<TouchableOpacity
style={styles.photoBox}
onPress={() => handlePhotoPick(setFieldValue, values.photos)}
>
<AddPhoto />
<Text style={styles.addPhotoText}>
{t("service.add-photos")}{" "}
</Text>
</TouchableOpacity>
<Text style={styles.helperText}>
{t("service.supported-formats")}
</Text>
{/* Selected Images Preview */}
<View style={styles.photosContainer}>
{values.photos.map((uri, index) => (
<View key={index} style={styles.photoPreviewContainer}>
<Image source={{ uri }} style={styles.photoPreview} />
<TouchableOpacity
style={styles.removePhotoButton}
onPress={() => {
const updatedPhotos = [...values.photos];
updatedPhotos.splice(index, 1);
setFieldValue("photos", updatedPhotos);
}}
>
<CrossIcon />
</TouchableOpacity>
</View>
))}
</View>
{touched.photos && errors.photos && (
<Text style={styles.error}>{errors.photos}</Text>
)}
<View style={{ marginTop: 16 }}>
<Text style={styles.label}>{t("service.comments")} </Text>
<TextInput
style={styles.commentInput}
multiline
maxLength={100}
onChangeText={handleChange("comments")}
onBlur={handleBlur("comments")}
value={values.comments}
/>
<Text style={styles.wordCount}>
{values.comments?.length || 0}/100 {t("service.words")}
</Text>
</View>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit as (e?: GestureResponderEvent) => void}
>
<Text style={styles.submitText}>{t("service.submit")} </Text>
</TouchableOpacity>
<IssueSelectorModal
visible={isIssueSelectorVisible}
onClose={toggleIssueSelector}
onSelect={(selectedIssues) => {
setFieldValue("issues", selectedIssues);
}}
initialSelectedValues={values.issues}
/>
{isSubmitting && <Overlay isUploading={isSubmitting} />}
</View>
)}
</Formik>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
inputContainer: {},
screen: {
flex: 1,
backgroundColor: "#F3F5F8",
},
scrollContent: {
paddingBottom: 116,
},
topBar: {
height: 56,
backgroundColor: "#F3F5F8",
justifyContent: "center",
paddingHorizontal: 16,
},
topBarText: {
fontSize: 16,
fontWeight: "600",
color: "#252A34",
},
formContainer: {
paddingHorizontal: 16,
},
label: {
fontSize: 14,
fontWeight: "500",
color: "#252A34",
marginBottom: 4,
},
required: {
color: "#D42210",
},
inputBox: {
backgroundColor: "#FFFFFF",
borderColor: "#D8DDE7",
borderRadius: 4,
paddingHorizontal: 8,
height: 40,
marginTop: 8,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
inputBoxDate: {
backgroundColor: "#FFFFFF",
borderColor: "#D8DDE7",
borderWidth: 1,
borderRadius: 4,
paddingHorizontal: 8,
height: 40,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 4,
},
picker: {
height: 40,
width: "100%",
},
issueText: {
color: "#006C4D",
},
dateText: {
color: "#949CAC",
},
photoBox: {
backgroundColor: "#FFFFFF",
borderColor: "#949CAC",
borderWidth: 1,
borderRadius: 4,
height: 64,
justifyContent: "center",
alignItems: "center",
marginBottom: 8,
borderStyle: "dashed",
marginTop: 8,
},
addPhotoText: {
fontSize: 14,
color: "#252A34",
fontWeight: "500",
},
helperText: {
fontSize: 12,
color: "#717B8F",
marginBottom: 8,
},
commentInput: {
backgroundColor: "#FFFFFF",
borderColor: "#D8DDE7",
borderWidth: 1,
borderRadius: 4,
padding: 10,
height: 80,
textAlignVertical: "top",
marginTop: 8,
},
wordCount: {
textAlign: "right",
color: "#717B8F",
fontSize: 14,
marginTop: 4,
},
submitButton: {
marginTop: 24,
backgroundColor: "#00875F",
borderRadius: 4,
height: 48,
justifyContent: "center",
alignItems: "center",
},
submitText: {
color: "#FCFCFC",
fontSize: 14,
fontWeight: "500",
},
photo: {
width: 80,
height: 80,
resizeMode: "cover",
},
error: {
color: "#D42210",
fontSize: 12,
marginBottom: 8,
},
photosContainer: {
flexDirection: "row",
flexWrap: "wrap",
marginTop: 8,
gap: 8,
},
photoPreviewContainer: {
position: "relative",
},
photoPreview: {
width: 80,
height: 80,
borderRadius: 4,
},
removePhotoButton: {
position: "absolute",
top: 4,
right: 4,
backgroundColor: "#252A345C",
borderRadius: 12,
width: 24,
height: 24,
justifyContent: "center",
alignItems: "center",
},
removePhotoText: {
color: "white",
fontSize: 18,
fontWeight: "bold",
lineHeight: 20,
},
dropdown: {
height: 40,
borderColor: "#D8DDE7",
borderWidth: 1,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: "#FFFFFF",
marginTop: 8,
},
placeholderStyle: {
fontSize: 14,
color: "#252A34",
},
selectedTextStyle: {
fontSize: 14,
color: "#252A34",
},
iconStyle: {
width: 20,
height: 20,
},
inputSearchStyle: {
height: 40,
fontSize: 16,
},
dropdownMenu: {
backgroundColor: "#FCFCFC",
borderRadius: 4,
shadowColor: "#0E1118",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 32,
elevation: 4,
},
dropdownItem: {
padding: 16,
height: 36,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
dropdownItemText: {
fontSize: 14,
color: "#252A34",
},
});

31
app/(tabs)/two.tsx Normal file
View File

@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function TabTwoScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab Two</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/two.tsx" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

View File

@ -1,12 +1,12 @@
import { Link, Stack } from "expo-router";
import { StyleSheet } from "react-native";
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Text, View } from "react-native";
import { Text, View } from '@/components/Themed';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
@ -21,13 +21,13 @@ export default function NotFoundScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: "bold",
fontWeight: 'bold',
},
link: {
marginTop: 15,
@ -35,6 +35,6 @@ const styles = StyleSheet.create({
},
linkText: {
fontSize: 14,
color: "#2e78b7",
color: '#2e78b7',
},
});

View File

@ -1,83 +1,59 @@
// app/_layout.tsx
import { Stack } from "expo-router";
import { useEffect, useState } from "react";
import { Provider, useDispatch, useSelector } from "react-redux";
import { store, AppDispatch, RootState } from "@/store";
import { PaperProvider } from "react-native-paper";
import { I18nextProvider } from "react-i18next";
import i18next from "@/services/i18n";
import SnackbarProvider from "@/contexts/Snackbar";
import * as SplashScreen from "expo-splash-screen";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "@/constants/config";
import { setIsLoggedIn } from "@/store/authSlice";
import { SocketProvider } from "@/contexts/SocketContext";
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import 'react-native-reanimated';
import { useColorScheme } from '@/components/useColorScheme';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
return (
<PaperProvider>
<SocketProvider>
<SnackbarProvider>
<Provider store={store}>
<I18nextProvider i18n={i18next}>
<SplashAndAuthRouter />
</I18nextProvider>
</Provider>
</SnackbarProvider>
</SocketProvider>
</PaperProvider>
);
}
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
function SplashAndAuthRouter() {
const dispatch = useDispatch<AppDispatch>();
const isLoggedIn = useSelector((state: RootState) => state.auth.isLoggedIn);
const [loading, setLoading] = useState(true);
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
const loadAuth = async () => {
try {
const token = await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
console.log(token, "token of backend");
if (!token) {
dispatch(setIsLoggedIn(false));
return;
}
// Token exists, now verify it by calling getUserDetails
// await dispatch(getUserDetails()).unwrap();
// If it reaches here, the token is valid
dispatch(setIsLoggedIn(true));
} catch (error) {
// getUserDetails failed => token is invalid
dispatch(setIsLoggedIn(false));
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); // optionally clean up
} finally {
setLoading(false);
if (loaded) {
SplashScreen.hideAsync();
}
};
}, [loaded]);
loadAuth();
}, []);
if (!loaded) {
return null;
}
if (loading) return null;
return <RootLayoutNav />;
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="user" />
</Stack.Protected>
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="init/language" />
<Stack.Screen name="auth" />
</Stack.Protected>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
);
}

View File

@ -1,63 +0,0 @@
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { View, StyleSheet, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import GoBack from "@/assets/icons/chevron_left.svg";
import { useRouter } from "expo-router";
export default function AuthLayout() {
const router = useRouter();
return (
<>
<StatusBar style="dark" />
<Stack
screenOptions={{
headerShown: false,
animation: "fade",
}}
/>
<Stack.Screen
name="login"
options={{
headerShown: true,
title: "",
headerShadowVisible: false,
headerLeft: () => {
return (
<TouchableOpacity onPress={() => router.back()}>
<GoBack />
</TouchableOpacity>
);
},
headerStyle: {
backgroundColor: "#F3F5F8",
},
}}
/>
</>
);
}
// useEffect(() => {
// navigation.setOptions({
// headerShown: true,
// title: "",
// headerLeft: () => {
// return (
// <TouchableOpacity onPress={() => router.back()}>
// <GoBack />
// </TouchableOpacity>
// );
// },
// headerShadowVisible: false,
// headerStyle: {
// backgroundColor: "#F3F5F8",
// },
// });
// }, []);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F3F5F8",
},
});

View File

@ -1,251 +0,0 @@
import React, { useEffect } from "react";
import {
Text,
View,
TextInput,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
Keyboard,
KeyboardAvoidingView,
Linking,
Platform,
} from "react-native";
import { useRouter } from "expo-router";
import { Formik } from "formik";
import * as Yup from "yup";
import { sendOTP, clearSendOTPError } from "../../store/authSlice";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import { AUTH_STATUSES } from "@/constants/types";
import { useTranslation } from "react-i18next";
import { SUPPORT } from "@/constants/config";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function WelcomeScreen() {
const {
status,
otpId,
phone: phoneNumber,
sendOTPError,
} = useSelector((state: RootState) => state.auth);
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
const router = useRouter();
const insets = useSafeAreaInsets();
// const navigation = useNavigation();
const phoneValidationSchema = Yup.object().shape({
phone: Yup.string()
.required("Phone number is required")
.matches(/^\d{10}$/, "Phone number must be exactly 10 digits"),
});
useEffect(() => {
if (status === AUTH_STATUSES.SUCCESS && otpId) {
router.push({
pathname: "/auth/verifyOtp",
params: { otpId, phoneNumber },
});
}
}, [status, otpId, router]);
const makePhoneCall = () => {
Linking.openURL(`tel:${SUPPORT.PHONE}`);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 0}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={[styles.inner, { paddingBottom: insets.bottom }]}>
<Text style={styles.title}>{t("onboarding.welcome")}</Text>
<Formik
initialValues={{ phone: "" }}
onSubmit={(values) => dispatch(sendOTP({ phone: values.phone }))}
validationSchema={phoneValidationSchema}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
}) => (
<View style={styles.formContainer}>
<View style={styles.inputContainer}>
<Text style={styles.label}>
{t("onboarding.enter-mobile-number")}
</Text>
<TextInput
style={{
...styles.input,
color: values.phone ? "black" : "#949CAC",
}}
onChangeText={(text) => {
handleChange("phone")(text);
if (sendOTPError) {
dispatch(clearSendOTPError());
}
}}
onBlur={handleBlur("phone")}
value={values.phone}
keyboardType="numeric"
placeholder={t("onboarding.enter-registered-mobile-number")}
placeholderTextColor={"#949CAC"}
/>
<View style={styles.errorContainer}>
{touched.phone && errors.phone && (
<Text style={styles.error}>{errors.phone}</Text>
)}
{sendOTPError && (
<Text style={styles.error}>{sendOTPError}</Text>
)}
</View>
</View>
<View style={styles.bottomSection}>
<View style={styles.contactContainer}>
<View style={{ flexDirection: "row" }}>
<Text>{t("onboarding.for-any-queries")}</Text>
<TouchableOpacity onPress={makePhoneCall}>
<Text style={styles.link}>
{t("onboarding.contact-us")}
</Text>
</TouchableOpacity>
</View>
</View>
<TouchableOpacity
onPress={handleSubmit as unknown as () => void}
style={{
...styles.button,
backgroundColor:
values.phone.length === 10 &&
!errors.phone &&
status !== AUTH_STATUSES.LOADING
? "#008761"
: "#B0B7C5",
}}
disabled={
values.phone.length !== 10 ||
!!errors.phone ||
status === AUTH_STATUSES.LOADING
}
>
<Text style={styles.buttonText}>
{t("onboarding.send-otp")}
</Text>
</TouchableOpacity>
</View>
</View>
)}
</Formik>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: "#F3F5F8",
},
inner: {
flex: 1,
},
formContainer: {
flex: 1,
justifyContent: "space-between",
},
inputContainer: {
flex: 1,
},
bottomSection: {
width: "100%",
},
contactContainer: {
width: "100%",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
},
link: {
fontSize: 14,
fontWeight: "500",
lineHeight: 20,
color: "#1249ED",
},
errorContainer: {
flexDirection: "column",
gap: 8,
marginTop: 8,
},
error: {
color: "#D51D10",
fontFamily: "Inter",
fontSize: 12,
fontWeight: "bold",
lineHeight: 20,
},
button: {
height: 48,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
width: "100%",
},
buttonText: {
color: "#FCFCFC",
fontFamily: "Inter",
fontSize: 14,
fontWeight: "bold",
lineHeight: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
marginBottom: 16,
color: "#1A1C1E",
fontStyle: "normal",
lineHeight: 36,
letterSpacing: -0.56,
width: "70%",
},
label: {
fontSize: 14,
marginBottom: 8,
color: "#252A34",
fontFamily: "Inter",
fontStyle: "normal",
fontWeight: "normal",
lineHeight: 20,
},
input: {
borderRadius: 4,
borderTopColor: "#D8DDE7",
borderBottomColor: "#D8DDE7",
borderLeftColor: "#D8DDE7",
borderRightColor: "#D8DDE7",
height: 40,
borderColor: "gray",
borderWidth: 1,
paddingHorizontal: 8,
fontSize: 14,
overflow: "hidden",
fontFamily: "Inter",
fontStyle: "normal",
lineHeight: 20,
fontWeight: "bold",
backgroundColor: "#ffffff",
},
});

View File

@ -1,259 +0,0 @@
import React, { useEffect, useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { AppDispatch, RootState } from "../../store";
import { clearVerifyOTPError, sendOTP, verifyOTP } from "../../store/authSlice";
import { OtpInput } from "react-native-otp-entry";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { AUTH_STATUSES } from "@/constants/types";
export default function VerifyOTP() {
const { phone, otpId, verifyOTPError, sendOTPError, status } = useSelector(
(state: RootState) => state.auth
);
const router = useRouter();
const dispatch = useDispatch<AppDispatch>();
const { isLoggedIn } = useSelector((state: RootState) => state.auth);
const [seconds, setSeconds] = useState(30);
const [isTimerActive, setIsTimerActive] = useState(true);
const [resendAttempts, setResendAttempts] = useState(0);
const maxResendAttempts = 5;
const { t } = useTranslation();
useEffect(() => {
dispatch(clearVerifyOTPError());
}, []);
useEffect(() => {
if (isTimerActive && seconds > 0) {
const timer = setInterval(() => {
setSeconds((prev) => prev - 1);
}, 1000);
return () => clearInterval(timer);
} else if (seconds === 0) {
setIsTimerActive(false);
}
}, [seconds, isTimerActive]);
const resendOTP = async () => {
if (resendAttempts < maxResendAttempts) {
const newAttempts = resendAttempts + 1;
setResendAttempts(newAttempts);
setSeconds(30);
setIsTimerActive(true);
dispatch(sendOTP({ phone }));
dispatch(clearVerifyOTPError());
}
};
const formattedSeconds = seconds.toString().padStart(2, "0");
useEffect(() => {
if (status === AUTH_STATUSES.SUCCESS && isLoggedIn) {
router.replace("/"); // This assumes "/" leads to your `TabLayout`
}
}, [status, isLoggedIn]);
return (
<View style={styles.container}>
<Text style={styles.heading}>{t("onboarding.verify-otp")}</Text>
<View style={styles.body}>
<Text>
<Text style={styles.order}>{t("onboarding.enter-otp")}</Text>
<Text style={styles.phone}> {phone}.</Text>
</Text>
<View style={styles.otpContainer}>
<OtpInput
numberOfDigits={4}
focusColor="green"
autoFocus={true}
hideStick={true}
blurOnFilled={true}
disabled={false}
type="numeric"
secureTextEntry={false}
focusStickBlinkingDuration={500}
onTextChange={(text) => console.log(text)}
onFilled={(text) => {
dispatch(verifyOTP({ otpId: `${otpId}`, otp: text, phone }));
}}
textInputProps={{
accessibilityLabel: "One-Time Password",
}}
textProps={{
accessibilityRole: "text",
accessibilityLabel: "OTP digit",
allowFontScaling: false,
}}
theme={{
containerStyle: styles.digitContainer,
pinCodeContainerStyle: styles.pinCodeContainer,
pinCodeTextStyle: styles.pinCodeText,
focusStickStyle: styles.focusStick,
placeholderTextStyle: styles.placeholderText,
disabledPinCodeContainerStyle: styles.disabledPinCodeContainer,
}}
/>
{(verifyOTPError || sendOTPError) && (
<Text style={styles.error}>{verifyOTPError || sendOTPError}</Text>
)}
<View style={styles.otpResend}>
{isTimerActive ? (
<Text>
<Text style={styles.otpText}>{t("onboarding.resend-otp")}</Text>{" "}
<Text style={styles.timer}>00:{formattedSeconds}</Text>
</Text>
) : (
<TouchableOpacity
onPress={resendOTP}
disabled={resendAttempts >= maxResendAttempts}
>
<Text
style={{
...styles.resendOTP,
color:
resendAttempts < maxResendAttempts
? "#006C4D"
: "#B0B7C5",
}}
>
{t("onboarding.resend-otp")}
</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
paddingTop: 0,
},
heading: {
fontSize: 28,
fontStyle: "normal",
fontWeight: "bold",
lineHeight: 36.4,
letterSpacing: -0.56,
},
body: {
marginTop: 24,
},
phone: {
color: "#252A34",
fontSize: 14,
fontWeight: "bold",
lineHeight: 20,
},
order: {
color: "#949CAC",
fontSize: 14,
fontWeight: "bold",
lineHeight: 20,
},
otpContainer: {
padding: 0,
marginTop: 32,
alignItems: "center",
justifyContent: "center",
},
otpInput: {
width: "80%",
justifyContent: "space-between",
},
inputField: {
borderColor: "#D8DDE7",
borderRadius: 4,
backgroundColor: "#ffffff",
color: "#252A34",
textAlign: "center",
fontFamily: "Inter",
fontSize: 20,
fontWeight: "bold",
height: 56,
width: 56,
},
error: {
color: "#D51D10",
fontFamily: "Inter",
fontSize: 12,
fontWeight: "bold",
lineHeight: 20,
marginTop: 8,
},
otpResend: {
marginTop: 28,
},
otpText: {
color: "#949CAC",
fontFamily: "Inter",
fontSize: 14,
fontWeight: "bold",
},
timer: {
color: "#252A34",
fontSize: 14,
fontWeight: "bold",
},
resendOTP: {
fontFamily: "Inter",
fontSize: 14,
fontWeight: "bold",
paddingTop: 8,
paddingBottom: 8,
},
digitContainer: {
width: "100%",
flexDirection: "row",
justifyContent: "center",
gap: 16,
alignItems: "center",
},
pinCodeContainer: {
width: 56,
height: 56,
borderWidth: 1,
borderColor: "#D8DDE7",
borderRadius: 4,
backgroundColor: "#FFFFFF",
justifyContent: "center",
alignItems: "center",
},
disabledPinCodeContainer: {
backgroundColor: "#F2F4F7",
borderColor: "#E0E4EA",
},
pinCodeText: {
fontSize: 20,
fontWeight: "bold",
color: "#252A34",
textAlign: "center",
fontFamily: "Inter",
},
placeholderText: {
fontSize: 20,
color: "#B0B7C5",
textAlign: "center",
fontWeight: "bold",
},
focusStick: {
width: 2,
height: 24,
backgroundColor: "#252A34",
},
});

View File

@ -1,128 +0,0 @@
import React, { useEffect, useState } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { setLanguage } from "../../services/i18n/index";
import { useRouter, useNavigation } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const LanguageScreen = () => {
const router = useRouter();
const navigation = useNavigation();
const [selectedLang, setSelectedLang] = useState<string | null>(null);
const insets = useSafeAreaInsets();
useEffect(() => {
navigation.setOptions({ headerShown: false });
}, [navigation]);
const handleLanguagePress = (lang: string) => {
setSelectedLang(lang);
};
const handleSelect = async () => {
if (selectedLang) {
await setLanguage(selectedLang);
router.navigate("/auth/login");
}
};
return (
<>
<StatusBar style="dark" />
<View style={styles.container}>
<Text style={styles.title}>Select Language</Text>
<Text style={styles.subtitle}>
Please select your preferred language
</Text>
<TouchableOpacity
style={[
styles.languageCard,
selectedLang === "en" && styles.selectedCard,
]}
onPress={() => handleLanguagePress("en")}
>
<Text style={styles.languageText}>English</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.languageCard,
selectedLang === "hi" && styles.selectedCard,
]}
onPress={() => handleLanguagePress("hi")}
>
<Text style={styles.languageText}>ि</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectButton,
selectedLang ? styles.selectEnabled : styles.selectDisabled,
{ marginBottom: insets.bottom + 16 },
]}
onPress={handleSelect}
disabled={!selectedLang}
>
<Text style={styles.selectButtonText}>Select</Text>
</TouchableOpacity>
</View>
</>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f3f5f8",
paddingTop: 108,
paddingHorizontal: 16,
},
title: {
fontSize: 28,
fontWeight: "600",
color: "#1a1c1e",
marginBottom: 8,
},
subtitle: {
fontSize: 14,
color: "#252a34",
marginBottom: 24,
},
languageCard: {
width: "100%",
borderWidth: 1,
borderColor: "#d8dde7",
borderRadius: 4,
padding: 16,
marginBottom: 16,
justifyContent: "center",
},
selectedCard: {
borderColor: "#009E71",
},
languageText: {
fontSize: 16,
color: "#2f465e",
},
selectButton: {
marginTop: "auto",
marginBottom: 16,
padding: 16,
borderRadius: 4,
alignItems: "center",
},
selectDisabled: {
backgroundColor: "#b0b7c5",
},
selectEnabled: {
backgroundColor: "#008000",
},
selectButtonText: {
color: "#fdfdfd",
fontSize: 14,
fontWeight: "500",
},
});
export default LanguageScreen;

35
app/modal.tsx Normal file
View File

@ -0,0 +1,35 @@
import { StatusBar } from 'expo-status-bar';
import { Platform, StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function ModalScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

View File

@ -1,287 +0,0 @@
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useRouter } from "expo-router";
import { useSelector } from "react-redux";
import { RootState } from "@/store/rootReducer";
import Header from "@/components/common/Header";
import CheckCircle from "@/assets/icons/check_circle.svg";
import Pending from "@/assets/icons/pending.svg";
import Failed from "@/assets/icons/cancel.svg";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
const PaymentConfirmationScreen = () => {
const router = useRouter();
const { paymentOrder, due_amount } = useSelector(
(state: RootState) => state.payments
);
const insets = useSafeAreaInsets();
// Get payment status - assuming it comes from paymentOrder.status
const paymentStatus = paymentOrder?.status || "confirmed";
// Format amount with currency
const formatAmount = (amount: number | null) => {
if (!amount) return "₹0";
return `${amount.toLocaleString("en-IN")}`;
};
// Format date
const formatDate = (dateString?: string) => {
if (!dateString)
return new Date().toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
return new Date(dateString).toLocaleString("en-IN", {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
weekday: "long",
});
};
// Get icon and text based on status
const getStatusDisplay = () => {
switch (paymentStatus) {
case "confirmed":
return {
icon: <CheckCircle />,
text: "Payment successful",
iconContainerStyle: styles.successIconContainer,
};
case "failure":
return {
icon: <Failed />,
text: "Payment failed",
iconContainerStyle: styles.failureIconContainer,
};
case "pending":
return {
icon: <Pending />,
text: "Payment pending",
iconContainerStyle: styles.pendingIconContainer,
};
default:
return {
icon: <CheckCircle />,
text: "Payment successful",
iconContainerStyle: styles.successIconContainer,
};
}
};
const statusDisplay = getStatusDisplay();
// Helper function to display value or "--" if missing
const displayValue = (value: any) => {
return value ? String(value) : "--";
};
const { t } = useTranslation();
return (
<View style={styles.container}>
<Header title={`${t("payment.payment-status")}`} />
<View style={styles.contentFrame}>
<View style={styles.qrFrame}>
<View style={styles.paymentStatusContainer}>
<View style={statusDisplay.iconContainerStyle}>
{statusDisplay.icon}
</View>
<Text style={styles.amountText}>
{formatAmount(paymentOrder?.amount || due_amount)}
</Text>
<View style={styles.statusContainer}>
<Text style={styles.statusText}>{statusDisplay.text}</Text>
<Text style={styles.dateText}>
{formatDate(paymentOrder?.transaction_date)}
</Text>
</View>
</View>
<View style={styles.divider} />
<View style={styles.transactionContainer}>
<Text style={styles.sectionHeader}>{`${t(
"payment.transaction-details"
)}`}</Text>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{`${t(
"payment.payment-mode"
)}`}</Text>
<Text style={styles.detailValue}>
{displayValue(paymentOrder?.payment_mode?.[0])}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{`${t(
"payment.paid-to"
)}`}</Text>
<Text style={styles.detailValue}>
{displayValue(paymentOrder?.upi_handle)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{`${t(
"payment.paid-by"
)}`}</Text>
<Text style={styles.detailValue}>
{displayValue(paymentOrder?.paid_by_upi_handle)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{`${t(
"payment.order-id"
)}`}</Text>
<Text style={styles.detailValue}>
{displayValue(paymentOrder?.order_id)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>{`${t(
"payment.transaction-id"
)}`}</Text>
<Text style={styles.detailValue}>
{displayValue(
paymentOrder?.transaction_id ||
paymentOrder?.transaction_order_id
)}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>RRN</Text>
<Text style={styles.detailValue}>
{displayValue(paymentOrder?.payment_reference_id)}
</Text>
</View>
</View>
</View>
<TouchableOpacity
style={[styles.primaryButton, { marginBottom: insets.bottom }]}
onPress={() => router.replace("/(tabs)/payments")}
>
<Text style={styles.buttonText}>OK</Text>
</TouchableOpacity>
</View>
</View>
);
};
export default PaymentConfirmationScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f3f4f6", // Light gray background
},
contentFrame: {
flex: 1,
paddingHorizontal: 16,
paddingBottom: 16,
justifyContent: "space-between",
marginTop: 8,
},
qrFrame: {
backgroundColor: "#fcfcfc",
borderRadius: 8,
padding: 16,
paddingVertical: 32,
},
paymentStatusContainer: {
alignItems: "center",
},
successIconContainer: {
marginBottom: 16,
alignItems: "center",
},
failureIconContainer: {
marginBottom: 16,
alignItems: "center",
},
pendingIconContainer: {
marginBottom: 16,
alignItems: "center",
},
successIcon: {
// Icon styling handled by Ionicons
},
amountText: {
fontSize: 20,
fontWeight: "600",
color: "#252a34",
textAlign: "center",
marginBottom: 16,
},
statusContainer: {
alignItems: "center",
gap: 4,
},
statusText: {
fontSize: 14,
color: "#252a34",
textAlign: "center",
},
dateText: {
fontSize: 14,
color: "#252a34",
textAlign: "center",
},
divider: {
height: 1,
backgroundColor: "#e5e9f0",
marginVertical: 24,
},
transactionContainer: {
gap: 8,
},
sectionHeader: {
fontSize: 14,
fontWeight: "600",
color: "#252a34",
},
detailRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
paddingVertical: 2,
},
detailLabel: {
fontSize: 14,
color: "#252a34",
flex: 1,
},
detailValue: {
fontSize: 14,
fontWeight: "600",
color: "#252a34",
textAlign: "left",
flex: 1,
},
primaryButton: {
backgroundColor: "#008761",
height: 40,
borderRadius: 4,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
color: "#fcfcfc",
fontSize: 14,
fontWeight: "500",
},
});

View File

@ -1,350 +0,0 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import { useNavigation, useRoute } from "@react-navigation/native";
import { useRouter } from "expo-router";
import api from "@/services/axiosClient";
import { BASE_URL } from "@/constants/config";
import { useSnackbar } from "@/contexts/Snackbar";
import { SafeAreaView } from "react-native-safe-area-context";
// Import your success/failure icons
import SuccessIcon from "@/assets/icons/check_circle.svg";
import FailureIcon from "@/assets/icons/cancel.svg";
import PendingIcon from "@/assets/icons/pending.svg";
import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next";
interface TransactionDetailData {
id: number;
amount: string;
status: string;
transaction_date: string | null;
upi_handle: string;
payment_mode: string[];
paid_by_upi_handle: string | null;
order_id: string;
payment_reference_id: string | null;
transaction_order_id: string;
}
interface TransactionResponse {
success: boolean;
data: {
payments: TransactionDetailData[];
pagination: {
total_records: number;
page_number: number;
page_size: number;
};
};
}
export default function TransactionDetailScreen() {
const navigation = useNavigation();
const route = useRoute();
const router = useRouter();
const { showSnackbar } = useSnackbar();
// Get payment ID from route params
const { paymentId } = route.params as { paymentId: number };
const [transactionData, setTransactionData] =
useState<TransactionDetailData | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchTransactionDetail();
}, [paymentId]);
const { t } = useTranslation();
const fetchTransactionDetail = async () => {
try {
setIsLoading(true);
const response = await api.get(
`${BASE_URL}/api/v1/payment-history?page_number=1&page_size=10&id=${paymentId}`
);
const result: TransactionResponse = response.data;
if (result.success && result.data.payments.length > 0) {
setTransactionData(result.data.payments[0]);
} else {
showSnackbar("Transaction details not found", "error");
router.back();
}
} catch (err) {
console.error("Error fetching transaction details:", err);
const errorMessage =
err instanceof Error
? err.message
: `${t("service.something-went-wrong")}`;
showSnackbar(errorMessage, "error");
router.back();
} finally {
setIsLoading(false);
}
};
const formatCurrency = (amount: string) => {
return `${parseFloat(amount).toLocaleString()}`;
};
const formatDateTime = (dateString: string | null) => {
if (!dateString) return "--";
const date = new Date(dateString);
const dateStr = date.toLocaleDateString("en-IN", {
day: "2-digit",
month: "long",
year: "numeric",
});
const timeStr = date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
const dayStr = date.toLocaleDateString("en-US", { weekday: "long" });
return `${dateStr}, ${timeStr}, ${dayStr}`;
};
const getStatusIcon = (status: string) => {
if (status.toLowerCase() === "confirmed") {
return <SuccessIcon width={80} height={80} />;
} else if (status.toLowerCase() === "pending") {
return <PendingIcon width={80} height={80} />;
} else {
return <FailureIcon width={80} height={80} />;
}
};
const getStatusText = (status: string) => {
switch (status.toLowerCase()) {
case "confirmed":
return "Payment successful";
case "failure":
return "Payment failed";
case "pending":
return "Payment pending";
default:
return `Payment ${status}`;
}
};
return (
<>
<Header
title={`${t("payment.transaction-details")}`}
showBackButton={true}
/>
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#00BE88" />
</View>
) : !transactionData ? (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Transaction not found</Text>
</View>
) : (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.iconContainer}>
{getStatusIcon(transactionData.status)}
</View>
<Text style={styles.amount}>
{formatCurrency(transactionData.amount)}
</Text>
<Text style={styles.statusText}>
{getStatusText(transactionData.status)}
</Text>
<Text style={styles.dateTime}>
{formatDateTime(transactionData.transaction_date)}
</Text>
<View style={styles.divider} />
<View style={styles.detailsCard}>
<Text style={styles.detailsTitle}>
{`${t("payment.transaction-details")}`}
</Text>
<DetailRow
label={`${t("payment.payment-mode")}`}
value={transactionData.payment_mode.join(", ") || "--"}
/>
<DetailRow
label={`${t("payment.paid-to")}`}
value={transactionData.upi_handle || "--"}
/>
<DetailRow
label={`${t("payment.paid-by")}`}
value={transactionData.paid_by_upi_handle || "--"}
/>
<DetailRow
label={`${t("payment.order-id")}`}
value={transactionData.order_id || "--"}
/>
<DetailRow
label={`${t("payment.transaction-id")}`}
value={transactionData.transaction_order_id || "--"}
/>
<DetailRow
label="RRN"
value={transactionData.payment_reference_id || "--"}
isLast={true}
/>
</View>
</View>
</View>
)}
</>
);
}
const DetailRow = ({
label,
value,
isLast = false,
}: {
label: string;
value: string;
isLast?: boolean;
}) => (
<View style={[styles.detailRow, isLast && styles.detailRowLast]}>
<Text style={styles.detailLabel}>{label}</Text>
<Text style={styles.detailValue}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
divider: {
height: 1,
width: "100%",
backgroundColor: "#E5E9F0",
},
container: {
flex: 1,
backgroundColor: "#F3F5F8",
paddingHorizontal: 16,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
errorContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
errorText: {
fontSize: 16,
color: "#6B7280",
},
content: {
padding: 16,
alignItems: "center",
backgroundColor: "#FCFCFC",
},
backButton: {
paddingLeft: 16,
paddingRight: 8,
paddingVertical: 8,
},
backText: {
fontSize: 24,
color: "#111827",
fontWeight: "300",
},
iconContainer: {
marginTop: 32,
marginBottom: 16,
},
successIcon: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#00BE88",
justifyContent: "center",
alignItems: "center",
},
failureIcon: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#EF4444",
justifyContent: "center",
alignItems: "center",
},
checkmark: {
color: "white",
fontSize: 24,
fontWeight: "bold",
},
xmark: {
color: "white",
fontSize: 20,
fontWeight: "bold",
},
amount: {
fontSize: 20,
fontWeight: "600",
color: "#252A34",
marginBottom: 16,
},
statusText: {
fontSize: 14,
fontWeight: "400",
color: "#252A34",
marginBottom: 4,
},
dateTime: {
fontSize: 14,
color: "#252A34",
textAlign: "center",
marginBottom: 24,
},
detailsCard: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 16,
width: "100%",
},
detailsTitle: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
marginBottom: 8,
},
detailRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
paddingVertical: 8,
borderBottomColor: "#E5E9F0",
},
detailRowLast: {
borderBottomWidth: 0,
},
detailLabel: {
fontSize: 14,
color: "#252A34",
flex: 1,
},
detailValue: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
flex: 1,
textAlign: "left",
},
});

View File

@ -1,290 +0,0 @@
import Header from "@/components/common/Header";
import ProgressCard from "@/components/common/ProgressCard";
import { RootState } from "@/store/rootReducer";
import { useRouter } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
RootViewStyleProvider,
ScrollView,
} from "react-native";
import { useSelector } from "react-redux";
const MyPlanScreen: React.FC = () => {
const myPlan = useSelector((state: RootState) => state.payments.myPlan);
const advance_balance = useSelector(
(state: RootState) => state.payments.advance_balance
);
const dueAmount = useSelector(
(state: RootState) => state.payments.due_amount
);
const router = useRouter();
// Helper function to format currency
const formatCurrency = (amount?: number | null): string => {
if (amount == null) return "---";
return `${amount.toLocaleString("en-IN")}`;
};
// Helper function to display value or fallback
const displayValue = (value?: string | number | null): string => {
if (value == null || value === "") return "---";
return value.toString();
};
// Calculate progress percentage for EMI progress bar
const getProgressPercentage = (
firstValue: number,
secondValue: number
): number => {
if (!firstValue || !secondValue) return 0;
return (firstValue / secondValue) * 100;
};
const { t } = useTranslation();
return (
<ScrollView style={styles.container}>
<Header title={t("payment.my-plan")} showBackButton={true} />
<View style={styles.contentFrame}>
{/* Plan Details Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("payment.plan-details")}</Text>
<View style={styles.divider} />
<View style={styles.content}>
{/* Plan Type Row */}
<View style={styles.row}>
<Text style={styles.label}>{t("payment.plan-type")}</Text>
<Text style={styles.value}>
{displayValue(myPlan?.no_of_emi)}
</Text>
</View>
{/* Total Cost Row */}
<View style={styles.row}>
<Text style={styles.label}>{t("payment.total-cost")}</Text>
<Text style={styles.value}>
{formatCurrency(myPlan?.total_amount)}
</Text>
</View>
{/* Down Payment Row */}
<View style={styles.row}>
<Text style={styles.label}>{t("payment.down-payment")}</Text>
<Text style={styles.value}>
{formatCurrency(myPlan?.down_payment)}
</Text>
</View>
{/* Total EMI Row */}
<View style={styles.row}>
<Text style={styles.label}>{t("payment.total-emi")}</Text>
<Text style={styles.value}>
{formatCurrency(myPlan?.total_emi)}
</Text>
</View>
</View>
</View>
{/* Total Amount Due Card */}
<View style={styles.card}>
<Text style={styles.centerLabel}>
{t("payment.total-amount-due")}
</Text>
<View style={styles.amountContainer}>
<Text style={styles.dueAmount}>{formatCurrency(dueAmount)}</Text>
</View>
</View>
<View style={styles.twoColumnContainer}>
{/* Monthly EMI Card */}
<View style={styles.halfCard}>
<Text style={styles.label}>{t("payment.monthly-emi")}</Text>
<Text style={styles.value}>
{formatCurrency(myPlan?.emi_amount)}
</Text>
</View>
<View style={styles.halfCard}>
<Text style={styles.label}>{t("payment.installments-paid")}</Text>
<View style={styles.installmentRow}>
<Text style={styles.value}>
{displayValue(myPlan?.installment_paid)}
</Text>
<Text style={styles.installmentTotal}>
/ {displayValue(myPlan?.no_of_emi)}
</Text>
</View>
</View>
</View>
<ProgressCard
title={t("payment.emi-paid-till-now")}
firstText={formatCurrency(myPlan?.current_amount)}
secondText={formatCurrency(myPlan?.total_emi)}
percentage={getProgressPercentage(
myPlan?.current_amount || 0,
myPlan?.total_emi || 0
)}
/>
<View style={styles.card}>
<Text style={styles.label}>{t("payment.advance-amount")}</Text>
<Text style={styles.amount}>{formatCurrency(advance_balance)}</Text>
</View>
<TouchableOpacity
style={styles.payButton}
onPress={() => router.push("/payments/selectAmount")}
>
<Text style={styles.payButtonText}>{t("payment.pay-emi")}</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
amount: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
height: 20,
lineHeight: 20,
},
container: {
flex: 1,
backgroundColor: "#F3F5F8",
},
headerTitle: {
fontSize: 18,
fontWeight: "600",
color: "#252A34",
},
contentFrame: {
flex: 1,
paddingHorizontal: 16,
paddingBottom: 16,
gap: 16,
},
card: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 12,
gap: 12,
},
cardTitle: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
height: 20,
},
divider: {
height: 1,
backgroundColor: "#E5E9F0",
},
content: {
gap: 8,
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
height: 20,
},
label: {
fontSize: 14,
fontWeight: "400",
color: "#252A34",
},
value: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
},
centerLabel: {
fontSize: 14,
fontWeight: "400",
color: "#252A34",
textAlign: "center",
height: 20,
},
amountContainer: {
height: 28,
justifyContent: "center",
alignItems: "center",
},
dueAmount: {
fontSize: 20,
fontWeight: "600",
color: "#252A34",
textAlign: "center",
},
twoColumnContainer: {
flexDirection: "row",
gap: 16,
},
halfCard: {
flex: 1,
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 12,
gap: 8,
height: 72,
},
installmentRow: {
flexDirection: "row",
alignItems: "flex-end",
gap: 4,
},
installmentTotal: {
fontSize: 14,
fontWeight: "500",
color: "#565F70",
},
progressAmountRow: {
flexDirection: "row",
alignItems: "flex-end",
gap: 4,
},
progressTotal: {
fontSize: 14,
fontWeight: "500",
color: "#565F70",
},
progressBarContainer: {
width: "100%",
height: 8,
},
progressBarBackground: {
width: "100%",
height: 8,
backgroundColor: "#E5EAF0",
borderRadius: 10,
overflow: "hidden",
},
progressBarFill: {
height: "100%",
backgroundColor: "#4CAF50",
borderRadius: 10,
},
payButton: {
backgroundColor: "#008761",
borderRadius: 8,
paddingVertical: 16,
alignItems: "center",
justifyContent: "center",
},
payButtonText: {
fontSize: 16,
fontWeight: "600",
color: "#FFFFFF",
},
});
export default MyPlanScreen;

View File

@ -1,454 +0,0 @@
import React, { useEffect, useState } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Alert,
Share,
Linking,
BackHandler,
ScrollView,
Dimensions,
} from "react-native";
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";
import * as Sharing from "expo-sharing";
import { Image } from "expo-image";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store";
import { setPaymentOrder, updatePaymentStatus } from "@/store/paymentSlice";
import Header from "@/components/common/Header";
import ShareIcon from "@/assets/icons/share.svg";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useSnackbar } from "@/contexts/Snackbar";
import DownloadIcon from "@/assets/icons/download.svg";
import { payments } from "@/constants/config";
import { useFocusEffect, useRouter } from "expo-router";
import { useSocket } from "@/contexts/SocketContext";
import { useTranslation } from "react-i18next";
const { height: screenHeight } = Dimensions.get("window");
const UpiPaymentScreen = () => {
const dispatch = useDispatch();
//paymentorder.amount is undefined
console.log("inside payemi ✅✅");
useEffect(() => {
console.log("inside pay emi useeffect 🔥🔥🔥🔥🔥");
}, []);
const paymentOrder = useSelector(
(state: RootState) => state.payments.paymentOrder
);
const router = useRouter();
const { onPaymentConfirmation, offPaymentConfirmation, disconnect } =
useSocket();
const { showSnackbar } = useSnackbar();
const insets = useSafeAreaInsets();
useFocusEffect(
React.useCallback(() => {
let backPressCount = 0;
let backPressTimer: NodeJS.Timeout | null = null;
const backAction = () => {
if (backPressCount === 0) {
backPressCount++;
showSnackbar("Press back again to cancel payment", "info");
backPressTimer = setTimeout(() => {
backPressCount = 0;
}, 2000);
return true;
} else {
if (backPressTimer) clearTimeout(backPressTimer);
offPaymentConfirmation();
disconnect();
router.back();
return true;
}
};
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
backAction
);
// ✅ Cleanup when screen loses focus
return () => {
if (backPressTimer) clearTimeout(backPressTimer);
backHandler.remove();
};
}, [offPaymentConfirmation, disconnect, router])
);
useFocusEffect(
React.useCallback(() => {
const handlePaymentConfirmation = (data: any) => {
dispatch(setPaymentOrder({ ...paymentOrder, ...data }));
offPaymentConfirmation();
disconnect();
router.replace("/payments/Confirmation");
};
onPaymentConfirmation(handlePaymentConfirmation);
return () => {
offPaymentConfirmation();
};
}, [
paymentOrder,
onPaymentConfirmation,
offPaymentConfirmation,
disconnect,
router,
])
);
const formatAmount = (amount: number): string => {
if (amount == null || amount == undefined) return `${0}`;
return `${amount.toLocaleString("en-IN")}`;
};
const isPaymentExpired = (): boolean => {
const expiryDate = new Date(paymentOrder.expiry_date);
const currentDate = new Date();
return currentDate > expiryDate;
};
const getUpiUrl = (): string => {
const upiString = paymentOrder.payment_link;
const upiMatch = upiString.match(/upi_string=([^&]+)/);
if (upiMatch) {
return decodeURIComponent(upiMatch[1]);
}
return `upi://pay?pa=${paymentOrder.upi_handle}&am=${paymentOrder.amount}&cu=INR&tr=${paymentOrder.order_id}`;
};
const payUsingUpiApp = async (): Promise<void> => {
try {
if (isPaymentExpired()) {
showSnackbar(payments.LINK_EXPIRED, "error");
return;
}
const upiUrl = getUpiUrl();
console.log("Opening UPI URL:", upiUrl);
dispatch(updatePaymentStatus("processing"));
const canOpenUrl = await Linking.canOpenURL(upiUrl);
if (canOpenUrl) {
await Linking.openURL(upiUrl);
} else {
showSnackbar("UPI App Required.", "error");
}
} catch (error) {
console.error("Error opening UPI app:", error);
dispatch(updatePaymentStatus("failed"));
showSnackbar("Unable to open UPI App", "error");
}
};
const shareQR = async (): Promise<void> => {
try {
if (await Sharing.isAvailableAsync()) {
await Share.share({
message: `Pay ${formatAmount(
paymentOrder.amount
)} using this QR code`,
url: paymentOrder.qr_code_url,
});
} else {
Alert.alert("Error", "Sharing is not available on this device");
}
} catch (error) {
Alert.alert("Error", "Failed to share QR code");
}
};
const downloadQR = async (): Promise<void> => {
try {
const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== "granted") {
showSnackbar("Please grant permission to save images", "error");
return;
}
const fileUri =
FileSystem.documentDirectory + `qr-${paymentOrder.order_id}.png`;
const downloadResult = await FileSystem.downloadAsync(
paymentOrder.qr_code_url,
fileUri
);
if (downloadResult.status === 200) {
const asset = await MediaLibrary.createAssetAsync(downloadResult.uri);
await MediaLibrary.createAlbumAsync("Payment QR Codes", asset, false);
showSnackbar("QR code saved to gallery", "success");
} else {
showSnackbar("Failed to Download QR code", "error");
}
} catch (error) {
showSnackbar("Failed to Download QR code", "error");
}
};
function handlePaymentDone() {
router.navigate("/(tabs)/payments");
}
const { t } = useTranslation();
return (
<View style={styles.container}>
<Header title={t("payment.pay-emi")} showBackButton={false} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
<View style={styles.qrFrame}>
<View style={styles.amountSection}>
<Text style={styles.amountLabel}>
{t("payment.amount-to-be-paid")}
</Text>
<Text style={styles.amount}>
{formatAmount(paymentOrder?.amount)}
</Text>
</View>
<View style={styles.qrCodeContainer}>
<Image
source={{ uri: paymentOrder?.qr_code_url }}
style={styles.qrCode}
contentFit="contain"
/>
</View>
<View style={styles.buttonsContainer}>
<TouchableOpacity
onPress={shareQR}
style={styles.secondaryButton}
>
<ShareIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.share-qr")}
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={downloadQR}
style={styles.secondaryButton}
>
<DownloadIcon />
<Text style={styles.secondaryButtonText}>
{t("payment.download-qr")}
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={payUsingUpiApp}
style={[styles.primaryButton]}
>
<Text style={[styles.primaryButtonText]}>
{t("payment.pay-using-upi")}
</Text>
</TouchableOpacity>
</View>
<View style={[styles.confirm, { marginBottom: insets.bottom }]}>
<Text style={styles.confirmTitle}>
{t("payment.confirm-payment")}
</Text>
<TouchableOpacity
onPress={() => handlePaymentDone()}
style={styles.paymentDone}
>
<Text style={styles.doneText}>{t("payment.payment-done")}</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F3F5F8",
},
scrollView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
},
content: {
flex: 1,
paddingHorizontal: 16,
paddingVertical: 16,
justifyContent: "space-between",
minHeight: screenHeight * 0.8, // Ensures minimum height for space-between to work
},
qrFrame: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 16,
alignItems: "center",
},
confirm: {
padding: 16,
flexDirection: "column",
backgroundColor: "#FCFCFC",
gap: 16,
borderRadius: 8,
},
doneText: {
textAlign: "center",
fontWeight: "500",
fontSize: 14,
lineHeight: 20,
},
paymentDone: {
padding: 16,
borderWidth: 1,
borderColor: "#DCE1E9",
borderRadius: 4,
},
confirmTitle: {
fontWeight: "400",
fontSize: 14,
lineHeight: 14,
color: "#252A34",
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: "#253342",
},
header: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: "#FFFFFF",
},
backButton: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 18,
fontWeight: "600",
color: "#253342",
},
amountSection: {
alignItems: "center",
marginBottom: 16,
},
amountLabel: {
fontSize: 14,
color: "#253342",
textAlign: "center",
marginBottom: 4,
},
amount: {
fontSize: 20,
fontWeight: "600",
color: "#253342",
textAlign: "center",
marginBottom: 8,
},
statusBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
fontWeight: "600",
color: "#FFFFFF",
},
qrCodeContainer: {
marginBottom: 16,
borderWidth: 2,
borderColor: "#E5E9F0",
padding: 16,
borderRadius: 8,
width: "100%",
height: "auto",
},
qrCode: {
width: "100%",
aspectRatio: 1,
alignSelf: "center",
},
upiIdSection: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginBottom: 12,
padding: 8,
backgroundColor: "#F3F5F8",
borderRadius: 4,
},
upiIdText: {
fontSize: 14,
color: "#253342",
marginRight: 8,
fontWeight: "500",
},
expirySection: {
alignItems: "center",
marginBottom: 16,
},
expiryLabel: {
fontSize: 12,
color: "#6C757D",
marginBottom: 2,
},
expiryTime: {
fontSize: 14,
color: "#253342",
fontWeight: "500",
},
buttonsContainer: {
flexDirection: "row",
gap: 8,
width: "100%",
},
secondaryButton: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 10,
paddingHorizontal: 16,
backgroundColor: "#F3F5F8",
borderColor: "#D8DDE7",
borderWidth: 1,
borderRadius: 4,
gap: 4,
},
secondaryButtonText: {
fontSize: 14,
fontWeight: "500",
color: "#253342",
},
primaryButton: {
backgroundColor: "#00876F",
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 4,
marginTop: 16,
width: "100%",
alignItems: "center",
},
primaryButtonText: {
fontSize: 14,
fontWeight: "500",
color: "#FCFCFC",
},
disabledButton: {
backgroundColor: "#D8DDE7",
},
disabledButtonText: {
color: "#6C757D",
},
});
export default UpiPaymentScreen;

View File

@ -1,552 +0,0 @@
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
ScrollView,
KeyboardAvoidingView,
Keyboard,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { Formik } from "formik";
import * as Yup from "yup";
import Header from "../../components/common/Header";
import { RootState } from "@/store/rootReducer";
import { BASE_URL, payments } from "@/constants/config";
import api from "@/services/axiosClient";
import { setPaymentOrder } from "@/store/paymentSlice";
import { Overlay } from "@/components/common/Overlay";
import { useFocusEffect, useRouter } from "expo-router";
import { useSocket } from "@/contexts/SocketContext";
import { useSnackbar } from "@/contexts/Snackbar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
const validationSchema = Yup.object().shape({
paymentType: Yup.string().required("Please select a payment option"),
customAmount: Yup.string().when("paymentType", {
is: "custom",
then: (schema) =>
schema
.required("Amount is required")
.test("valid-number", "Please enter a valid amount", (value) => {
const numValue = parseFloat(value);
return !isNaN(numValue) && numValue > 0;
})
.test(
"min-amount",
`Minimum amount is ₹${payments.MIN_AMOUNT}`,
(value) => {
const numValue = parseFloat(value);
return !isNaN(numValue) && numValue >= payments.MIN_AMOUNT;
}
),
otherwise: (schema) => schema.notRequired(),
}),
});
const SelectAmountScreen = () => {
const dueAmount = useSelector(
(state: RootState) => state.payments?.due_amount || 0
);
const { showSnackbar } = useSnackbar();
const router = useRouter();
const [isFetching, setIsFetching] = useState<boolean>(false);
const { registerTransaction } = useSocket();
const quickAmounts = [50, 100, 500, 1000];
const initialValues = {
paymentType: "due",
customAmount: "",
};
const existingPaymentOrder = useSelector(
(state: RootState) => state.payments?.paymentOrder
);
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const [keyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
const showSub = Keyboard.addListener("keyboardDidShow", () =>
setKeyboardVisible(true)
);
const hideSub = Keyboard.addListener("keyboardDidHide", () =>
setKeyboardVisible(false)
);
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
const { t } = useTranslation();
useFocusEffect(
React.useCallback(() => {
console.log(
"SelectAmountScreen focused - clearing paymentOrder and remounting"
);
// Clear the payment order
dispatch(setPaymentOrder(null));
setIsFetching(false);
}, [dispatch])
);
const handleSubmit = async (values: any) => {
setIsFetching(true);
const paymentAmount =
values.paymentType === "due"
? dueAmount
: parseFloat(values.customAmount);
try {
let orderData = existingPaymentOrder;
if (
existingPaymentOrder &&
existingPaymentOrder.amount === paymentAmount
) {
console.log(
"Order for current amount already exists, using existing order"
);
orderData = existingPaymentOrder;
} else {
console.log("Creating new order for amount:", paymentAmount);
const res = await api.post(`/api/v1/create-order`, {
amount: paymentAmount,
});
console.log(res.data, "response from select amount");
if (res.data && res.data.success) {
orderData = res.data.data;
dispatch(setPaymentOrder(orderData));
} else {
throw new Error("Failed to create order");
}
}
try {
await registerTransaction(orderData?.transaction_id);
console.log("Transaction registered successfully");
router.push("/payments/payEmi");
} catch (socketError) {
console.error("Socket connection failed:", socketError);
throw socketError;
}
} catch (err) {
console.error(err, "Error in creating order.");
showSnackbar(`${t("service.something-went-wrong")}`, "error");
} finally {
setIsFetching(false);
}
console.log("Payment Amount:", paymentAmount);
};
return (
<>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
// Add validateOnChange to make validation more responsive
validateOnChange={true}
validateOnBlur={true}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue,
isValid,
validateField,
dirty,
}) => {
const handleQuickAmountPress = (amount: number) => {
const currentAmount = values.customAmount
? parseFloat(values.customAmount)
: 0;
const newAmount = currentAmount + amount;
setFieldValue("customAmount", newAmount.toString());
validateField("customAmount");
};
const handleCustomAmountChange = (text: string) => {
const numericText = text.replace(/[^0-9.]/g, "");
const parts = numericText.split(".");
const formattedText =
parts.length > 2
? parts[0] + "." + parts.slice(1).join("")
: numericText;
setFieldValue("customAmount", formattedText, true);
setFieldValue("paymentType", "custom");
validateField("customAmount"); // Trigger validation after change
};
const getPaymentAmount = () => {
if (values.paymentType === "due") {
return dueAmount;
}
return values.customAmount ? parseFloat(values.customAmount) : 0;
};
const paymentAmount = getPaymentAmount() || 0;
const isButtonEnabled =
values.paymentType === "due" ||
(values.paymentType === "custom" &&
!isNaN(paymentAmount) &&
paymentAmount >= payments.MIN_AMOUNT);
// Button text logic
let buttonText = "";
if (values.paymentType === "due") {
buttonText = `Pay ₹${paymentAmount.toFixed(2)}`;
} else {
buttonText =
paymentAmount >= payments.MIN_AMOUNT
? `Pay ₹${paymentAmount.toFixed(2)}`
: "Select Amount";
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={"height"}
// Improved keyboard offset
// keyboardVerticalOffset={Platform.OS === "ios" ? 88 : 0}
>
<Header
title={t("payment.select-amount")}
showBackButton={true}
/>
<ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
// Add keyboard dismiss on scroll
keyboardShouldPersistTaps="handled"
contentContainerStyle={styles.scrollContent}
>
<View style={styles.selectAmountContainer}>
<TouchableOpacity
style={[
styles.option,
values.paymentType === "due" && styles.selectedOption,
]}
onPress={() => setFieldValue("paymentType", "due")}
>
<View style={styles.radioContainer}>
<View
style={[
styles.radioDot,
values.paymentType === "due" &&
styles.selectedRadioDot,
]}
>
{values.paymentType === "due" && (
<View style={styles.radioInner} />
)}
</View>
<Text style={styles.radioLabel}>
{t("payment.pay-amount-due")}
</Text>
</View>
<Text style={styles.amountText}>
{dueAmount?.toFixed(2)}
</Text>
</TouchableOpacity>
<View
style={[
styles.customOption,
values.paymentType === "custom" && styles.selectedOption,
touched.customAmount &&
errors.customAmount &&
styles.errorOption,
]}
>
<TouchableOpacity
style={styles.radioContainer}
onPress={() => setFieldValue("paymentType", "custom")}
>
<View
style={[
styles.radioDot,
values.paymentType === "custom" &&
styles.selectedRadioDot,
]}
>
{values.paymentType === "custom" && (
<View style={styles.radioInner} />
)}
</View>
<Text style={styles.radioLabel}>
{t("payment.enter-custom-amount")}
</Text>
</TouchableOpacity>
<View style={styles.inputContainer}>
<TextInput
style={[
styles.textInput,
touched.customAmount &&
errors.customAmount &&
styles.errorInput,
]}
value={values.customAmount}
onChangeText={handleCustomAmountChange}
onBlur={handleBlur("customAmount")}
placeholder="₹"
placeholderTextColor="#94A3B8"
keyboardType="numeric"
onFocus={() => setFieldValue("paymentType", "custom")}
// Add return key handling
returnKeyType="done"
/>
<View style={styles.helperContainer}>
<Text
style={[
styles.helperText,
touched.customAmount &&
errors.customAmount &&
styles.errorText,
]}
>
{touched.customAmount && errors.customAmount
? errors.customAmount
: `${t("payment.minimum")}: ₹${
payments.MIN_AMOUNT
}`}
</Text>
</View>
</View>
<View style={styles.chipsContainer}>
{quickAmounts.map((amount) => (
<TouchableOpacity
key={amount}
style={styles.chip}
onPress={() => handleQuickAmountPress(amount)}
>
<Text style={styles.chipText}>+{amount}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* General form error */}
{touched.paymentType && errors.paymentType && (
<Text style={styles.generalErrorText}>
{errors.paymentType}
</Text>
)}
</View>
</ScrollView>
<View
style={[
styles.buttonContainer,
!keyboardVisible && { marginBottom: insets.bottom },
]}
>
<TouchableOpacity
style={[
styles.payButton,
!isButtonEnabled && styles.disabledButton,
]}
onPress={() => handleSubmit()}
disabled={!isButtonEnabled}
>
{getPaymentAmount() < payments.MIN_AMOUNT ? (
<Text style={styles.payButtonText}>
{t("payment.select-amount")}
</Text>
) : (
<Text style={styles.payButtonText}>
Pay {getPaymentAmount().toFixed(2)}
</Text>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}}
</Formik>
<Overlay isUploading={isFetching} />
</>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F3F4F6",
},
content: {
flex: 1,
paddingHorizontal: 16,
},
// Add scroll content style for better keyboard handling
scrollContent: {
paddingBottom: 20,
},
selectAmountContainer: {
paddingTop: 16,
gap: 16,
},
option: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: "transparent",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
minHeight: 56,
},
selectedOption: {
borderColor: "#009E71",
},
errorOption: {
borderColor: "#EF4444",
},
customOption: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: "transparent",
minHeight: 180,
},
radioContainer: {
flexDirection: "row",
alignItems: "center",
flex: 1,
marginBottom: 16,
},
radioDot: {
width: 18,
height: 18,
borderRadius: 9,
borderWidth: 2,
borderColor: "#D1D5DB",
alignItems: "center",
justifyContent: "center",
marginRight: 12,
},
selectedRadioDot: {
borderColor: "#009E71",
},
radioInner: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: "#009E71",
},
radioLabel: {
fontSize: 14,
color: "#252936",
fontWeight: "400",
},
amountText: {
fontSize: 14,
color: "#252936",
fontWeight: "400",
},
inputContainer: {
marginBottom: 16,
},
textInput: {
backgroundColor: "#FFFFFF",
borderWidth: 1,
borderColor: "#D8DDE7",
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 10,
fontSize: 14,
color: "#252936",
height: 40,
},
errorInput: {
borderColor: "#EF4444",
},
helperContainer: {
marginTop: 4,
},
helperText: {
fontSize: 14,
color: "#565F70",
},
errorText: {
color: "#EF4444",
},
generalErrorText: {
fontSize: 14,
color: "#EF4444",
textAlign: "center",
marginTop: 8,
},
chipsContainer: {
flexDirection: "row",
gap: 8,
flexWrap: "wrap",
},
chip: {
backgroundColor: "#F3F4F6",
borderWidth: 1,
borderColor: "#D8DDE7",
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 4,
minWidth: 68,
alignItems: "center",
justifyContent: "center",
height: 28,
},
chipText: {
fontSize: 14,
color: "#252936",
fontWeight: "500",
},
buttonContainer: {
padding: 16,
backgroundColor: "#F3F4F6",
},
payButton: {
backgroundColor: "#009E71",
borderRadius: 4,
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: "center",
justifyContent: "center",
height: 40,
},
disabledButton: {
backgroundColor: "#94A3B8",
},
payButtonText: {
fontSize: 14,
color: "#FCFCFC",
fontWeight: "500",
},
});
export default SelectAmountScreen;

View File

@ -1,188 +0,0 @@
import React, { useEffect } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
SafeAreaView,
StatusBar,
ActivityIndicator,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store/rootReducer"; // Adjust path as needed
import { getUserDetails } from "../../store/userSlice";
import { AppDispatch } from "@/store";
import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next";
interface MyVehicleScreenProps {
navigation?: any; // Replace with proper navigation type
}
const MyVehicleScreen: React.FC<MyVehicleScreenProps> = ({ navigation }) => {
const dispatch = useDispatch<AppDispatch>();
const {
data: userData,
loading,
error,
} = useSelector((state: RootState) => state.user);
useEffect(() => {
// Fetch user details when component mounts
if (!userData) {
dispatch(getUserDetails());
}
}, [dispatch, userData]);
// Get the first vehicle from the user data
const vehicle = userData?.vehicles?.[0];
if (loading) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#25324B" />
<Text style={styles.loadingText}>Loading vehicle details...</Text>
</View>
</SafeAreaView>
);
}
if (error) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Error: {error}</Text>
</View>
</SafeAreaView>
);
}
const { t } = useTranslation();
return (
<>
<Header title={t("profile.my-vehicle")} showBackButton={true} />
<View style={styles.content}>
<View style={styles.itemContainer}>
{/* OEM - Model */}
<View style={styles.item}>
<Text style={styles.label}>{t("profile.oem-model")}</Text>
<Text style={styles.value}>
{vehicle?.model ? `Yatri - ${vehicle.model}` : "--"}
</Text>
</View>
<View style={styles.divider} />
{/* Chassis Number */}
<View style={styles.item}>
<Text style={styles.label}>{t("profile.chassis-number")}</Text>
<Text style={styles.value}>{vehicle?.chasis_number || "--"}</Text>
</View>
<View style={styles.divider} />
{/* Vehicle ID */}
<View style={styles.item}>
<Text style={styles.label}>{t("profile.vehicle-id")}</Text>
<Text style={styles.value}>
{vehicle?.vehicle_id ? vehicle.vehicle_id.toString() : "--"}
</Text>
</View>
</View>
</View>
</>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#ffffff",
},
header: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
backButton: {
marginRight: 12,
padding: 4,
},
backIcon: {
width: 24,
height: 24,
justifyContent: "center",
alignItems: "center",
},
backIconText: {
fontSize: 20,
fontWeight: "600",
color: "#25324B",
},
headerTitle: {
fontSize: 18,
fontWeight: "600",
color: "#25324B",
},
content: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 16,
},
itemContainer: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
overflow: "hidden",
},
item: {
paddingHorizontal: 16,
paddingVertical: 16,
},
label: {
fontSize: 14,
color: "#25324B",
fontWeight: "400",
marginBottom: 4,
lineHeight: 20,
},
value: {
fontSize: 14,
color: "#25324B",
fontWeight: "600",
lineHeight: 20,
},
divider: {
height: 1,
backgroundColor: "#E5E9F0",
marginHorizontal: 16,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: "#25324B",
},
errorContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 16,
},
errorText: {
fontSize: 16,
color: "#FF6B6B",
textAlign: "center",
},
});
export default MyVehicleScreen;

View File

@ -1,47 +0,0 @@
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { TouchableOpacity } from "react-native";
import GoBack from "@/assets/icons/chevron_left.svg";
import { useRouter } from "expo-router";
export default function UserLayout() {
const router = useRouter();
return (
<>
<StatusBar style="dark" />
<Stack
screenOptions={{
headerShown: false,
headerTitleStyle: {
fontSize: 16,
color: "#252A34",
},
headerShadowVisible: false,
headerLeft: () => (
<TouchableOpacity onPress={() => router.back()}>
<GoBack />
</TouchableOpacity>
),
headerStyle: {
backgroundColor: "#F3F5F8",
},
animation: "slide_from_right",
}}
>
<Stack.Screen
name="profile"
options={{
title: "My Account",
}}
/>
<Stack.Screen
name="edit_name"
options={{
title: "Edit Name",
}}
/>
</Stack>
</>
);
}

View File

@ -1,203 +0,0 @@
import React, { useState } from "react";
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
Keyboard,
TouchableWithoutFeedback,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store";
import { Formik } from "formik";
import * as Yup from "yup";
import api from "@/services/axiosClient";
import { BASE_URL } from "@/constants/config";
import { setUserData } from "@/store/userSlice";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { router } from "expo-router";
import { useSnackbar } from "@/contexts/Snackbar";
import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next";
import { Overlay } from "@/components/common/Overlay";
export default function EditName() {
const { data } = useSelector((state: RootState) => state.user);
const [isLoading, setIsLoading] = useState<boolean>();
const originalName = data?.name || "";
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const nameSchema = Yup.object().shape({
name: Yup.string()
.required("Name is required")
.max(57, "Name cannot exceed 57 characters"),
});
const { showSnackbar } = useSnackbar();
const { t } = useTranslation();
const handleSave = async (values: { name: string }) => {
try {
setIsLoading(true);
await api.put(`${BASE_URL}/api/v1/update-user-information`, {
name: values.name,
mobile: data?.mobile,
});
dispatch(setUserData({ name: values.name }));
showSnackbar(`${t("profile.name-changed")}`, "success");
router.back();
} catch (error) {
showSnackbar(`${t("service.something-went-wrong")}`, "error");
console.error("Error updating name:", error);
} finally {
setIsLoading(false);
}
};
return (
<>
<Header title={t("profile.edit-name")} showBackButton={true} />
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.inner}>
<Formik
initialValues={{ name: originalName }}
validationSchema={nameSchema}
onSubmit={handleSave}
enableReinitialize
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
touched,
errors,
}) => {
const hasChanged = values.name !== originalName;
const hasError = !!errors.name;
return (
<View style={styles.formContainer}>
<View style={styles.inputContainer}>
<Text style={styles.label}>
{t("profile.enter-name")}
</Text>
<TextInput
style={[
styles.input,
{
borderColor:
touched.name && errors.name
? "#D51D10"
: "#D8DDE7",
},
]}
value={values.name}
onChangeText={handleChange("name")}
onBlur={handleBlur("name")}
placeholder="Enter your name"
placeholderTextColor="#949CAC"
/>
{touched.name && errors.name && (
<Text style={styles.error}>{errors.name}</Text>
)}
</View>
<TouchableOpacity
onPress={handleSubmit as unknown as () => void}
style={[
styles.button,
hasChanged && !hasError
? styles.buttonEnabled
: styles.buttonDisabled,
{ marginBottom: insets.bottom },
]}
disabled={!hasChanged || hasError}
>
<Text style={styles.buttonText}>{t("profile.save")}</Text>
</TouchableOpacity>
</View>
);
}}
</Formik>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
<Overlay isUploading={isLoading ?? false} />
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#F3F5F8",
paddingHorizontal: 16,
},
inner: {
flex: 1,
justifyContent: "space-between",
backgroundColor: "#F3F5F8",
},
formContainer: {
flex: 1,
justifyContent: "space-between",
},
inputContainer: {
marginBottom: 24,
},
label: {
fontSize: 14,
marginBottom: 8,
color: "#252A34",
fontFamily: "Inter",
lineHeight: 20,
},
input: {
backgroundColor: "#ffffff",
borderRadius: 4,
borderWidth: 1,
height: 40,
paddingHorizontal: 8,
fontSize: 14,
fontFamily: "Inter",
fontWeight: "500",
lineHeight: 20,
},
error: {
marginTop: 8,
color: "#D51D10",
fontSize: 12,
fontFamily: "Inter",
fontWeight: "bold",
lineHeight: 20,
},
button: {
height: 48,
borderRadius: 4,
alignItems: "center",
justifyContent: "center",
width: "100%",
paddingHorizontal: 16,
},
buttonEnabled: {
backgroundColor: "#008761",
},
buttonDisabled: {
backgroundColor: "#B0B7C5", // from Figma: rgba(176, 183, 197)
},
buttonText: {
color: "#FCFCFC",
fontSize: 14,
fontFamily: "Inter",
fontWeight: "bold",
lineHeight: 20,
},
});

View File

@ -1,266 +0,0 @@
import React from "react";
import {
View,
Text,
StyleSheet,
Image,
TouchableOpacity,
ScrollView,
} from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import LanguageModal from "@/components/Profile/LangaugeModal";
import { useSelector } from "react-redux";
import { AppDispatch, RootState } from "@/store";
import EditIcon from "../../assets/icons/edit.svg";
import { router } from "expo-router";
import { useDispatch } from "react-redux";
import { logout } from "@/store/authSlice";
import * as ImagePicker from "expo-image-picker";
import { useSnackbar } from "@/contexts/Snackbar";
import { AWS, BASE_URL, USER_PROFILE } from "@/constants/config";
import { bytesToMB, updateUserProfile, uploadImage } from "@/utils/User";
import { setUserData } from "@/store/userSlice";
import { Overlay } from "@/components/common/Overlay";
import Header from "@/components/common/Header";
import { useTranslation } from "react-i18next";
export default function ProfileScreen() {
const [isLangaugeModalVisible, setLanguageModalVisible] =
React.useState(false);
const { showSnackbar } = useSnackbar();
const toggleLanguageModal = () => {
setLanguageModalVisible(!isLangaugeModalVisible);
};
const [isUploading, setIsUploading] = React.useState(false);
const { data } = useSelector((state: RootState) => state.user);
const userName = data?.name || "User";
const mobileNumber = data?.mobile || "Not provided";
const userImageUrl = data?.profile_url;
const dispatch = useDispatch<AppDispatch>();
const handleLogout = () => {
dispatch(logout());
router.replace("/auth/login");
};
const handlePickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
allowsEditing: true,
aspect: [1, 1],
});
if (!result.canceled) {
try {
setIsUploading(true);
const { uri, fileSize } = result.assets[0];
console.log(uri, "File size:", fileSize);
if (!fileSize) {
showSnackbar("Image size is not available", "error");
return;
}
const size = bytesToMB(fileSize);
if (size > USER_PROFILE.MAX_IMAGE_SIZE_IN_MB) {
showSnackbar(
`Image size exceeds ${USER_PROFILE.MAX_IMAGE_SIZE_IN_MB}MB limit`,
"error"
);
throw new Error(
`Image size exceeds ${USER_PROFILE.MAX_IMAGE_SIZE_IN_MB}MB limit`
);
}
const uploadedImageUrl = await uploadImage(uri);
console.log("Uploaded image URL:", uploadedImageUrl);
await updateUserProfile({
profile_url: uploadedImageUrl,
mobile: mobileNumber,
});
dispatch(setUserData({ profile_url: uri }));
showSnackbar("Image uploaded successfully", "success");
} catch (error) {
console.error("Error uploading image:", error);
showSnackbar("Image upload failed", "error");
} finally {
setIsUploading(false);
}
}
};
const { t } = useTranslation();
return (
<>
<Header title={t("profile.my-account")} showBackButton={true} />
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.avatarContainer}>
<Image
source={
userImageUrl
? { uri: userImageUrl }
: require("@/assets/images/user_image.jpg")
}
style={styles.avatar}
/>
<TouchableOpacity
style={styles.editAvatar}
onPress={() => handlePickImage()}
>
<EditIcon />
</TouchableOpacity>
</View>
<View style={styles.card}>
<View style={styles.row}>
<View style={styles.textGroup}>
<Text style={styles.label}>{t("profile.name")}</Text>
<Text style={styles.value}>{userName}</Text>
</View>
<TouchableOpacity
onPress={() => {
router.push("/user/edit_name");
}}
style={styles.edit_button}
>
<MaterialIcons name="edit" size={20} color="#555C70" />
</TouchableOpacity>
</View>
<View style={styles.divider} />
<View style={styles.row}>
<View style={styles.textGroup}>
<Text style={styles.label}>{t("profile.mobile-number")}</Text>
<Text style={styles.value}>{mobileNumber}</Text>
</View>
</View>
</View>
<View style={styles.card}>
{menuItem(`${t("profile.my-vehicle")}`, () =>
router.push("/user/MyVechicle")
)}
<View style={styles.divider} />
{menuItem(`${t("profile.language")}`, () => toggleLanguageModal())}
</View>
<View style={styles.card}>
{menuItem(`${t("profile.about-app")}`)}
<View style={styles.divider} />
{menuItem(`${t("profile.logout")}`, handleLogout)}
</View>
</ScrollView>
<LanguageModal
onClose={() => setLanguageModalVisible(false)}
visible={isLangaugeModalVisible}
/>
<Overlay isUploading={isUploading} />
</>
);
}
const menuItem = (title: string, onPress?: () => void) => (
<TouchableOpacity style={styles.menuRow} onPress={onPress}>
<Text style={styles.menuText}>{title}</Text>
<MaterialIcons name="chevron-right" size={20} color="#555C70" />
</TouchableOpacity>
);
const styles = StyleSheet.create({
edit_button: {
position: "absolute",
bottom: 0,
right: 0,
width: 40,
height: 40,
},
container: {
borderWidth: 1,
flex: 1,
backgroundColor: "#F3F5F8",
},
topBar: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 12,
backgroundColor: "#F3F5F8",
},
backButton: {
padding: 8,
},
headerText: {
fontSize: 16,
fontWeight: "600",
marginLeft: 8,
color: "#252A34",
},
scrollContent: {
paddingHorizontal: 16,
paddingBottom: 32,
backgroundColor: "#F3F5F8",
},
avatarContainer: {
alignItems: "center",
marginVertical: 24,
},
avatar: {
width: 120,
height: 120,
borderRadius: 60,
},
editAvatar: {
position: "absolute",
bottom: 0,
right: "35%",
width: 40,
height: 40,
backgroundColor: "#008866",
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
},
card: {
backgroundColor: "#FCFCFC",
borderRadius: 8,
marginBottom: 16,
paddingHorizontal: 16,
paddingVertical: 8,
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
},
textGroup: {
flexDirection: "column",
},
label: {
fontSize: 14,
color: "#252A34",
},
value: {
fontSize: 14,
fontWeight: "600",
color: "#252A34",
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: "#E5E9F0",
marginVertical: 4,
},
menuRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 16,
},
menuText: {
fontSize: 14,
color: "#252A34",
},
});

View File

@ -1,8 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_5357" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="18">
<rect width="18" height="18" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_5357)">
<path d="M4.04922 15.3C3.67797 15.3 3.36016 15.1678 3.09578 14.9035C2.83141 14.6391 2.69922 14.3213 2.69922 13.95V4.05001C2.69922 3.67876 2.83141 3.36095 3.09578 3.09658C3.36016 2.8322 3.67797 2.70001 4.04922 2.70001H9.52422C9.74922 2.70001 9.92422 2.80001 10.0492 3.00001C10.1742 3.20001 10.1867 3.40764 10.0867 3.62289C10.0242 3.79514 9.97734 3.97501 9.94609 4.16251C9.91484 4.35001 9.89922 4.53926 9.89922 4.73026C9.89922 5.66251 10.2278 6.4572 10.8849 7.11433C11.542 7.77145 12.3367 8.10001 13.269 8.10001C13.46 8.10001 13.6492 8.08439 13.8367 8.05314C14.0242 8.02189 14.2068 7.97764 14.3846 7.92039C14.6068 7.84014 14.8148 7.85939 15.0086 7.97814C15.2023 8.09689 15.2992 8.26251 15.2992 8.47501V13.95C15.2992 14.3213 15.167 14.6391 14.9027 14.9035C14.6383 15.1678 14.3205 15.3 13.9492 15.3H4.04922ZM4.94922 12.6H13.0492L10.3492 9.00001L8.32422 11.7L6.97422 9.90001L4.94922 12.6ZM13.2782 6.75001C13.088 6.75001 12.9281 6.68533 12.7983 6.55595C12.6685 6.42658 12.6035 6.26626 12.6035 6.07501V5.40001H11.9265C11.7346 5.40001 11.5737 5.33533 11.4438 5.20595C11.3141 5.07658 11.2492 4.91626 11.2492 4.72501C11.2492 4.53376 11.3141 4.37345 11.4438 4.24407C11.5737 4.1147 11.7346 4.05001 11.9265 4.05001H12.6035V3.37501C12.6035 3.18376 12.6685 3.02345 12.7983 2.89407C12.9281 2.7647 13.088 2.70001 13.2782 2.70001C13.4683 2.70001 13.6277 2.7647 13.7563 2.89407C13.8849 3.02345 13.9492 3.18376 13.9492 3.37501V4.05001H14.6203C14.8104 4.05001 14.9711 4.11432 15.1023 4.24295C15.2336 4.37157 15.2992 4.53095 15.2992 4.72107C15.2992 4.9112 15.2345 5.07095 15.1052 5.20032C14.9758 5.3297 14.8155 5.39439 14.6242 5.39439H13.9492V6.06939C13.9492 6.26064 13.8849 6.42189 13.7563 6.55314C13.6277 6.68439 13.4683 6.75001 13.2782 6.75001Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_1085" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_1085)">
<path d="M6 4V2.5H9V4H6Z" fill="#00BE88"/>
<path d="M15 4V2.5H18V4H15Z" fill="#00BE88"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 6C3 5.44772 3.44772 5 4 5H20C20.5523 5 21 5.44772 21 6V18C21 18.5523 20.5523 19 20 19H4C3.44772 19 3 18.5523 3 18V6ZM14.5 9.9751V11.5251H13V13.0001H14.5V14.6251H16V13.0001H17.5V11.5251H16V9.9751H14.5ZM6.5 11.5251V13.0001H10.5V11.5251H6.5Z" fill="#00BE88"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 703 B

View File

@ -1,8 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_1013" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="25" height="24">
<rect x="0.09375" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_1013)">
<path d="M6.09375 2.5V4H9.09375V2.5H6.09375ZM15.0938 2.5V4H18.0938V2.5H15.0938ZM4.09375 5C3.54147 5 3.09375 5.44772 3.09375 6V18C3.09375 18.5523 3.54147 19 4.09375 19H20.0938C20.646 19 21.0938 18.5523 21.0938 18V6C21.0938 5.44772 20.646 5 20.0938 5H4.09375ZM5.09375 7H19.0938V17H5.09375V7ZM14.5938 9.975V11.525H13.0938V13H14.5938V14.625H16.0938V13H17.5938V11.525H16.0938V9.975H14.5938ZM6.59375 11.525V13H10.5938V11.525H6.59375Z" fill="#B0B7C5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 763 B

View File

@ -1,8 +0,0 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_901_1089" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="4" width="21" height="20">
<rect x="4.57031" y="4" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_901_1089)">
<path d="M16.6536 19C16.0703 19 15.5773 18.7986 15.1745 18.3958C14.7717 17.993 14.5703 17.5 14.5703 16.9167C14.5703 16.3333 14.7717 15.8403 15.1745 15.4375C15.5773 15.0347 16.0703 14.8333 16.6536 14.8333C17.237 14.8333 17.73 15.0347 18.1328 15.4375C18.5356 15.8403 18.737 16.3333 18.737 16.9167C18.737 17.5 18.5356 17.993 18.1328 18.3958C17.73 18.7986 17.237 19 16.6536 19ZM8.73698 22.3333C8.27865 22.3333 7.88628 22.1701 7.5599 21.8437C7.23351 21.5174 7.07031 21.125 7.07031 20.6667V8.99999C7.07031 8.54166 7.23351 8.1493 7.5599 7.82291C7.88628 7.49652 8.27865 7.33332 8.73698 7.33332H9.57031V6.49999C9.57031 6.26388 9.65017 6.06596 9.8099 5.90624C9.96962 5.74652 10.1675 5.66666 10.4036 5.66666C10.6398 5.66666 10.8377 5.74652 10.9974 5.90624C11.1571 6.06596 11.237 6.26388 11.237 6.49999V7.33332H17.9036V6.49999C17.9036 6.26388 17.9835 6.06596 18.1432 5.90624C18.303 5.74652 18.5009 5.66666 18.737 5.66666C18.9731 5.66666 19.171 5.74652 19.3307 5.90624C19.4905 6.06596 19.5703 6.26388 19.5703 6.49999V7.33332H20.4036C20.862 7.33332 21.2543 7.49652 21.5807 7.82291C21.9071 8.1493 22.0703 8.54166 22.0703 8.99999V20.6667C22.0703 21.125 21.9071 21.5174 21.5807 21.8437C21.2543 22.1701 20.862 22.3333 20.4036 22.3333H8.73698ZM8.73698 20.6667H20.4036V12.3333H8.73698V20.6667ZM8.73698 10.6667H20.4036V8.99999H8.73698V10.6667Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_710" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="21" height="21">
<rect x="0.5" y="0.439453" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_710)">
<path d="M17.125 17.9395C15.3889 17.9395 13.6736 17.561 11.9792 16.804C10.2847 16.0471 8.74306 14.9742 7.35417 13.5853C5.96528 12.1964 4.89236 10.6547 4.13542 8.96029C3.37847 7.26584 3 5.55056 3 3.81445C3 3.56445 3.08333 3.35612 3.25 3.18945C3.41667 3.02279 3.625 2.93945 3.875 2.93945H7.25C7.44444 2.93945 7.61806 3.00543 7.77083 3.13737C7.92361 3.26931 8.01389 3.42556 8.04167 3.60612L8.58333 6.52279C8.61111 6.74501 8.60417 6.93251 8.5625 7.08529C8.52083 7.23806 8.44444 7.37001 8.33333 7.48112L6.3125 9.52279C6.59028 10.0367 6.92014 10.5332 7.30208 11.0124C7.68403 11.4915 8.10417 11.9533 8.5625 12.3978C8.99306 12.8283 9.44444 13.2276 9.91667 13.5957C10.3889 13.9638 10.8889 14.3006 11.4167 14.6061L13.375 12.6478C13.5 12.5228 13.6632 12.429 13.8646 12.3665C14.066 12.304 14.2639 12.2867 14.4583 12.3145L17.3333 12.8978C17.5278 12.9533 17.6875 13.054 17.8125 13.1999C17.9375 13.3457 18 13.5089 18 13.6895V17.0645C18 17.3145 17.9167 17.5228 17.75 17.6895C17.5833 17.8561 17.375 17.9395 17.125 17.9395ZM5.52083 7.93945L6.89583 6.56445L6.54167 4.60612H4.6875C4.75694 5.17556 4.85417 5.73806 4.97917 6.29362C5.10417 6.84918 5.28472 7.39779 5.52083 7.93945ZM12.9792 15.3978C13.5208 15.6339 14.0729 15.8214 14.6354 15.9603C15.1979 16.0992 15.7639 16.1895 16.3333 16.2311V14.3978L14.375 14.002L12.9792 15.3978Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +0,0 @@
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_709_4789" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="81" height="81">
<rect x="0.09375" y="0.123047" width="80" height="80" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_709_4789)">
<path d="M40.0931 44.7897L49.7598 54.4564C50.3709 55.0675 51.1487 55.3731 52.0931 55.3731C53.0375 55.3731 53.8153 55.0675 54.4264 54.4564C55.0375 53.8453 55.3431 53.0675 55.3431 52.1231C55.3431 51.1786 55.0375 50.4008 54.4264 49.7897L44.7598 40.1231L54.4264 30.4564C55.0375 29.8453 55.3431 29.0675 55.3431 28.1231C55.3431 27.1786 55.0375 26.4008 54.4264 25.7897C53.8153 25.1786 53.0375 24.8731 52.0931 24.8731C51.1487 24.8731 50.3709 25.1786 49.7598 25.7897L40.0931 35.4564L30.4264 25.7897C29.8153 25.1786 29.0375 24.8731 28.0931 24.8731C27.1487 24.8731 26.3709 25.1786 25.7598 25.7897C25.1487 26.4008 24.8431 27.1786 24.8431 28.1231C24.8431 29.0675 25.1487 29.8453 25.7598 30.4564L35.4264 40.1231L25.7598 49.7897C25.1487 50.4008 24.8431 51.1786 24.8431 52.1231C24.8431 53.0675 25.1487 53.8453 25.7598 54.4564C26.3709 55.0675 27.1487 55.3731 28.0931 55.3731C29.0375 55.3731 29.8153 55.0675 30.4264 54.4564L40.0931 44.7897ZM40.0931 73.4564C35.482 73.4564 31.1487 72.5814 27.0931 70.8314C23.0375 69.0814 19.5098 66.7064 16.5098 63.7064C13.5098 60.7064 11.1348 57.1786 9.38477 53.1231C7.63477 49.0675 6.75977 44.7342 6.75977 40.1231C6.75977 35.512 7.63477 31.1786 9.38477 27.1231C11.1348 23.0675 13.5098 19.5397 16.5098 16.5397C19.5098 13.5397 23.0375 11.1647 27.0931 9.41473C31.1487 7.66473 35.482 6.78973 40.0931 6.78973C44.7042 6.78973 49.0375 7.66473 53.0931 9.41473C57.1487 11.1647 60.6764 13.5397 63.6764 16.5397C66.6764 19.5397 69.0514 23.0675 70.8014 27.1231C72.5514 31.1786 73.4264 35.512 73.4264 40.1231C73.4264 44.7342 72.5514 49.0675 70.8014 53.1231C69.0514 57.1786 66.6764 60.7064 63.6764 63.7064C60.6764 66.7064 57.1487 69.0814 53.0931 70.8314C49.0375 72.5814 44.7042 73.4564 40.0931 73.4564Z" fill="#D51D10"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,8 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_4184" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="21">
<rect y="0.5" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_4184)">
<path d="M8.83366 12L7.04199 10.2083C6.88921 10.0556 6.69477 9.97917 6.45866 9.97917C6.22255 9.97917 6.0281 10.0556 5.87533 10.2083C5.72255 10.3611 5.64616 10.5556 5.64616 10.7917C5.64616 11.0278 5.72255 11.2222 5.87533 11.375L8.25033 13.75C8.41699 13.9167 8.61144 14 8.83366 14C9.05588 14 9.25033 13.9167 9.41699 13.75L14.1253 9.04167C14.2781 8.88889 14.3545 8.69445 14.3545 8.45834C14.3545 8.22223 14.2781 8.02778 14.1253 7.87501C13.9725 7.72223 13.7781 7.64584 13.542 7.64584C13.3059 7.64584 13.1114 7.72223 12.9587 7.87501L8.83366 12ZM10.0003 18.8333C8.84755 18.8333 7.76421 18.6146 6.75033 18.1771C5.73644 17.7396 4.85449 17.1458 4.10449 16.3958C3.35449 15.6458 2.76074 14.7639 2.32324 13.75C1.88574 12.7361 1.66699 11.6528 1.66699 10.5C1.66699 9.34723 1.88574 8.26389 2.32324 7.25001C2.76074 6.23612 3.35449 5.35417 4.10449 4.60417C4.85449 3.85417 5.73644 3.26042 6.75033 2.82292C7.76421 2.38542 8.84755 2.16667 10.0003 2.16667C11.1531 2.16667 12.2364 2.38542 13.2503 2.82292C14.2642 3.26042 15.1462 3.85417 15.8962 4.60417C16.6462 5.35417 17.2399 6.23612 17.6774 7.25001C18.1149 8.26389 18.3337 9.34723 18.3337 10.5C18.3337 11.6528 18.1149 12.7361 17.6774 13.75C17.2399 14.7639 16.6462 15.6458 15.8962 16.3958C15.1462 17.1458 14.2642 17.7396 13.2503 18.1771C12.2364 18.6146 11.1531 18.8333 10.0003 18.8333ZM10.0003 17.1667C11.8614 17.1667 13.4378 16.5208 14.7295 15.2292C16.0212 13.9375 16.667 12.3611 16.667 10.5C16.667 8.63889 16.0212 7.06251 14.7295 5.77084C13.4378 4.47917 11.8614 3.83334 10.0003 3.83334C8.13921 3.83334 6.56283 4.47917 5.27116 5.77084C3.97949 7.06251 3.33366 8.63889 3.33366 10.5C3.33366 12.3611 3.97949 13.9375 5.27116 15.2292C6.56283 16.5208 8.13921 17.1667 10.0003 17.1667Z" fill="#008761"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,8 +0,0 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_709_4688" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="80" height="80">
<rect width="80" height="80" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_709_4688)">
<path d="M35.3327 46L28.166 38.8334C27.5549 38.2222 26.7771 37.9167 25.8327 37.9167C24.8882 37.9167 24.1105 38.2222 23.4993 38.8334C22.8882 39.4445 22.5827 40.2222 22.5827 41.1667C22.5827 42.1111 22.8882 42.8889 23.4993 43.5L32.9993 53C33.666 53.6667 34.4438 54 35.3327 54C36.2216 54 36.9993 53.6667 37.666 53L56.4993 34.1667C57.1105 33.5556 57.416 32.7778 57.416 31.8334C57.416 30.8889 57.1105 30.1111 56.4993 29.5C55.8882 28.8889 55.1105 28.5834 54.166 28.5834C53.2216 28.5834 52.4438 28.8889 51.8327 29.5L35.3327 46ZM39.9993 73.3334C35.3882 73.3334 31.0549 72.4584 26.9993 70.7084C22.9438 68.9584 19.416 66.5834 16.416 63.5834C13.416 60.5834 11.041 57.0556 9.29102 53C7.54102 48.9445 6.66602 44.6111 6.66602 40C6.66602 35.3889 7.54102 31.0556 9.29102 27C11.041 22.9445 13.416 19.4167 16.416 16.4167C19.416 13.4167 22.9438 11.0417 26.9993 9.29169C31.0549 7.54169 35.3882 6.66669 39.9993 6.66669C44.6105 6.66669 48.9438 7.54169 52.9994 9.29169C57.0549 11.0417 60.5827 13.4167 63.5827 16.4167C66.5827 19.4167 68.9577 22.9445 70.7077 27C72.4577 31.0556 73.3327 35.3889 73.3327 40C73.3327 44.6111 72.4577 48.9445 70.7077 53C68.9577 57.0556 66.5827 60.5834 63.5827 63.5834C60.5827 66.5834 57.0549 68.9584 52.9994 70.7084C48.9438 72.4584 44.6105 73.3334 39.9993 73.3334Z" fill="#009E71"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_213_3296" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_213_3296)">
<path d="M10.8001 12L14.7001 15.9C14.8834 16.0833 14.9751 16.3166 14.9751 16.6C14.9751 16.8833 14.8834 17.1166 14.7001 17.3C14.5168 17.4833 14.2834 17.575 14.0001 17.575C13.7168 17.575 13.4834 17.4833 13.3001 17.3L8.7001 12.7C8.6001 12.6 8.52926 12.4916 8.4876 12.375C8.44593 12.2583 8.4251 12.1333 8.4251 12C8.4251 11.8666 8.44593 11.7416 8.4876 11.625C8.52926 11.5083 8.6001 11.4 8.7001 11.3L13.3001 6.69995C13.4834 6.51662 13.7168 6.42495 14.0001 6.42495C14.2834 6.42495 14.5168 6.51662 14.7001 6.69995C14.8834 6.88328 14.9751 7.11662 14.9751 7.39995C14.9751 7.68328 14.8834 7.91662 14.7001 8.09995L10.8001 12Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 939 B

View File

@ -1,8 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_871" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<rect width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_871)">
<path d="M10.4997 10L7.24967 6.75002C7.0969 6.59724 7.02051 6.4028 7.02051 6.16669C7.02051 5.93058 7.0969 5.73613 7.24967 5.58335C7.40245 5.43058 7.5969 5.35419 7.83301 5.35419C8.06912 5.35419 8.26356 5.43058 8.41634 5.58335L12.2497 9.41669C12.333 9.50002 12.392 9.5903 12.4268 9.68752C12.4615 9.78474 12.4788 9.88891 12.4788 10C12.4788 10.1111 12.4615 10.2153 12.4268 10.3125C12.392 10.4097 12.333 10.5 12.2497 10.5834L8.41634 14.4167C8.26356 14.5695 8.06912 14.6459 7.83301 14.6459C7.5969 14.6459 7.40245 14.5695 7.24967 14.4167C7.0969 14.2639 7.02051 14.0695 7.02051 13.8334C7.02051 13.5972 7.0969 13.4028 7.24967 13.25L10.4997 10Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

View File

@ -1,8 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_2273" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="21">
<rect y="0.439453" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_2273)">
<path d="M10.0007 11.6061L5.91732 15.6894C5.76454 15.8422 5.5701 15.9186 5.33398 15.9186C5.09787 15.9186 4.90343 15.8422 4.75065 15.6894C4.59787 15.5367 4.52148 15.3422 4.52148 15.1061C4.52148 14.87 4.59787 14.6756 4.75065 14.5228L8.83398 10.4394L4.75065 6.35611C4.59787 6.20334 4.52148 6.00889 4.52148 5.77278C4.52148 5.53667 4.59787 5.34223 4.75065 5.18945C4.90343 5.03667 5.09787 4.96028 5.33398 4.96028C5.5701 4.96028 5.76454 5.03667 5.91732 5.18945L10.0007 9.27278L14.084 5.18945C14.2368 5.03667 14.4312 4.96028 14.6673 4.96028C14.9034 4.96028 15.0979 5.03667 15.2507 5.18945C15.4034 5.34223 15.4798 5.53667 15.4798 5.77278C15.4798 6.00889 15.4034 6.20334 15.2507 6.35611L11.1673 10.4394L15.2507 14.5228C15.4034 14.6756 15.4798 14.87 15.4798 15.1061C15.4798 15.3422 15.4034 15.5367 15.2507 15.6894C15.0979 15.8422 14.9034 15.9186 14.6673 15.9186C14.4312 15.9186 14.2368 15.8422 14.084 15.6894L10.0007 11.6061Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,8 +0,0 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_382_3096" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="17">
<rect x="0.613281" y="0.726562" width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_382_3096)">
<path d="M8.6138 9.6599L5.34714 12.9266C5.22491 13.0488 5.06936 13.1099 4.88047 13.1099C4.69158 13.1099 4.53602 13.0488 4.4138 12.9266C4.29158 12.8043 4.23047 12.6488 4.23047 12.4599C4.23047 12.271 4.29158 12.1155 4.4138 11.9932L7.68047 8.72656L4.4138 5.4599C4.29158 5.33768 4.23047 5.18212 4.23047 4.99323C4.23047 4.80434 4.29158 4.64879 4.4138 4.52656C4.53602 4.40434 4.69158 4.34323 4.88047 4.34323C5.06936 4.34323 5.22491 4.40434 5.34714 4.52656L8.6138 7.79323L11.8805 4.52656C12.0027 4.40434 12.1582 4.34323 12.3471 4.34323C12.536 4.34323 12.6916 4.40434 12.8138 4.52656C12.936 4.64879 12.9971 4.80434 12.9971 4.99323C12.9971 5.18212 12.936 5.33768 12.8138 5.4599L9.54713 8.72656L12.8138 11.9932C12.936 12.1155 12.9971 12.271 12.9971 12.4599C12.9971 12.6488 12.936 12.8043 12.8138 12.9266C12.6916 13.0488 12.536 13.1099 12.3471 13.1099C12.1582 13.1099 12.0027 13.0488 11.8805 12.9266L8.6138 9.6599Z" fill="white"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,8 +0,0 @@
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_4161" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="10" y="10" width="21" height="20">
<rect x="10.168" y="10" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_4161)">
<path d="M14.3346 26.6667C13.8763 26.6667 13.4839 26.5035 13.1576 26.1771C12.8312 25.8507 12.668 25.4584 12.668 25V19.1667C12.668 18.1389 12.8659 17.1702 13.2617 16.2604C13.6576 15.3507 14.1957 14.5556 14.8763 13.875C15.5569 13.1945 16.352 12.6563 17.2617 12.2604C18.1714 11.8646 19.1402 11.6667 20.168 11.6667C21.1957 11.6667 22.1645 11.8646 23.0742 12.2604C23.9839 12.6563 24.7791 13.1945 25.4596 13.875C26.1402 14.5556 26.6784 15.3507 27.0742 16.2604C27.4701 17.1702 27.668 18.1389 27.668 19.1667V27.5C27.668 27.9584 27.5048 28.3507 27.1784 28.6771C26.852 29.0035 26.4596 29.1667 26.0013 29.1667H21.0013C20.7652 29.1667 20.5673 29.0868 20.4076 28.9271C20.2478 28.7674 20.168 28.5695 20.168 28.3334C20.168 28.0972 20.2478 27.8993 20.4076 27.7396C20.5673 27.5799 20.7652 27.5 21.0013 27.5H26.0013V26.6667H24.3346C23.8763 26.6667 23.4839 26.5035 23.1576 26.1771C22.8312 25.8507 22.668 25.4584 22.668 25V21.6667C22.668 21.2084 22.8312 20.816 23.1576 20.4896C23.4839 20.1632 23.8763 20 24.3346 20H26.0013V19.1667C26.0013 17.5556 25.4319 16.1806 24.293 15.0417C23.1541 13.9028 21.7791 13.3334 20.168 13.3334C18.5569 13.3334 17.1819 13.9028 16.043 15.0417C14.9041 16.1806 14.3346 17.5556 14.3346 19.1667V20H16.0013C16.4596 20 16.852 20.1632 17.1784 20.4896C17.5048 20.816 17.668 21.2084 17.668 21.6667V25C17.668 25.4584 17.5048 25.8507 17.1784 26.1771C16.852 26.5035 16.4596 26.6667 16.0013 26.6667H14.3346ZM14.3346 25H16.0013V21.6667H14.3346V25ZM24.3346 25H26.0013V21.6667H24.3346V25Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,8 +0,0 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_88" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="25" height="25">
<rect x="0.832031" y="0.370117" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_88)">
<path d="M12.832 17.3701C13.1154 17.3701 13.3529 17.2743 13.5445 17.0826C13.7362 16.891 13.832 16.6535 13.832 16.3701C13.832 16.0868 13.7362 15.8493 13.5445 15.6576C13.3529 15.466 13.1154 15.3701 12.832 15.3701C12.5487 15.3701 12.3112 15.466 12.1195 15.6576C11.9279 15.8493 11.832 16.0868 11.832 16.3701C11.832 16.6535 11.9279 16.891 12.1195 17.0826C12.3112 17.2743 12.5487 17.3701 12.832 17.3701ZM12.832 13.3701C13.1154 13.3701 13.3529 13.2743 13.5445 13.0826C13.7362 12.891 13.832 12.6535 13.832 12.3701V8.37012C13.832 8.08678 13.7362 7.84928 13.5445 7.65762C13.3529 7.46595 13.1154 7.37012 12.832 7.37012C12.5487 7.37012 12.3112 7.46595 12.1195 7.65762C11.9279 7.84928 11.832 8.08678 11.832 8.37012V12.3701C11.832 12.6535 11.9279 12.891 12.1195 13.0826C12.3112 13.2743 12.5487 13.3701 12.832 13.3701ZM12.832 22.3701C11.4487 22.3701 10.1487 22.1076 8.93203 21.5826C7.71536 21.0576 6.65703 20.3451 5.75703 19.4451C4.85703 18.5451 4.14453 17.4868 3.61953 16.2701C3.09453 15.0535 2.83203 13.7535 2.83203 12.3701C2.83203 10.9868 3.09453 9.68678 3.61953 8.47012C4.14453 7.25345 4.85703 6.19512 5.75703 5.29512C6.65703 4.39512 7.71536 3.68262 8.93203 3.15762C10.1487 2.63262 11.4487 2.37012 12.832 2.37012C14.2154 2.37012 15.5154 2.63262 16.732 3.15762C17.9487 3.68262 19.007 4.39512 19.907 5.29512C20.807 6.19512 21.5195 7.25345 22.0445 8.47012C22.5695 9.68678 22.832 10.9868 22.832 12.3701C22.832 13.7535 22.5695 15.0535 22.0445 16.2701C21.5195 17.4868 20.807 18.5451 19.907 19.4451C19.007 20.3451 17.9487 21.0576 16.732 21.5826C15.5154 22.1076 14.2154 22.3701 12.832 22.3701Z" fill="#D51D10"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,8 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_746_3070" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<rect width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_746_3070)">
<path d="M10.0007 12.9792C9.88954 12.9792 9.78537 12.9618 9.68815 12.9271C9.59093 12.8924 9.50065 12.8333 9.41732 12.75L6.41732 9.75C6.25065 9.58333 6.17079 9.38888 6.17773 9.16666C6.18468 8.94444 6.26454 8.74999 6.41732 8.58333C6.58398 8.41666 6.7819 8.32986 7.01107 8.32291C7.24023 8.31597 7.43815 8.39583 7.60482 8.56249L9.16732 10.125V4.16666C9.16732 3.93055 9.24718 3.73263 9.4069 3.57291C9.56662 3.41319 9.76454 3.33333 10.0007 3.33333C10.2368 3.33333 10.4347 3.41319 10.5944 3.57291C10.7541 3.73263 10.834 3.93055 10.834 4.16666V10.125L12.3965 8.56249C12.5632 8.39583 12.7611 8.31597 12.9902 8.32291C13.2194 8.32986 13.4173 8.41666 13.584 8.58333C13.7368 8.74999 13.8166 8.94444 13.8236 9.16666C13.8305 9.38888 13.7507 9.58333 13.584 9.75L10.584 12.75C10.5007 12.8333 10.4104 12.8924 10.3132 12.9271C10.2159 12.9618 10.1118 12.9792 10.0007 12.9792ZM5.00065 16.6667C4.54232 16.6667 4.14996 16.5035 3.82357 16.1771C3.49718 15.8507 3.33398 15.4583 3.33398 15V13.3333C3.33398 13.0972 3.41385 12.8993 3.57357 12.7396C3.73329 12.5799 3.93121 12.5 4.16732 12.5C4.40343 12.5 4.60135 12.5799 4.76107 12.7396C4.92079 12.8993 5.00065 13.0972 5.00065 13.3333V15H15.0007V13.3333C15.0007 13.0972 15.0805 12.8993 15.2402 12.7396C15.4 12.5799 15.5979 12.5 15.834 12.5C16.0701 12.5 16.268 12.5799 16.4277 12.7396C16.5875 12.8993 16.6673 13.0972 16.6673 13.3333V15C16.6673 15.4583 16.5041 15.8507 16.1777 16.1771C15.8513 16.5035 15.459 16.6667 15.0007 16.6667H5.00065Z" fill="#717B8C"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,8 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_4327" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<rect width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_4327)">
<path d="M4.16667 15.8333H5.35417L13.5 7.6875L12.3125 6.5L4.16667 14.6458V15.8333ZM3.33333 17.5C3.09722 17.5 2.89931 17.4201 2.73958 17.2604C2.57986 17.1007 2.5 16.9028 2.5 16.6667V14.6458C2.5 14.4236 2.54167 14.2118 2.625 14.0104C2.70833 13.809 2.82639 13.6319 2.97917 13.4792L13.5 2.97917C13.6667 2.82639 13.8507 2.70833 14.0521 2.625C14.2535 2.54167 14.4653 2.5 14.6875 2.5C14.9097 2.5 15.125 2.54167 15.3333 2.625C15.5417 2.70833 15.7222 2.83333 15.875 3L17.0208 4.16667C17.1875 4.31944 17.309 4.5 17.3854 4.70833C17.4618 4.91667 17.5 5.125 17.5 5.33333C17.5 5.55556 17.4618 5.76736 17.3854 5.96875C17.309 6.17014 17.1875 6.35417 17.0208 6.52083L6.52083 17.0208C6.36806 17.1736 6.19097 17.2917 5.98958 17.375C5.78819 17.4583 5.57639 17.5 5.35417 17.5H3.33333ZM12.8958 7.10417L12.3125 6.5L13.5 7.6875L12.8958 7.10417Z" fill="#FCFCFC"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_862" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_862)">
<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16C13 15.7167 12.9042 15.4792 12.7125 15.2875C12.5208 15.0958 12.2833 15 12 15C11.7167 15 11.4792 15.0958 11.2875 15.2875C11.0958 15.4792 11 15.7167 11 16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 13C12.2833 13 12.5208 12.9042 12.7125 12.7125C12.9042 12.5208 13 12.2833 13 12V8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8V12C11 12.2833 11.0958 12.5208 11.2875 12.7125C11.4792 12.9042 11.7167 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#1249ED"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_159" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_159)">
<path d="M4 19V10C4 9.68333 4.07083 9.38333 4.2125 9.1C4.35417 8.81667 4.55 8.58333 4.8 8.4L10.8 3.9C11.15 3.63333 11.55 3.5 12 3.5C12.45 3.5 12.85 3.63333 13.2 3.9L19.2 8.4C19.45 8.58333 19.6458 8.81667 19.7875 9.1C19.9292 9.38333 20 9.68333 20 10V19C20 19.55 19.8042 20.0208 19.4125 20.4125C19.0208 20.8042 18.55 21 18 21H15C14.7167 21 14.4792 20.9042 14.2875 20.7125C14.0958 20.5208 14 20.2833 14 20V15C14 14.7167 13.9042 14.4792 13.7125 14.2875C13.5208 14.0958 13.2833 14 13 14H11C10.7167 14 10.4792 14.0958 10.2875 14.2875C10.0958 14.4792 10 14.7167 10 15V20C10 20.2833 9.90417 20.5208 9.7125 20.7125C9.52083 20.9042 9.28333 21 9 21H6C5.45 21 4.97917 20.8042 4.5875 20.4125C4.19583 20.0208 4 19.55 4 19Z" fill="#00BE88"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,8 +0,0 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_324" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_324)">
<path d="M6 19.5H9V14.5C9 14.2167 9.09583 13.9792 9.2875 13.7875C9.47917 13.5958 9.71667 13.5 10 13.5H14C14.2833 13.5 14.5208 13.5958 14.7125 13.7875C14.9042 13.9792 15 14.2167 15 14.5V19.5H18V10.5L12 6L6 10.5V19.5ZM4 19.5V10.5C4 10.1833 4.07083 9.88333 4.2125 9.6C4.35417 9.31667 4.55 9.08333 4.8 8.9L10.8 4.4C11.15 4.13333 11.55 4 12 4C12.45 4 12.85 4.13333 13.2 4.4L19.2 8.9C19.45 9.08333 19.6458 9.31667 19.7875 9.6C19.9292 9.88333 20 10.1833 20 10.5V19.5C20 20.05 19.8042 20.5208 19.4125 20.9125C19.0208 21.3042 18.55 21.5 18 21.5H14C13.7167 21.5 13.4792 21.4042 13.2875 21.2125C13.0958 21.0208 13 20.7833 13 20.5V15.5H11V20.5C11 20.7833 10.9042 21.0208 10.7125 21.2125C10.5208 21.4042 10.2833 21.5 10 21.5H6C5.45 21.5 4.97917 21.3042 4.5875 20.9125C4.19583 20.5208 4 20.05 4 19.5Z" fill="#B0B7C5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,8 +0,0 @@
<svg width="49" height="48" viewBox="0 0 49 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_14_4139" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="49" height="48">
<rect x="0.167969" width="48" height="48" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_4139)">
<path d="M24.1684 42.65C23.7017 42.65 23.235 42.5667 22.7684 42.4C22.3017 42.2333 21.885 41.9833 21.5184 41.65C19.3517 39.65 17.435 37.7 15.7684 35.8C14.1017 33.9 12.71 32.0583 11.5934 30.275C10.4767 28.4917 9.62669 26.775 9.04336 25.125C8.46003 23.475 8.16836 21.9 8.16836 20.4C8.16836 19.3333 8.25169 18.3167 8.41836 17.35C8.58503 16.3833 8.81836 15.4667 9.11836 14.6L2.91836 8.4C2.51836 8 2.31836 7.525 2.31836 6.975C2.31836 6.425 2.51836 5.95 2.91836 5.55C3.31836 5.15 3.79336 4.95 4.34336 4.95C4.89336 4.95 5.36836 5.15 5.76836 5.55L42.5684 42.35C42.9684 42.75 43.1684 43.225 43.1684 43.775C43.1684 44.325 42.9684 44.8 42.5684 45.2C42.1684 45.6 41.6934 45.8 41.1434 45.8C40.5934 45.8 40.1184 45.6 39.7184 45.2L31.5184 37C30.685 37.8667 29.8517 38.725 29.0184 39.575C28.185 40.425 27.4517 41.1167 26.8184 41.65C26.4517 41.9833 26.035 42.2333 25.5684 42.4C25.1017 42.5667 24.635 42.65 24.1684 42.65ZM24.1684 4C28.4017 4 32.1267 5.48333 35.3434 8.45C38.56 11.4167 40.1684 15.4 40.1684 20.4C40.1684 21.5667 40.0017 22.775 39.6684 24.025C39.335 25.275 38.835 26.5667 38.1684 27.9C37.8017 28.6333 37.21 29.0417 36.3934 29.125C35.5767 29.2083 34.885 28.9667 34.3184 28.4L27.7184 21.8C27.885 21.5333 28.0017 21.25 28.0684 20.95C28.135 20.65 28.1684 20.3333 28.1684 20C28.1684 19.4333 28.0684 18.9083 27.8684 18.425C27.6684 17.9417 27.385 17.5167 27.0184 17.15C26.6517 16.7833 26.2267 16.5 25.7434 16.3C25.26 16.1 24.735 16 24.1684 16C23.835 16 23.5184 16.0333 23.2184 16.1C22.9184 16.1667 22.635 16.2833 22.3684 16.45L15.9184 10C15.3184 9.4 15.06 8.69167 15.1434 7.875C15.2267 7.05833 15.6184 6.43333 16.3184 6C17.485 5.33333 18.735 4.83333 20.0684 4.5C21.4017 4.16667 22.7684 4 24.1684 4Z" fill="#949CAC"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,8 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_71_2253" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="21">
<rect y="0.439453" width="20" height="20" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_71_2253)">
<path d="M3.33268 17.1061C2.87435 17.1061 2.48199 16.9429 2.1556 16.6165C1.82921 16.2901 1.66602 15.8978 1.66602 15.4395V5.43945C1.66602 4.98112 1.82921 4.58876 2.1556 4.26237C2.48199 3.93598 2.87435 3.77279 3.33268 3.77279H16.666C17.1243 3.77279 17.5167 3.93598 17.8431 4.26237C18.1695 4.58876 18.3327 4.98112 18.3327 5.43945V15.4395C18.3327 15.8978 18.1695 16.2901 17.8431 16.6165C17.5167 16.9429 17.1243 17.1061 16.666 17.1061H3.33268ZM16.666 7.10612L10.4368 11.002C10.3674 11.0436 10.2945 11.0749 10.2181 11.0957C10.1417 11.1165 10.0688 11.127 9.99935 11.127C9.9299 11.127 9.85699 11.1165 9.7806 11.0957C9.70421 11.0749 9.63129 11.0436 9.56185 11.002L3.33268 7.10612V15.4395H16.666V7.10612ZM9.99935 9.60612L16.666 5.43945H3.33268L9.99935 9.60612ZM3.33268 7.31445V6.08529V6.10612V6.0957V7.31445Z" fill="#565F70"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,9 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="27.6632" width="28" height="28" rx="14" transform="rotate(-81.0919 0 27.6632)" fill="#1249ED"/>
<mask id="mask0_14_6042" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="4" width="24" height="24">
<rect x="4.57031" y="24.3309" width="20" height="20" transform="rotate(-81.0919 4.57031 24.3309)" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_14_6042)">
<path d="M17.3516 17.857L14.3549 22.849C14.2583 23.0167 14.1315 23.1303 13.9746 23.1901C13.8176 23.2498 13.6636 23.2679 13.5127 23.2442C13.3618 23.2206 13.2212 23.1528 13.0912 23.0411C12.9611 22.9293 12.8757 22.7789 12.835 22.5897L10.1231 9.21681C10.0803 9.0414 10.0914 8.88145 10.1562 8.73697C10.221 8.59249 10.3169 8.47396 10.4439 8.3814C10.5708 8.28883 10.713 8.2338 10.8704 8.21629C11.0278 8.19879 11.1834 8.23724 11.3374 8.33166L23.2392 15.0052C23.4068 15.1017 23.5239 15.229 23.5905 15.3871C23.6571 15.5451 23.6786 15.6996 23.6549 15.8506C23.6313 16.0015 23.567 16.1426 23.4621 16.2737C23.3572 16.4049 23.2102 16.4908 23.021 16.5315L17.3516 17.857Z" fill="#FCFCFC"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

View File

@ -1,8 +0,0 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_2598" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_2598)">
<path d="M14.275 17.75C14.4583 17.5833 14.5583 17.375 14.575 17.125C14.5917 16.875 14.5083 16.6583 14.325 16.475L11.55 13.55L11.575 13.475H11.825C12.725 13.475 13.4708 13.2 14.0625 12.65C14.6542 12.1 15.0167 11.4583 15.15 10.725H15.575C15.7417 10.725 15.8792 10.6667 15.9875 10.55C16.0958 10.4333 16.15 10.2917 16.15 10.125C16.15 9.95833 16.0958 9.82083 15.9875 9.7125C15.8792 9.60417 15.7417 9.55 15.575 9.55H15.125C15.075 9.3 14.9875 9.0625 14.8625 8.8375C14.7375 8.6125 14.5833 8.39167 14.4 8.175H15.575C15.7417 8.175 15.8792 8.11667 15.9875 8C16.0958 7.88333 16.15 7.74167 16.15 7.575C16.15 7.40833 16.0958 7.27083 15.9875 7.1625C15.8792 7.05417 15.7417 7 15.575 7H8.575C8.375 7 8.20417 7.07083 8.0625 7.2125C7.92083 7.35417 7.85 7.525 7.85 7.725C7.85 7.925 7.92083 8.09167 8.0625 8.225C8.20417 8.35833 8.375 8.425 8.575 8.425H11.75C12.1833 8.425 12.5375 8.53333 12.8125 8.75C13.0875 8.96667 13.275 9.23333 13.375 9.55H8.425C8.25833 9.55 8.12083 9.60833 8.0125 9.725C7.90417 9.84167 7.85 9.98333 7.85 10.15C7.85 10.3167 7.90417 10.4542 8.0125 10.5625C8.12083 10.6708 8.25833 10.725 8.425 10.725H13.4C13.3 11.0583 13.1083 11.3458 12.825 11.5875C12.5417 11.8292 12.1583 11.95 11.675 11.95H9.975C9.75833 11.95 9.57083 12 9.4125 12.1C9.25417 12.2 9.13333 12.3417 9.05 12.525C8.96667 12.7083 8.94167 12.8958 8.975 13.0875C9.00833 13.2792 9.1 13.4583 9.25 13.625L13 17.725C13.1667 17.9083 13.375 18 13.625 18C13.875 18 14.0917 17.9167 14.275 17.75ZM12 22.5C10.6167 22.5 9.31667 22.2375 8.1 21.7125C6.88333 21.1875 5.825 20.475 4.925 19.575C4.025 18.675 3.3125 17.6167 2.7875 16.4C2.2625 15.1833 2 13.8833 2 12.5C2 11.1167 2.2625 9.81667 2.7875 8.6C3.3125 7.38333 4.025 6.325 4.925 5.425C5.825 4.525 6.88333 3.8125 8.1 3.2875C9.31667 2.7625 10.6167 2.5 12 2.5C13.3833 2.5 14.6833 2.7625 15.9 3.2875C17.1167 3.8125 18.175 4.525 19.075 5.425C19.975 6.325 20.6875 7.38333 21.2125 8.6C21.7375 9.81667 22 11.1167 22 12.5C22 13.8833 21.7375 15.1833 21.2125 16.4C20.6875 17.6167 19.975 18.675 19.075 19.575C18.175 20.475 17.1167 21.1875 15.9 21.7125C14.6833 22.2375 13.3833 22.5 12 22.5Z" fill="#00BE88"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_59_2303" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_59_2303)">
<path d="M14.275 17.25C14.4583 17.0833 14.5583 16.875 14.575 16.625C14.5917 16.375 14.5083 16.1583 14.325 15.975L11.55 13.05L11.575 12.975H11.825C12.725 12.975 13.4708 12.7 14.0625 12.15C14.6542 11.6 15.0167 10.9583 15.15 10.225H15.575C15.7417 10.225 15.8792 10.1667 15.9875 10.05C16.0958 9.93333 16.15 9.79167 16.15 9.625C16.15 9.45833 16.0958 9.32083 15.9875 9.2125C15.8792 9.10417 15.7417 9.05 15.575 9.05H15.125C15.075 8.8 14.9875 8.5625 14.8625 8.3375C14.7375 8.1125 14.5833 7.89167 14.4 7.675H15.575C15.7417 7.675 15.8792 7.61667 15.9875 7.5C16.0958 7.38333 16.15 7.24167 16.15 7.075C16.15 6.90833 16.0958 6.77083 15.9875 6.6625C15.8792 6.55417 15.7417 6.5 15.575 6.5H8.575C8.375 6.5 8.20417 6.57083 8.0625 6.7125C7.92083 6.85417 7.85 7.025 7.85 7.225C7.85 7.425 7.92083 7.59167 8.0625 7.725C8.20417 7.85833 8.375 7.925 8.575 7.925H11.75C12.1833 7.925 12.5375 8.03333 12.8125 8.25C13.0875 8.46667 13.275 8.73333 13.375 9.05H8.425C8.25833 9.05 8.12083 9.10833 8.0125 9.225C7.90417 9.34167 7.85 9.48333 7.85 9.65C7.85 9.81667 7.90417 9.95417 8.0125 10.0625C8.12083 10.1708 8.25833 10.225 8.425 10.225H13.4C13.3 10.5583 13.1083 10.8458 12.825 11.0875C12.5417 11.3292 12.1583 11.45 11.675 11.45H9.975C9.75833 11.45 9.57083 11.5 9.4125 11.6C9.25417 11.7 9.13333 11.8417 9.05 12.025C8.96667 12.2083 8.94167 12.3958 8.975 12.5875C9.00833 12.7792 9.1 12.9583 9.25 13.125L13 17.225C13.1667 17.4083 13.375 17.5 13.625 17.5C13.875 17.5 14.0917 17.4167 14.275 17.25ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22ZM12 20C14.2333 20 16.125 19.225 17.675 17.675C19.225 16.125 20 14.2333 20 12C20 9.76667 19.225 7.875 17.675 6.325C16.125 4.775 14.2333 4 12 4C9.76667 4 7.875 4.775 6.325 6.325C4.775 7.875 4 9.76667 4 12C4 14.2333 4.775 16.125 6.325 17.675C7.875 19.225 9.76667 20 12 20Z" fill="#B0B7C5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,8 +0,0 @@
<svg width="80" height="81" viewBox="0 0 80 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_709_4861" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="80" height="81">
<rect y="0.123047" width="80" height="80" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_709_4861)">
<path d="M39.9993 56.7897C40.9438 56.7897 41.7355 56.4703 42.3744 55.8314C43.0132 55.1925 43.3327 54.4008 43.3327 53.4564C43.3327 52.512 43.0132 51.7203 42.3744 51.0814C41.7355 50.4425 40.9438 50.1231 39.9993 50.1231C39.0549 50.1231 38.2632 50.4425 37.6243 51.0814C36.9855 51.7203 36.666 52.512 36.666 53.4564C36.666 54.4008 36.9855 55.1925 37.6243 55.8314C38.2632 56.4703 39.0549 56.7897 39.9993 56.7897ZM39.9993 43.4564C40.9438 43.4564 41.7355 43.137 42.3744 42.4981C43.0132 41.8592 43.3327 41.0675 43.3327 40.1231V26.7897C43.3327 25.8453 43.0132 25.0536 42.3744 24.4147C41.7355 23.7758 40.9438 23.4564 39.9993 23.4564C39.0549 23.4564 38.2632 23.7758 37.6243 24.4147C36.9855 25.0536 36.666 25.8453 36.666 26.7897V40.1231C36.666 41.0675 36.9855 41.8592 37.6243 42.4981C38.2632 43.137 39.0549 43.4564 39.9993 43.4564ZM39.9993 73.4564C35.3882 73.4564 31.0549 72.5814 26.9993 70.8314C22.9438 69.0814 19.416 66.7064 16.416 63.7064C13.416 60.7064 11.041 57.1786 9.29102 53.1231C7.54102 49.0675 6.66602 44.7342 6.66602 40.1231C6.66602 35.512 7.54102 31.1786 9.29102 27.1231C11.041 23.0675 13.416 19.5397 16.416 16.5397C19.416 13.5397 22.9438 11.1647 26.9993 9.41473C31.0549 7.66473 35.3882 6.78973 39.9993 6.78973C44.6105 6.78973 48.9438 7.66473 52.9994 9.41473C57.0549 11.1647 60.5827 13.5397 63.5827 16.5397C66.5827 19.5397 68.9577 23.0675 70.7077 27.1231C72.4577 31.1786 73.3327 35.512 73.3327 40.1231C73.3327 44.7342 72.4577 49.0675 70.7077 53.1231C68.9577 57.1786 66.5827 60.7064 63.5827 63.7064C60.5827 66.7064 57.0549 69.0814 52.9994 70.8314C48.9438 72.5814 44.6105 73.4564 39.9993 73.4564Z" fill="#FF7B00"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

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