diff --git a/.gitignore b/.gitignore
index de99955..9b4b623 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,5 @@ yarn-error.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+.claude
\ No newline at end of file
diff --git a/App.tsx b/App.tsx
index 5e963b1..c11a788 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,45 +1,44 @@
/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * @format
+ * Contexta Mobile App
*/
-import { NewAppScreen } from '@react-native/new-app-screen';
-import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
-import {
- SafeAreaProvider,
- useSafeAreaInsets,
-} from 'react-native-safe-area-context';
+import React from 'react';
+import { StatusBar, useColorScheme } from 'react-native';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+import { NavigationContainer } from '@react-navigation/native';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ToastProvider } from './src/contexts/ToastContext';
+import { RootNavigator } from './src/navigation/RootNavigator';
-function App() {
- const isDarkMode = useColorScheme() === 'dark';
-
- return (
-
-
-
-
- );
-}
-
-function AppContent() {
- const safeAreaInsets = useSafeAreaInsets();
-
- return (
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ staleTime: 30 * 1000,
+ gcTime: 5 * 60 * 1000,
+ },
},
});
+function App() {
+ const isDark = useColorScheme() === 'dark';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
export default App;
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5e073da..8630538 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,6 +107,8 @@ android {
}
}
+apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
+
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index a2f5908..26faca5 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 1b52399..26faca5 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index ff10afd..c7b7d9f 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 115a4c7..c7b7d9f 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index dcd3cd8..95fb71f 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 459ca60..95fb71f 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 8ca12fe..4419553 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 8e19b41..4419553 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index b824ebd..79f6fca 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 4c19a13..79f6fca 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 7247b72..c29ee5d 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- contexta_mb
+ Contexta
diff --git a/ios/contexta_mb/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/contexta_mb/Images.xcassets/AppIcon.appiconset/Contents.json
index 8121323..8669687 100644
--- a/ios/contexta_mb/Images.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/contexta_mb/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -1,53 +1,62 @@
{
- "images" : [
+ "images": [
{
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "20x20"
+ "filename": "icon_20@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
},
{
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "20x20"
+ "filename": "icon_20@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
},
{
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "29x29"
+ "filename": "icon_29@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
},
{
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "29x29"
+ "filename": "icon_29@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
},
{
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "40x40"
+ "filename": "icon_40@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
},
{
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "40x40"
+ "filename": "icon_40@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
},
{
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "60x60"
+ "filename": "icon_60@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
},
{
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "60x60"
+ "filename": "icon_60@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
},
{
- "idiom" : "ios-marketing",
- "scale" : "1x",
- "size" : "1024x1024"
+ "filename": "icon_1024@1x.png",
+ "idiom": "ios-marketing",
+ "scale": "1x",
+ "size": "1024x1024"
}
],
- "info" : {
- "author" : "xcode",
- "version" : 1
+ "info": {
+ "author": "xcode",
+ "version": 1
}
-}
+}
\ No newline at end of file
diff --git a/ios/contexta_mb/Info.plist b/ios/contexta_mb/Info.plist
index 8f11668..6e76e9a 100644
--- a/ios/contexta_mb/Info.plist
+++ b/ios/contexta_mb/Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
en
CFBundleDisplayName
- contexta_mb
+ Contexta
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/package-lock.json b/package-lock.json
index f792e94..015d2db 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,10 +8,21 @@
"name": "contexta_mb",
"version": "0.0.1",
"dependencies": {
+ "@react-native-async-storage/async-storage": "^1.24.0",
"@react-native/new-app-screen": "0.84.1",
+ "@react-navigation/bottom-tabs": "^7.15.7",
+ "@react-navigation/native": "^7.2.0",
+ "@react-navigation/native-stack": "^7.14.7",
+ "@tanstack/react-query": "^5.95.2",
+ "@types/react-native-vector-icons": "^6.4.18",
+ "axios": "^1.13.6",
"react": "19.2.3",
"react-native": "0.84.1",
- "react-native-safe-area-context": "^5.5.2"
+ "react-native-gesture-handler": "^2.30.0",
+ "react-native-safe-area-context": "^5.5.2",
+ "react-native-screens": "^4.24.0",
+ "react-native-vector-icons": "^10.3.0",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -52,9 +63,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.29.3",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
- "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -157,9 +168,9 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.29.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz",
- "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz",
+ "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -168,7 +179,7 @@
"@babel/helper-optimise-call-expression": "^7.27.1",
"@babel/helper-replace-supers": "^7.28.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
- "@babel/traverse": "^7.29.0",
+ "@babel/traverse": "^7.28.6",
"semver": "^6.3.1"
},
"engines": {
@@ -394,9 +405,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
- "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
@@ -457,23 +468,6 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
- "version": "7.29.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz",
- "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
@@ -1279,9 +1273,9 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.29.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz",
- "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
+ "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1837,20 +1831,19 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.29.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz",
- "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz",
+ "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.29.3",
+ "@babel/compat-data": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
- "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
@@ -1882,7 +1875,7 @@
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
- "@babel/plugin-transform-modules-systemjs": "^7.29.4",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.0",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
"@babel/plugin-transform-new-target": "^7.27.1",
@@ -1998,6 +1991,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@egjs/hammerjs": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hammerjs": "^2.0.36"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -2072,9 +2077,9 @@
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2156,9 +2161,9 @@
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2310,9 +2315,9 @@
}
},
"node_modules/@istanbuljs/schema": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
- "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2720,6 +2725,18 @@
"node": ">= 8"
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
+ "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
+ "license": "MIT",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.60 <1.0"
+ }
+ },
"node_modules/@react-native-community/cli": {
"version": "20.1.0",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz",
@@ -3330,6 +3347,118 @@
}
}
},
+ "node_modules/@react-navigation/bottom-tabs": {
+ "version": "7.15.7",
+ "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.7.tgz",
+ "integrity": "sha512-OXNvU0esvyZf//KpaIsFh2GD1otxqG+Lv48VfkNIh+3DEbOnqni8pOrKdICgoC4T0R8oVln7pnVeHl89Gipv8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.9.12",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.2.0",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/core": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.0.tgz",
+ "integrity": "sha512-E4Kr1PRrhKiVn1RdMdPIG1rCfrKh+HiVJ2smdLsh9D95Q2z0a9dGE9yHpRQ2pAUiiwOfgloLqegkPb8g+TcCBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/routers": "^7.5.3",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "query-string": "^7.1.3",
+ "react-is": "^19.1.0",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0"
+ }
+ },
+ "node_modules/@react-navigation/core/node_modules/react-is": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
+ "license": "MIT"
+ },
+ "node_modules/@react-navigation/elements": {
+ "version": "2.9.12",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.12.tgz",
+ "integrity": "sha512-LSaQUj5SV9OXVRcxT8mqETDoM7BOKCveCvuLjdAr9NZnPDM5HW8uDnvW/sCa8oEFy+22+ojoXtHFKsfnesgBbw==",
+ "license": "MIT",
+ "dependencies": {
+ "color": "^4.2.3",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@react-native-masked-view/masked-view": ">= 0.2.0",
+ "@react-navigation/native": "^7.2.0",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-masked-view/masked-view": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-navigation/native": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.0.tgz",
+ "integrity": "sha512-kEuqIS1MkzzLD45Fp17CrxAchoB4W6tMfc541merUgtAeNNsg06gRrvmuLv6LAYvpLifGdXuSjpluPIu/VmbQw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@react-navigation/core": "^7.17.0",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "use-latest-callback": "^0.2.4"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0",
+ "react-native": "*"
+ }
+ },
+ "node_modules/@react-navigation/native-stack": {
+ "version": "7.14.7",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.7.tgz",
+ "integrity": "sha512-MxKdKS817YK7iirlyW+XZnXJ339eRE7aA3E55zHVDS+R+bqro+PwRwNGqL1Y9e3w0KjAKZVsOfn5erJRWrO4iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.9.12",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0",
+ "warn-once": "^0.1.1"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.2.0",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/routers": {
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz",
+ "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -3378,6 +3507,32 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.95.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz",
+ "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.95.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz",
+ "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.95.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3428,6 +3583,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hammerjs": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
+ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
+ "license": "MIT"
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -3464,25 +3625,43 @@
}
},
"node_modules/@types/node": {
- "version": "25.6.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
- "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
+ "version": "25.5.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~7.19.0"
+ "undici-types": "~7.18.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
+ "node_modules/@types/react-native": {
+ "version": "0.70.19",
+ "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz",
+ "integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-native-vector-icons": {
+ "version": "6.4.18",
+ "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz",
+ "integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*",
+ "@types/react-native": "^0.70"
+ }
+ },
"node_modules/@types/react-test-renderer": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
@@ -3515,21 +3694,21 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
- "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
+ "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/type-utils": "8.59.2",
- "@typescript-eslint/utils": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/type-utils": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.5.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3539,23 +3718,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.59.2",
+ "@typescript-eslint/parser": "^8.57.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
- "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz",
+ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
"debug": "^4.4.3"
},
"engines": {
@@ -3567,18 +3746,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
- "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz",
+ "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.59.2",
- "@typescript-eslint/types": "^8.59.2",
+ "@typescript-eslint/tsconfig-utils": "^8.57.2",
+ "@typescript-eslint/types": "^8.57.2",
"debug": "^4.4.3"
},
"engines": {
@@ -3589,18 +3768,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
- "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz",
+ "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2"
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3611,9 +3790,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
- "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz",
+ "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3624,21 +3803,21 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
- "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz",
+ "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2",
- "@typescript-eslint/utils": "8.59.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
"debug": "^4.4.3",
- "ts-api-utils": "^2.5.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3649,13 +3828,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
- "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
+ "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3667,21 +3846,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
- "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz",
+ "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.59.2",
- "@typescript-eslint/tsconfig-utils": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/visitor-keys": "8.59.2",
+ "@typescript-eslint/project-service": "8.57.2",
+ "@typescript-eslint/tsconfig-utils": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.5.0"
+ "ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3691,7 +3870,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
@@ -3708,16 +3887,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
- "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz",
+ "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.59.2",
- "@typescript-eslint/types": "8.59.2",
- "@typescript-eslint/typescript-estree": "8.59.2"
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3728,17 +3907,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.59.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
- "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz",
+ "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.2",
+ "@typescript-eslint/types": "8.57.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -3763,9 +3942,9 @@
}
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
- "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true,
"license": "ISC"
},
@@ -3845,9 +4024,9 @@
}
},
"node_modules/ajv": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
- "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4153,6 +4332,12 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -4169,6 +4354,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -4371,9 +4567,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.27",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
- "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
+ "version": "2.10.10",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz",
+ "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -4395,9 +4591,9 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.5",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
- "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -4409,7 +4605,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
- "qs": "~6.15.1",
+ "qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@@ -4437,9 +4633,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4462,9 +4658,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
@@ -4482,11 +4678,11 @@
"license": "MIT",
"peer": true,
"dependencies": {
- "baseline-browser-mapping": "^2.10.12",
- "caniuse-lite": "^1.0.30001782",
- "electron-to-chromium": "^1.5.328",
- "node-releases": "^2.0.36",
- "update-browserslist-db": "^1.2.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -4546,15 +4742,15 @@
}
},
"node_modules/call-bind": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
- "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "get-intrinsic": "^1.3.0",
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
@@ -4568,7 +4764,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4615,9 +4810,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001792",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
- "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
+ "version": "1.0.30001781",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz",
+ "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==",
"funding": [
{
"type": "opencollective",
@@ -4806,6 +5001,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4824,6 +5032,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@@ -4831,6 +5049,18 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@@ -5030,7 +5260,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/data-view-buffer": {
@@ -5121,6 +5350,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/dedent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
@@ -5202,6 +5440,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -5258,7 +5505,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -5276,9 +5522,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.352",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz",
- "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==",
+ "version": "1.5.325",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz",
+ "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==",
"license": "ISC"
},
"node_modules/emittery": {
@@ -5370,9 +5616,9 @@
}
},
"node_modules/es-abstract": {
- "version": "1.24.2",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
- "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "version": "1.24.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+ "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5442,7 +5688,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5452,23 +5697,22 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-iterator-helpers": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz",
- "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz",
+ "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.9",
+ "call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
- "es-abstract": "^1.24.2",
+ "es-abstract": "^1.24.1",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.1.0",
"function-bind": "^1.1.2",
@@ -5480,7 +5724,8 @@
"has-symbols": "^1.1.0",
"internal-slot": "^1.1.0",
"iterator.prototype": "^1.1.5",
- "math-intrinsics": "^1.1.0"
+ "math-intrinsics": "^1.1.0",
+ "safe-array-concat": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
@@ -5490,7 +5735,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -5503,7 +5747,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -5703,9 +5946,9 @@
}
},
"node_modules/eslint-plugin-jest": {
- "version": "29.15.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.15.2.tgz",
- "integrity": "sha512-kEN4r9RZl1xcsb4arGq89LrcVdOUFII/JSCwtTPJyv16mDwmPrcuEQwpxqZHeINvcsd7oK5O/rhdGlxFRaZwvQ==",
+ "version": "29.15.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.15.1.tgz",
+ "integrity": "sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5766,9 +6009,9 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
- "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5782,7 +6025,7 @@
"node": ">=18"
},
"peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": {
@@ -5830,9 +6073,9 @@
"license": "MIT"
},
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5932,9 +6175,9 @@
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6163,7 +6406,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -6210,9 +6452,9 @@
"license": "MIT"
},
"node_modules/fast-xml-parser": {
- "version": "4.5.6",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz",
- "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==",
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.5.tgz",
+ "integrity": "sha512-cK9c5I/DwIOI7/Q7AlGN3DuTdwN61gwSfL8rvuVPK+0mcCNHHGxRrpiFtaZZRfRMJL3Gl8B2AFlBG6qXf03w9A==",
"devOptional": true,
"funding": [
{
@@ -6284,6 +6526,15 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -6374,6 +6625,26 @@
"integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==",
"license": "MIT"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -6390,6 +6661,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -6438,7 +6725,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6507,7 +6793,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -6541,7 +6826,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -6623,9 +6907,9 @@
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -6681,7 +6965,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6758,7 +7041,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6771,7 +7053,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -6784,10 +7065,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
- "devOptional": true,
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -6817,6 +7097,21 @@
"hermes-estree": "0.32.0"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -7114,13 +7409,13 @@
}
},
"node_modules/is-core-module": {
- "version": "2.16.2",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
- "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "hasown": "^2.0.3"
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7330,6 +7625,15 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -8431,9 +8735,9 @@
}
},
"node_modules/lodash": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
- "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
@@ -8685,7 +8989,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8707,6 +9010,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
+ "node_modules/merge-options": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -8724,9 +9039,9 @@
}
},
"node_modules/metro": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.7.tgz",
- "integrity": "sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.5.tgz",
+ "integrity": "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
@@ -8737,30 +9052,31 @@
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"accepts": "^2.0.0",
+ "chalk": "^4.0.0",
"ci-info": "^2.0.0",
"connect": "^3.6.5",
"debug": "^4.4.0",
"error-stack-parser": "^2.0.6",
"flow-enums-runtime": "^0.0.6",
"graceful-fs": "^4.2.4",
- "hermes-parser": "0.35.0",
+ "hermes-parser": "0.33.3",
"image-size": "^1.0.2",
"invariant": "^2.2.4",
"jest-worker": "^29.7.0",
"jsc-safe-url": "^0.2.2",
"lodash.throttle": "^4.1.1",
- "metro-babel-transformer": "0.83.7",
- "metro-cache": "0.83.7",
- "metro-cache-key": "0.83.7",
- "metro-config": "0.83.7",
- "metro-core": "0.83.7",
- "metro-file-map": "0.83.7",
- "metro-resolver": "0.83.7",
- "metro-runtime": "0.83.7",
- "metro-source-map": "0.83.7",
- "metro-symbolicate": "0.83.7",
- "metro-transform-plugins": "0.83.7",
- "metro-transform-worker": "0.83.7",
+ "metro-babel-transformer": "0.83.5",
+ "metro-cache": "0.83.5",
+ "metro-cache-key": "0.83.5",
+ "metro-config": "0.83.5",
+ "metro-core": "0.83.5",
+ "metro-file-map": "0.83.5",
+ "metro-resolver": "0.83.5",
+ "metro-runtime": "0.83.5",
+ "metro-source-map": "0.83.5",
+ "metro-symbolicate": "0.83.5",
+ "metro-transform-plugins": "0.83.5",
+ "metro-transform-worker": "0.83.5",
"mime-types": "^3.0.1",
"nullthrows": "^1.1.1",
"serialize-error": "^2.1.0",
@@ -8777,15 +9093,14 @@
}
},
"node_modules/metro-babel-transformer": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.7.tgz",
- "integrity": "sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.5.tgz",
+ "integrity": "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
"flow-enums-runtime": "^0.0.6",
- "hermes-parser": "0.35.0",
- "metro-cache-key": "0.83.7",
+ "hermes-parser": "0.33.3",
"nullthrows": "^1.1.1"
},
"engines": {
@@ -8793,39 +9108,39 @@
}
},
"node_modules/metro-babel-transformer/node_modules/hermes-estree": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
- "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "version": "0.33.3",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz",
+ "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==",
"license": "MIT"
},
"node_modules/metro-babel-transformer/node_modules/hermes-parser": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
- "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "version": "0.33.3",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz",
+ "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==",
"license": "MIT",
"dependencies": {
- "hermes-estree": "0.35.0"
+ "hermes-estree": "0.33.3"
}
},
"node_modules/metro-cache": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.7.tgz",
- "integrity": "sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.5.tgz",
+ "integrity": "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==",
"license": "MIT",
"dependencies": {
"exponential-backoff": "^3.1.1",
"flow-enums-runtime": "^0.0.6",
"https-proxy-agent": "^7.0.5",
- "metro-core": "0.83.7"
+ "metro-core": "0.83.5"
},
"engines": {
"node": ">=20.19.4"
}
},
"node_modules/metro-cache-key": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.7.tgz",
- "integrity": "sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.5.tgz",
+ "integrity": "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -8835,18 +9150,18 @@
}
},
"node_modules/metro-config": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.7.tgz",
- "integrity": "sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.5.tgz",
+ "integrity": "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==",
"license": "MIT",
"dependencies": {
"connect": "^3.6.5",
"flow-enums-runtime": "^0.0.6",
"jest-validate": "^29.7.0",
- "metro": "0.83.7",
- "metro-cache": "0.83.7",
- "metro-core": "0.83.7",
- "metro-runtime": "0.83.7",
+ "metro": "0.83.5",
+ "metro-cache": "0.83.5",
+ "metro-core": "0.83.5",
+ "metro-runtime": "0.83.5",
"yaml": "^2.6.1"
},
"engines": {
@@ -8854,23 +9169,23 @@
}
},
"node_modules/metro-core": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.7.tgz",
- "integrity": "sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.5.tgz",
+ "integrity": "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
"lodash.throttle": "^4.1.1",
- "metro-resolver": "0.83.7"
+ "metro-resolver": "0.83.5"
},
"engines": {
"node": ">=20.19.4"
}
},
"node_modules/metro-file-map": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.7.tgz",
- "integrity": "sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.5.tgz",
+ "integrity": "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -8888,9 +9203,9 @@
}
},
"node_modules/metro-minify-terser": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.7.tgz",
- "integrity": "sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.5.tgz",
+ "integrity": "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
@@ -8901,9 +9216,9 @@
}
},
"node_modules/metro-resolver": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.7.tgz",
- "integrity": "sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.5.tgz",
+ "integrity": "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -8913,9 +9228,9 @@
}
},
"node_modules/metro-runtime": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.7.tgz",
- "integrity": "sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.5.tgz",
+ "integrity": "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
@@ -8926,18 +9241,18 @@
}
},
"node_modules/metro-source-map": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.7.tgz",
- "integrity": "sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.5.tgz",
+ "integrity": "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==",
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4",
- "metro-symbolicate": "0.83.7",
+ "metro-symbolicate": "0.83.5",
"nullthrows": "^1.1.1",
- "ob1": "0.83.7",
+ "ob1": "0.83.5",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
},
@@ -8955,14 +9270,14 @@
}
},
"node_modules/metro-symbolicate": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.7.tgz",
- "integrity": "sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.5.tgz",
+ "integrity": "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4",
- "metro-source-map": "0.83.7",
+ "metro-source-map": "0.83.5",
"nullthrows": "^1.1.1",
"source-map": "^0.5.6",
"vlq": "^1.0.0"
@@ -8984,9 +9299,9 @@
}
},
"node_modules/metro-transform-plugins": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.7.tgz",
- "integrity": "sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.5.tgz",
+ "integrity": "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -9001,9 +9316,9 @@
}
},
"node_modules/metro-transform-worker": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.7.tgz",
- "integrity": "sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.5.tgz",
+ "integrity": "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
@@ -9011,13 +9326,13 @@
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"flow-enums-runtime": "^0.0.6",
- "metro": "0.83.7",
- "metro-babel-transformer": "0.83.7",
- "metro-cache": "0.83.7",
- "metro-cache-key": "0.83.7",
- "metro-minify-terser": "0.83.7",
- "metro-source-map": "0.83.7",
- "metro-transform-plugins": "0.83.7",
+ "metro": "0.83.5",
+ "metro-babel-transformer": "0.83.5",
+ "metro-cache": "0.83.5",
+ "metro-cache-key": "0.83.5",
+ "metro-minify-terser": "0.83.5",
+ "metro-source-map": "0.83.5",
+ "metro-transform-plugins": "0.83.5",
"nullthrows": "^1.1.1"
},
"engines": {
@@ -9044,18 +9359,18 @@
"license": "MIT"
},
"node_modules/metro/node_modules/hermes-estree": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz",
- "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==",
+ "version": "0.33.3",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz",
+ "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==",
"license": "MIT"
},
"node_modules/metro/node_modules/hermes-parser": {
- "version": "0.35.0",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz",
- "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==",
+ "version": "0.33.3",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz",
+ "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==",
"license": "MIT",
"dependencies": {
- "hermes-estree": "0.35.0"
+ "hermes-estree": "0.33.3"
}
},
"node_modules/metro/node_modules/mime-types": {
@@ -9152,7 +9467,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -9165,7 +9479,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -9182,13 +9495,13 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
- "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^5.0.5"
+ "brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -9215,6 +9528,24 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -9268,9 +9599,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.38",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
- "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
"license": "MIT"
},
"node_modules/node-stream-zip": {
@@ -9316,9 +9647,9 @@
"license": "MIT"
},
"node_modules/ob1": {
- "version": "0.83.7",
- "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.7.tgz",
- "integrity": "sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==",
+ "version": "0.83.5",
+ "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.5.tgz",
+ "integrity": "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==",
"license": "MIT",
"dependencies": {
"flow-enums-runtime": "^0.0.6"
@@ -9331,7 +9662,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9857,7 +10187,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -9869,7 +10198,12 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
@@ -9900,9 +10234,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.15.1",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
- "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -9915,6 +10249,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/query-string": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
+ "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+ "license": "MIT",
+ "dependencies": {
+ "decode-uri-component": "^0.2.2",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -10011,6 +10363,18 @@
}
}
},
+ "node_modules/react-freeze": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
+ "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -10076,16 +10440,102 @@
}
}
},
+ "node_modules/react-native-gesture-handler": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz",
+ "integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@egjs/hammerjs": "^2.0.17",
+ "hoist-non-react-statics": "^3.3.0",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-safe-area-context": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
"integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
+ "node_modules/react-native-screens": {
+ "version": "4.24.0",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz",
+ "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "react-freeze": "^1.0.0",
+ "warn-once": "^0.1.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-vector-icons": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
+ "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==",
+ "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.7.2",
+ "yargs": "^16.1.1"
+ },
+ "bin": {
+ "fa-upgrade.sh": "bin/fa-upgrade.sh",
+ "fa5-upgrade": "bin/fa5-upgrade.sh",
+ "fa6-upgrade": "bin/fa6-upgrade.sh",
+ "generate-icon": "bin/generate-icon.js"
+ }
+ },
+ "node_modules/react-native-vector-icons/node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/react-native-vector-icons/node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/react-native-vector-icons/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -10152,9 +10602,9 @@
}
},
"node_modules/react-test-renderer/node_modules/react-is": {
- "version": "19.2.6",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz",
- "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
"dev": true,
"license": "MIT"
},
@@ -10269,9 +10719,9 @@
"license": "MIT"
},
"node_modules/regjsparser": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz",
- "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
+ "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -10298,13 +10748,12 @@
"license": "ISC"
},
"node_modules/resolve": {
- "version": "1.22.12",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
- "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
@@ -10428,15 +10877,15 @@
}
},
"node_modules/safe-array-concat": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
- "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.9",
- "call-bound": "^1.0.4",
- "get-intrinsic": "^1.3.0",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
"has-symbols": "^1.1.0",
"isarray": "^2.0.5"
},
@@ -10689,6 +11138,15 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/sf-symbols-typescript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
+ "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -10743,14 +11201,14 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.4"
+ "object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
@@ -10804,6 +11262,21 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -10885,6 +11358,15 @@
"source-map": "^0.6.0"
}
},
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -10962,6 +11444,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -11198,9 +11689,9 @@
}
},
"node_modules/terser": {
- "version": "5.47.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz",
- "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==",
+ "version": "5.46.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
+ "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -11252,9 +11743,9 @@
"license": "MIT"
},
"node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -11287,13 +11778,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.4"
+ "picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -11534,9 +12025,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.19.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
- "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -11642,6 +12133,24 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-latest-callback": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
+ "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -11698,6 +12207,12 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/warn-once": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
+ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
+ "license": "MIT"
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -11897,9 +12412,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.4",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
- "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -11952,9 +12467,9 @@
}
},
"node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -11974,6 +12489,35 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 5c5cc92..4bb8563 100644
--- a/package.json
+++ b/package.json
@@ -10,10 +10,21 @@
"test": "jest"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "^1.24.0",
+ "@react-native/new-app-screen": "0.84.1",
+ "@react-navigation/bottom-tabs": "^7.15.7",
+ "@react-navigation/native": "^7.2.0",
+ "@react-navigation/native-stack": "^7.14.7",
+ "@tanstack/react-query": "^5.95.2",
+ "@types/react-native-vector-icons": "^6.4.18",
+ "axios": "^1.13.6",
"react": "19.2.3",
"react-native": "0.84.1",
- "@react-native/new-app-screen": "0.84.1",
- "react-native-safe-area-context": "^5.5.2"
+ "react-native-gesture-handler": "^2.30.0",
+ "react-native-safe-area-context": "^5.5.2",
+ "react-native-screens": "^4.24.0",
+ "react-native-vector-icons": "^10.3.0",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -38,4 +49,4 @@
"engines": {
"node": ">= 22.11.0"
}
-}
\ No newline at end of file
+}
diff --git a/src/assets/favicon.png b/src/assets/favicon.png
new file mode 100644
index 0000000..e7a65e4
Binary files /dev/null and b/src/assets/favicon.png differ
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 0000000..ecbd7e6
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
new file mode 100644
index 0000000..422e6d6
--- /dev/null
+++ b/src/components/ChatInterface.tsx
@@ -0,0 +1,306 @@
+import React, { useState, useRef, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TextInput,
+ TouchableOpacity,
+ KeyboardAvoidingView,
+ Platform,
+ Animated,
+} from 'react-native';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../theme';
+import { useTheme } from '../theme';
+import { chatAPI } from '../services/api';
+
+interface ChatInterfaceProps {
+ chatbotId: string;
+ welcomeMessage?: string;
+ primaryColor?: string;
+}
+
+interface LocalMessage {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ pending?: boolean;
+ time?: string;
+}
+
+function getTime() {
+ return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+}
+
+export function ChatInterface({ chatbotId, welcomeMessage, primaryColor }: ChatInterfaceProps) {
+ const { theme, isDark } = useTheme();
+ const accentColor = primaryColor ?? COLORS.primary;
+ const flatRef = useRef(null);
+
+ const [messages, setMessages] = useState(() =>
+ welcomeMessage
+ ? [{ id: 'welcome', role: 'assistant', content: welcomeMessage, time: getTime() }]
+ : [],
+ );
+ const [input, setInput] = useState('');
+ const [sending, setSending] = useState(false);
+ const [sessionId, setSessionId] = useState();
+
+ const scrollToBottom = useCallback(() => {
+ setTimeout(() => flatRef.current?.scrollToEnd({ animated: true }), 80);
+ }, []);
+
+ const sendMessage = async () => {
+ const text = input.trim();
+ if (!text || sending) return;
+
+ const userMsg: LocalMessage = { id: Date.now().toString(), role: 'user', content: text, time: getTime() };
+ const loadingMsg: LocalMessage = { id: 'loading', role: 'assistant', content: '', pending: true };
+
+ setMessages(prev => [...prev, userMsg, loadingMsg]);
+ setInput('');
+ setSending(true);
+ scrollToBottom();
+
+ try {
+ const data = await chatAPI.sendMessage(chatbotId, text, sessionId);
+ if (!sessionId) setSessionId(data.session_id);
+ setMessages(prev => [
+ ...prev.filter(m => m.id !== 'loading'),
+ { id: (Date.now() + 1).toString(), role: 'assistant', content: data.response, time: getTime() },
+ ]);
+ } catch {
+ setMessages(prev => [
+ ...prev.filter(m => m.id !== 'loading'),
+ { id: (Date.now() + 1).toString(), role: 'assistant', content: "Sorry, I couldn't process your message. Please try again.", time: getTime() },
+ ]);
+ } finally {
+ setSending(false);
+ scrollToBottom();
+ }
+ };
+
+ const renderMessage = ({ item }: { item: LocalMessage }) => {
+ const isUser = item.role === 'user';
+ return (
+
+ );
+ };
+
+ return (
+
+
+ item.id}
+ renderItem={renderMessage}
+ contentContainerStyle={styles.messageList}
+ showsVerticalScrollIndicator={false}
+ onContentSizeChange={scrollToBottom}
+ />
+
+ {/* Input bar */}
+
+
+
+
+
+ ↑
+
+
+
+ );
+}
+
+function MessageBubble({
+ message,
+ isUser,
+ accentColor,
+ theme,
+ isDark,
+}: {
+ message: LocalMessage;
+ isUser: boolean;
+ accentColor: string;
+ theme: any;
+ isDark: boolean;
+}) {
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(8)).current;
+
+ React.useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 250, useNativeDriver: true }),
+ Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 20, bounciness: 2 }),
+ ]).start();
+ }, []);
+
+ return (
+
+ {!isUser && (
+
+ AI
+
+ )}
+
+
+
+ {message.pending ? (
+
+ ) : (
+
+ {message.content}
+
+ )}
+
+ {message.time && !message.pending ? (
+
+ {message.time}
+
+ ) : null}
+
+
+ );
+}
+
+function TypingIndicator() {
+ const dot1 = useRef(new Animated.Value(0.3)).current;
+ const dot2 = useRef(new Animated.Value(0.3)).current;
+ const dot3 = useRef(new Animated.Value(0.3)).current;
+
+ React.useEffect(() => {
+ const pulse = (dot: Animated.Value, delay: number) =>
+ Animated.loop(
+ Animated.sequence([
+ Animated.delay(delay),
+ Animated.timing(dot, { toValue: 1, duration: 300, useNativeDriver: true }),
+ Animated.timing(dot, { toValue: 0.3, duration: 300, useNativeDriver: true }),
+ Animated.delay(600 - delay),
+ ]),
+ );
+
+ const a1 = pulse(dot1, 0);
+ const a2 = pulse(dot2, 200);
+ const a3 = pulse(dot3, 400);
+ a1.start(); a2.start(); a3.start();
+ return () => { a1.stop(); a2.stop(); a3.stop(); };
+ }, []);
+
+ return (
+
+ {[dot1, dot2, dot3].map((dot, i) => (
+
+ ))}
+
+ );
+}
+
+const typingStyles = StyleSheet.create({
+ row: { flexDirection: 'row', gap: 5, paddingVertical: 4, paddingHorizontal: 2 },
+ dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: COLORS.primary },
+});
+
+const styles = StyleSheet.create({
+ container: { flex: 1 },
+ messageList: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.lg,
+ gap: SPACING.md,
+ paddingBottom: SPACING.xl,
+ },
+
+ msgRow: { flexDirection: 'row', gap: SPACING.sm, alignItems: 'flex-end', maxWidth: '100%' },
+ msgRowUser: { flexDirection: 'row-reverse' },
+
+ avatar: {
+ width: 32,
+ height: 32,
+ borderRadius: RADIUS.full,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ avatarText: { color: COLORS.white, fontSize: 10, fontWeight: '800' },
+
+ bubbleWrapper: { flex: 1, gap: 4 },
+ bubble: { maxWidth: '80%', borderRadius: RADIUS.xl, padding: SPACING.md },
+ bubbleUser: { alignSelf: 'flex-end', borderBottomRightRadius: RADIUS.xs },
+ bubbleBot: {
+ alignSelf: 'flex-start',
+ borderWidth: 1,
+ borderBottomLeftRadius: RADIUS.xs,
+ },
+ bubbleText: { fontSize: FONT_SIZE.md, lineHeight: 22 },
+ timestamp: { fontSize: 10, paddingHorizontal: 2 },
+
+ inputBar: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ borderTopWidth: 1,
+ gap: SPACING.sm,
+ },
+ inputWrapper: {
+ flex: 1,
+ borderRadius: RADIUS.xl,
+ borderWidth: 1.5,
+ paddingHorizontal: SPACING.md,
+ paddingVertical: Platform.OS === 'ios' ? SPACING.sm : 0,
+ maxHeight: 120,
+ },
+ textInput: {
+ fontSize: FONT_SIZE.md,
+ minHeight: 40,
+ paddingTop: Platform.OS === 'android' ? SPACING.sm : 0,
+ },
+ sendBtn: {
+ width: 46,
+ height: 46,
+ borderRadius: RADIUS.full,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ sendBtnDisabled: { opacity: 0.45 },
+ sendIcon: { color: COLORS.white, fontSize: 20, fontWeight: '700', marginTop: -2 },
+});
diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx
new file mode 100644
index 0000000..ec293b6
--- /dev/null
+++ b/src/components/ui/Badge.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { COLORS, RADIUS, FONT_SIZE } from '../../theme';
+
+type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'purple' | 'plan';
+
+interface BadgeProps {
+ label: string;
+ variant?: BadgeVariant;
+}
+
+const BG: Record = {
+ default: '#f3f4f6',
+ success: '#d1fae5',
+ warning: '#fef3c7',
+ error: '#fee2e2',
+ info: '#dbeafe',
+ purple: '#ede9fe',
+ plan: COLORS.primaryUltraLight,
+};
+
+const FG: Record = {
+ default: '#374151',
+ success: '#065f46',
+ warning: '#92400e',
+ error: '#991b1b',
+ info: '#1e40af',
+ purple: '#5b21b6',
+ plan: COLORS.primaryDark,
+};
+
+export function Badge({ label, variant = 'default' }: BadgeProps) {
+ return (
+
+ {label}
+
+ );
+}
+
+export function PlanBadge({ plan }: { plan: string }) {
+ const variant =
+ plan === 'free' ? 'default'
+ : plan === 'starter' ? 'info'
+ : plan === 'business' ? 'success'
+ : plan === 'agency' ? 'purple'
+ : 'plan';
+
+ return ;
+}
+
+export function StatusBadge({ status }: { status: string }) {
+ const variant =
+ status === 'completed' || status === 'active' || status === 'published' ? 'success'
+ : status === 'processing' ? 'info'
+ : status === 'pending' ? 'warning'
+ : status === 'failed' ? 'error'
+ : 'default';
+
+ return ;
+}
+
+const styles = StyleSheet.create({
+ badge: {
+ borderRadius: RADIUS.full,
+ paddingVertical: 2,
+ paddingHorizontal: 8,
+ alignSelf: 'flex-start',
+ },
+ label: {
+ fontSize: FONT_SIZE.xs,
+ fontWeight: '600',
+ textTransform: 'capitalize',
+ },
+});
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 0000000..3e61c43
--- /dev/null
+++ b/src/components/ui/Button.tsx
@@ -0,0 +1,132 @@
+import React, { useRef } from 'react';
+import {
+ Animated,
+ TouchableWithoutFeedback,
+ Text,
+ StyleSheet,
+ ActivityIndicator,
+ ViewStyle,
+ TextStyle,
+ View,
+} from 'react-native';
+import { COLORS, RADIUS, SPACING, FONT_SIZE, FONT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+
+type Variant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
+type Size = 'sm' | 'md' | 'lg';
+
+interface ButtonProps {
+ title: string;
+ onPress: () => void;
+ variant?: Variant;
+ size?: Size;
+ loading?: boolean;
+ disabled?: boolean;
+ style?: ViewStyle;
+ textStyle?: TextStyle;
+ fullWidth?: boolean;
+}
+
+export function Button({
+ title,
+ onPress,
+ variant = 'primary',
+ size = 'md',
+ loading = false,
+ disabled = false,
+ style,
+ textStyle,
+ fullWidth = false,
+}: ButtonProps) {
+ const { theme, isDark } = useTheme();
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const onPressIn = () =>
+ Animated.spring(scale, { toValue: 0.96, useNativeDriver: true, speed: 50, bounciness: 0 }).start();
+
+ const onPressOut = () =>
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
+
+ const containerBg = getContainerBg(variant, theme, isDark);
+ const labelColor = getLabelColor(variant);
+ const shadow = variant === 'primary' ? SHADOWS.primary : variant === 'danger' ? SHADOWS.sm : {};
+
+ return (
+
+
+ {loading ? (
+
+ ) : (
+
+ {title}
+
+ )}
+
+
+ );
+}
+
+function getContainerBg(variant: Variant, theme: any, isDark: boolean): ViewStyle {
+ switch (variant) {
+ case 'primary':
+ return { backgroundColor: COLORS.primary };
+ case 'secondary':
+ return { backgroundColor: isDark ? theme.surfaceHover : theme.bgSecondary };
+ case 'outline':
+ return { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: COLORS.primary };
+ case 'ghost':
+ return { backgroundColor: 'transparent' };
+ case 'danger':
+ return { backgroundColor: COLORS.error };
+ }
+}
+
+function getLabelColor(variant: Variant): string {
+ switch (variant) {
+ case 'primary':
+ case 'danger':
+ return COLORS.white;
+ case 'secondary':
+ return COLORS.primary;
+ case 'outline':
+ case 'ghost':
+ return COLORS.primary;
+ }
+}
+
+const styles = StyleSheet.create({
+ base: {
+ borderRadius: RADIUS.md,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'row',
+ },
+ fullWidth: { width: '100%' },
+ disabled: { opacity: 0.55 },
+
+ size_sm: { paddingVertical: SPACING.xs + 2, paddingHorizontal: SPACING.md, borderRadius: RADIUS.sm },
+ size_md: { paddingVertical: SPACING.md - 1, paddingHorizontal: SPACING.xl, borderRadius: RADIUS.md },
+ size_lg: { paddingVertical: SPACING.md + 1, paddingHorizontal: SPACING.xxl, borderRadius: RADIUS.lg, minHeight: 52 },
+
+ label: { fontWeight: FONT.semibold as any, letterSpacing: 0.1 },
+ label_sm: { fontSize: FONT_SIZE.sm },
+ label_md: { fontSize: FONT_SIZE.md },
+ label_lg: { fontSize: FONT_SIZE.md },
+});
diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx
new file mode 100644
index 0000000..8052c5f
--- /dev/null
+++ b/src/components/ui/Card.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { View, StyleSheet, ViewStyle } from 'react-native';
+import { RADIUS, SPACING, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+
+type Elevation = 'none' | 'xs' | 'sm' | 'md' | 'lg';
+
+interface CardProps {
+ children: React.ReactNode;
+ style?: ViewStyle;
+ padding?: number;
+ elevation?: Elevation;
+}
+
+export function Card({ children, style, padding, elevation = 'sm' }: CardProps) {
+ const { theme } = useTheme();
+ const shadow = SHADOWS[elevation];
+
+ return (
+
+ {children}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: RADIUS.xl,
+ padding: SPACING.lg,
+ borderWidth: 1,
+ },
+});
diff --git a/src/components/ui/EmptyState.tsx b/src/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..692029b
--- /dev/null
+++ b/src/components/ui/EmptyState.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { FONT_SIZE, SPACING } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button } from './Button';
+
+interface EmptyStateProps {
+ icon?: React.ReactNode;
+ title: string;
+ description?: string;
+ action?: { label: string; onPress: () => void };
+}
+
+export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
+ const { theme } = useTheme();
+
+ return (
+
+ {icon ? {icon} : null}
+ {title}
+ {description ? (
+ {description}
+ ) : null}
+ {action ? (
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: SPACING.xxxl,
+ gap: SPACING.md,
+ },
+ icon: { marginBottom: SPACING.sm },
+ title: { fontSize: FONT_SIZE.lg, fontWeight: '600', textAlign: 'center' },
+ desc: { fontSize: FONT_SIZE.md, textAlign: 'center', lineHeight: 22 },
+ action: { marginTop: SPACING.sm },
+});
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
new file mode 100644
index 0000000..608c810
--- /dev/null
+++ b/src/components/ui/Input.tsx
@@ -0,0 +1,130 @@
+import React, { useState, useRef } from 'react';
+import {
+ View,
+ Text,
+ TextInput,
+ TextInputProps,
+ StyleSheet,
+ TouchableOpacity,
+ Animated,
+} from 'react-native';
+import { COLORS, RADIUS, SPACING, FONT_SIZE, FONT } from '../../theme';
+import { useTheme } from '../../theme';
+
+interface InputProps extends TextInputProps {
+ label?: string;
+ error?: string;
+ hint?: string;
+ rightIcon?: React.ReactNode;
+}
+
+export function Input({ label, error, hint, rightIcon, style, onFocus, onBlur, ...props }: InputProps) {
+ const { theme } = useTheme();
+ const [focused, setFocused] = useState(false);
+ const borderAnim = useRef(new Animated.Value(0)).current;
+
+ const handleFocus = (e: any) => {
+ setFocused(true);
+ Animated.timing(borderAnim, { toValue: 1, duration: 180, useNativeDriver: false }).start();
+ onFocus?.(e);
+ };
+
+ const handleBlur = (e: any) => {
+ setFocused(false);
+ Animated.timing(borderAnim, { toValue: 0, duration: 180, useNativeDriver: false }).start();
+ onBlur?.(e);
+ };
+
+ const borderColor = borderAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [error ? COLORS.error : theme.border, error ? COLORS.error : COLORS.primary],
+ });
+
+ const borderWidth = borderAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1.5, 2],
+ });
+
+ return (
+
+ {label ? (
+
+ {label}
+
+ ) : null}
+
+
+ {rightIcon ? {rightIcon} : null}
+
+ {error ? (
+ {error}
+ ) : hint ? (
+ {hint}
+ ) : null}
+
+ );
+}
+
+export function SecureInput({ showToggle = true, ...props }: InputProps & { showToggle?: boolean }) {
+ const [visible, setVisible] = useState(false);
+ const { theme } = useTheme();
+
+ return (
+ setVisible(v => !v)} style={styles.toggleBtn}>
+
+ {visible ? 'Hide' : 'Show'}
+
+
+ ) : undefined
+ }
+ />
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: { marginBottom: SPACING.md },
+ label: {
+ fontSize: FONT_SIZE.sm,
+ fontWeight: FONT.medium as any,
+ marginBottom: SPACING.xs,
+ letterSpacing: 0.1,
+ },
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: RADIUS.md,
+ paddingHorizontal: SPACING.md,
+ minHeight: 50,
+ overflow: 'hidden',
+ },
+ input: {
+ flex: 1,
+ fontSize: FONT_SIZE.md,
+ paddingVertical: SPACING.sm,
+ },
+ rightIcon: { marginLeft: SPACING.xs },
+ error: {
+ fontSize: FONT_SIZE.xs,
+ color: COLORS.error,
+ marginTop: SPACING.xs,
+ fontWeight: FONT.medium as any,
+ },
+ hint: { fontSize: FONT_SIZE.xs, marginTop: SPACING.xs },
+ toggleBtn: { padding: SPACING.xs },
+ toggleText: { fontSize: FONT_SIZE.sm, fontWeight: FONT.semibold as any },
+});
diff --git a/src/components/ui/Spinner.tsx b/src/components/ui/Spinner.tsx
new file mode 100644
index 0000000..5829b73
--- /dev/null
+++ b/src/components/ui/Spinner.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { ActivityIndicator, View, StyleSheet, Text } from 'react-native';
+import { COLORS, FONT_SIZE, SPACING } from '../../theme';
+import { useTheme } from '../../theme';
+
+interface SpinnerProps {
+ size?: 'small' | 'large';
+ color?: string;
+ centered?: boolean;
+ label?: string;
+}
+
+export function Spinner({ size = 'large', color, centered = false, label }: SpinnerProps) {
+ const { theme } = useTheme();
+ const spinnerColor = color ?? COLORS.primary;
+
+ if (centered) {
+ return (
+
+
+ {label ? {label} : null}
+
+ );
+ }
+
+ return ;
+}
+
+const styles = StyleSheet.create({
+ centered: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: SPACING.xxxl,
+ gap: SPACING.md,
+ },
+ label: { fontSize: FONT_SIZE.sm },
+});
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
new file mode 100644
index 0000000..b6a8192
--- /dev/null
+++ b/src/components/ui/index.ts
@@ -0,0 +1,6 @@
+export { Button } from './Button';
+export { Input, SecureInput } from './Input';
+export { Card } from './Card';
+export { Badge, PlanBadge, StatusBadge } from './Badge';
+export { Spinner } from './Spinner';
+export { EmptyState } from './EmptyState';
diff --git a/src/contexts/ToastContext.tsx b/src/contexts/ToastContext.tsx
new file mode 100644
index 0000000..35d0a4d
--- /dev/null
+++ b/src/contexts/ToastContext.tsx
@@ -0,0 +1,97 @@
+import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
+import { View, Text, StyleSheet, Animated } from 'react-native';
+import { COLORS, RADIUS, SPACING, FONT_SIZE } from '../theme';
+
+type ToastType = 'success' | 'error' | 'info';
+
+interface Toast {
+ id: number;
+ message: string;
+ type: ToastType;
+}
+
+interface ToastContextType {
+ success: (message: string) => void;
+ error: (message: string) => void;
+ info: (message: string) => void;
+}
+
+const ToastContext = createContext({
+ success: () => {},
+ error: () => {},
+ info: () => {},
+});
+
+export function ToastProvider({ children }: { children: React.ReactNode }) {
+ const [toasts, setToasts] = useState([]);
+ const counter = useRef(0);
+
+ const show = useCallback((message: string, type: ToastType) => {
+ const id = counter.current++;
+ setToasts(prev => [...prev, { id, message, type }]);
+ setTimeout(() => {
+ setToasts(prev => prev.filter(t => t.id !== id));
+ }, 3500);
+ }, []);
+
+ const success = useCallback((msg: string) => show(msg, 'success'), [show]);
+ const error = useCallback((msg: string) => show(msg, 'error'), [show]);
+ const info = useCallback((msg: string) => show(msg, 'info'), [show]);
+
+ return (
+
+ {children}
+
+ {toasts.map(toast => (
+
+ ))}
+
+
+ );
+}
+
+function ToastItem({ toast }: { toast: Toast }) {
+ const opacity = useRef(new Animated.Value(0)).current;
+
+ React.useEffect(() => {
+ Animated.sequence([
+ Animated.timing(opacity, { toValue: 1, duration: 250, useNativeDriver: true }),
+ Animated.delay(2800),
+ Animated.timing(opacity, { toValue: 0, duration: 300, useNativeDriver: true }),
+ ]).start();
+ }, [opacity]);
+
+ const bg =
+ toast.type === 'success' ? COLORS.success
+ : toast.type === 'error' ? COLORS.error
+ : COLORS.info;
+
+ return (
+
+ {toast.message}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ bottom: 90,
+ left: SPACING.lg,
+ right: SPACING.lg,
+ zIndex: 9999,
+ gap: SPACING.sm,
+ },
+ toast: {
+ borderRadius: RADIUS.md,
+ paddingVertical: SPACING.md,
+ paddingHorizontal: SPACING.lg,
+ },
+ toastText: {
+ color: '#fff',
+ fontSize: FONT_SIZE.md,
+ fontWeight: '500',
+ },
+});
+
+export const useToast = () => useContext(ToastContext);
diff --git a/src/data/templates.ts b/src/data/templates.ts
new file mode 100644
index 0000000..8708735
--- /dev/null
+++ b/src/data/templates.ts
@@ -0,0 +1,83 @@
+export interface ChatbotTemplate {
+ id: string;
+ name: string;
+ description: string;
+ icon: string;
+ category: string;
+ system_prompt: string;
+ welcome_message: string;
+ lead_capture_enabled: boolean;
+}
+
+export const CHATBOT_TEMPLATES: ChatbotTemplate[] = [
+ {
+ id: 'customer-support',
+ name: 'Customer Support',
+ description: 'Handle customer inquiries, returns, and product questions',
+ icon: '🎧',
+ category: 'Customer Support',
+ system_prompt: 'You are a friendly and helpful customer support assistant. Help customers with their inquiries, returns, product questions, and order issues. Be empathetic, professional, and solution-focused. If you cannot resolve an issue, offer to escalate to a human agent.',
+ welcome_message: "Hi there! I'm your customer support assistant. How can I help you today?",
+ lead_capture_enabled: false,
+ },
+ {
+ id: 'sales-assistant',
+ name: 'Sales Assistant',
+ description: 'Qualify leads, answer product questions, and book demos',
+ icon: '💼',
+ category: 'Sales',
+ system_prompt: 'You are an enthusiastic sales assistant. Help prospects understand our products and services, qualify their needs, and guide them toward the right solution. Collect their contact information so our sales team can follow up.',
+ welcome_message: "Welcome! I'm here to help you find the perfect solution. What are you looking to achieve?",
+ lead_capture_enabled: true,
+ },
+ {
+ id: 'hr-onboarding',
+ name: 'HR Onboarding',
+ description: 'Answer employee questions about policies, benefits, and procedures',
+ icon: '👥',
+ category: 'HR',
+ system_prompt: 'You are an HR onboarding assistant. Help new and existing employees with questions about company policies, benefits, procedures, time-off requests, and workplace guidelines. Be accurate and direct employees to HR for complex matters.',
+ welcome_message: "Hello! I'm your HR assistant. I can help with policies, benefits, and onboarding questions. What do you need?",
+ lead_capture_enabled: false,
+ },
+ {
+ id: 'ecommerce',
+ name: 'E-commerce Helper',
+ description: 'Guide shoppers through products, shipping, and returns',
+ icon: '🛍️',
+ category: 'E-commerce',
+ system_prompt: 'You are a helpful shopping assistant. Help customers find products, answer questions about shipping times, return policies, product specifications, and availability. Make shopping easy and enjoyable.',
+ welcome_message: "Welcome to our store! I'm here to help you find exactly what you're looking for. What can I help you with?",
+ lead_capture_enabled: false,
+ },
+ {
+ id: 'real-estate',
+ name: 'Real Estate Agent',
+ description: 'Answer questions about listings, viewings, and the buying process',
+ icon: '🏠',
+ category: 'Real Estate',
+ system_prompt: 'You are a knowledgeable real estate assistant. Help potential buyers and renters with property listings, neighborhood information, pricing guidance, and the buying/renting process. Collect contact details to schedule viewings.',
+ welcome_message: "Hello! Looking for your dream home? I can help you explore properties and answer any questions. Where shall we start?",
+ lead_capture_enabled: true,
+ },
+ {
+ id: 'restaurant',
+ name: 'Restaurant Assistant',
+ description: 'Share menu info, hours, and take reservation inquiries',
+ icon: '🍽️',
+ category: 'Food & Beverage',
+ system_prompt: 'You are a friendly restaurant assistant. Help guests with menu questions, dietary restrictions, opening hours, location information, and reservation inquiries. Be warm and welcoming.',
+ welcome_message: "Welcome! I'm here to help with our menu, reservations, and any questions. What can I do for you?",
+ lead_capture_enabled: false,
+ },
+ {
+ id: 'healthcare-faq',
+ name: 'Healthcare FAQ',
+ description: 'Answer general health questions and help with appointment booking',
+ icon: '🏥',
+ category: 'Healthcare',
+ system_prompt: 'You are a helpful healthcare information assistant. Provide general health information, answer questions about services, help with appointment scheduling inquiries, and direct patients to appropriate resources. Always clarify that you provide general information only and patients should consult a qualified healthcare professional for medical advice.',
+ welcome_message: "Hello! I can help with general health information, appointment questions, and our services. How can I assist you?",
+ lead_capture_enabled: false,
+ },
+];
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
new file mode 100644
index 0000000..58a9dc6
--- /dev/null
+++ b/src/i18n/en.ts
@@ -0,0 +1,343 @@
+export const en = {
+ // Navigation tabs
+ nav: {
+ explore: 'Explore',
+ chatbots: 'Chatbots',
+ inbox: 'Inbox',
+ account: 'Account',
+ },
+
+ // Auth
+ auth: {
+ login_title: 'Welcome back',
+ login_subtitle: 'Sign in to your Contexta account',
+ signup_title: 'Create your account',
+ signup_subtitle: 'Start building AI chatbots — free forever',
+ email: 'Email',
+ password: 'Password',
+ company_name: 'Company name',
+ sign_in: 'Sign In',
+ create_account: 'Create free account',
+ no_account: "Don't have an account?",
+ already_account: 'Already have an account?',
+ sign_up_free: 'Sign up free',
+ forgot_password: 'Forgot password?',
+ forgot_title: 'Reset your password',
+ forgot_subtitle: "Enter your email and we'll send a reset link.",
+ send_reset: 'Send reset link',
+ back_to_signin: 'Back to sign in',
+ check_email_title: 'Check your email',
+ check_email_desc: 'A reset link has been sent if that address is registered.',
+ },
+
+ // Dashboard
+ dashboard: {
+ title: 'My Chatbots',
+ new: '+ New',
+ empty_title: 'No chatbots yet',
+ empty_desc: 'Create your first AI chatbot powered by your documents.',
+ create_first: 'Create Chatbot',
+ delete_title: 'Delete Chatbot',
+ delete_confirm: (name: string) => `Delete "${name}"? This cannot be undone.`,
+ edit: 'Edit',
+ preview: 'Preview',
+ publish: 'Publish',
+ unpublish: 'Unpublish',
+ docs: 'docs',
+ chats: 'chats',
+ },
+
+ // Onboarding checklist
+ onboarding: {
+ title: 'Getting started',
+ step_create: 'Create your first chatbot',
+ step_docs: 'Add knowledge documents or URLs',
+ step_publish: 'Publish your chatbot',
+ },
+
+ // Chatbot builder
+ builder: {
+ title_new: 'Chatbot Builder',
+ title_edit: 'Chatbot Builder',
+ tab_settings: 'Settings',
+ tab_docs: 'Docs',
+ tab_preview: 'Preview',
+ tab_testing: 'Testing',
+ tab_deploy: 'Deploy',
+ save_first: 'Save the chatbot first.',
+ save_first_preview: 'Save the chatbot first to preview it.',
+ save_first_test: 'Save the chatbot first to run tests.',
+ section_basic: 'Basic Info',
+ name_label: 'Chatbot Name *',
+ name_placeholder: 'My Support Bot',
+ description_label: 'Description',
+ description_placeholder: 'What does this chatbot do?',
+ section_behavior: 'Behavior',
+ system_prompt_label: 'System Prompt',
+ system_prompt_placeholder: 'You are a helpful assistant...',
+ section_model: 'AI Model',
+ section_temperature: 'Temperature',
+ temp_hint: '0 = focused, 1 = creative',
+ section_appearance: 'Appearance',
+ color_label: 'Primary Color',
+ welcome_label: 'Welcome Message',
+ section_classification: 'Classification',
+ category_label: 'Category',
+ section_advanced: 'Advanced',
+ branding_label: 'Show Branding',
+ branding_desc: "Display 'Powered by Contexta' in the chat widget",
+ lead_label: 'Lead Capture',
+ lead_desc: 'Collect visitor contact info during chats',
+ handoff_label: 'Human Handoff',
+ handoff_desc: 'Escalate conversations to a human agent',
+ handoff_email_label: 'Handoff Email',
+ save: 'Save Changes',
+ create: 'Create Chatbot',
+ saving: 'Saving...',
+ // Templates
+ template_title: 'Choose a template',
+ template_skip: 'Start blank →',
+ // Testing tab
+ testing_title: 'Test Questions',
+ testing_desc: 'Enter up to 10 questions to test how your chatbot responds.',
+ testing_placeholder: 'Ask a question...',
+ testing_add: '+ Add question',
+ testing_run: '▶ Run Tests',
+ testing_running: 'Running...',
+ testing_results: (n: number) => `${n} RESULT${n !== 1 ? 'S' : ''}`,
+ testing_sources: 'SOURCES',
+ testing_model: 'Model',
+ testing_error: 'Test failed. Make sure your chatbot has a knowledge base.',
+ },
+
+ // Documents
+ documents: {
+ title: 'Documents',
+ add_url: 'Add URL',
+ url_placeholder: 'https://...',
+ upload: 'Upload Document',
+ empty_title: 'No documents yet',
+ empty_desc: 'Upload files or add URLs to build the knowledge base.',
+ delete_title: 'Delete Document',
+ delete_confirm: 'Remove this document from the knowledge base?',
+ status_pending: 'Pending',
+ status_processing: 'Processing...',
+ status_completed: 'Ready',
+ status_failed: 'Failed',
+ chunks: (n: number) => `${n} chunks`,
+ retry: 'Retry',
+ url_sources: 'URL Sources',
+ refreshing: 'Refreshing...',
+ },
+
+ // Deploy tab
+ deploy: {
+ publish_label: 'Published',
+ publish_desc: 'Make this chatbot publicly accessible',
+ chat_link: 'Public Chat Link',
+ chat_link_desc: 'Share this link directly with anyone',
+ copy: 'Copy',
+ copied: 'Copied!',
+ publish_first: 'Publish your chatbot to get a public link.',
+ embed_title: 'Embed Code',
+ embed_desc: 'Add the chat widget to any website',
+ telegram_title: 'Telegram',
+ telegram_connected: 'Connected',
+ telegram_disconnect: 'Disconnect',
+ telegram_token_placeholder: 'Bot token from @BotFather',
+ telegram_connect: 'Connect',
+ whatsapp_title: 'WhatsApp',
+ whatsapp_keyword: 'Keyword (optional)',
+ whatsapp_connect: 'Connect',
+ section_lead: 'Lead Capture',
+ section_handoff: 'Human Handoff',
+ section_booking: 'Appointments',
+ booking_enable: 'Enable appointment booking',
+ booking_enable_sub: 'Chatbot will guide users to book appointments',
+ },
+
+ // Inbox
+ inbox: {
+ title: 'Inbox',
+ filter_all: 'All',
+ filter_open: 'Open',
+ filter_handling: 'Handling',
+ filter_resolved: 'Resolved',
+ empty_title: 'No conversations',
+ empty_desc: 'Conversations with your chatbots will appear here.',
+ empty_filtered: (status: string) => `No ${status} conversations.`,
+ no_messages: 'No messages yet',
+ type_reply: 'Write a reply as agent...',
+ send: 'Send',
+ status_open: 'Open',
+ status_agent: 'Handling',
+ status_resolved: 'Resolved',
+ take_over: 'Take over',
+ resolve: 'Resolve',
+ reopen: 'Reopen',
+ delete: 'Delete',
+ delete_confirm: 'Delete this conversation?',
+ },
+
+ // Leads
+ leads: {
+ title: 'Leads',
+ subtitle: (n: number) => `${n} contact${n !== 1 ? 's' : ''} collected`,
+ export: '⬆ Export',
+ empty_title: 'No leads yet',
+ empty_desc: 'Enable lead capture on your chatbots to collect visitor contact info.',
+ no_export: 'There are no leads to export yet.',
+ detail_title: 'Lead Details',
+ contact_info: 'Contact Info',
+ status_section: 'Status',
+ notes_section: 'Notes',
+ notes_placeholder: 'Add notes about this lead...',
+ cancel: 'Cancel',
+ save: 'Save',
+ status_new: 'New',
+ status_contacted: 'Contacted',
+ status_qualified: 'Qualified',
+ status_closed: 'Closed',
+ status_lost: 'Lost',
+ field_name: 'Name',
+ field_email: 'Email',
+ field_phone: 'Phone',
+ field_company: 'Company',
+ field_chatbot: 'Chatbot',
+ field_date: 'Date',
+ },
+
+ // Analytics
+ analytics: {
+ title: 'Analytics',
+ subtitle: 'Overview of your chatbot performance',
+ conversations: 'Conversations',
+ messages: 'Messages',
+ chatbots: 'Chatbots',
+ avg_conv: 'Avg / Conv',
+ by_chatbot: 'By Chatbot',
+ confidence: 'Confidence',
+ empty_title: 'No data yet',
+ empty_desc: 'Analytics will appear once your chatbots start receiving conversations.',
+ unanswered: (n: number) => `${n} unanswered`,
+ show_gaps: 'Show gaps ▼',
+ hide_gaps: 'Hide ▲',
+ },
+
+ // Campaigns
+ campaigns: {
+ title: 'Campaigns',
+ new: 'New Campaign',
+ empty_title: 'No campaigns yet',
+ empty_desc: 'Create a campaign to broadcast a message to all your Telegram subscribers at once.',
+ select_chatbot: 'Select chatbot',
+ campaign_name: 'Campaign Name',
+ name_placeholder: 'e.g. Summer promo, New menu announcement...',
+ message_label: 'Message',
+ message_placeholder: 'Write your broadcast message here...',
+ create: 'Create Campaign',
+ send: 'Send',
+ delete: 'Delete',
+ send_confirm: (title: string, n: number) => `Send "${title}" to ${n} subscriber${n !== 1 ? 's' : ''}?`,
+ send_warning: 'This cannot be undone. The message will be delivered immediately.',
+ send_now: 'Send Now',
+ cancel: 'Cancel',
+ status_draft: 'Draft',
+ status_sending: 'Sending...',
+ status_sent: 'Sent',
+ status_failed: 'Failed',
+ subscribers: (n: number) => `${n} subscriber${n !== 1 ? 's' : ''}`,
+ delivered: 'delivered',
+ },
+
+ // Appointments
+ appointments: {
+ title: 'Appointments',
+ subtitle: 'Bookings made via your chatbots',
+ empty_title: 'No appointments yet',
+ empty_desc: 'Once customers book via your chatbot, appointments will appear here.',
+ configure_hours: 'Configure Hours',
+ save_hours: 'Save Hours',
+ status_pending: 'Pending',
+ status_confirmed: 'Confirmed',
+ status_cancelled: 'Cancelled',
+ status_completed: 'Completed',
+ confirm: 'Confirm',
+ decline: 'Decline',
+ complete: 'Complete',
+ cancel: 'Cancel',
+ days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ closed: 'Closed',
+ open_from: 'Open from',
+ to: 'to',
+ },
+
+ // Settings
+ settings: {
+ title: 'Settings',
+ subscription: 'Subscription',
+ reports: 'Reports',
+ analytics: 'Analytics',
+ leads: 'Leads',
+ campaigns: 'Campaigns',
+ appointments: 'Appointments',
+ appearance: 'Appearance',
+ theme_system: 'System',
+ theme_light: 'Light',
+ theme_dark: 'Dark',
+ profile: 'Profile',
+ company_label: 'Company / Team Name',
+ company_placeholder: 'Acme Inc.',
+ language_label: 'Language',
+ change_password: 'Change Password',
+ current_password: 'Current Password',
+ new_password: 'New Password',
+ save_profile: 'Save Profile',
+ account: 'Account',
+ sign_out: 'Sign Out',
+ sign_out_confirm: 'Are you sure you want to sign out?',
+ delete_account: 'Delete Account',
+ delete_confirm: 'This will permanently delete your account and all chatbots. This cannot be undone.',
+ renews: 'Renews',
+ version: 'Contexta v1.0.0',
+ profile_updated: 'Profile updated!',
+ update_failed: 'Failed to update profile',
+ company_required: 'Company name is required',
+ },
+
+ // Marketplace
+ marketplace: {
+ title: 'Marketplace',
+ search_placeholder: 'Search chatbots...',
+ empty_title: 'No chatbots found',
+ empty_desc: 'Try adjusting your search or filters.',
+ chat_now: 'Chat Now →',
+ by: (name: string) => `by ${name}`,
+ conversations: (n: number) => `${n} conversations`,
+ rating: 'Your rating',
+ rate_submit: 'Submit',
+ login_to_rate: 'Sign in to rate',
+ },
+
+ // Guest screen
+ guest: {
+ sign_in: 'Sign In',
+ sign_up: 'Create Account',
+ or: 'or',
+ },
+
+ // Common
+ common: {
+ loading: 'Loading...',
+ cancel: 'Cancel',
+ delete: 'Delete',
+ save: 'Save',
+ close: 'Close',
+ confirm: 'Confirm',
+ retry: 'Retry',
+ refresh: 'Refresh',
+ error: 'Something went wrong. Please try again.',
+ },
+};
+
+export type Translations = typeof en;
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
new file mode 100644
index 0000000..fe6b607
--- /dev/null
+++ b/src/i18n/fr.ts
@@ -0,0 +1,325 @@
+import type { Translations } from './en';
+
+export const fr: Translations = {
+ nav: {
+ explore: 'Explorer',
+ chatbots: 'Chatbots',
+ inbox: 'Boîte de réception',
+ account: 'Compte',
+ },
+
+ auth: {
+ login_title: 'Bon retour',
+ login_subtitle: 'Connectez-vous à votre compte Contexta',
+ signup_title: 'Créez votre compte',
+ signup_subtitle: 'Commencez à créer des chatbots IA — gratuit pour toujours',
+ email: 'E-mail',
+ password: 'Mot de passe',
+ company_name: "Nom de l'entreprise",
+ sign_in: 'Se connecter',
+ create_account: 'Créer un compte gratuit',
+ no_account: 'Pas de compte ?',
+ already_account: 'Déjà un compte ?',
+ sign_up_free: "S'inscrire gratuitement",
+ forgot_password: 'Mot de passe oublié ?',
+ forgot_title: 'Réinitialisez votre mot de passe',
+ forgot_subtitle: 'Entrez votre e-mail, nous vous enverrons un lien de réinitialisation.',
+ send_reset: 'Envoyer le lien',
+ back_to_signin: 'Retour à la connexion',
+ check_email_title: 'Vérifiez votre boîte mail',
+ check_email_desc: "Un lien de réinitialisation a été envoyé si cette adresse est enregistrée.",
+ },
+
+ dashboard: {
+ title: 'Mes Chatbots',
+ new: '+ Nouveau',
+ empty_title: 'Aucun chatbot pour l\'instant',
+ empty_desc: 'Créez votre premier chatbot IA alimenté par vos documents.',
+ create_first: 'Créer un chatbot',
+ delete_title: 'Supprimer le chatbot',
+ delete_confirm: (name: string) => `Supprimer "${name}" ? Cette action est irréversible.`,
+ edit: 'Modifier',
+ preview: 'Aperçu',
+ publish: 'Publier',
+ unpublish: 'Dépublier',
+ docs: 'docs',
+ chats: 'chats',
+ },
+
+ onboarding: {
+ title: 'Premiers pas 🚀',
+ step_create: 'Créez votre premier chatbot',
+ step_docs: 'Ajoutez des documents ou des URLs',
+ step_publish: 'Publiez votre chatbot',
+ },
+
+ builder: {
+ title_new: 'Créer un chatbot',
+ title_edit: 'Modifier le chatbot',
+ tab_settings: 'Paramètres',
+ tab_docs: 'Documents',
+ tab_preview: 'Aperçu',
+ tab_testing: 'Tests',
+ tab_deploy: 'Déploiement',
+ save_first: 'Enregistrez le chatbot d\'abord.',
+ save_first_preview: 'Enregistrez le chatbot d\'abord pour l\'aperçu.',
+ save_first_test: 'Enregistrez le chatbot d\'abord pour lancer des tests.',
+ section_basic: 'Informations de base',
+ name_label: 'Nom du chatbot *',
+ name_placeholder: 'Mon Bot Support',
+ description_label: 'Description',
+ description_placeholder: 'Que fait ce chatbot ?',
+ section_behavior: 'Comportement',
+ system_prompt_label: 'Invite système',
+ system_prompt_placeholder: 'Vous êtes un assistant utile...',
+ section_model: 'Modèle IA',
+ section_temperature: 'Température',
+ temp_hint: '0 = précis, 1 = créatif',
+ section_appearance: 'Apparence',
+ color_label: 'Couleur principale',
+ welcome_label: "Message d'accueil",
+ section_classification: 'Classification',
+ category_label: 'Catégorie',
+ section_advanced: 'Avancé',
+ branding_label: 'Afficher la marque',
+ branding_desc: "Afficher « Propulsé par Contexta » dans le widget",
+ lead_label: 'Capture de prospects',
+ lead_desc: 'Collecter les coordonnées des visiteurs',
+ handoff_label: 'Transfert humain',
+ handoff_desc: 'Escalader les conversations à un agent humain',
+ handoff_email_label: 'E-mail de transfert',
+ save: 'Enregistrer',
+ create: 'Créer le chatbot',
+ saving: 'Enregistrement...',
+ template_title: 'Choisir un modèle',
+ template_skip: 'Commencer de zéro →',
+ testing_title: 'Questions de test',
+ testing_desc: "Entrez jusqu'à 10 questions pour tester les réponses de votre chatbot.",
+ testing_placeholder: "ex. Quels sont vos horaires d'ouverture ?",
+ testing_add: '+ Ajouter une question',
+ testing_run: '▶ Lancer les tests',
+ testing_running: 'En cours...',
+ testing_results: (n: number) => `${n} RÉSULTAT${n !== 1 ? 'S' : ''}`,
+ testing_sources: 'SOURCES',
+ testing_model: 'Modèle',
+ testing_error: 'Test échoué. Vérifiez que votre chatbot a une base de connaissances.',
+ },
+
+ documents: {
+ title: 'Documents',
+ add_url: 'Ajouter une URL',
+ url_placeholder: 'https://...',
+ upload: 'Importer un document',
+ empty_title: 'Aucun document pour l\'instant',
+ empty_desc: 'Importez des fichiers ou ajoutez des URLs pour construire la base de connaissances.',
+ delete_title: 'Supprimer le document',
+ delete_confirm: 'Retirer ce document de la base de connaissances ?',
+ status_pending: 'En attente',
+ status_processing: 'Traitement...',
+ status_completed: 'Prêt',
+ status_failed: 'Échec',
+ chunks: (n: number) => `${n} fragments`,
+ retry: 'Réessayer',
+ url_sources: 'Sources URL',
+ refreshing: 'Actualisation...',
+ },
+
+ deploy: {
+ publish_label: 'Publié',
+ publish_desc: 'Rendre ce chatbot accessible publiquement',
+ chat_link: 'Lien de chat public',
+ chat_link_desc: "Partagez ce lien directement avec n'importe qui",
+ copy: 'Copier',
+ copied: 'Copié !',
+ publish_first: 'Publiez votre chatbot pour obtenir un lien public.',
+ embed_title: "Code d'intégration",
+ embed_desc: 'Ajoutez le widget de chat à n\'importe quel site web',
+ telegram_title: 'Telegram',
+ telegram_connected: 'Connecté',
+ telegram_disconnect: 'Déconnecter',
+ telegram_token_placeholder: 'Token du bot depuis @BotFather',
+ telegram_connect: 'Connecter',
+ whatsapp_title: 'WhatsApp',
+ whatsapp_keyword: 'Mot-clé (optionnel)',
+ whatsapp_connect: 'Connecter',
+ section_lead: 'Capture de prospects',
+ section_handoff: 'Transfert humain',
+ section_booking: 'Rendez-vous',
+ booking_enable: 'Activer la prise de rendez-vous',
+ booking_enable_sub: 'Le chatbot guidera les utilisateurs pour réserver',
+ },
+
+ inbox: {
+ title: 'Boîte de réception',
+ filter_all: 'Tous',
+ filter_open: 'Ouvert',
+ filter_handling: 'Agent',
+ filter_resolved: 'Résolu',
+ empty_title: 'Aucune conversation',
+ empty_desc: 'Les conversations avec vos chatbots apparaîtront ici.',
+ empty_filtered: (status: string) => `Aucune conversation « ${status} ».`,
+ no_messages: 'Aucun message',
+ type_reply: 'Écrire une réponse en tant qu\'agent...',
+ send: 'Envoyer',
+ status_open: 'Ouvert',
+ status_agent: 'Agent',
+ status_resolved: 'Résolu',
+ take_over: 'Prendre en charge',
+ resolve: 'Résoudre',
+ reopen: 'Rouvrir',
+ delete: 'Supprimer',
+ delete_confirm: 'Supprimer cette conversation ?',
+ },
+
+ leads: {
+ title: 'Prospects',
+ subtitle: (n: number) => `${n} contact${n !== 1 ? 's' : ''} collecté${n !== 1 ? 's' : ''}`,
+ export: '⬆ Exporter',
+ empty_title: 'Aucun prospect pour l\'instant',
+ empty_desc: 'Activez la capture de prospects sur vos chatbots pour collecter des contacts.',
+ no_export: 'Aucun prospect à exporter pour l\'instant.',
+ detail_title: 'Détails du prospect',
+ contact_info: 'Informations de contact',
+ status_section: 'Statut',
+ notes_section: 'Notes',
+ notes_placeholder: 'Ajouter des notes sur ce prospect...',
+ cancel: 'Annuler',
+ save: 'Enregistrer',
+ status_new: 'Nouveau',
+ status_contacted: 'Contacté',
+ status_qualified: 'Qualifié',
+ status_closed: 'Fermé',
+ status_lost: 'Perdu',
+ field_name: 'Nom',
+ field_email: 'E-mail',
+ field_phone: 'Téléphone',
+ field_company: 'Entreprise',
+ field_chatbot: 'Chatbot',
+ field_date: 'Date',
+ },
+
+ analytics: {
+ title: 'Analytiques',
+ subtitle: 'Vue d\'ensemble des performances de vos chatbots',
+ conversations: 'Conversations',
+ messages: 'Messages',
+ chatbots: 'Chatbots',
+ avg_conv: 'Moy. / Conv.',
+ by_chatbot: 'Par chatbot',
+ confidence: 'Confiance',
+ empty_title: 'Aucune donnée pour l\'instant',
+ empty_desc: 'Les analytiques apparaîtront dès que vos chatbots recevront des conversations.',
+ unanswered: (n: number) => `${n} sans réponse`,
+ show_gaps: 'Voir les lacunes ▼',
+ hide_gaps: 'Masquer ▲',
+ },
+
+ campaigns: {
+ title: 'Campagnes',
+ new: 'Nouvelle campagne',
+ empty_title: 'Aucune campagne pour l\'instant',
+ empty_desc: 'Créez une campagne pour diffuser un message à tous vos abonnés Telegram en une fois.',
+ select_chatbot: 'Sélectionner un chatbot',
+ campaign_name: 'Nom de la campagne',
+ name_placeholder: 'ex. Promotion estivale, Annonce du nouveau menu...',
+ message_label: 'Message',
+ message_placeholder: 'Rédigez votre message de diffusion ici...',
+ create: 'Créer la campagne',
+ send: 'Envoyer',
+ delete: 'Supprimer',
+ send_confirm: (title: string, n: number) => `Envoyer "${title}" à ${n} abonné${n !== 1 ? 's' : ''} ?`,
+ send_warning: 'Cette action est irréversible. Le message sera délivré immédiatement.',
+ send_now: 'Envoyer maintenant',
+ cancel: 'Annuler',
+ status_draft: 'Brouillon',
+ status_sending: 'Envoi en cours...',
+ status_sent: 'Envoyé',
+ status_failed: 'Échec',
+ subscribers: (n: number) => `${n} abonné${n !== 1 ? 's' : ''}`,
+ delivered: 'délivrés',
+ },
+
+ appointments: {
+ title: 'Rendez-vous',
+ subtitle: 'Réservations effectuées via vos chatbots',
+ empty_title: 'Aucun rendez-vous pour l\'instant',
+ empty_desc: 'Une fois que des clients réservent via votre chatbot, les rendez-vous apparaîtront ici.',
+ configure_hours: 'Configurer les horaires',
+ save_hours: 'Enregistrer les horaires',
+ status_pending: 'En attente',
+ status_confirmed: 'Confirmé',
+ status_cancelled: 'Annulé',
+ status_completed: 'Terminé',
+ confirm: 'Confirmer',
+ decline: 'Refuser',
+ complete: 'Terminer',
+ cancel: 'Annuler',
+ days: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
+ closed: 'Fermé',
+ open_from: 'Ouvert de',
+ to: 'à',
+ },
+
+ settings: {
+ title: 'Paramètres',
+ subscription: 'Abonnement',
+ reports: 'Rapports',
+ analytics: 'Analytiques',
+ leads: 'Prospects',
+ campaigns: 'Campagnes',
+ appointments: 'Rendez-vous',
+ appearance: 'Apparence',
+ theme_system: 'Système',
+ theme_light: 'Clair',
+ theme_dark: 'Sombre',
+ profile: 'Profil',
+ company_label: 'Nom de l\'entreprise',
+ company_placeholder: 'Acme Inc.',
+ language_label: 'Langue',
+ change_password: 'Modifier le mot de passe',
+ current_password: 'Mot de passe actuel',
+ new_password: 'Nouveau mot de passe',
+ save_profile: 'Enregistrer',
+ account: 'Compte',
+ sign_out: 'Se déconnecter',
+ sign_out_confirm: 'Voulez-vous vraiment vous déconnecter ?',
+ delete_account: 'Supprimer le compte',
+ delete_confirm: 'Cela supprimera définitivement votre compte et tous vos chatbots. Cette action est irréversible.',
+ renews: 'Renouvellement',
+ version: 'Contexta v1.0.0',
+ profile_updated: 'Profil mis à jour !',
+ update_failed: 'Échec de la mise à jour du profil',
+ company_required: "Le nom de l'entreprise est requis",
+ },
+
+ marketplace: {
+ title: 'Marketplace',
+ search_placeholder: 'Rechercher des chatbots...',
+ empty_title: 'Aucun chatbot trouvé',
+ empty_desc: 'Essayez d\'ajuster votre recherche ou vos filtres.',
+ chat_now: 'Démarrer →',
+ by: (name: string) => `par ${name}`,
+ conversations: (n: number) => `${n} conversations`,
+ rating: 'Votre note',
+ rate_submit: 'Soumettre',
+ login_to_rate: 'Connectez-vous pour noter',
+ },
+
+ guest: {
+ sign_in: 'Se connecter',
+ sign_up: 'Créer un compte',
+ or: 'ou',
+ },
+
+ common: {
+ loading: 'Chargement...',
+ cancel: 'Annuler',
+ delete: 'Supprimer',
+ save: 'Enregistrer',
+ close: 'Fermer',
+ confirm: 'Confirmer',
+ retry: 'Réessayer',
+ refresh: 'Actualiser',
+ error: 'Une erreur s\'est produite. Veuillez réessayer.',
+ },
+};
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
new file mode 100644
index 0000000..a171182
--- /dev/null
+++ b/src/i18n/index.ts
@@ -0,0 +1,11 @@
+import { useLanguageStore } from '../stores/languageStore';
+import { en } from './en';
+import { fr } from './fr';
+
+const translations = { en, fr };
+
+export function useTranslation() {
+ const language = useLanguageStore(s => s.language);
+ const t = translations[language] ?? en;
+ return { t, language };
+}
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
new file mode 100644
index 0000000..5bb7314
--- /dev/null
+++ b/src/navigation/AppNavigator.tsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { useColorScheme } from 'react-native';
+import { COLORS } from '../theme';
+import { CustomTabBar } from './CustomTabBar';
+import { useAuthStore } from '../stores/authStore';
+import {
+ AppTabParamList,
+ MarketplaceStackParamList,
+ DashboardStackParamList,
+ InboxStackParamList,
+ AccountStackParamList,
+} from './types';
+
+// Public screens
+import { MarketplaceScreen } from '../screens/marketplace/MarketplaceScreen';
+import { ChatbotDetailScreen } from '../screens/marketplace/ChatbotDetailScreen';
+import { PublicChatScreen } from '../screens/marketplace/PublicChatScreen';
+
+// Auth-required screens
+import { DashboardScreen } from '../screens/dashboard/DashboardScreen';
+import { ChatbotBuilderScreen } from '../screens/chatbots/ChatbotBuilderScreen';
+import { ChatPreviewScreen } from '../screens/chatbots/ChatPreviewScreen';
+import { InboxScreen } from '../screens/inbox/InboxScreen';
+import { ConversationScreen } from '../screens/inbox/ConversationScreen';
+import { SettingsScreen } from '../screens/settings/SettingsScreen';
+import { LeadsScreen } from '../screens/leads/LeadsScreen';
+import { AnalyticsScreen } from '../screens/analytics/AnalyticsScreen';
+import { CampaignsScreen } from '../screens/campaigns/CampaignsScreen';
+import { AppointmentsScreen } from '../screens/appointments/AppointmentsScreen';
+
+// Guest screen for unauthenticated users on protected tabs
+import { GuestScreen } from '../screens/GuestScreen';
+
+const Tab = createBottomTabNavigator();
+const MktStack = createNativeStackNavigator();
+const DashStack = createNativeStackNavigator();
+const InboxStack = createNativeStackNavigator();
+const AccStack = createNativeStackNavigator();
+
+// ─── Marketplace (always public) ─────────────────────────────────────────────
+
+function MarketplaceNavigator() {
+ return (
+
+
+
+ ({ title: route.params.chatbotName })}
+ />
+
+ );
+}
+
+// ─── Dashboard (requires auth) ────────────────────────────────────────────────
+
+function DashboardNavigator() {
+ const isAuthenticated = useAuthStore(s => s.isAuthenticated);
+ if (!isAuthenticated) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+ ({ title: route.params.chatbotName })}
+ />
+
+ );
+}
+
+// ─── Inbox (requires auth) ────────────────────────────────────────────────────
+
+function InboxNavigator() {
+ const isAuthenticated = useAuthStore(s => s.isAuthenticated);
+ if (!isAuthenticated) {
+ return (
+
+ );
+ }
+ return (
+
+
+ ({ title: route.params.chatbotName ?? 'Conversation' })}
+ />
+
+ );
+}
+
+// ─── Account (guest CTA or full settings) ────────────────────────────────────
+
+function AccountNavigator() {
+ const isAuthenticated = useAuthStore(s => s.isAuthenticated);
+ if (!isAuthenticated) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── Main Tab Navigator ───────────────────────────────────────────────────────
+
+export function AppNavigator() {
+ return (
+ }
+ screenOptions={{ headerShown: false }}>
+
+
+
+
+
+ );
+}
diff --git a/src/navigation/AuthNavigator.tsx b/src/navigation/AuthNavigator.tsx
new file mode 100644
index 0000000..a17dd1a
--- /dev/null
+++ b/src/navigation/AuthNavigator.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { AuthStackParamList } from './types';
+import { LoginScreen } from '../screens/auth/LoginScreen';
+import { SignupScreen } from '../screens/auth/SignupScreen';
+import { ForgotPasswordScreen } from '../screens/auth/ForgotPasswordScreen';
+
+const Stack = createNativeStackNavigator();
+
+export function AuthNavigator() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/navigation/CustomTabBar.tsx b/src/navigation/CustomTabBar.tsx
new file mode 100644
index 0000000..7978959
--- /dev/null
+++ b/src/navigation/CustomTabBar.tsx
@@ -0,0 +1,160 @@
+import React, { useRef } from 'react';
+import {
+ View,
+ Text,
+ TouchableWithoutFeedback,
+ Animated,
+ StyleSheet,
+ Platform,
+} from 'react-native';
+import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { COLORS, SPACING, RADIUS, FONT_SIZE, FONT, SHADOWS } from '../theme';
+import { useTheme } from '../theme';
+
+const TAB_CONFIG: Record = {
+ MarketplaceTab: { icon: '🧭', label: 'Explore' },
+ DashboardTab: { icon: '⊞', label: 'Chatbots' },
+ InboxTab: { icon: '✉', label: 'Inbox' },
+ AccountTab: { icon: '◎', label: 'Account' },
+};
+
+export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
+ const { theme, isDark } = useTheme();
+ const insets = useSafeAreaInsets();
+
+ return (
+
+
+ {state.routes.map((route, index) => {
+ const isFocused = state.index === index;
+ const config = TAB_CONFIG[route.name] ?? { icon: '•', label: route.name };
+
+ const onPress = () => {
+ const event = navigation.emit({ type: 'tabPress', target: route.key, canPreventDefault: true });
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.navigate(route.name);
+ }
+ };
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
+function TabItem({
+ icon,
+ label,
+ isFocused,
+ onPress,
+}: {
+ icon: string;
+ label: string;
+ isFocused: boolean;
+ onPress: () => void;
+}) {
+ const { theme } = useTheme();
+ const scale = useRef(new Animated.Value(1)).current;
+ const bgOpacity = useRef(new Animated.Value(isFocused ? 1 : 0)).current;
+
+ React.useEffect(() => {
+ Animated.timing(bgOpacity, {
+ toValue: isFocused ? 1 : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ }, [isFocused, bgOpacity]);
+
+ const onPressIn = () =>
+ Animated.spring(scale, { toValue: 0.88, useNativeDriver: true, speed: 60, bounciness: 0 }).start();
+
+ const onPressOut = () =>
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 6 }).start();
+
+ const pillBg = bgOpacity.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['rgba(99,102,241,0)', 'rgba(99,102,241,0.1)'],
+ });
+
+ return (
+
+
+
+ {icon}
+
+ {label}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ borderTopWidth: 1,
+ borderTopColor: 'transparent',
+ },
+ tabBar: {
+ flexDirection: 'row',
+ marginHorizontal: SPACING.md,
+ marginTop: SPACING.xs,
+ borderRadius: RADIUS.xl,
+ padding: SPACING.xs,
+ ...SHADOWS.md,
+ },
+ tabBarLight: {
+ backgroundColor: COLORS.white,
+ borderWidth: 1,
+ borderColor: COLORS.light.border,
+ ...Platform.select({
+ ios: { shadowColor: '#6366f1', shadowOpacity: 0.12, shadowRadius: 20, shadowOffset: { width: 0, height: 6 } },
+ android: { elevation: 8 },
+ }),
+ },
+ tabBarDark: {
+ backgroundColor: COLORS.dark.surface,
+ borderWidth: 1,
+ borderColor: COLORS.dark.border,
+ ...Platform.select({
+ ios: { shadowColor: '#000', shadowOpacity: 0.4, shadowRadius: 20, shadowOffset: { width: 0, height: 6 } },
+ android: { elevation: 8 },
+ }),
+ },
+ tabItem: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ pill: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: SPACING.sm,
+ paddingHorizontal: SPACING.sm,
+ borderRadius: RADIUS.lg,
+ width: '100%',
+ gap: 3,
+ },
+ icon: { fontSize: 22 },
+ label: { fontSize: FONT_SIZE.xs, letterSpacing: 0.2 },
+});
diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx
new file mode 100644
index 0000000..8b6685d
--- /dev/null
+++ b/src/navigation/RootNavigator.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { RootStackParamList } from './types';
+import { AppNavigator } from './AppNavigator';
+import { LoginScreen } from '../screens/auth/LoginScreen';
+import { SignupScreen } from '../screens/auth/SignupScreen';
+import { ForgotPasswordScreen } from '../screens/auth/ForgotPasswordScreen';
+import { useTheme } from '../theme';
+import { COLORS } from '../theme';
+
+const Stack = createNativeStackNavigator();
+
+export function RootNavigator() {
+ const { theme } = useTheme();
+
+ return (
+
+ {/* Tabs are always the base — marketplace is public */}
+
+
+ {/* Auth screens slide up as modals from any tab */}
+
+
+
+
+ );
+}
diff --git a/src/navigation/types.ts b/src/navigation/types.ts
new file mode 100644
index 0000000..32a5b89
--- /dev/null
+++ b/src/navigation/types.ts
@@ -0,0 +1,76 @@
+import type { NativeStackScreenProps } from '@react-navigation/native-stack';
+
+// Root stack — tabs always visible, auth screens are modals on top
+export type RootStackParamList = {
+ Main: undefined;
+ Login: undefined;
+ Signup: undefined;
+ ForgotPassword: undefined;
+};
+
+// App tabs
+export type AppTabParamList = {
+ MarketplaceTab: undefined;
+ DashboardTab: undefined;
+ InboxTab: undefined;
+ AccountTab: undefined;
+};
+
+// Marketplace stack (fully public)
+export type MarketplaceStackParamList = {
+ MarketplaceList: undefined;
+ MarketplaceDetail: { chatbotId: string };
+ PublicChat: { chatbotId: string; chatbotName: string };
+};
+
+// Dashboard stack (requires auth)
+export type DashboardStackParamList = {
+ ChatbotList: undefined;
+ ChatbotBuilder: { chatbotId?: string };
+ ChatPreview: { chatbotId: string; chatbotName: string };
+};
+
+// Inbox stack (requires auth)
+export type InboxStackParamList = {
+ InboxList: undefined;
+ Conversation: { conversationId: string; chatbotName?: string };
+};
+
+// Account stack (guest screen or settings)
+export type AccountStackParamList = {
+ AccountHome: undefined;
+ Leads: undefined;
+ Analytics: undefined;
+ Campaigns: undefined;
+ Appointments: undefined;
+};
+
+// Screen prop helpers
+export type RootScreenProps =
+ NativeStackScreenProps;
+
+export type MarketplaceScreenProps =
+ NativeStackScreenProps;
+
+export type DashboardScreenProps =
+ NativeStackScreenProps;
+
+export type InboxScreenProps =
+ NativeStackScreenProps;
+
+export type AccountScreenProps =
+ NativeStackScreenProps;
+
+// Auth screen props (kept for backward compat)
+export type AuthStackParamList = {
+ Login: undefined;
+ Signup: undefined;
+ ForgotPassword: undefined;
+};
+export type AuthScreenProps =
+ NativeStackScreenProps;
+
+// Legacy aliases
+export type SettingsStackParamList = AccountStackParamList;
+export type SettingsScreenProps =
+ AccountScreenProps;
diff --git a/src/screens/GuestScreen.tsx b/src/screens/GuestScreen.tsx
new file mode 100644
index 0000000..9dc9a13
--- /dev/null
+++ b/src/screens/GuestScreen.tsx
@@ -0,0 +1,131 @@
+import React, { useEffect, useRef } from 'react';
+import { View, Text, StyleSheet, Animated } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { RootStackParamList } from '../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../theme';
+import { useTheme } from '../theme';
+import { Button } from '../components/ui';
+
+interface GuestScreenProps {
+ icon?: string;
+ title?: string;
+ description?: string;
+}
+
+const FEATURES = [
+ { icon: '🤖', text: 'Build AI chatbots from your documents' },
+ { icon: '📊', text: 'Analytics, leads & conversation history' },
+ { icon: '🚀', text: 'Deploy to Telegram, WhatsApp & web' },
+ { icon: '🔌', text: 'Free plan available — no credit card needed' },
+];
+
+export function GuestScreen({
+ icon = '🔐',
+ title = 'Sign in to continue',
+ description = 'Create a free account to build and manage your AI chatbots.',
+}: GuestScreenProps) {
+ const { theme } = useTheme();
+ const navigation = useNavigation>();
+
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(24)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 400, useNativeDriver: true }),
+ Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 16, bounciness: 4 }),
+ ]).start();
+ }, []);
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+ C
+
+ Contexta
+
+
+ {/* Card */}
+
+ {icon}
+ {title}
+ {description}
+
+
+
+
+
+ {/* Features */}
+
+ {FEATURES.map((f, i) => (
+
+
+ {f.icon}
+
+ {f.text}
+
+ ))}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ container: {
+ flex: 1,
+ paddingHorizontal: SPACING.xl,
+ paddingVertical: SPACING.xl,
+ justifyContent: 'center',
+ gap: SPACING.xl,
+ },
+
+ logoRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: SPACING.md },
+ logoCircle: {
+ width: 52,
+ height: 52,
+ borderRadius: RADIUS.xl,
+ backgroundColor: COLORS.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ logoText: { color: COLORS.white, fontSize: 22, fontWeight: '800' },
+ appName: { ...TEXT.h2 },
+
+ card: {
+ borderRadius: RADIUS.xxl,
+ padding: SPACING.xxl,
+ borderWidth: 1,
+ alignItems: 'center',
+ gap: SPACING.md,
+ },
+ cardIcon: { fontSize: 44 },
+ cardTitle: { ...TEXT.h3, textAlign: 'center' },
+ cardDesc: { ...TEXT.body, textAlign: 'center', lineHeight: 22 },
+ btnGroup: { width: '100%', gap: SPACING.sm, marginTop: SPACING.xs },
+
+ features: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ overflow: 'hidden',
+ },
+ featureRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: SPACING.md,
+ paddingHorizontal: SPACING.lg,
+ gap: SPACING.md,
+ },
+ featureIcon: { width: 32, alignItems: 'center' },
+ featureText: { ...TEXT.small, flex: 1, lineHeight: 20 },
+});
diff --git a/src/screens/analytics/AnalyticsScreen.tsx b/src/screens/analytics/AnalyticsScreen.tsx
new file mode 100644
index 0000000..f8f995a
--- /dev/null
+++ b/src/screens/analytics/AnalyticsScreen.tsx
@@ -0,0 +1,254 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ RefreshControl,
+ TouchableOpacity,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Spinner, EmptyState } from '../../components/ui';
+import { analyticsAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+import { ChatbotAnalytics } from '../../types';
+
+export function AnalyticsScreen() {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['analytics-overview'],
+ queryFn: analyticsAPI.overview,
+ });
+
+ if (isLoading) return ;
+
+ const overview = data;
+ const chatbots: ChatbotAnalytics[] = overview?.chatbots ?? [];
+ const maxConvos = Math.max(...chatbots.map(c => c.conversations), 1);
+
+ return (
+
+
+ }>
+
+ {/* Header */}
+
+ {t.analytics.title}
+ {t.analytics.subtitle}
+
+
+ {/* Summary grid */}
+
+
+
+
+
+
+
+ {/* Per-chatbot breakdown */}
+ {chatbots.length > 0 && (
+ <>
+ {t.analytics.by_chatbot}
+ {chatbots.map(bot => (
+
+ ))}
+ >
+ )}
+
+ {chatbots.length === 0 && (
+ 📊}
+ title={t.analytics.empty_title}
+ description={t.analytics.empty_desc}
+ />
+ )}
+
+
+ );
+}
+
+function SummaryCard({
+ label,
+ value,
+ icon,
+ color,
+ theme,
+}: {
+ label: string;
+ value: number;
+ icon: string;
+ color: string;
+ theme: any;
+}) {
+ return (
+
+
+ {icon}
+
+ {value.toLocaleString()}
+ {label}
+
+ );
+}
+
+function BotCard({
+ bot,
+ maxConvos,
+ theme,
+}: {
+ bot: ChatbotAnalytics;
+ maxConvos: number;
+ theme: any;
+}) {
+ const { t } = useTranslation();
+ const [showGaps, setShowGaps] = React.useState(false);
+ const pct = maxConvos > 0 ? (bot.conversations / maxConvos) * 100 : 0;
+ const confidence = ((bot.avg_confidence ?? 0) * 100).toFixed(0);
+ const unanswered = bot.unanswered_queries ?? [];
+
+ return (
+
+
+
+
+ {bot.chatbot_name[0]?.toUpperCase()}
+
+
+ {bot.chatbot_name}
+
+
+
+
+
+
+
+
+ {/* Bar */}
+
+
+
+
+ {bot.conversations} {t.analytics.conversations.toLowerCase()}
+
+
+ {/* Gap analysis */}
+ {unanswered.length > 0 && (
+
+ setShowGaps(v => !v)}>
+
+
+ {t.analytics.unanswered(unanswered.length)}
+
+
+
+ {showGaps ? t.analytics.hide_gaps : t.analytics.show_gaps}
+
+
+ {showGaps && unanswered.map((q, i) => (
+
+ {i + 1}.
+ {q.query}
+
+ {q.count}×
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+function StatChip({ label, value, theme }: { label: string; value: number | string; theme: any }) {
+ return (
+
+ {value}
+ {label}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ scroll: { padding: SPACING.lg, gap: SPACING.lg, paddingBottom: SPACING.xxxl },
+
+ pageHeader: { gap: 4 },
+ pageTitle: { ...TEXT.h3 },
+ pageSub: { ...TEXT.small },
+
+ summaryGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm },
+ summaryCard: {
+ width: '47.5%',
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.md,
+ gap: SPACING.xs,
+ alignItems: 'flex-start',
+ },
+ summaryIconBg: {
+ width: 40,
+ height: 40,
+ borderRadius: RADIUS.md,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ summaryIcon: { fontSize: 20 },
+ summaryValue: { fontSize: FONT_SIZE.xxl, fontWeight: '800', marginTop: SPACING.xs },
+ summaryLabel: { ...TEXT.small },
+
+ sectionTitle: { ...TEXT.h4 },
+
+ botCard: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.md,
+ },
+ botCardHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ botAvatar: { width: 38, height: 38, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center' },
+ botAvatarText: { fontSize: FONT_SIZE.md, fontWeight: '800' },
+ botName: { ...TEXT.bodyM, flex: 1 },
+
+ botStats: { flexDirection: 'row', gap: SPACING.sm },
+ statChip: { flex: 1, alignItems: 'center', borderRadius: RADIUS.md, paddingVertical: SPACING.sm, gap: 2 },
+ statValue: { fontSize: FONT_SIZE.lg, fontWeight: '700' },
+ statLabel: { fontSize: FONT_SIZE.xs, textAlign: 'center' },
+
+ barTrack: { height: 8, borderRadius: RADIUS.full, overflow: 'hidden' },
+ barFill: { height: '100%', borderRadius: RADIUS.full },
+ barLabel: { ...TEXT.caption, marginTop: 4 },
+});
+
+const gapStyles = StyleSheet.create({
+ section: { borderTopWidth: 1, paddingTop: SPACING.md, gap: SPACING.xs },
+ toggle: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
+ badge: { borderRadius: RADIUS.full, paddingVertical: 3, paddingHorizontal: SPACING.sm },
+ badgeText: { fontSize: FONT_SIZE.xs, fontWeight: '700' },
+ toggleLabel: { fontSize: FONT_SIZE.xs, fontWeight: '500' },
+ queryRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: SPACING.xs,
+ paddingVertical: SPACING.xs,
+ borderBottomWidth: 1,
+ },
+ queryNum: { fontSize: FONT_SIZE.xs, width: 18, marginTop: 1 },
+ queryText: { flex: 1, fontSize: FONT_SIZE.sm, lineHeight: 18 },
+ countBadge: { borderRadius: RADIUS.sm, paddingHorizontal: SPACING.xs, paddingVertical: 2 },
+ countText: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+});
diff --git a/src/screens/appointments/AppointmentsScreen.tsx b/src/screens/appointments/AppointmentsScreen.tsx
new file mode 100644
index 0000000..a90e943
--- /dev/null
+++ b/src/screens/appointments/AppointmentsScreen.tsx
@@ -0,0 +1,608 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Alert,
+ RefreshControl,
+ Modal,
+ Switch,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Spinner, EmptyState } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { appointmentsAPI, chatbotsAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+import type { Appointment, Chatbot, BusinessHoursEntry } from '../../types';
+
+const STATUS_COLORS_APPT: Record = {
+ pending: { color: '#d97706', bg: '#fef3c7', emoji: '⏳' },
+ confirmed: { color: '#16a34a', bg: '#dcfce7', emoji: '✅' },
+ cancelled: { color: '#dc2626', bg: '#fee2e2', emoji: '❌' },
+ completed: { color: '#6b7280', bg: '#f3f4f6', emoji: '✔' },
+};
+
+// Day labels are sourced from translations at render time
+
+const DEFAULT_HOURS: BusinessHoursEntry[] = Array.from({ length: 7 }, (_, i) => ({
+ day_of_week: i,
+ is_open: i < 5,
+ open_time: '09:00',
+ close_time: '17:00',
+ slot_duration_minutes: 60,
+}));
+
+// ── Business Hours Modal ──────────────────────────────────────────────────────
+
+function BusinessHoursModal({
+ visible,
+ chatbotId,
+ onClose,
+}: {
+ visible: boolean;
+ chatbotId: string;
+ onClose: () => void;
+}) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const qc = useQueryClient();
+ const [hours, setHours] = useState(DEFAULT_HOURS);
+
+ const { isLoading } = useQuery({
+ queryKey: ['business-hours', chatbotId],
+ queryFn: () => appointmentsAPI.getHours(chatbotId),
+ enabled: visible && !!chatbotId,
+ onSuccess: (data: BusinessHoursEntry[]) => {
+ if (data && data.length > 0) {
+ const merged = DEFAULT_HOURS.map(d => {
+ const found = data.find(h => h.day_of_week === d.day_of_week);
+ return found ? { ...d, ...found } : d;
+ });
+ setHours(merged);
+ }
+ },
+ } as any);
+
+ const save = useMutation({
+ mutationFn: () => appointmentsAPI.saveHours(chatbotId, hours),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['business-hours', chatbotId] });
+ toast.success(t.appointments.save_hours);
+ onClose();
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? t.common.error),
+ });
+
+ const toggle = (idx: number, field: keyof BusinessHoursEntry, value: any) => {
+ setHours(prev => prev.map((h, i) => i === idx ? { ...h, [field]: value } : h));
+ };
+
+ const SLOT_OPTIONS = [
+ { label: '15 min', value: 15 },
+ { label: '30 min', value: 30 },
+ { label: '1 hr', value: 60 },
+ { label: '1.5 hr', value: 90 },
+ { label: '2 hr', value: 120 },
+ ];
+
+ return (
+
+
+
+
+ Business Hours
+
+ ✕
+
+
+
+
+ Set when appointments can be booked for this chatbot.
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {hours.map((h, i) => (
+
+
+
+ {t.appointments.days[i]}
+
+ toggle(i, 'is_open', v)}
+ trackColor={{ false: theme.border, true: COLORS.primary }}
+ thumbColor={COLORS.white}
+ />
+
+
+ {h.is_open && (
+
+
+ {h.open_time}
+
+ →
+
+ {h.close_time}
+
+
+ {SLOT_OPTIONS.map(opt => (
+ toggle(i, 'slot_duration_minutes', opt.value)}
+ style={[
+ styles.slotChip,
+ {
+ backgroundColor: h.slot_duration_minutes === opt.value ? COLORS.primary : theme.bgSecondary,
+ borderColor: h.slot_duration_minutes === opt.value ? COLORS.primary : theme.border,
+ },
+ ]}>
+
+ {opt.label}
+
+
+ ))}
+
+
+ )}
+
+ ))}
+
+ )}
+
+ save.mutate()}
+ loading={save.isPending}
+ fullWidth
+ />
+
+
+
+ );
+}
+
+// ── Appointment Card ──────────────────────────────────────────────────────────
+
+function AppointmentCard({
+ appt,
+ onUpdateStatus,
+ updating,
+ theme,
+}: {
+ appt: Appointment;
+ onUpdateStatus: (id: string, status: string) => void;
+ updating: boolean;
+ theme: any;
+}) {
+ const { t } = useTranslation();
+ const STATUS_LABELS: Record = {
+ pending: t.appointments.status_pending,
+ confirmed: t.appointments.status_confirmed,
+ cancelled: t.appointments.status_cancelled,
+ completed: t.appointments.status_completed,
+ };
+ const sc = STATUS_COLORS_APPT[appt.status] ?? STATUS_COLORS_APPT.pending;
+ const statusLabel = STATUS_LABELS[appt.status] ?? appt.status;
+ const slotDate = new Date(appt.slot_start);
+ const slotEnd = new Date(appt.slot_end);
+ const isToday = slotDate.toDateString() === new Date().toDateString();
+
+ const monthLabel = slotDate.toLocaleDateString(undefined, { month: 'short' });
+ const dayNum = slotDate.getDate();
+ const timeRange = `${slotDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} – ${slotEnd.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`;
+
+ return (
+
+
+ {/* Date block */}
+
+ {monthLabel}
+ {dayNum}
+ {isToday && Today}
+
+
+ {/* Details */}
+
+
+
+ {appt.customer_name}
+
+
+ {sc.emoji} {statusLabel}
+
+
+
+ {appt.service && (
+ {appt.service}
+ )}
+
+
+ 🕐 {timeRange}
+ 📞 {appt.customer_contact}
+
+
+ {appt.notes && (
+
+ "{appt.notes}"
+
+ )}
+
+ {/* Actions */}
+ {appt.status === 'pending' && (
+
+ onUpdateStatus(appt.id, 'confirmed')}
+ disabled={updating}>
+ ✅ {t.appointments.confirm}
+
+ onUpdateStatus(appt.id, 'cancelled')}
+ disabled={updating}>
+ ❌ {t.appointments.decline}
+
+
+ )}
+
+ {appt.status === 'confirmed' && (
+
+ onUpdateStatus(appt.id, 'completed')}
+ disabled={updating}>
+ ✔ {t.appointments.complete}
+
+ onUpdateStatus(appt.id, 'cancelled')}
+ disabled={updating}>
+ ❌ {t.appointments.cancel}
+
+
+ )}
+
+ {appt.status === 'cancelled' && (
+ onUpdateStatus(appt.id, 'pending')}
+ disabled={updating}>
+ ↩ {t.appointments.status_pending}
+
+ )}
+
+
+
+ );
+}
+
+// ── Main Screen ───────────────────────────────────────────────────────────────
+
+export function AppointmentsScreen() {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const qc = useQueryClient();
+ const [chatbotFilter, setChatbotFilter] = useState('');
+ const [statusFilter, setStatusFilter] = useState('');
+ const [hoursModalChatbotId, setHoursModalChatbotId] = useState(null);
+
+ const { data: chatbots = [] } = useQuery({
+ queryKey: ['chatbots'],
+ queryFn: chatbotsAPI.list,
+ });
+
+ const { data: appointments = [], isLoading, refetch, error } = useQuery({
+ queryKey: ['appointments', chatbotFilter, statusFilter],
+ queryFn: () => appointmentsAPI.list({
+ chatbot_id: chatbotFilter || undefined,
+ status: statusFilter || undefined,
+ }),
+ retry: false,
+ });
+
+ const updateStatus = useMutation({
+ mutationFn: ({ id, status }: { id: string; status: string }) =>
+ appointmentsAPI.updateStatus(id, status),
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['appointments'] }),
+ });
+
+ const isPlanError = (error as any)?.response?.status === 402;
+
+ const bookingEnabledChatbots = chatbots.filter(c => c.booking_enabled);
+
+ const now = new Date();
+ const upcoming = appointments.filter(a => new Date(a.slot_start) >= now && a.status !== 'cancelled');
+ const today = appointments.filter(a => new Date(a.slot_start).toDateString() === now.toDateString());
+
+ const STATUS_FILTERS = [
+ { value: '', label: t.inbox.filter_all },
+ { value: 'pending', label: `⏳ ${t.appointments.status_pending}` },
+ { value: 'confirmed', label: `✅ ${t.appointments.status_confirmed}` },
+ { value: 'cancelled', label: `❌ ${t.appointments.status_cancelled}` },
+ { value: 'completed', label: `✔ ${t.appointments.status_completed}` },
+ ];
+
+ if (isPlanError) {
+ return (
+
+
+ 🔒
+ Appointments Require Starter+
+
+ Upgrade your plan to enable appointment booking through your chatbots.
+
+
+
+ );
+ }
+
+ return (
+
+ }>
+
+ {/* Header */}
+
+
+ {t.appointments.title}
+
+ {t.appointments.subtitle}
+
+
+ {bookingEnabledChatbots.length > 0 && (
+ setHoursModalChatbotId(bookingEnabledChatbots[0].id)}>
+ ⏱ {t.appointments.configure_hours}
+
+ )}
+
+
+ {/* Setup prompt when no chatbots have booking enabled */}
+ {!isLoading && bookingEnabledChatbots.length === 0 && (
+
+ 📅
+
+ Enable booking on a chatbot
+
+ Go to your chatbot's settings and enable the booking feature to start accepting appointments.
+
+
+
+ )}
+
+ {/* Stats */}
+ {appointments.length > 0 && (
+
+ {[
+ { label: t.analytics.conversations, value: today.length, emoji: '📆', color: '#2563eb' },
+ { label: t.appointments.subtitle, value: upcoming.length, emoji: '🗓', color: COLORS.primary },
+ { label: t.appointments.status_confirmed, value: appointments.filter(a => a.status === 'confirmed').length, emoji: '✅', color: '#16a34a' },
+ { label: t.appointments.status_pending, value: appointments.filter(a => a.status === 'pending').length, emoji: '⏳', color: '#d97706' },
+ ].map(s => (
+
+ {s.emoji}
+ {s.value}
+ {s.label}
+
+ ))}
+
+ )}
+
+ {/* Filters */}
+
+ {/* Status filter */}
+
+ {STATUS_FILTERS.map(f => (
+ setStatusFilter(f.value)}
+ style={[
+ styles.filterChip,
+ {
+ backgroundColor: statusFilter === f.value ? COLORS.primary : theme.bgSecondary,
+ borderColor: statusFilter === f.value ? COLORS.primary : theme.border,
+ },
+ ]}>
+
+ {f.label}
+
+
+ ))}
+
+
+ {/* Chatbot filter */}
+ {bookingEnabledChatbots.length > 1 && (
+
+ {[{ id: '', name: 'All chatbots' }, ...bookingEnabledChatbots].map(c => (
+ setChatbotFilter(c.id)}
+ style={[
+ styles.filterChip,
+ {
+ backgroundColor: chatbotFilter === c.id ? COLORS.primaryLight : theme.bgSecondary,
+ borderColor: chatbotFilter === c.id ? COLORS.primary : theme.border,
+ },
+ ]}>
+
+ {c.name}
+
+
+ ))}
+
+ )}
+
+
+ {/* Appointments list */}
+ {isLoading ? (
+
+ ) : appointments.length === 0 ? (
+ 📅}
+ title={t.appointments.empty_title}
+ description={t.appointments.empty_desc}
+ />
+ ) : (
+
+ {appointments.map(appt => (
+ updateStatus.mutate({ id, status })}
+ updating={updateStatus.isPending}
+ theme={theme}
+ />
+ ))}
+
+ )}
+
+
+ {hoursModalChatbotId && (
+ setHoursModalChatbotId(null)}
+ />
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ scroll: { padding: SPACING.lg, gap: SPACING.lg, paddingBottom: SPACING.xxxl },
+
+ headerRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: SPACING.md },
+ pageTitle: { ...TEXT.h3 },
+ pageSub: { ...TEXT.small, marginTop: 2 },
+ hoursBtn: {
+ borderWidth: 1,
+ borderRadius: RADIUS.lg,
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.xs,
+ flexShrink: 0,
+ },
+ hoursBtnText: { ...TEXT.small },
+
+ setupCard: {
+ borderWidth: 1,
+ borderRadius: RADIUS.xl,
+ padding: SPACING.lg,
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: SPACING.md,
+ },
+ setupEmoji: { fontSize: 28 },
+ setupText: { flex: 1, gap: 4 },
+ setupTitle: { ...TEXT.bodyM },
+ setupDesc: { ...TEXT.small },
+
+ statsGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm },
+ statCard: {
+ width: '47.5%',
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.md,
+ alignItems: 'center',
+ gap: 4,
+ },
+ statEmoji: { fontSize: 22 },
+ statValue: { fontSize: FONT_SIZE.xxl, fontWeight: '800' },
+ statLabel: { ...TEXT.caption },
+
+ filtersSection: { gap: SPACING.sm },
+ filterRow: {},
+ filterChip: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.xs,
+ borderRadius: RADIUS.full,
+ borderWidth: 1,
+ marginRight: SPACING.xs,
+ },
+ filterText: { ...TEXT.small },
+
+ list: { gap: SPACING.md },
+
+ apptCard: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ },
+ apptRow: { flexDirection: 'row', gap: SPACING.md },
+ dateBlock: {
+ width: 56,
+ borderRadius: RADIUS.lg,
+ padding: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ dateMonth: { fontSize: FONT_SIZE.xs, fontWeight: '700', textTransform: 'uppercase' },
+ dateDay: { fontSize: FONT_SIZE.xxl, fontWeight: '900', lineHeight: 28 },
+ todayLabel: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+
+ apptDetails: { flex: 1, gap: SPACING.xs },
+ apptTopRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: SPACING.sm },
+ customerName: { ...TEXT.bodyM, flex: 1 },
+ statusBadge: { borderRadius: RADIUS.full, paddingHorizontal: SPACING.sm, paddingVertical: 3, flexShrink: 0 },
+ statusText: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+ serviceText: { ...TEXT.small },
+ metaRow: { gap: 4 },
+ metaText: { ...TEXT.caption },
+ notesText: { ...TEXT.caption, fontStyle: 'italic' },
+
+ actionRow: { flexDirection: 'row', gap: SPACING.sm, marginTop: SPACING.xs },
+ actionBtn: { borderRadius: RADIUS.md, paddingVertical: SPACING.xs, paddingHorizontal: SPACING.md, alignItems: 'center' },
+ actionBtnText: { fontWeight: '600', fontSize: FONT_SIZE.sm },
+ confirmBtn: { backgroundColor: COLORS.success },
+ confirmBtnText: { color: COLORS.white, fontWeight: '600', fontSize: FONT_SIZE.sm },
+ declineBtn: { backgroundColor: '#fee2e210', borderWidth: 1 },
+ declineBtnText: { fontWeight: '600', fontSize: FONT_SIZE.sm },
+
+ // Modal
+ modalSafe: { flex: 1 },
+ modalScroll: { padding: SPACING.xl, gap: SPACING.md, paddingBottom: SPACING.xxxl },
+ modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: SPACING.sm },
+ modalTitle: { ...TEXT.h3 },
+ closeBtn: { width: 32, height: 32, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center' },
+ closeBtnText: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ modalDesc: { ...TEXT.small, marginBottom: SPACING.sm },
+
+ daysList: { gap: 0 },
+ dayRow: { paddingVertical: SPACING.md, borderBottomWidth: 1, gap: SPACING.sm },
+ dayToggleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
+ dayLabel: { ...TEXT.bodyM, width: 40 },
+ timeRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, flexWrap: 'wrap' },
+ timeInput: { borderWidth: 1, borderRadius: RADIUS.md, paddingHorizontal: SPACING.sm, paddingVertical: SPACING.xs },
+ timeText: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ timeSep: { fontSize: FONT_SIZE.sm },
+ slotChip: {
+ paddingHorizontal: SPACING.sm,
+ paddingVertical: 4,
+ borderRadius: RADIUS.full,
+ borderWidth: 1,
+ marginRight: SPACING.xs,
+ },
+ slotText: { fontSize: FONT_SIZE.xs, fontWeight: '500' },
+
+ // Upgrade
+ upgradeContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: SPACING.xxxl, gap: SPACING.md },
+ upgradeEmoji: { fontSize: 48 },
+ upgradeTitle: { ...TEXT.h3, textAlign: 'center' },
+ upgradeDesc: { ...TEXT.body, textAlign: 'center' },
+});
diff --git a/src/screens/auth/ForgotPasswordScreen.tsx b/src/screens/auth/ForgotPasswordScreen.tsx
new file mode 100644
index 0000000..e5af3e8
--- /dev/null
+++ b/src/screens/auth/ForgotPasswordScreen.tsx
@@ -0,0 +1,145 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ TouchableOpacity,
+ Animated,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { RootScreenProps } from '../../navigation/types';
+import { COLORS, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Input } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { authAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+
+type Props = RootScreenProps<'ForgotPassword'>;
+
+export function ForgotPasswordScreen({ navigation }: Props) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const [email, setEmail] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [sent, setSent] = useState(false);
+
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(32)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 350, useNativeDriver: true }),
+ Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 14, bounciness: 3 }),
+ ]).start();
+ }, []);
+
+ const handleSubmit = async () => {
+ if (!email.trim()) {
+ toast.error(t.auth.email + ' is required');
+ return;
+ }
+ setLoading(true);
+ try {
+ await authAPI.forgotPassword(email.trim());
+ setSent(true);
+ } catch {
+ toast.error(t.common.error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ navigation.goBack()} style={styles.back} hitSlop={{ top: 8, bottom: 8 }}>
+ ← {t.auth.back_to_signin}
+
+
+
+ {sent ? (
+
+ ✉️
+ {t.auth.check_email_title}
+
+ {t.auth.check_email_desc}{'\n'}
+ {email}
+
+ navigation.navigate('Login')}
+ />
+
+ ) : (
+ <>
+ 🔐
+ {t.auth.forgot_title}
+
+ {t.auth.forgot_subtitle}
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ kav: { flex: 1 },
+ scroll: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: SPACING.xl, paddingVertical: SPACING.xxxl },
+ content: { gap: SPACING.xl },
+
+ back: { alignSelf: 'flex-start' },
+ backText: { ...TEXT.bodyM },
+
+ card: {
+ borderRadius: RADIUS.xxl,
+ padding: SPACING.xxl,
+ borderWidth: 1,
+ gap: SPACING.lg,
+ alignItems: 'center',
+ },
+ headerIcon: { fontSize: 44, textAlign: 'center' },
+ cardTitle: { ...TEXT.h3, textAlign: 'center' },
+ cardSub: { ...TEXT.body, textAlign: 'center', lineHeight: 22 },
+ fields: { width: '100%', gap: SPACING.md },
+
+ successContent: { alignItems: 'center', gap: SPACING.lg, width: '100%' },
+ successIcon: { fontSize: 56, textAlign: 'center' },
+});
diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx
new file mode 100644
index 0000000..fe695c0
--- /dev/null
+++ b/src/screens/auth/LoginScreen.tsx
@@ -0,0 +1,169 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ TouchableOpacity,
+ Animated,
+ Image,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQueryClient } from '@tanstack/react-query';
+import { RootScreenProps } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Input, SecureInput } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { authAPI } from '../../services/api';
+import { useAuthStore } from '../../stores/authStore';
+import { useTranslation } from '../../i18n';
+
+type Props = RootScreenProps<'Login'>;
+
+export function LoginScreen({ navigation }: Props) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const setAuth = useAuthStore(s => s.setAuth);
+ const qc = useQueryClient();
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
+
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(32)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 350, useNativeDriver: true }),
+ Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 14, bounciness: 3 }),
+ ]).start();
+ }, []);
+
+ const validate = () => {
+ const e: typeof errors = {};
+ if (!email.trim()) e.email = t.auth.email + ' is required';
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) e.email = 'Invalid email';
+ if (!password) e.password = t.auth.password + ' is required';
+ setErrors(e);
+ return Object.keys(e).length === 0;
+ };
+
+ const handleLogin = async () => {
+ if (!validate()) return;
+ setLoading(true);
+ try {
+ const data = await authAPI.login(email.trim(), password);
+ setAuth(data.user, data.access_token);
+ qc.clear();
+ navigation.goBack();
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? t.common.error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Form */}
+
+ {t.auth.login_title} 👋
+
+ {t.auth.login_subtitle}
+
+
+
+
+
+
+ navigation.navigate('ForgotPassword')}
+ style={styles.forgotRow}
+ hitSlop={{ top: 8, bottom: 8 }}>
+ {t.auth.forgot_password}
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ {t.auth.no_account}{' '}
+
+ navigation.navigate('Signup')}>
+ {t.auth.sign_up_free}
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ kav: { flex: 1 },
+ scroll: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: SPACING.xl, paddingVertical: SPACING.xxxl },
+ content: { gap: SPACING.xxl },
+
+ brand: { alignItems: 'center' },
+ logoImage: { width: 200, height: 100 },
+
+ card: {
+ borderRadius: RADIUS.xxl,
+ padding: SPACING.xxl,
+ borderWidth: 1,
+ gap: SPACING.lg,
+ },
+ formTitle: { ...TEXT.h3 },
+ formSub: { ...TEXT.body, marginTop: -SPACING.sm },
+ fields: { gap: SPACING.xs },
+ forgotRow: { alignSelf: 'flex-end', marginTop: -SPACING.xs, marginBottom: SPACING.xs },
+ forgotText: { ...TEXT.smallM },
+
+ footer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
+ footerText: { ...TEXT.body },
+ footerLink: { ...TEXT.bodyM },
+});
diff --git a/src/screens/auth/SignupScreen.tsx b/src/screens/auth/SignupScreen.tsx
new file mode 100644
index 0000000..29c9d7a
--- /dev/null
+++ b/src/screens/auth/SignupScreen.tsx
@@ -0,0 +1,183 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ TouchableOpacity,
+ Animated,
+ Image,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQueryClient } from '@tanstack/react-query';
+import { RootScreenProps } from '../../navigation/types';
+import { COLORS, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Input, SecureInput } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { authAPI } from '../../services/api';
+import { useAuthStore } from '../../stores/authStore';
+import { useTranslation } from '../../i18n';
+
+type Props = RootScreenProps<'Signup'>;
+
+export function SignupScreen({ navigation }: Props) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const setAuth = useAuthStore(s => s.setAuth);
+ const qc = useQueryClient();
+
+ const [companyName, setCompanyName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState>({});
+
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(32)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, { toValue: 1, duration: 350, useNativeDriver: true }),
+ Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, speed: 14, bounciness: 3 }),
+ ]).start();
+ }, []);
+
+ const validate = () => {
+ const e: Record = {};
+ if (!companyName.trim()) e.companyName = t.settings.company_required;
+ if (!email.trim()) e.email = t.auth.email + ' is required';
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) e.email = 'Invalid email';
+ if (!password) e.password = t.auth.password + ' is required';
+ else if (password.length < 8) e.password = 'At least 8 characters';
+ if (password !== confirmPassword) e.confirmPassword = 'Passwords do not match';
+ setErrors(e);
+ return Object.keys(e).length === 0;
+ };
+
+ const handleSignup = async () => {
+ if (!validate()) return;
+ setLoading(true);
+ try {
+ const data = await authAPI.signup(email.trim(), password, companyName.trim());
+ setAuth(data.user, data.access_token);
+ qc.clear();
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? t.common.error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Logo */}
+
+
+
+
+ {/* Form */}
+
+ {t.auth.signup_title}
+
+ {t.auth.signup_subtitle}
+
+
+
+
+
+
+
+
+
+
+
+ By signing up, you agree to our Terms of Service and Privacy Policy.
+
+
+
+
+ {/* Footer */}
+
+
+ {t.auth.already_account}{' '}
+
+ navigation.navigate('Login')}>
+ {t.auth.sign_in}
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ kav: { flex: 1 },
+ scroll: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: SPACING.xl, paddingVertical: SPACING.xxxl },
+ content: { gap: SPACING.xxl },
+
+ brand: { alignItems: 'center' },
+ logoImage: { width: 200, height: 100 },
+
+ card: {
+ borderRadius: RADIUS.xxl,
+ padding: SPACING.xxl,
+ borderWidth: 1,
+ gap: SPACING.lg,
+ },
+ formTitle: { ...TEXT.h3 },
+ formSub: { ...TEXT.body, marginTop: -SPACING.sm },
+ fields: { gap: SPACING.xs },
+ terms: { ...TEXT.caption, textAlign: 'center', lineHeight: 18 },
+
+ footer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
+ footerText: { ...TEXT.body },
+ footerLink: { ...TEXT.bodyM },
+});
diff --git a/src/screens/campaigns/CampaignsScreen.tsx b/src/screens/campaigns/CampaignsScreen.tsx
new file mode 100644
index 0000000..1cc08be
--- /dev/null
+++ b/src/screens/campaigns/CampaignsScreen.tsx
@@ -0,0 +1,498 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ FlatList,
+ TextInput,
+ TouchableOpacity,
+ Alert,
+ RefreshControl,
+ Modal,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Spinner, EmptyState } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { campaignsAPI, chatbotsAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+import type { Campaign, Chatbot } from '../../types';
+
+const STATUS_COLORS: Record = {
+ draft: { color: '#6b7280', bg: '#f3f4f6', emoji: '📝' },
+ sending: { color: '#2563eb', bg: '#dbeafe', emoji: '⏳' },
+ sent: { color: '#16a34a', bg: '#dcfce7', emoji: '✅' },
+ failed: { color: '#dc2626', bg: '#fee2e2', emoji: '❌' },
+};
+
+// ── New Campaign Modal ────────────────────────────────────────────────────────
+
+function NewCampaignModal({
+ visible,
+ chatbots,
+ onClose,
+ onCreate,
+ creating,
+}: {
+ visible: boolean;
+ chatbots: Chatbot[];
+ onClose: () => void;
+ onCreate: (data: { chatbot_id: string; title: string; message: string }) => void;
+ creating: boolean;
+}) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const [chatbotIdx, setChatbotIdx] = useState(0);
+ const [title, setTitle] = useState('');
+ const [message, setMessage] = useState('');
+
+ const handleCreate = () => {
+ if (!title.trim() || !message.trim()) return;
+ const chatbot_id = chatbots[chatbotIdx]?.id ?? '';
+ if (!chatbot_id) return;
+ onCreate({ chatbot_id, title: title.trim(), message: message.trim() });
+ setTitle('');
+ setMessage('');
+ setChatbotIdx(0);
+ };
+
+ return (
+
+
+
+
+ {t.campaigns.new}
+
+ ✕
+
+
+
+ {t.campaigns.select_chatbot}
+ {chatbots.length === 0 ? (
+
+ {t.campaigns.empty_desc}
+
+ ) : (
+
+ {chatbots.map((c, idx) => (
+ setChatbotIdx(idx)}
+ style={[
+ styles.chip,
+ {
+ backgroundColor: idx === chatbotIdx ? COLORS.primary : theme.bgSecondary,
+ borderColor: idx === chatbotIdx ? COLORS.primary : theme.border,
+ },
+ ]}>
+
+ {c.name}
+
+
+ ))}
+
+ )}
+
+ {t.campaigns.campaign_name}
+
+
+ {t.campaigns.message_label}
+
+ {message.length} characters
+
+
+
+
+
+
+
+
+ );
+}
+
+// ── Campaign Card ─────────────────────────────────────────────────────────────
+
+function CampaignCard({
+ campaign,
+ chatbotName,
+ onSend,
+ onDelete,
+ theme,
+}: {
+ campaign: Campaign;
+ chatbotName: string;
+ onSend: () => void;
+ onDelete: () => void;
+ theme: any;
+}) {
+ const { t } = useTranslation();
+ const STATUS_LABELS: Record = {
+ draft: t.campaigns.status_draft,
+ sending: t.campaigns.status_sending,
+ sent: t.campaigns.status_sent,
+ failed: t.campaigns.status_failed,
+ };
+ const sc = STATUS_COLORS[campaign.status] ?? STATUS_COLORS.draft;
+ const statusLabel = STATUS_LABELS[campaign.status] ?? campaign.status;
+ const createdAt = campaign.created_at ? new Date(campaign.created_at).toLocaleDateString() : '';
+ const sentAt = campaign.sent_at ? new Date(campaign.sent_at).toLocaleDateString() : '';
+
+ return (
+
+
+ {campaign.title}
+
+ {sc.emoji} {statusLabel}
+
+
+
+
+ {chatbotName} · {createdAt}
+
+
+
+
+ {campaign.message}
+
+
+
+
+
+ 👥 {t.campaigns.subscribers(campaign.recipients_count)}
+
+ {campaign.status === 'sent' && (
+
+ ✅ {campaign.sent_count} {t.campaigns.delivered} · {sentAt}
+
+ )}
+
+
+ {campaign.status === 'draft' && (
+
+
+ 📤 {t.campaigns.send}
+
+
+ 🗑 {t.campaigns.delete}
+
+
+ )}
+
+ {campaign.status === 'sent' && (
+
+ 🗑 {t.campaigns.delete}
+
+ )}
+
+ );
+}
+
+// ── Main Screen ───────────────────────────────────────────────────────────────
+
+export function CampaignsScreen() {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const qc = useQueryClient();
+ const [showForm, setShowForm] = useState(false);
+ const [chatbotFilter, setChatbotFilter] = useState('');
+
+ const { data: chatbots = [] } = useQuery({
+ queryKey: ['chatbots'],
+ queryFn: chatbotsAPI.list,
+ });
+
+ const { data: campaigns = [], isLoading, refetch, error } = useQuery({
+ queryKey: ['campaigns', chatbotFilter],
+ queryFn: () => campaignsAPI.list(chatbotFilter ? { chatbot_id: chatbotFilter } : undefined),
+ retry: false,
+ refetchInterval: 8000,
+ });
+
+ const createCampaign = useMutation({
+ mutationFn: campaignsAPI.create,
+ onSuccess: () => {
+ setShowForm(false);
+ qc.invalidateQueries({ queryKey: ['campaigns'] });
+ toast.success(t.campaigns.create);
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? t.common.error),
+ });
+
+ const sendCampaign = useMutation({
+ mutationFn: (id: string) => campaignsAPI.send(id),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['campaigns'] });
+ toast.success(t.campaigns.send);
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? t.common.error),
+ });
+
+ const deleteCampaign = useMutation({
+ mutationFn: (id: string) => campaignsAPI.delete(id),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['campaigns'] });
+ toast.success(t.campaigns.delete);
+ },
+ });
+
+ const isPlanError = (error as any)?.response?.status === 402;
+
+ const chatbotMap = Object.fromEntries(chatbots.map(c => [c.id, c.name]));
+ const sentCampaigns = campaigns.filter(c => c.status === 'sent');
+ const totalDelivered = sentCampaigns.reduce((sum, c) => sum + c.sent_count, 0);
+
+ const handleSend = (campaign: Campaign) => {
+ Alert.alert(
+ t.campaigns.send,
+ t.campaigns.send_confirm(campaign.title, campaign.recipients_count),
+ [
+ { text: t.campaigns.cancel, style: 'cancel' },
+ { text: t.campaigns.send_now, style: 'default', onPress: () => sendCampaign.mutate(campaign.id) },
+ ],
+ );
+ };
+
+ const handleDelete = (campaign: Campaign) => {
+ Alert.alert(t.campaigns.delete, `"${campaign.title}"?`, [
+ { text: t.campaigns.cancel, style: 'cancel' },
+ { text: t.campaigns.delete, style: 'destructive', onPress: () => deleteCampaign.mutate(campaign.id) },
+ ]);
+ };
+
+ if (isLoading) return ;
+
+ if (isPlanError) {
+ return (
+
+
+ 🔒
+ Campaigns Require Starter+
+
+ Upgrade your plan to send broadcast messages to chatbot subscribers.
+
+
+
+ );
+ }
+
+ return (
+
+ }>
+
+ {/* Header */}
+
+
+ {t.campaigns.title}
+
+ {t.campaigns.empty_desc}
+
+
+ setShowForm(true)} size="sm" />
+
+
+ {/* Stats */}
+ {campaigns.length > 0 && (
+
+ {[
+ { label: t.campaigns.title, value: campaigns.length, emoji: '📣' },
+ { label: t.campaigns.status_sent, value: sentCampaigns.length, emoji: '✅' },
+ { label: t.campaigns.delivered, value: totalDelivered, emoji: '📬' },
+ ].map(s => (
+
+ {s.emoji}
+ {s.value.toLocaleString()}
+ {s.label}
+
+ ))}
+
+ )}
+
+ {/* Chatbot filter */}
+ {chatbots.length > 1 && (
+
+ {[{ id: '', name: 'All' }, ...chatbots].map(c => (
+ setChatbotFilter(c.id)}
+ style={[
+ styles.filterChip,
+ {
+ backgroundColor: chatbotFilter === c.id ? COLORS.primary : theme.bgSecondary,
+ borderColor: chatbotFilter === c.id ? COLORS.primary : theme.border,
+ },
+ ]}>
+
+ {c.name}
+
+
+ ))}
+
+ )}
+
+ {/* Campaign list */}
+ {campaigns.length === 0 ? (
+ 📣}
+ title={t.campaigns.empty_title}
+ description={t.campaigns.empty_desc}
+ />
+ ) : (
+
+ {campaigns.map(campaign => (
+ handleSend(campaign)}
+ onDelete={() => handleDelete(campaign)}
+ theme={theme}
+ />
+ ))}
+
+ )}
+
+
+ setShowForm(false)}
+ onCreate={data => createCampaign.mutate(data)}
+ creating={createCampaign.isPending}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ scroll: { padding: SPACING.lg, gap: SPACING.lg, paddingBottom: SPACING.xxxl },
+
+ headerRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: SPACING.md },
+ pageTitle: { ...TEXT.h3 },
+ pageSub: { ...TEXT.small, marginTop: 2 },
+
+ statsGrid: { flexDirection: 'row', gap: SPACING.sm },
+ statCard: {
+ flex: 1,
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.md,
+ alignItems: 'center',
+ gap: 4,
+ },
+ statEmoji: { fontSize: 20 },
+ statValue: { fontSize: FONT_SIZE.xl, fontWeight: '800' },
+ statLabel: { ...TEXT.caption },
+
+ filterRow: { marginBottom: -SPACING.sm },
+ filterChip: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.xs,
+ borderRadius: RADIUS.full,
+ borderWidth: 1,
+ marginRight: SPACING.xs,
+ },
+ filterText: { ...TEXT.small },
+
+ list: { gap: SPACING.md },
+
+ campaignCard: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.sm,
+ },
+ cardTopRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: SPACING.sm },
+ campaignTitle: { ...TEXT.bodyM, flex: 1 },
+ statusBadge: { borderRadius: RADIUS.full, paddingHorizontal: SPACING.sm, paddingVertical: 3 },
+ statusText: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+ cardMeta: { ...TEXT.caption },
+ messageBox: { borderRadius: RADIUS.md, padding: SPACING.md },
+ messageText: { ...TEXT.small },
+ statsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.md },
+ statText: { ...TEXT.caption },
+
+ actionRow: { flexDirection: 'row', gap: SPACING.sm, marginTop: SPACING.xs },
+ sendBtn: {
+ flex: 1,
+ backgroundColor: COLORS.success,
+ borderRadius: RADIUS.md,
+ paddingVertical: SPACING.sm,
+ alignItems: 'center',
+ },
+ sendBtnText: { color: COLORS.white, fontWeight: '600', fontSize: FONT_SIZE.sm },
+ deleteBtn: {
+ borderWidth: 1,
+ borderRadius: RADIUS.md,
+ paddingVertical: SPACING.sm,
+ paddingHorizontal: SPACING.md,
+ alignItems: 'center',
+ },
+ deleteBtnText: { fontWeight: '600', fontSize: FONT_SIZE.sm },
+ deleteSentBtn: { paddingVertical: SPACING.xs, alignSelf: 'flex-start' },
+ deleteSentText: { ...TEXT.small },
+
+ // Modal
+ modalSafe: { flex: 1 },
+ modalScroll: { padding: SPACING.xl, gap: SPACING.md, paddingBottom: SPACING.xxxl },
+ modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: SPACING.sm },
+ modalTitle: { ...TEXT.h3 },
+ closeBtn: { width: 32, height: 32, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center' },
+ closeBtnText: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ fieldLabel: { ...TEXT.smallM, marginBottom: -SPACING.xs },
+ hintText: { ...TEXT.small },
+ chipRow: { marginBottom: SPACING.xs },
+ chip: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.xs,
+ borderRadius: RADIUS.full,
+ borderWidth: 1,
+ marginRight: SPACING.xs,
+ },
+ chipText: { ...TEXT.small },
+ textInput: {
+ borderWidth: 1,
+ borderRadius: RADIUS.lg,
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.sm,
+ fontSize: FONT_SIZE.sm,
+ },
+ textArea: { height: 120, paddingTop: SPACING.sm },
+ charCount: { ...TEXT.caption, textAlign: 'right', marginTop: -SPACING.xs },
+ modalActions: { flexDirection: 'row', gap: SPACING.sm, marginTop: SPACING.sm },
+ modalActionBtn: { flex: 1 },
+
+ // Upgrade
+ upgradeContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: SPACING.xxxl, gap: SPACING.md },
+ upgradeEmoji: { fontSize: 48 },
+ upgradeTitle: { ...TEXT.h3, textAlign: 'center' },
+ upgradeDesc: { ...TEXT.body, textAlign: 'center' },
+});
diff --git a/src/screens/chatbots/ChatPreviewScreen.tsx b/src/screens/chatbots/ChatPreviewScreen.tsx
new file mode 100644
index 0000000..7f84818
--- /dev/null
+++ b/src/screens/chatbots/ChatPreviewScreen.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { View, StyleSheet } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { DashboardStackParamList } from '../../navigation/types';
+import { chatbotsAPI } from '../../services/api';
+import { ChatInterface } from '../../components/ChatInterface';
+import { Spinner } from '../../components/ui';
+import { useTheme } from '../../theme';
+
+type Props = NativeStackScreenProps;
+
+export function ChatPreviewScreen({ route }: Props) {
+ const { chatbotId } = route.params;
+ const { theme } = useTheme();
+
+ const { data: chatbot, isLoading } = useQuery({
+ queryKey: ['chatbot', chatbotId],
+ queryFn: () => chatbotsAPI.get(chatbotId),
+ });
+
+ if (isLoading) return ;
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+});
diff --git a/src/screens/chatbots/ChatbotBuilderScreen.tsx b/src/screens/chatbots/ChatbotBuilderScreen.tsx
new file mode 100644
index 0000000..4030f2c
--- /dev/null
+++ b/src/screens/chatbots/ChatbotBuilderScreen.tsx
@@ -0,0 +1,602 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Switch,
+ Alert,
+ TextInput,
+ ActivityIndicator,
+ Modal,
+ FlatList,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { DashboardStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Button, Input, Card, Spinner } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { chatbotsAPI, modelsAPI, documentsAPI, urlSourcesAPI, channelsAPI } from '../../services/api';
+import { Chatbot, Document, URLSource, AIModel, ChannelConnection } from '../../types';
+import { DocumentsTab } from './tabs/DocumentsTab';
+import { DeployTab } from './tabs/DeployTab';
+import { TestingTab } from './tabs/TestingTab';
+import { ChatInterface } from '../../components/ChatInterface';
+import { CHATBOT_TEMPLATES, ChatbotTemplate } from '../../data/templates';
+
+type Props = NativeStackScreenProps;
+
+type Tab = 'settings' | 'documents' | 'preview' | 'testing' | 'deploy';
+
+const TABS: { key: Tab; label: string }[] = [
+ { key: 'settings', label: 'Settings' },
+ { key: 'documents', label: 'Docs' },
+ { key: 'preview', label: 'Preview' },
+ { key: 'testing', label: 'Testing' },
+ { key: 'deploy', label: 'Deploy' },
+];
+
+const CATEGORIES = ['Customer Support', 'Sales', 'HR', 'Education', 'Healthcare', 'Finance', 'Legal', 'Other'];
+const INDUSTRIES = ['Technology', 'Retail', 'Healthcare', 'Finance', 'Education', 'Manufacturing', 'Other'];
+const TEMPERATURES = [0, 0.3, 0.5, 0.7, 1.0];
+
+export function ChatbotBuilderScreen({ route, navigation }: Props) {
+ const { chatbotId } = route.params ?? {};
+ const isEditing = Boolean(chatbotId);
+ const { theme } = useTheme();
+ const toast = useToast();
+ const qc = useQueryClient();
+
+ const [activeTab, setActiveTab] = useState('settings');
+ const [showTemplates, setShowTemplates] = useState(!isEditing);
+
+ // Form state
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [systemPrompt, setSystemPrompt] = useState('');
+ const [selectedModel, setSelectedModel] = useState('');
+ const [temperature, setTemperature] = useState(0.7);
+ const [maxTokens, setMaxTokens] = useState(1024);
+ const [primaryColor, setPrimaryColor] = useState('#6366f1');
+ const [welcomeMessage, setWelcomeMessage] = useState('Hello! How can I help you today?');
+ const [category, setCategory] = useState('');
+ const [industry, setIndustry] = useState('');
+ const [showBranding, setShowBranding] = useState(true);
+ const [leadCaptureEnabled, setLeadCaptureEnabled] = useState(false);
+ const [handoffEnabled, setHandoffEnabled] = useState(false);
+ const [handoffEmail, setHandoffEmail] = useState('');
+ const [errors, setErrors] = useState>({});
+
+ // Load existing chatbot
+ const { data: chatbot, isLoading: loadingBot } = useQuery({
+ queryKey: ['chatbot', chatbotId],
+ queryFn: () => chatbotsAPI.get(chatbotId!),
+ enabled: isEditing,
+ });
+
+ // Load available models
+ const { data: modelsData } = useQuery({
+ queryKey: ['models'],
+ queryFn: modelsAPI.available,
+ });
+
+ useEffect(() => {
+ if (chatbot) {
+ setName(chatbot.name ?? '');
+ setDescription(chatbot.description ?? '');
+ setSystemPrompt(chatbot.system_prompt ?? '');
+ setSelectedModel(chatbot.model ?? '');
+ setTemperature(chatbot.temperature ?? 0.7);
+ setMaxTokens(chatbot.max_tokens ?? 1024);
+ setPrimaryColor(chatbot.primary_color ?? '#6366f1');
+ setWelcomeMessage(chatbot.welcome_message ?? '');
+ setCategory(chatbot.category ?? '');
+ setIndustry(chatbot.industry ?? '');
+ setShowBranding(chatbot.show_branding ?? true);
+ setLeadCaptureEnabled(chatbot.lead_capture_enabled ?? false);
+ setHandoffEnabled(chatbot.handoff_enabled ?? false);
+ setHandoffEmail(chatbot.handoff_email ?? '');
+ } else if (!isEditing && modelsData?.default_model) {
+ setSelectedModel(modelsData.default_model);
+ }
+ }, [chatbot, modelsData, isEditing]);
+
+ const saveMutation = useMutation({
+ mutationFn: (payload: Partial) =>
+ isEditing
+ ? chatbotsAPI.update(chatbotId!, payload)
+ : chatbotsAPI.create(payload),
+ onSuccess: (data) => {
+ qc.invalidateQueries({ queryKey: ['chatbots'] });
+ qc.invalidateQueries({ queryKey: ['chatbot', chatbotId] });
+ toast.success(isEditing ? 'Chatbot updated!' : 'Chatbot created!');
+ if (!isEditing) {
+ navigation.replace('ChatbotBuilder', { chatbotId: data.id });
+ }
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? 'Save failed'),
+ });
+
+ const validate = () => {
+ const e: Record = {};
+ if (!name.trim()) e.name = 'Name is required';
+ setErrors(e);
+ return Object.keys(e).length === 0;
+ };
+
+ const applyTemplate = (tpl: ChatbotTemplate) => {
+ setSystemPrompt(tpl.system_prompt);
+ setWelcomeMessage(tpl.welcome_message);
+ setCategory(tpl.category);
+ setLeadCaptureEnabled(tpl.lead_capture_enabled);
+ setShowTemplates(false);
+ };
+
+ const handleSave = () => {
+ if (!validate()) return;
+ saveMutation.mutate({
+ name: name.trim(),
+ description: description.trim(),
+ system_prompt: systemPrompt.trim(),
+ model: selectedModel,
+ temperature,
+ max_tokens: maxTokens,
+ primary_color: primaryColor,
+ welcome_message: welcomeMessage.trim(),
+ category,
+ industry,
+ show_branding: showBranding,
+ lead_capture_enabled: leadCaptureEnabled,
+ handoff_enabled: handoffEnabled,
+ handoff_email: handoffEmail.trim(),
+ });
+ };
+
+ if (isEditing && loadingBot) {
+ return ;
+ }
+
+ const PRESET_COLORS = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#14b8a6'];
+
+ return (
+
+ {/* Template picker modal (only for new chatbots) */}
+ setShowTemplates(false)}>
+
+
+ Choose a template
+ setShowTemplates(false)}>
+ Start blank →
+
+
+ item.id}
+ contentContainerStyle={tplStyles.list}
+ renderItem={({ item }) => (
+ applyTemplate(item)}>
+ {item.icon}
+
+ {item.name}
+ {item.description}
+
+ ›
+
+ )}
+ />
+
+
+
+ {/* Tab bar */}
+
+ {TABS.map(tab => (
+ setActiveTab(tab.key)}>
+
+ {tab.label}
+
+
+ ))}
+
+
+ {/* Settings Tab */}
+ {activeTab === 'settings' && (
+
+
+
+
+
+
+
+
+ System Prompt
+
+
+
+
+
+
+ {(modelsData?.models ?? []).map((m: AIModel) => (
+ m.available && setSelectedModel(m.id)}
+ disabled={!m.available}>
+
+ {m.name}
+
+ {m.upgrade_required ? (
+ {m.upgrade_required}
+ ) : null}
+
+ ))}
+
+
+
+
+
+ {TEMPERATURES.map(t => (
+ setTemperature(t)}>
+
+ {t}
+
+
+ ))}
+
+
+ 0 = focused, 1 = creative
+
+
+
+ Primary Color
+
+ {PRESET_COLORS.map(c => (
+ setPrimaryColor(c)}
+ />
+ ))}
+
+
+
+
+
+
+
+ Category
+
+
+ {CATEGORIES.map(c => (
+ setCategory(c === category ? '' : c)}>
+
+ {c}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {handoffEnabled && (
+
+ )}
+
+
+
+ )}
+
+ {/* Documents Tab */}
+ {activeTab === 'documents' && chatbotId && (
+
+ )}
+ {activeTab === 'documents' && !chatbotId && (
+
+
+ Save the chatbot first to add documents.
+
+
+ )}
+
+ {/* Testing Tab */}
+ {activeTab === 'testing' && chatbotId && (
+
+ )}
+ {activeTab === 'testing' && !chatbotId && (
+
+
+ Save the chatbot first to run tests.
+
+
+ )}
+
+ {/* Preview Tab */}
+ {activeTab === 'preview' && chatbotId && (
+
+ )}
+ {activeTab === 'preview' && !chatbotId && (
+
+
+ Save the chatbot first to preview it.
+
+
+ )}
+
+ {/* Deploy Tab */}
+ {activeTab === 'deploy' && chatbotId && (
+
+ )}
+ {activeTab === 'deploy' && !chatbotId && (
+
+
+ Save the chatbot first to view deploy options.
+
+
+ )}
+
+ );
+}
+
+function SectionHeader({ title }: { title: string }) {
+ const { theme } = useTheme();
+ return (
+ {title.toUpperCase()}
+ );
+}
+
+function ToggleRow({
+ label,
+ description,
+ value,
+ onValueChange,
+}: {
+ label: string;
+ description: string;
+ value: boolean;
+ onValueChange: (v: boolean) => void;
+}) {
+ const { theme } = useTheme();
+ return (
+
+
+ {label}
+ {description}
+
+
+
+ );
+}
+
+const sectionStyles = StyleSheet.create({
+ header: {
+ fontSize: FONT_SIZE.xs,
+ fontWeight: '700',
+ letterSpacing: 0.8,
+ marginTop: SPACING.xl,
+ marginBottom: SPACING.sm,
+ },
+});
+
+const toggleStyles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: SPACING.sm,
+ gap: SPACING.md,
+ },
+ labelCol: { flex: 1 },
+ label: { fontSize: FONT_SIZE.md, fontWeight: '500' },
+ desc: { fontSize: FONT_SIZE.xs, marginTop: 2 },
+});
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ tabBar: {
+ flexDirection: 'row',
+ borderBottomWidth: 1,
+ paddingHorizontal: SPACING.sm,
+ },
+ tab: {
+ flex: 1,
+ paddingVertical: SPACING.md,
+ alignItems: 'center',
+ borderBottomWidth: 2,
+ borderBottomColor: 'transparent',
+ },
+ tabActive: { borderBottomColor: COLORS.primary },
+ tabText: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ tabTextActive: { fontWeight: '700' },
+
+ scroll: { flex: 1 },
+ scrollContent: { padding: SPACING.lg, paddingBottom: SPACING.xxxl },
+
+ field: { marginBottom: SPACING.md },
+ label: { fontSize: FONT_SIZE.sm, fontWeight: '500', marginBottom: SPACING.xs },
+ textarea: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.md,
+ padding: SPACING.md,
+ fontSize: FONT_SIZE.md,
+ minHeight: 110,
+ },
+ hint: { fontSize: FONT_SIZE.xs, marginTop: -SPACING.xs, marginBottom: SPACING.sm },
+
+ modelsScroll: { marginBottom: SPACING.md },
+ modelsRow: { flexDirection: 'row', gap: SPACING.sm, paddingBottom: SPACING.xs },
+ modelPill: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.full,
+ paddingVertical: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ gap: 2,
+ },
+ modelPillText: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ modelDisabled: { opacity: 0.4 },
+ modelUpgrade: { fontSize: 9, color: COLORS.warning, fontWeight: '600' },
+
+ tempRow: { flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap', marginBottom: SPACING.xs },
+ tempPill: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.full,
+ paddingVertical: SPACING.xs,
+ paddingHorizontal: SPACING.lg,
+ },
+ tempText: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+
+ colorRow: { flexDirection: 'row', gap: SPACING.sm, marginBottom: SPACING.lg },
+ colorSwatch: { width: 32, height: 32, borderRadius: RADIUS.full },
+ colorSwatchSelected: { borderWidth: 3, borderColor: '#fff', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, elevation: 4 },
+
+ pillRow: { flexDirection: 'row', gap: SPACING.sm },
+ pill: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.full,
+ paddingVertical: 4,
+ paddingHorizontal: SPACING.md,
+ },
+ pillText: { fontSize: FONT_SIZE.xs, fontWeight: '500' },
+
+ row: { marginBottom: SPACING.md },
+
+ saveBtn: { marginTop: SPACING.xxxl },
+
+ noIdState: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: SPACING.xxxl },
+ noIdText: { fontSize: FONT_SIZE.md, textAlign: 'center' },
+});
+
+const tplStyles = StyleSheet.create({
+ safe: { flex: 1 },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: SPACING.lg,
+ borderBottomWidth: 1,
+ },
+ title: { fontSize: FONT_SIZE.lg, fontWeight: '700' },
+ skip: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ list: { padding: SPACING.lg, gap: SPACING.sm },
+ card: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.md,
+ padding: SPACING.lg,
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ },
+ icon: { fontSize: 32 },
+ cardInfo: { flex: 1 },
+ cardName: { fontSize: FONT_SIZE.md, fontWeight: '700' },
+ cardDesc: { fontSize: FONT_SIZE.sm, marginTop: 2, lineHeight: 18 },
+ chevron: { fontSize: 22, fontWeight: '300' },
+});
diff --git a/src/screens/chatbots/tabs/DeployTab.tsx b/src/screens/chatbots/tabs/DeployTab.tsx
new file mode 100644
index 0000000..c23a74f
--- /dev/null
+++ b/src/screens/chatbots/tabs/DeployTab.tsx
@@ -0,0 +1,234 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Clipboard,
+ Platform,
+} from 'react-native';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS } from '../../../theme';
+import { useTheme } from '../../../theme';
+import { Card, Button, Input } from '../../../components/ui';
+import { useToast } from '../../../contexts/ToastContext';
+import { chatbotsAPI, channelsAPI } from '../../../services/api';
+import { ChannelConnection } from '../../../types';
+
+interface Props {
+ chatbotId: string;
+ chatbotName: string;
+}
+
+export function DeployTab({ chatbotId, chatbotName }: Props) {
+ const { theme } = useTheme();
+ const toast = useToast();
+ const qc = useQueryClient();
+ const [telegramToken, setTelegramToken] = useState('');
+ const [connectingTelegram, setConnectingTelegram] = useState(false);
+ const [showTelegramForm, setShowTelegramForm] = useState(false);
+
+ const { data: embedData } = useQuery({
+ queryKey: ['embed', chatbotId],
+ queryFn: () => chatbotsAPI.getEmbed(chatbotId),
+ });
+
+ const { data: channels } = useQuery({
+ queryKey: ['channels', chatbotId],
+ queryFn: () => channelsAPI.list(chatbotId),
+ });
+
+ const disconnectMutation = useMutation({
+ mutationFn: channelsAPI.disconnect,
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['channels', chatbotId] });
+ toast.success('Channel disconnected');
+ },
+ });
+
+ const handleCopy = (text: string, label: string) => {
+ Clipboard.setString(text);
+ toast.success(`${label} copied!`);
+ };
+
+ const handleConnectTelegram = async () => {
+ if (!telegramToken.trim()) {
+ toast.error('Enter your Telegram bot token');
+ return;
+ }
+ setConnectingTelegram(true);
+ try {
+ await channelsAPI.connectTelegram(chatbotId, telegramToken.trim());
+ qc.invalidateQueries({ queryKey: ['channels', chatbotId] });
+ setTelegramToken('');
+ setShowTelegramForm(false);
+ toast.success('Telegram bot connected!');
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? 'Failed to connect Telegram');
+ } finally {
+ setConnectingTelegram(false);
+ }
+ };
+
+ const telegramConnection = (channels as ChannelConnection[] ?? []).find(c => c.channel === 'telegram');
+ const whatsappConnection = (channels as ChannelConnection[] ?? []).find(c => c.channel === 'whatsapp');
+
+ return (
+
+
+ {/* Web Embed */}
+
+ 🌐 Web Embed
+
+ Paste this script on your website to add the chat widget.
+
+ {embedData?.embed_script ? (
+
+
+ {embedData.embed_script}
+
+ handleCopy(embedData.embed_script, 'Embed script')}>
+ Copy
+
+
+ ) : null}
+ {embedData?.chat_url ? (
+
+ Direct link:
+
+ {embedData.chat_url}
+
+ handleCopy(embedData.chat_url, 'Chat URL')}>
+ Copy
+
+
+ ) : null}
+
+
+ {/* Telegram */}
+
+
+ ✈️
+
+ Telegram
+
+ Connect a Telegram bot to use your chatbot
+
+
+
+ {telegramConnection ? (
+
+
+
+
+ @{telegramConnection.bot_username ?? 'Connected'}
+
+
+ disconnectMutation.mutate(telegramConnection.id)}
+ />
+
+ ) : (
+ <>
+ {showTelegramForm ? (
+
+
+
+ setShowTelegramForm(false)} />
+
+
+
+ ) : (
+ setShowTelegramForm(true)} />
+ )}
+ >
+ )}
+
+
+ {/* WhatsApp */}
+
+
+ 💬
+
+ WhatsApp
+
+ Connect via Meta Cloud API (Business+ plan)
+
+
+
+ {whatsappConnection ? (
+
+
+
+
+ Keyword: {whatsappConnection.wa_keyword}
+
+
+ disconnectMutation.mutate(whatsappConnection.id)}
+ />
+
+ ) : (
+
+ Requires Business plan or higher
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ scroll: { flex: 1 },
+ content: { padding: SPACING.lg, gap: SPACING.lg },
+ section: { gap: SPACING.md },
+ sectionTitle: { fontSize: FONT_SIZE.lg, fontWeight: '600' },
+ sectionDesc: { fontSize: FONT_SIZE.sm, lineHeight: 20 },
+
+ codeBlock: {
+ borderRadius: RADIUS.md,
+ padding: SPACING.md,
+ borderWidth: 1,
+ gap: SPACING.sm,
+ },
+ codeText: { fontSize: 11, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace' },
+ copyBtn: { alignSelf: 'flex-end' },
+ copyBtnText: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+
+ urlRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, flexWrap: 'wrap' },
+ urlLabel: { fontSize: FONT_SIZE.sm },
+ urlText: { flex: 1, fontSize: FONT_SIZE.sm },
+
+ channelHeader: { flexDirection: 'row', gap: SPACING.md, alignItems: 'flex-start' },
+ channelIcon: { fontSize: 28 },
+ channelTitleCol: { flex: 1, gap: 2 },
+
+ connectedRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
+ connectedInfo: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ connectedDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: COLORS.success },
+ connectedText: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+
+ connectForm: { gap: SPACING.sm },
+ formActions: { flexDirection: 'row', gap: SPACING.sm, justifyContent: 'flex-end' },
+
+ upgradeNote: { fontSize: FONT_SIZE.sm, fontStyle: 'italic' },
+});
+
diff --git a/src/screens/chatbots/tabs/DocumentsTab.tsx b/src/screens/chatbots/tabs/DocumentsTab.tsx
new file mode 100644
index 0000000..5a0f500
--- /dev/null
+++ b/src/screens/chatbots/tabs/DocumentsTab.tsx
@@ -0,0 +1,286 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ Alert,
+} from 'react-native';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS } from '../../../theme';
+import { useTheme } from '../../../theme';
+import { Card, StatusBadge, Spinner, EmptyState, Button, Input } from '../../../components/ui';
+import { useToast } from '../../../contexts/ToastContext';
+import { documentsAPI, urlSourcesAPI } from '../../../services/api';
+import { Document, URLSource } from '../../../types';
+
+interface Props {
+ chatbotId: string;
+}
+
+export function DocumentsTab({ chatbotId }: Props) {
+ const { theme } = useTheme();
+ const toast = useToast();
+ const qc = useQueryClient();
+ const [urlInput, setUrlInput] = useState('');
+ const [addingUrl, setAddingUrl] = useState(false);
+ const [showUrlInput, setShowUrlInput] = useState(false);
+
+ const { data: documents, isLoading: loadingDocs } = useQuery({
+ queryKey: ['documents', chatbotId],
+ queryFn: () => documentsAPI.list(chatbotId),
+ });
+
+ const { data: urlSources } = useQuery({
+ queryKey: ['urlSources', chatbotId],
+ queryFn: () => urlSourcesAPI.list(chatbotId),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (docId: string) => documentsAPI.delete(chatbotId, docId),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['documents', chatbotId] });
+ toast.success('Document deleted');
+ },
+ onError: () => toast.error('Failed to delete document'),
+ });
+
+ const retryMutation = useMutation({
+ mutationFn: (docId: string) => documentsAPI.retry(chatbotId, docId),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['documents', chatbotId] });
+ toast.success('Retry started');
+ },
+ });
+
+ const deleteUrlMutation = useMutation({
+ mutationFn: (sourceId: string) => urlSourcesAPI.delete(chatbotId, sourceId),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['urlSources', chatbotId] });
+ toast.success('URL source removed');
+ },
+ });
+
+ const handleAddUrl = async () => {
+ const url = urlInput.trim();
+ if (!url) return;
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ toast.error('Enter a valid URL starting with http:// or https://');
+ return;
+ }
+ setAddingUrl(true);
+ try {
+ await urlSourcesAPI.add(chatbotId, url);
+ qc.invalidateQueries({ queryKey: ['urlSources', chatbotId] });
+ setUrlInput('');
+ setShowUrlInput(false);
+ toast.success('URL added! Scraping in background...');
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? 'Failed to add URL');
+ } finally {
+ setAddingUrl(false);
+ }
+ };
+
+ const confirmDelete = (doc: Document) => {
+ Alert.alert('Delete Document', `Remove "${doc.file_name}"?`, [
+ { text: 'Cancel', style: 'cancel' },
+ { text: 'Delete', style: 'destructive', onPress: () => deleteMutation.mutate(doc.id) },
+ ]);
+ };
+
+ const formatSize = (bytes: number) => {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
+ type ListItem =
+ | { type: 'doc'; data: Document }
+ | { type: 'url'; data: URLSource }
+ | { type: 'upload-tip' };
+
+ const listData: ListItem[] = [
+ { type: 'upload-tip' },
+ ...(documents ?? []).map((d: Document) => ({ type: 'doc' as const, data: d })),
+ ...(urlSources ?? []).map((u: URLSource) => ({ type: 'url' as const, data: u })),
+ ];
+
+ return (
+
+ {/* Add URL bar */}
+
+ setShowUrlInput(v => !v)}
+ />
+
+ {(documents ?? []).length + (urlSources ?? []).length} sources
+
+
+
+ {showUrlInput && (
+
+
+
+ { setShowUrlInput(false); setUrlInput(''); }} />
+
+
+
+ )}
+
+ (item.type === 'upload-tip' ? 'tip' : item.type === 'doc' ? item.data.id : item.data.id)}
+ contentContainerStyle={styles.list}
+ renderItem={({ item }) => {
+ if (item.type === 'upload-tip') {
+ return (
+
+ 💡
+
+ Upload files from the web app
+
+ PDF, DOCX, CSV, XLSX, TXT and MD files can be uploaded from the Contexta web dashboard. Use "Add URL" here to scrape web pages directly.
+
+
+
+ );
+ }
+
+ if (item.type === 'doc') {
+ const doc = item.data as Document;
+ const icon = doc.file_type?.includes('pdf') ? '📄'
+ : doc.file_type?.includes('csv') || doc.file_type?.includes('xlsx') ? '📊'
+ : '📝';
+ return (
+
+
+ {icon}
+
+
+ {doc.file_name}
+
+ {formatSize(doc.file_size)}
+ {doc.chunk_count > 0 && (
+ · {doc.chunk_count} chunks
+ )}
+
+
+
+ {doc.status === 'failed' && (
+ retryMutation.mutate(doc.id)}>
+ Retry
+
+ )}
+
+
+ confirmDelete(doc)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ ✕
+
+
+ );
+ }
+
+ // url source
+ const src = item.data as URLSource;
+ return (
+
+
+ 🌐
+
+
+
+ {src.page_title ?? src.url}
+
+ {src.url}
+
+
+ {src.chunk_count ? (
+ {src.chunk_count} chunks
+ ) : null}
+
+
+ deleteUrlMutation.mutate(src.id)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ ✕
+
+
+ );
+ }}
+ ListEmptyComponent={
+ loadingDocs ? : null
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1 },
+
+ toolbar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.sm,
+ borderBottomWidth: 1,
+ },
+ toolbarHint: { fontSize: FONT_SIZE.xs },
+
+ urlBar: {
+ paddingHorizontal: SPACING.md,
+ paddingBottom: SPACING.sm,
+ borderBottomWidth: 1,
+ gap: SPACING.xs,
+ },
+ urlInput: { marginBottom: 0 },
+ urlActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: SPACING.sm },
+
+ list: { padding: SPACING.md, gap: SPACING.sm },
+
+ tipCard: {
+ flexDirection: 'row',
+ gap: SPACING.md,
+ backgroundColor: '#f0f4ff',
+ borderWidth: 1,
+ },
+ tipIcon: { fontSize: 22 },
+ tipBody: { flex: 1, gap: 4 },
+ tipTitle: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ tipDesc: { fontSize: FONT_SIZE.xs, lineHeight: 18 },
+
+ docItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: RADIUS.lg,
+ padding: SPACING.md,
+ borderWidth: 1,
+ gap: SPACING.sm,
+ },
+ docIcon: {
+ width: 40,
+ height: 40,
+ borderRadius: RADIUS.md,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ docIconText: { fontSize: 20 },
+ docInfo: { flex: 1, gap: 3 },
+ docName: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ docMeta: { flexDirection: 'row', alignItems: 'center' },
+ docSize: { fontSize: FONT_SIZE.xs },
+ docFooter: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginTop: 2 },
+ retryText: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+ deleteIcon: { fontSize: 16, color: '#9ca3af', padding: SPACING.xs },
+});
diff --git a/src/screens/chatbots/tabs/TestingTab.tsx b/src/screens/chatbots/tabs/TestingTab.tsx
new file mode 100644
index 0000000..7e07618
--- /dev/null
+++ b/src/screens/chatbots/tabs/TestingTab.tsx
@@ -0,0 +1,237 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TextInput,
+ TouchableOpacity,
+ ActivityIndicator,
+} from 'react-native';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../../theme';
+import { useTheme } from '../../../theme';
+import { chatAPI } from '../../../services/api';
+
+interface TestResult {
+ question: string;
+ response: string;
+ confidence_score: number;
+ sources: { document_name: string; chunk_text: string; score: number }[];
+ model_used: string;
+}
+
+export function TestingTab({ chatbotId }: { chatbotId: string }) {
+ const { theme } = useTheme();
+ const [questions, setQuestions] = useState(['']);
+ const [results, setResults] = useState([]);
+ const [expandedIdx, setExpandedIdx] = useState(null);
+ const [running, setRunning] = useState(false);
+ const [error, setError] = useState('');
+
+ const addQuestion = () => {
+ if (questions.length < 10) setQuestions(prev => [...prev, '']);
+ };
+
+ const updateQuestion = (idx: number, val: string) => {
+ setQuestions(prev => prev.map((q, i) => (i === idx ? val : q)));
+ };
+
+ const removeQuestion = (idx: number) => {
+ setQuestions(prev => prev.filter((_, i) => i !== idx));
+ };
+
+ const runTests = async () => {
+ const valid = questions.map(q => q.trim()).filter(Boolean);
+ if (!valid.length) return;
+ setRunning(true);
+ setError('');
+ setResults([]);
+ setExpandedIdx(null);
+ try {
+ const data = await chatAPI.test(chatbotId, valid);
+ setResults(data);
+ setExpandedIdx(0);
+ } catch (e: any) {
+ setError(e?.response?.data?.detail ?? 'Test failed. Please try again.');
+ } finally {
+ setRunning(false);
+ }
+ };
+
+ const confidenceColor = (score: number) => {
+ if (score >= 0.7) return COLORS.success;
+ if (score >= 0.4) return COLORS.warning;
+ return COLORS.error;
+ };
+
+ return (
+
+
+
+ Test Questions
+
+ Enter up to 10 questions to test how your chatbot responds.
+
+
+ {questions.map((q, idx) => (
+
+ {idx + 1}.
+ updateQuestion(idx, val)}
+ placeholder="Ask a question..."
+ placeholderTextColor={theme.placeholder}
+ />
+ {questions.length > 1 && (
+ removeQuestion(idx)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ ✕
+
+ )}
+
+ ))}
+
+
+ {questions.length < 10 && (
+
+ + Add question
+
+ )}
+ q.trim())) && styles.runBtnDisabled,
+ ]}
+ onPress={runTests}
+ disabled={running || !questions.some(q => q.trim())}>
+ {running
+ ?
+ : ▶ Run Tests
+ }
+
+
+
+ {error ? (
+ {error}
+ ) : null}
+
+
+ {results.length > 0 && (
+ <>
+
+ {results.length} RESULT{results.length !== 1 ? 'S' : ''}
+
+ {results.map((r, idx) => (
+
+ setExpandedIdx(expandedIdx === idx ? null : idx)}>
+
+
+ {Math.round(r.confidence_score * 100)}%
+
+
+ {r.question}
+ {expandedIdx === idx ? '▲' : '▼'}
+
+
+ {expandedIdx === idx && (
+
+ {r.response}
+
+ {r.sources.length > 0 && (
+ <>
+ SOURCES
+ {r.sources.map((src, si) => (
+
+
+ {src.document_name}
+ · {Math.round(src.score * 100)}%
+
+ {src.chunk_text}
+
+ ))}
+ >
+ )}
+
+ Model: {r.model_used}
+
+ )}
+
+ ))}
+ >
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ scroll: { flex: 1 },
+ scrollContent: { padding: SPACING.lg, gap: SPACING.lg, paddingBottom: SPACING.xxxl },
+
+ card: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.md,
+ },
+ cardTitle: { ...TEXT.h4 },
+ cardDesc: { ...TEXT.small },
+
+ questionRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ questionNum: { width: 18, fontSize: FONT_SIZE.sm, textAlign: 'right' },
+ questionInput: {
+ flex: 1,
+ borderWidth: 1.5,
+ borderRadius: RADIUS.md,
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.sm,
+ fontSize: FONT_SIZE.md,
+ },
+ removeBtn: { color: '#ccc', fontSize: FONT_SIZE.md, paddingHorizontal: SPACING.xs },
+
+ actions: { flexDirection: 'row', alignItems: 'center', marginTop: SPACING.xs },
+ addBtn: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ runBtn: {
+ marginLeft: 'auto',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.xs,
+ borderRadius: RADIUS.md,
+ paddingVertical: SPACING.sm,
+ paddingHorizontal: SPACING.lg,
+ minWidth: 100,
+ justifyContent: 'center',
+ },
+ runBtnDisabled: { opacity: 0.5 },
+ runBtnText: { color: COLORS.white, fontSize: FONT_SIZE.sm, fontWeight: '700' },
+ error: { fontSize: FONT_SIZE.sm },
+
+ resultsLabel: { fontSize: FONT_SIZE.xs, fontWeight: '700', letterSpacing: 0.8 },
+
+ resultCard: { borderRadius: RADIUS.xl, borderWidth: 1, overflow: 'hidden' },
+ resultHeader: { flexDirection: 'row', alignItems: 'center', padding: SPACING.md, gap: SPACING.sm },
+ confBadge: { borderRadius: RADIUS.sm, paddingVertical: 3, paddingHorizontal: SPACING.sm },
+ confText: { fontSize: FONT_SIZE.xs, fontWeight: '800' },
+ resultQuestion: { flex: 1, ...TEXT.smallM },
+ chevron: { fontSize: FONT_SIZE.sm },
+
+ resultBody: { borderTopWidth: 1, padding: SPACING.md, gap: SPACING.sm },
+ responseText: { ...TEXT.body, lineHeight: 22 },
+
+ sourcesLabel: { fontSize: FONT_SIZE.xs, fontWeight: '700', letterSpacing: 0.8, marginTop: SPACING.xs },
+ sourceChip: {
+ borderRadius: RADIUS.md,
+ borderWidth: 1,
+ padding: SPACING.sm,
+ gap: 3,
+ },
+ sourceName: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
+ sourceChunk: { fontSize: FONT_SIZE.xs, lineHeight: 16 },
+ modelLabel: { fontSize: FONT_SIZE.xs, marginTop: SPACING.xs },
+});
diff --git a/src/screens/dashboard/DashboardScreen.tsx b/src/screens/dashboard/DashboardScreen.tsx
new file mode 100644
index 0000000..c03065c
--- /dev/null
+++ b/src/screens/dashboard/DashboardScreen.tsx
@@ -0,0 +1,406 @@
+import React, { useCallback, useRef, useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ Alert,
+ RefreshControl,
+ Animated,
+} from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { DashboardStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { PlanBadge, Spinner, EmptyState, Button } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { chatbotsAPI } from '../../services/api';
+import { useAuthStore } from '../../stores/authStore';
+import { useTranslation } from '../../i18n';
+import { Chatbot } from '../../types';
+
+type Props = NativeStackScreenProps;
+
+function OnboardingChecklist({
+ chatbots,
+ userId,
+ onNavigateCreate,
+}: {
+ chatbots: Chatbot[];
+ userId: string;
+ onNavigateCreate: () => void;
+}) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const storageKey = `onboarding_v1_${userId}`;
+ const [dismissed, setDismissed] = useState(false);
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ AsyncStorage.getItem(storageKey).then(val => {
+ if (val === 'dismissed') setDismissed(true);
+ setLoaded(true);
+ });
+ }, [storageKey]);
+
+ const dismiss = async () => {
+ setDismissed(true);
+ await AsyncStorage.setItem(storageKey, 'dismissed');
+ };
+
+ if (!loaded || dismissed) return null;
+
+ const steps = [
+ {
+ id: 'create',
+ label: t.onboarding.step_create,
+ done: chatbots.length > 0,
+ action: onNavigateCreate,
+ },
+ {
+ id: 'docs',
+ label: t.onboarding.step_docs,
+ done: chatbots.some(c => (c.document_count ?? 0) > 0),
+ action: chatbots[0]
+ ? () => {}
+ : onNavigateCreate,
+ },
+ {
+ id: 'publish',
+ label: t.onboarding.step_publish,
+ done: chatbots.some(c => c.is_published),
+ action: undefined,
+ },
+ ];
+
+ const completedCount = steps.filter(s => s.done).length;
+ const allDone = completedCount === steps.length;
+
+ if (allDone) {
+ dismiss();
+ return null;
+ }
+
+ const progress = (completedCount / steps.length) * 100;
+
+ return (
+
+
+
+ 🚀 {t.onboarding.title}
+
+
+
+ {completedCount}/{steps.length}
+
+
+
+ ✕
+
+
+
+
+
+
+
+ {steps.map((step, i) => (
+
+
+ {step.done && ✓}
+ {!step.done && (
+ {i + 1}
+ )}
+
+ {step.label}
+
+ ))}
+
+ );
+}
+
+export function DashboardScreen({ navigation }: Props) {
+ const { theme, isDark } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const user = useAuthStore(s => s.user);
+ const qc = useQueryClient();
+
+ const { data: chatbots, isLoading, refetch } = useQuery({
+ queryKey: ['chatbots'],
+ queryFn: chatbotsAPI.list,
+ });
+
+ const publishMutation = useMutation({
+ mutationFn: ({ id, published }: { id: string; published: boolean }) =>
+ published ? chatbotsAPI.unpublish(id) : chatbotsAPI.publish(id),
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['chatbots'] }),
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? 'Action failed'),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: chatbotsAPI.delete,
+ onSuccess: () => { qc.invalidateQueries({ queryKey: ['chatbots'] }); toast.success(t.dashboard.delete_title); },
+ onError: (err: any) => toast.error(err?.response?.data?.detail ?? t.common.error),
+ });
+
+ const handleDelete = useCallback((bot: Chatbot) => {
+ Alert.alert(
+ t.dashboard.delete_title,
+ t.dashboard.delete_confirm(bot.name),
+ [
+ { text: t.common.cancel, style: 'cancel' },
+ { text: t.common.delete, style: 'destructive', onPress: () => deleteMutation.mutate(bot.id) },
+ ],
+ );
+ }, [deleteMutation]);
+
+ const renderItem = ({ item, index }: { item: Chatbot; index: number }) => (
+ navigation.navigate('ChatbotBuilder', { chatbotId: item.id })}
+ onPreview={() => navigation.navigate('ChatPreview', { chatbotId: item.id, chatbotName: item.name })}
+ onTogglePublish={() => publishMutation.mutate({ id: item.id, published: item.is_published })}
+ onDelete={() => handleDelete(item)}
+ publishLoading={publishMutation.isPending}
+ />
+ );
+
+ return (
+
+ {/* Header */}
+
+
+ {t.dashboard.title}
+
+ {user?.company_name}
+
+
+
+ navigation.navigate('ChatbotBuilder', {})} />
+
+
+ {isLoading ? (
+
+ ) : (
+ item.id}
+ renderItem={renderItem}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ ListHeaderComponent={
+ user ? (
+ navigation.navigate('ChatbotBuilder', {})}
+ />
+ ) : null
+ }
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ 🤖}
+ title={t.dashboard.empty_title}
+ description={t.dashboard.empty_desc}
+ action={{ label: t.dashboard.create_first, onPress: () => navigation.navigate('ChatbotBuilder', {}) }}
+ />
+ }
+ />
+ )}
+
+ );
+}
+
+function ChatbotCard({
+ bot, index, onEdit, onPreview, onTogglePublish, onDelete, publishLoading,
+}: {
+ bot: Chatbot;
+ index: number;
+ onEdit: () => void;
+ onPreview: () => void;
+ onTogglePublish: () => void;
+ onDelete: () => void;
+ publishLoading: boolean;
+}) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const scale = useRef(new Animated.Value(1)).current;
+ const accentColor = bot.primary_color || COLORS.primary;
+
+ return (
+
+ {/* Accent strip */}
+
+
+
+ {/* Top row */}
+
+
+
+ {bot.name[0]?.toUpperCase()}
+
+
+
+ {bot.name}
+ {bot.description ? (
+
+ {bot.description}
+
+ ) : null}
+
+
+
+
+ {/* Stats row */}
+
+
+
+ {bot.model ? (
+
+
+ {bot.model.split('/').pop() ?? bot.model}
+
+
+ ) : null}
+
+
+ {/* Actions */}
+
+
+
+
+
+ 🗑
+
+
+
+
+ );
+}
+
+function Stat({ icon, value, label, theme }: { icon: string; value: number; label: string; theme: any }) {
+ return (
+
+ {icon}
+ {value}
+ {label}
+
+ );
+}
+
+const statStyles = StyleSheet.create({
+ chip: { flexDirection: 'row', alignItems: 'center', borderRadius: RADIUS.sm, paddingVertical: 5, paddingHorizontal: SPACING.sm, gap: 3 },
+ icon: { fontSize: 12 },
+ value: { ...TEXT.smallM },
+ label: { ...TEXT.caption },
+});
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: SPACING.lg,
+ paddingVertical: SPACING.md,
+ borderBottomWidth: 1,
+ },
+ headerLeft: { flex: 1, marginRight: SPACING.md },
+ headerTitle: { ...TEXT.h3 },
+ headerMeta: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm, marginTop: 3 },
+ headerSub: { ...TEXT.small },
+
+ list: { padding: SPACING.lg, gap: SPACING.md, flexGrow: 1, paddingBottom: SPACING.xxxl },
+
+ card: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ overflow: 'hidden',
+ flexDirection: 'row',
+ },
+ accentStrip: { width: 4 },
+ cardBody: { flex: 1, padding: SPACING.lg, gap: SPACING.md },
+
+ cardTop: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md },
+ botAvatar: { width: 42, height: 42, borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center' },
+ botAvatarText: { fontSize: FONT_SIZE.lg, fontWeight: '700' },
+ botInfo: { flex: 1 },
+ botName: { ...TEXT.h4 },
+ botDesc: { ...TEXT.small, marginTop: 2 },
+ statusDot: { width: 9, height: 9, borderRadius: RADIUS.full },
+
+ statsRow: { flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap', alignItems: 'center' },
+ modelPill: { borderRadius: RADIUS.sm, paddingVertical: 5, paddingHorizontal: SPACING.sm },
+ modelText: { fontSize: FONT_SIZE.xs },
+
+ actions: { flexDirection: 'row', gap: SPACING.sm, alignItems: 'center', flexWrap: 'wrap' },
+ actionBtn: {},
+ deleteBtn: { marginLeft: 'auto', padding: SPACING.xs },
+ deleteIcon: { fontSize: 17 },
+});
+
+const onboardStyles = StyleSheet.create({
+ card: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ marginBottom: SPACING.md,
+ gap: SPACING.md,
+ },
+ cardHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ title: { flex: 1, fontSize: FONT_SIZE.md, fontWeight: '700' },
+ progressLabel: {
+ borderRadius: RADIUS.full,
+ paddingVertical: 2,
+ paddingHorizontal: SPACING.sm,
+ },
+ progressLabelText: { fontSize: FONT_SIZE.xs, fontWeight: '700' },
+ dismiss: { fontSize: FONT_SIZE.md, paddingHorizontal: SPACING.xs },
+ bar: { height: 6, borderRadius: RADIUS.full, overflow: 'hidden' },
+ barFill: {
+ height: '100%',
+ borderRadius: RADIUS.full,
+ backgroundColor: COLORS.primary,
+ },
+ step: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ stepDot: {
+ width: 22,
+ height: 22,
+ borderRadius: RADIUS.full,
+ borderWidth: 1.5,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ checkmark: { color: COLORS.white, fontSize: 12, fontWeight: '800' },
+ stepNum: { fontSize: FONT_SIZE.xs, fontWeight: '700' },
+ stepLabel: { flex: 1, fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ stepLabelDone: { textDecorationLine: 'line-through' },
+});
diff --git a/src/screens/inbox/ConversationScreen.tsx b/src/screens/inbox/ConversationScreen.tsx
new file mode 100644
index 0000000..525451d
--- /dev/null
+++ b/src/screens/inbox/ConversationScreen.tsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ Alert,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { InboxStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT } from '../../theme';
+import { useTheme } from '../../theme';
+import { Spinner, EmptyState, Button } from '../../components/ui';
+import { inboxAPI } from '../../services/api';
+import { Message } from '../../types';
+import { useToast } from '../../contexts/ToastContext';
+import { useTranslation } from '../../i18n';
+
+type Props = NativeStackScreenProps;
+
+export function ConversationScreen({ route, navigation }: Props) {
+ const { conversationId } = route.params;
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const qc = useQueryClient();
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['conversation', conversationId],
+ queryFn: () => inboxAPI.getConversation(conversationId),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: () => inboxAPI.deleteConversation(conversationId),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['inbox'] });
+ toast.success(t.inbox.delete);
+ navigation.goBack();
+ },
+ });
+
+ const handleDelete = () => {
+ Alert.alert(t.inbox.delete, t.inbox.delete_confirm, [
+ { text: t.common.cancel, style: 'cancel' },
+ { text: t.common.delete, style: 'destructive', onPress: () => deleteMutation.mutate() },
+ ]);
+ };
+
+ const messages: Message[] = data?.messages ?? [];
+
+ const renderMessage = ({ item }: { item: Message }) => {
+ const isUser = item.role === 'user';
+ return (
+
+ {!isUser && (
+
+ AI
+
+ )}
+
+
+ {item.content}
+
+ {!isUser && item.confidence_score != null && (
+
+ Confidence: {(item.confidence_score * 100).toFixed(0)}%
+
+ )}
+ {item.is_handoff && (
+ ⚡ Handoff requested
+ )}
+ {item.created_at ? (
+
+ {new Date(item.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+ ) : null}
+
+
+ );
+ };
+
+ if (isLoading) return ;
+
+ return (
+
+
+
+
+ {data?.chatbot_name ?? 'Conversation'}
+
+
+ {messages.length} {t.inbox.filter_all.toLowerCase()}
+
+
+
+
+
+ item.id}
+ renderItem={renderMessage}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ ListEmptyComponent={
+
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: SPACING.lg,
+ paddingVertical: SPACING.md,
+ borderBottomWidth: 1,
+ },
+ headerTitle: { ...TEXT.h4 },
+ headerSub: { ...TEXT.small },
+
+ list: { padding: SPACING.md, gap: SPACING.md, paddingBottom: SPACING.xl },
+
+ msgRow: { flexDirection: 'row', gap: SPACING.sm, alignItems: 'flex-end' },
+ msgRowUser: { flexDirection: 'row-reverse' },
+
+ botAvatar: { width: 30, height: 30, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center' },
+ botAvatarText: { fontSize: 10, fontWeight: '700' },
+
+ bubble: { maxWidth: '75%', borderRadius: RADIUS.xl, padding: SPACING.md, gap: 4 },
+ bubbleUser: { borderBottomRightRadius: RADIUS.sm },
+ bubbleBot: { borderWidth: 1, borderBottomLeftRadius: RADIUS.sm },
+ bubbleText: { fontSize: FONT_SIZE.md, lineHeight: 22 },
+
+ confidence: { fontSize: FONT_SIZE.xs },
+ handoffTag: { fontSize: FONT_SIZE.xs, color: COLORS.warning, fontWeight: '600' },
+ timestamp: { fontSize: 10 },
+});
diff --git a/src/screens/inbox/InboxScreen.tsx b/src/screens/inbox/InboxScreen.tsx
new file mode 100644
index 0000000..5a35926
--- /dev/null
+++ b/src/screens/inbox/InboxScreen.tsx
@@ -0,0 +1,264 @@
+import React, { useRef, useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TouchableOpacity,
+ RefreshControl,
+ Animated,
+ ScrollView,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { InboxStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Spinner, EmptyState } from '../../components/ui';
+import { inboxAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+import { Conversation } from '../../types';
+
+type Props = NativeStackScreenProps;
+
+type StatusFilter = 'all' | 'open' | 'agent_handling' | 'resolved';
+
+const STATUS_COLORS: Record = {
+ open: COLORS.success,
+ agent_handling: COLORS.warning,
+ resolved: COLORS.info,
+};
+
+export function InboxScreen({ navigation }: Props) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const [statusFilter, setStatusFilter] = useState('all');
+
+ const FILTERS: { key: StatusFilter; label: string }[] = [
+ { key: 'all', label: t.inbox.filter_all },
+ { key: 'open', label: t.inbox.filter_open },
+ { key: 'agent_handling', label: t.inbox.filter_handling },
+ { key: 'resolved', label: t.inbox.filter_resolved },
+ ];
+
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['inbox', statusFilter],
+ queryFn: () =>
+ inboxAPI.listConversations({
+ limit: 50,
+ ...(statusFilter !== 'all' ? { status: statusFilter } : {}),
+ }),
+ });
+
+ const conversations: Conversation[] = data?.conversations ?? data?.items ?? data ?? [];
+
+ const formatTime = (iso?: string) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const now = new Date();
+ const diffH = (now.getTime() - d.getTime()) / 3600000;
+ if (diffH < 24) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ if (diffH < 48) return 'Yesterday';
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
+ };
+
+ const renderItem = ({ item }: { item: Conversation }) => (
+ navigation.navigate('Conversation', {
+ conversationId: item.id,
+ chatbotName: item.chatbot_name,
+ })}
+ theme={theme}
+ />
+ );
+
+ return (
+
+
+ {t.inbox.title}
+ {conversations.length > 0 && (
+
+ {conversations.length}
+
+ )}
+
+
+ {/* Status filter tabs */}
+
+ {FILTERS.map(f => (
+ setStatusFilter(f.key)}>
+
+ {f.label}
+
+
+ ))}
+
+
+ {isLoading ? (
+
+ ) : (
+ item.id}
+ renderItem={renderItem}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ ✉️}
+ title={t.inbox.empty_title}
+ description={statusFilter === 'all' ? t.inbox.empty_desc : t.inbox.empty_filtered(statusFilter.replace('_', ' '))}
+ />
+ }
+ />
+ )}
+
+ );
+}
+
+function ConversationRow({
+ item,
+ time,
+ onPress,
+ theme,
+}: {
+ item: Conversation;
+ time: string;
+ onPress: () => void;
+ theme: any;
+}) {
+ const { t } = useTranslation();
+ const scale = useRef(new Animated.Value(1)).current;
+ const statusColor = STATUS_COLORS[(item as any).status] ?? theme.border;
+
+ const onPressIn = () => Animated.spring(scale, { toValue: 0.98, useNativeDriver: true, speed: 60 }).start();
+ const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
+
+ return (
+
+
+
+
+ {(item.chatbot_name ?? 'C')[0].toUpperCase()}
+
+
+
+
+
+
+ {item.chatbot_name ?? 'Conversation'}
+
+ {time}
+
+
+
+ {item.last_message ?? t.inbox.no_messages}
+
+ {(item as any).status && (item as any).status !== 'open' && (
+
+ )}
+ {(item.message_count ?? 0) > 0 && (
+
+ {item.message_count}
+
+ )}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: SPACING.lg,
+ paddingVertical: SPACING.md,
+ borderBottomWidth: 1,
+ gap: SPACING.sm,
+ },
+ headerTitle: { ...TEXT.h3 },
+ countBadge: {
+ borderRadius: RADIUS.full,
+ paddingVertical: 3,
+ paddingHorizontal: SPACING.sm,
+ minWidth: 28,
+ alignItems: 'center',
+ },
+ countBadgeText: { fontSize: FONT_SIZE.xs, fontWeight: '700' },
+
+ filterBar: { borderBottomWidth: 1, flexGrow: 0 },
+ filterBarContent: { paddingHorizontal: SPACING.lg },
+ filterTab: {
+ paddingVertical: SPACING.sm,
+ paddingHorizontal: SPACING.md,
+ marginRight: SPACING.xs,
+ },
+ filterTabText: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+
+ item: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: SPACING.lg,
+ paddingVertical: SPACING.md,
+ borderBottomWidth: 1,
+ gap: SPACING.md,
+ },
+ avatar: {
+ width: 50,
+ height: 50,
+ borderRadius: RADIUS.full,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ avatarText: { fontSize: FONT_SIZE.lg, fontWeight: '800' },
+
+ itemContent: { flex: 1 },
+ itemHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 },
+ chatbotName: { ...TEXT.bodyM, flex: 1, marginRight: SPACING.sm },
+ time: { fontSize: FONT_SIZE.xs },
+
+ itemFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: SPACING.xs },
+ preview: { ...TEXT.small, flex: 1 },
+ statusDot: { width: 8, height: 8, borderRadius: RADIUS.full, flexShrink: 0 },
+ msgCount: {
+ borderRadius: RADIUS.full,
+ minWidth: 20,
+ height: 20,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 6,
+ },
+ msgCountText: { color: COLORS.white, fontSize: 11, fontWeight: '700' },
+});
diff --git a/src/screens/leads/LeadsScreen.tsx b/src/screens/leads/LeadsScreen.tsx
new file mode 100644
index 0000000..ad775ca
--- /dev/null
+++ b/src/screens/leads/LeadsScreen.tsx
@@ -0,0 +1,432 @@
+import React, { useRef, useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ RefreshControl,
+ TouchableOpacity,
+ Animated,
+ Share,
+ Alert,
+ Modal,
+ TextInput,
+ ScrollView,
+ ActivityIndicator,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Spinner, EmptyState } from '../../components/ui';
+import { leadsAPI } from '../../services/api';
+import { useTranslation } from '../../i18n';
+import { Lead } from '../../types';
+
+type LeadStatus = 'new' | 'contacted' | 'qualified' | 'closed' | 'lost';
+
+const STATUS_COLORS: Record = {
+ new: COLORS.info,
+ contacted: COLORS.warning,
+ qualified: COLORS.purple,
+ closed: COLORS.success,
+ lost: COLORS.error,
+};
+
+function buildCSV(leads: Lead[]): string {
+ const header = 'Name,Email,Phone,Company,Chatbot,Status,Date';
+ const rows = leads.map(l => [
+ l.name ?? '',
+ l.email ?? '',
+ l.phone ?? '',
+ l.company ?? '',
+ l.chatbot_name ?? '',
+ (l as any).status ?? '',
+ l.created_at ? new Date(l.created_at).toLocaleDateString() : '',
+ ].map(v => `"${v.replace(/"/g, '""')}"`).join(','));
+ return [header, ...rows].join('\n');
+}
+
+export function LeadsScreen() {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const qc = useQueryClient();
+ const [selectedLead, setSelectedLead] = useState(null);
+
+ const STATUS_OPTIONS: { key: LeadStatus; label: string; color: string }[] = [
+ { key: 'new', label: t.leads.status_new, color: COLORS.info },
+ { key: 'contacted', label: t.leads.status_contacted, color: COLORS.warning },
+ { key: 'qualified', label: t.leads.status_qualified, color: COLORS.purple },
+ { key: 'closed', label: t.leads.status_closed, color: COLORS.success },
+ { key: 'lost', label: t.leads.status_lost, color: COLORS.error },
+ ];
+
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['leads'],
+ queryFn: () => leadsAPI.list({ limit: 200 }),
+ });
+
+ const leads: Lead[] = data?.leads ?? data?.items ?? data ?? [];
+
+ const updateMutation = useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Record }) =>
+ leadsAPI.update(id, data),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['leads'] });
+ setSelectedLead(null);
+ },
+ });
+
+ const handleExport = async () => {
+ if (leads.length === 0) {
+ Alert.alert(t.leads.title, t.leads.no_export);
+ return;
+ }
+ const csv = buildCSV(leads);
+ await Share.share({ message: csv, title: 'Leads export' });
+ };
+
+ const renderItem = ({ item }: { item: Lead }) => (
+ setSelectedLead(item)} />
+ );
+
+ return (
+
+
+
+ {t.leads.title}
+
+ {t.leads.subtitle(leads.length)}
+
+
+
+ {t.leads.export}
+
+
+
+ {isLoading ? (
+
+ ) : (
+ item.id}
+ renderItem={renderItem}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ 👥}
+ title={t.leads.empty_title}
+ description={t.leads.empty_desc}
+ />
+ }
+ />
+ )}
+
+ {/* Lead detail / edit modal */}
+ {selectedLead && (
+ setSelectedLead(null)}
+ onSave={(status, notes) =>
+ updateMutation.mutate({ id: selectedLead.id, data: { status, notes } })
+ }
+ />
+ )}
+
+ );
+}
+
+function LeadDetailModal({
+ lead,
+ theme,
+ saving,
+ statusOptions,
+ onClose,
+ onSave,
+}: {
+ lead: Lead;
+ theme: any;
+ saving: boolean;
+ statusOptions: { key: LeadStatus; label: string; color: string }[];
+ onClose: () => void;
+ onSave: (status: string, notes: string) => void;
+}) {
+ const { t } = useTranslation();
+ const [status, setStatus] = useState((lead as any).status ?? 'new');
+ const [notes, setNotes] = useState((lead as any).notes ?? '');
+
+ return (
+
+
+
+
+ {t.leads.cancel}
+
+ {t.leads.detail_title}
+ onSave(status, notes)} disabled={saving}>
+ {saving
+ ?
+ : {t.leads.save}}
+
+
+
+
+ {/* Info */}
+
+ {t.leads.contact_info}
+ {lead.name ? : null}
+ {lead.email ? : null}
+ {lead.phone ? : null}
+ {lead.company ? : null}
+ {lead.chatbot_name ? : null}
+ {lead.created_at ? (
+
+ ) : null}
+
+
+ {/* Status */}
+
+ {t.leads.status_section}
+
+ {statusOptions.map(opt => (
+ setStatus(opt.key)}>
+
+ {opt.label}
+
+
+ ))}
+
+
+
+ {/* Notes */}
+
+ {t.leads.notes_section}
+
+
+
+
+
+ );
+}
+
+function InfoRow({ label, value, theme }: { label: string; value: string; theme: any }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+function LeadCard({ item, theme, onPress }: { item: Lead; theme: any; onPress: () => void }) {
+ const { t } = useTranslation();
+ const scale = useRef(new Animated.Value(1)).current;
+ const onPressIn = () => Animated.spring(scale, { toValue: 0.98, useNativeDriver: true, speed: 60 }).start();
+ const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
+
+ const initials = (item.name ?? item.email ?? '?')[0].toUpperCase();
+ const colors = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444'];
+ const colorIdx = initials.charCodeAt(0) % colors.length;
+ const avatarColor = colors[colorIdx];
+ const itemStatus = (item as any).status as LeadStatus | undefined;
+ const statusLabels: Record = {
+ new: t.leads.status_new,
+ contacted: t.leads.status_contacted,
+ qualified: t.leads.status_qualified,
+ closed: t.leads.status_closed,
+ lost: t.leads.status_lost,
+ };
+ const statusColor = itemStatus ? STATUS_COLORS[itemStatus] : undefined;
+ const statusLabel = itemStatus ? statusLabels[itemStatus] : undefined;
+
+ return (
+
+
+
+
+ {initials}
+
+
+
+ {item.name ? (
+ {item.name}
+ ) : null}
+ {item.email ? (
+ {item.email}
+ ) : null}
+ {item.company ? (
+ {item.company}
+ ) : null}
+
+
+
+ {item.created_at ? (
+
+ {new Date(item.created_at).toLocaleDateString([], { month: 'short', day: 'numeric' })}
+
+ ) : null}
+ {statusColor && statusLabel && (
+
+ {statusLabel}
+
+ )}
+
+
+
+ {(item.phone || item.chatbot_name) && (
+
+ {item.phone ? (
+
+ ) : null}
+ {item.chatbot_name ? (
+
+ ) : null}
+
+ )}
+
+
+ );
+}
+
+function MetaChip({ icon, value, theme }: { icon: string; value: string; theme: any }) {
+ return (
+
+ {icon}
+ {value}
+
+ );
+}
+
+const chipStyles = StyleSheet.create({
+ chip: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: RADIUS.full,
+ borderWidth: 1,
+ paddingVertical: 4,
+ paddingHorizontal: SPACING.sm,
+ gap: 4,
+ },
+ icon: { fontSize: 12 },
+ value: { fontSize: FONT_SIZE.xs },
+});
+
+const infoStyles = StyleSheet.create({
+ row: { flexDirection: 'row', paddingVertical: SPACING.xs, gap: SPACING.md },
+ label: { width: 70, fontSize: FONT_SIZE.sm },
+ value: { flex: 1, fontSize: FONT_SIZE.sm, fontWeight: '500' },
+});
+
+const modalStyles = StyleSheet.create({
+ safe: { flex: 1 },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: SPACING.lg,
+ borderBottomWidth: 1,
+ },
+ title: { fontSize: FONT_SIZE.md, fontWeight: '700' },
+ scroll: { padding: SPACING.lg, gap: SPACING.md, paddingBottom: SPACING.xxxl },
+ section: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.sm,
+ },
+ sectionTitle: { fontSize: FONT_SIZE.md, fontWeight: '700', marginBottom: SPACING.xs },
+ statusGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm },
+ statusPill: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.full,
+ paddingVertical: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ },
+ statusLabel: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+ notesInput: {
+ borderWidth: 1.5,
+ borderRadius: RADIUS.md,
+ padding: SPACING.md,
+ fontSize: FONT_SIZE.md,
+ minHeight: 110,
+ },
+});
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: SPACING.lg,
+ paddingVertical: SPACING.md,
+ borderBottomWidth: 1,
+ },
+ headerTitle: { ...TEXT.h3 },
+ headerSub: { ...TEXT.small, marginTop: 2 },
+ exportBtn: {
+ borderRadius: RADIUS.full,
+ paddingVertical: 7,
+ paddingHorizontal: SPACING.md,
+ },
+ exportBtnText: { fontSize: FONT_SIZE.sm, fontWeight: '700' },
+
+ list: { padding: SPACING.lg, gap: SPACING.md, flexGrow: 1, paddingBottom: SPACING.xxxl },
+
+ card: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.md,
+ },
+ cardTop: { flexDirection: 'row', alignItems: 'flex-start', gap: SPACING.md },
+ avatar: {
+ width: 46,
+ height: 46,
+ borderRadius: RADIUS.full,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ avatarText: { color: COLORS.white, fontSize: FONT_SIZE.md, fontWeight: '800' },
+ info: { flex: 1 },
+ name: { ...TEXT.bodyM },
+ email: { ...TEXT.small, marginTop: 2 },
+ company: { fontSize: FONT_SIZE.xs, marginTop: 2 },
+ rightCol: { alignItems: 'flex-end', gap: 4 },
+ date: { fontSize: FONT_SIZE.xs },
+ statusBadge: { borderRadius: RADIUS.full, paddingVertical: 2, paddingHorizontal: SPACING.sm },
+ statusText: { fontSize: FONT_SIZE.xs, fontWeight: '700' },
+ chips: { flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap' },
+});
diff --git a/src/screens/marketplace/ChatbotDetailScreen.tsx b/src/screens/marketplace/ChatbotDetailScreen.tsx
new file mode 100644
index 0000000..a941265
--- /dev/null
+++ b/src/screens/marketplace/ChatbotDetailScreen.tsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { MarketplaceStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Card, Badge, Spinner, Button } from '../../components/ui';
+import { marketplaceAPI } from '../../services/api';
+
+type Props = NativeStackScreenProps;
+
+export function ChatbotDetailScreen({ route, navigation }: Props) {
+ const { chatbotId } = route.params;
+ const { theme } = useTheme();
+
+ const { data: bot, isLoading } = useQuery({
+ queryKey: ['marketplace-bot', chatbotId],
+ queryFn: () => marketplaceAPI.get(chatbotId),
+ });
+
+ if (isLoading) return ;
+ if (!bot) return null;
+
+ return (
+
+
+
+ {/* Hero card */}
+
+
+ {bot.name[0]?.toUpperCase()}
+
+ {bot.name}
+ {bot.company_name ? (
+ by {bot.company_name}
+ ) : null}
+
+
+
+
+ {bot.category ? : null}
+
+
+ navigation.navigate('PublicChat', { chatbotId: bot.id, chatbotName: bot.name })}
+ fullWidth
+ size="lg"
+ style={styles.chatBtn}
+ />
+
+
+ {/* Description */}
+ {bot.description ? (
+
+ About
+ {bot.description}
+
+ ) : null}
+
+ {/* Details */}
+
+ Details
+ {bot.industry ? : null}
+ {bot.category ? : null}
+ {(bot.languages ?? []).length > 0 ? (
+
+ Languages
+
+ {bot.languages.map((lang: string) => (
+
+ ))}
+
+
+ ) : null}
+
+
+
+ );
+}
+
+function StatItem({ icon, value, label }: { icon: string; value: string; label: string }) {
+ const { theme } = useTheme();
+ return (
+
+ {icon}
+ {value}
+ {label}
+
+ );
+}
+
+function DetailRow({ label, value, theme }: { label: string; value: string; theme: any }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+const statStyles = StyleSheet.create({
+ item: { alignItems: 'center', flex: 1, gap: 2 },
+ icon: { fontSize: 20 },
+ value: { fontSize: FONT_SIZE.md, fontWeight: '700' },
+ label: { fontSize: FONT_SIZE.xs },
+});
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ scroll: { padding: SPACING.lg, gap: SPACING.lg },
+
+ hero: { alignItems: 'center', gap: SPACING.sm },
+ heroAvatar: { width: 72, height: 72, borderRadius: RADIUS.xl, alignItems: 'center', justifyContent: 'center' },
+ heroAvatarText: { color: '#fff', fontSize: 32, fontWeight: '700' },
+ heroName: { fontSize: FONT_SIZE.xxl, fontWeight: '700', textAlign: 'center' },
+ heroCompany: { fontSize: FONT_SIZE.sm },
+ heroStats: { flexDirection: 'row', width: '100%', paddingVertical: SPACING.md },
+ chatBtn: { marginTop: SPACING.sm },
+
+ section: { gap: SPACING.md },
+ sectionTitle: { fontSize: FONT_SIZE.lg, fontWeight: '600' },
+ sectionText: { fontSize: FONT_SIZE.md, lineHeight: 22 },
+
+ detailRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: SPACING.xs },
+ detailLabel: { fontSize: FONT_SIZE.sm },
+ detailValue: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ languagesRow: { flexDirection: 'row', gap: SPACING.xs, flexWrap: 'wrap' },
+});
diff --git a/src/screens/marketplace/MarketplaceScreen.tsx b/src/screens/marketplace/MarketplaceScreen.tsx
new file mode 100644
index 0000000..0410753
--- /dev/null
+++ b/src/screens/marketplace/MarketplaceScreen.tsx
@@ -0,0 +1,225 @@
+import React, { useState, useRef } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ TextInput,
+ TouchableOpacity,
+ RefreshControl,
+ Animated,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { MarketplaceStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { Spinner, EmptyState } from '../../components/ui';
+import { marketplaceAPI } from '../../services/api';
+import { MarketplaceChatbot } from '../../types';
+
+type Props = NativeStackScreenProps;
+
+export function MarketplaceScreen({ navigation }: Props) {
+ const { theme, isDark } = useTheme();
+ const [search, setSearch] = useState('');
+ const [debouncedSearch, setDebouncedSearch] = useState('');
+ const searchTimer = useRef | null>(null);
+
+ const handleSearchChange = (text: string) => {
+ setSearch(text);
+ if (searchTimer.current) clearTimeout(searchTimer.current);
+ searchTimer.current = setTimeout(() => setDebouncedSearch(text), 400);
+ };
+
+ const { data, isLoading, refetch } = useQuery({
+ queryKey: ['marketplace', debouncedSearch],
+ queryFn: () => marketplaceAPI.list({ search: debouncedSearch || undefined, limit: 20 }),
+ });
+
+ const chatbots: MarketplaceChatbot[] = data?.chatbots ?? data?.items ?? data ?? [];
+
+ return (
+
+ {/* Header */}
+
+ Explore
+
+ Discover AI chatbots built by the community
+
+
+
+ {/* Search */}
+
+ 🔍
+
+ {search.length > 0 && (
+ { setSearch(''); setDebouncedSearch(''); }}>
+
+ ✕
+
+
+ )}
+
+
+ {isLoading ? (
+
+ ) : (
+ item.id}
+ renderItem={({ item }) => (
+ navigation.navigate('MarketplaceDetail', { chatbotId: item.id })} />
+ )}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ 🧭}
+ title={debouncedSearch ? 'No results found' : 'Marketplace is empty'}
+ description={debouncedSearch ? 'Try a different search term.' : 'No chatbots published yet.'}
+ />
+ }
+ />
+ )}
+
+ );
+}
+
+function BotCard({ item, onPress }: { item: MarketplaceChatbot; onPress: () => void }) {
+ const { theme } = useTheme();
+ const scale = useRef(new Animated.Value(1)).current;
+ const accentColor = item.primary_color || COLORS.primary;
+
+ const onPressIn = () => Animated.spring(scale, { toValue: 0.97, useNativeDriver: true, speed: 60 }).start();
+ const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
+
+ return (
+
+
+ {/* Top */}
+
+
+ {item.name[0]?.toUpperCase()}
+
+
+ {item.name}
+ {item.company_name ? (
+ by {item.company_name}
+ ) : null}
+
+
+ ★
+ {item.average_rating?.toFixed(1) ?? '–'}
+
+
+
+ {/* Description */}
+ {item.description ? (
+
+ {item.description}
+
+ ) : null}
+
+ {/* Footer chips */}
+
+ {item.category ? (
+
+ {item.category}
+
+ ) : null}
+ {(item.conversation_count ?? 0) > 0 ? (
+
+
+ 💬 {item.conversation_count?.toLocaleString()} chats
+
+
+ ) : null}
+ {(item.languages ?? []).length > 0 ? (
+
+
+ 🌐 {item.languages.slice(0, 2).join(', ')}
+
+
+ ) : null}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+
+ header: { paddingHorizontal: SPACING.lg, paddingTop: SPACING.md, paddingBottom: SPACING.sm },
+ headerTitle: { ...TEXT.h2 },
+ headerSub: { ...TEXT.small, marginTop: 4 },
+
+ searchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginHorizontal: SPACING.lg,
+ marginBottom: SPACING.sm,
+ borderRadius: RADIUS.full,
+ borderWidth: 1.5,
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.xs,
+ gap: SPACING.sm,
+ },
+ searchIcon: { fontSize: 15 },
+ searchInput: { flex: 1, fontSize: FONT_SIZE.md, paddingVertical: SPACING.xs },
+ clearBtn: { width: 22, height: 22, borderRadius: RADIUS.full, alignItems: 'center', justifyContent: 'center' },
+
+ list: { paddingHorizontal: SPACING.lg, paddingTop: SPACING.sm, paddingBottom: SPACING.xxxl, gap: SPACING.md },
+
+ card: {
+ borderRadius: RADIUS.xl,
+ padding: SPACING.lg,
+ borderWidth: 1,
+ gap: SPACING.md,
+ },
+ cardTop: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md },
+ botAvatar: { width: 48, height: 48, borderRadius: RADIUS.lg, alignItems: 'center', justifyContent: 'center' },
+ botAvatarText: { color: COLORS.white, fontSize: FONT_SIZE.xl, fontWeight: '700' },
+ botMeta: { flex: 1 },
+ botName: { ...TEXT.h4 },
+ companyName: { ...TEXT.caption, marginTop: 2 },
+
+ ratingPill: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: '#fef9c3',
+ borderRadius: RADIUS.full,
+ paddingVertical: 4,
+ paddingHorizontal: SPACING.sm,
+ gap: 3,
+ },
+ ratingStar: { fontSize: 12, color: '#d97706' },
+ ratingText: { fontSize: FONT_SIZE.sm, fontWeight: '700', color: '#92400e' },
+
+ desc: { ...TEXT.small, lineHeight: 20 },
+
+ chips: { flexDirection: 'row', gap: SPACING.sm, flexWrap: 'wrap' },
+ chip: { borderRadius: RADIUS.full, paddingVertical: 4, paddingHorizontal: SPACING.sm },
+ chipText: { ...TEXT.captionM },
+});
diff --git a/src/screens/marketplace/PublicChatScreen.tsx b/src/screens/marketplace/PublicChatScreen.tsx
new file mode 100644
index 0000000..385e415
--- /dev/null
+++ b/src/screens/marketplace/PublicChatScreen.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { useQuery } from '@tanstack/react-query';
+import { MarketplaceStackParamList } from '../../navigation/types';
+import { ChatInterface } from '../../components/ChatInterface';
+import { Spinner } from '../../components/ui';
+import { useTheme } from '../../theme';
+import { marketplaceAPI } from '../../services/api';
+
+type Props = NativeStackScreenProps;
+
+export function PublicChatScreen({ route }: Props) {
+ const { chatbotId } = route.params;
+ const { theme } = useTheme();
+
+ const { data: bot, isLoading } = useQuery({
+ queryKey: ['marketplace-bot', chatbotId],
+ queryFn: () => marketplaceAPI.get(chatbotId),
+ staleTime: 5 * 60 * 1000,
+ });
+
+ if (isLoading) return ;
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+});
diff --git a/src/screens/settings/SettingsScreen.tsx b/src/screens/settings/SettingsScreen.tsx
new file mode 100644
index 0000000..24fb720
--- /dev/null
+++ b/src/screens/settings/SettingsScreen.tsx
@@ -0,0 +1,414 @@
+import React, { useState, useRef } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Alert,
+ Animated,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { AccountStackParamList } from '../../navigation/types';
+import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../theme';
+import { useTheme } from '../../theme';
+import { PlanBadge, Button, Input, SecureInput } from '../../components/ui';
+import { useToast } from '../../contexts/ToastContext';
+import { authAPI, billingAPI } from '../../services/api';
+import { useAuthStore } from '../../stores/authStore';
+import { useThemeStore, ThemeMode } from '../../stores/themeStore';
+import { useLanguageStore, AppLanguage } from '../../stores/languageStore';
+import { useTranslation } from '../../i18n';
+
+type Props = NativeStackScreenProps;
+
+const PLAN_FEATURES: Record = {
+ free: ['1 published chatbot', '3 docs/bot', '100 convos/mo', 'Llama 3.3 model'],
+ starter: ['1 published chatbot', '10 docs/bot', '1,500 convos/mo', 'Telegram integration', 'Inbox & Leads'],
+ business: ['3 published chatbots', '50 docs/bot', '5,000 convos/mo', 'All models', 'WhatsApp + Telegram'],
+ agency: ['Unlimited chatbots', 'Unlimited docs', '20,000 convos/mo', 'Code export'],
+ enterprise: ['Everything in Agency', 'Unlimited convos', 'Priority support'],
+};
+
+export function SettingsScreen({ navigation }: Props) {
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const qc = useQueryClient();
+ const user = useAuthStore(s => s.user);
+ const updateUser = useAuthStore(s => s.updateUser);
+ const logout = useAuthStore(s => s.logout);
+
+ const appLanguage = useLanguageStore(s => s.language);
+ const setAppLanguage = useLanguageStore(s => s.setLanguage);
+
+ const [companyName, setCompanyName] = useState(user?.company_name ?? '');
+ const [currentPassword, setCurrentPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [savingProfile, setSavingProfile] = useState(false);
+
+ const LANGUAGE_OPTIONS: { key: AppLanguage; label: string; flag: string }[] = [
+ { key: 'fr', label: 'Français', flag: '🇫🇷' },
+ { key: 'en', label: 'English', flag: '🇬🇧' },
+ ];
+
+ const { data: subscription } = useQuery({
+ queryKey: ['subscription'],
+ queryFn: billingAPI.subscription,
+ });
+
+ const themeMode = useThemeStore(s => s.mode);
+ const setThemeMode = useThemeStore(s => s.setMode);
+
+ const THEME_OPTIONS: { key: ThemeMode; label: string; icon: string }[] = [
+ { key: 'system', label: t.settings.theme_system, icon: '⚙️' },
+ { key: 'light', label: t.settings.theme_light, icon: '☀️' },
+ { key: 'dark', label: t.settings.theme_dark, icon: '🌙' },
+ ];
+
+ const handleSaveProfile = async () => {
+ if (!companyName.trim()) {
+ toast.error(t.settings.company_required);
+ return;
+ }
+ setSavingProfile(true);
+ try {
+ const payload: any = { company_name: companyName.trim(), language: appLanguage };
+ if (currentPassword && newPassword) {
+ payload.current_password = currentPassword;
+ payload.new_password = newPassword;
+ }
+ const updated = await authAPI.updateProfile(payload);
+ updateUser(updated.user ?? { company_name: companyName.trim() });
+ setCurrentPassword('');
+ setNewPassword('');
+ toast.success(t.settings.profile_updated);
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? t.settings.update_failed);
+ } finally {
+ setSavingProfile(false);
+ }
+ };
+
+ const handleLogout = () => {
+ Alert.alert(t.settings.sign_out, t.settings.sign_out_confirm, [
+ { text: t.common.cancel, style: 'cancel' },
+ {
+ text: t.settings.sign_out,
+ style: 'destructive',
+ onPress: async () => {
+ try { await authAPI.logout(); } catch {}
+ logout();
+ qc.clear();
+ },
+ },
+ ]);
+ };
+
+ const handleDeleteAccount = () => {
+ Alert.alert(
+ t.settings.delete_account,
+ t.settings.delete_confirm,
+ [
+ { text: t.common.cancel, style: 'cancel' },
+ {
+ text: t.settings.delete_account,
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await authAPI.deleteAccount();
+ logout();
+ qc.clear();
+ } catch (err: any) {
+ toast.error(err?.response?.data?.detail ?? 'Failed to delete account');
+ }
+ },
+ },
+ ],
+ );
+ };
+
+ const currentPlan = subscription?.plan ?? user?.plan ?? 'free';
+ const planFeatures = PLAN_FEATURES[currentPlan] ?? [];
+
+ return (
+
+
+
+ {/* User hero card */}
+
+
+
+ {(user?.email ?? 'U')[0].toUpperCase()}
+
+
+
+ {user?.company_name}
+ {user?.email}
+
+
+
+
+ {/* Subscription */}
+
+ {t.settings.subscription}
+
+
+
+ {subscription?.status && (
+
+ {subscription.status}
+
+ )}
+
+
+ {subscription?.current_period_end && (
+
+ Renews {new Date(subscription.current_period_end).toLocaleDateString()}
+
+ )}
+
+
+ {planFeatures.map(f => (
+
+ ✓
+ {f}
+
+ ))}
+
+
+
+ {/* Quick nav */}
+
+ {t.settings.reports}
+ navigation.navigate('Analytics')} theme={theme} />
+ navigation.navigate('Leads')} theme={theme} />
+ navigation.navigate('Campaigns')} theme={theme} />
+ navigation.navigate('Appointments')} theme={theme} isLast />
+
+
+ {/* Appearance */}
+
+ {t.settings.appearance}
+
+ {THEME_OPTIONS.map(opt => (
+ setThemeMode(opt.key)}>
+ {opt.icon}
+
+ {opt.label}
+
+
+ ))}
+
+
+
+ {/* Profile */}
+
+ {t.settings.profile}
+
+
+
+ {t.settings.language_label}
+
+ {LANGUAGE_OPTIONS.map(opt => (
+ setAppLanguage(opt.key)}>
+ {opt.flag}
+
+ {opt.label}
+
+
+ ))}
+
+
+
+
+ {t.settings.change_password}
+
+
+
+
+
+
+
+ {/* Account actions */}
+
+ {t.settings.account}
+
+
+
+
+ Contexta v1.0.0
+
+
+ );
+}
+
+function MenuRow({
+ icon,
+ label,
+ onPress,
+ theme,
+ isLast,
+}: {
+ icon: string;
+ label: string;
+ onPress: () => void;
+ theme: any;
+ isLast?: boolean;
+}) {
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const onPressIn = () => Animated.spring(scale, { toValue: 0.98, useNativeDriver: true, speed: 60 }).start();
+ const onPressOut = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 30, bounciness: 4 }).start();
+
+ return (
+
+
+
+ {icon}
+
+ {label}
+ ›
+
+
+ );
+}
+
+const menuStyles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: SPACING.md,
+ gap: SPACING.md,
+ },
+ iconBg: { width: 36, height: 36, borderRadius: RADIUS.md, alignItems: 'center', justifyContent: 'center' },
+ icon: { fontSize: 18 },
+ label: { flex: 1, ...TEXT.body },
+ chevron: { fontSize: 20, fontWeight: '300' },
+});
+
+const styles = StyleSheet.create({
+ safe: { flex: 1 },
+ scroll: { padding: SPACING.lg, gap: SPACING.md, paddingBottom: SPACING.xxxl },
+
+ heroCard: {
+ borderRadius: RADIUS.xxl,
+ padding: SPACING.xl,
+ borderWidth: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.lg,
+ },
+ userAvatar: {
+ width: 64,
+ height: 64,
+ borderRadius: RADIUS.full,
+ backgroundColor: COLORS.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ },
+ userAvatarText: { color: COLORS.white, fontSize: FONT_SIZE.xxl, fontWeight: '800' },
+ userInfo: { flex: 1, gap: SPACING.xs },
+ userName: { ...TEXT.h4 },
+ userEmail: { ...TEXT.small },
+
+ section: {
+ borderRadius: RADIUS.xl,
+ borderWidth: 1,
+ padding: SPACING.lg,
+ gap: SPACING.md,
+ },
+ dangerSection: {},
+ sectionTitle: { ...TEXT.h4 },
+
+ planRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
+ statusPill: { borderRadius: RADIUS.full, paddingVertical: 3, paddingHorizontal: SPACING.sm },
+ statusText: { fontSize: FONT_SIZE.xs, fontWeight: '500', textTransform: 'capitalize' },
+ renewDate: { ...TEXT.small },
+
+ featuresList: { borderTopWidth: 1, paddingTop: SPACING.md, gap: SPACING.sm },
+ featureItem: { flexDirection: 'row', alignItems: 'flex-start', gap: SPACING.xs },
+ featureCheck: { fontSize: FONT_SIZE.sm, fontWeight: '700', lineHeight: 20 },
+ featureText: { ...TEXT.small, flex: 1, lineHeight: 20 },
+
+ divider: { borderTopWidth: 1, paddingTop: SPACING.sm },
+ subTitle: { ...TEXT.smallM },
+
+ version: { ...TEXT.caption, textAlign: 'center' },
+});
+
+const langStyles = StyleSheet.create({
+ field: { gap: SPACING.xs },
+ label: { fontSize: FONT_SIZE.sm, fontWeight: '500' },
+ row: { flexDirection: 'row', gap: SPACING.sm },
+ pill: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: SPACING.xs,
+ borderWidth: 1.5,
+ borderRadius: RADIUS.lg,
+ paddingVertical: SPACING.sm,
+ },
+ flag: { fontSize: 18 },
+ pillLabel: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+});
+
+const themeStyles = StyleSheet.create({
+ row: { flexDirection: 'row', gap: SPACING.sm },
+ pill: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: SPACING.xs,
+ borderWidth: 1.5,
+ borderRadius: RADIUS.lg,
+ paddingVertical: SPACING.sm,
+ },
+ pillIcon: { fontSize: 16 },
+ pillLabel: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
+});
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..9aa8898
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,264 @@
+import axios from 'axios';
+import { useAuthStore } from '../stores/authStore';
+
+// Change this to your backend URL
+// Android emulator: http://10.0.2.2:8000
+// iOS simulator: http://localhost:8000
+// Physical device: http://:8000
+export const API_BASE_URL = "https://contexta-production-672d.up.railway.app"; //'http://192.168.1.72:8000';
+
+const api = axios.create({
+ baseURL: `${API_BASE_URL}/api/v1`,
+ timeout: 30000,
+ headers: { 'Content-Type': 'application/json' },
+});
+
+api.interceptors.request.use(config => {
+ const token = useAuthStore.getState().token;
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
+api.interceptors.response.use(
+ response => response,
+ error => {
+ if (error.response?.status === 401) {
+ useAuthStore.getState().logout();
+ }
+ return Promise.reject(error);
+ },
+);
+
+// ─── Auth ────────────────────────────────────────────────────────────────────
+
+export const authAPI = {
+ login: (email: string, password: string) =>
+ api.post('/auth/login', { email, password }).then(r => r.data),
+
+ signup: (email: string, password: string, company_name: string) =>
+ api.post('/auth/signup', { email, password, company_name }).then(r => r.data),
+
+ me: () => api.get('/auth/me').then(r => r.data),
+
+ logout: () => api.post('/auth/logout').then(r => r.data),
+
+ forgotPassword: (email: string) =>
+ api.post('/auth/forgot-password', { email }).then(r => r.data),
+
+ resetPassword: (access_token: string, new_password: string) =>
+ api.post('/auth/reset-password', { access_token, new_password }).then(r => r.data),
+
+ updateProfile: (data: { company_name?: string; current_password?: string; new_password?: string }) =>
+ api.patch('/auth/profile', data).then(r => r.data),
+
+ deleteAccount: () => api.delete('/auth/account').then(r => r.data),
+};
+
+// ─── Chatbots ────────────────────────────────────────────────────────────────
+
+export const chatbotsAPI = {
+ list: () => api.get('/chatbots').then(r => r.data),
+
+ get: (id: string) => api.get(`/chatbots/${id}`).then(r => r.data),
+
+ create: (data: Partial) =>
+ api.post('/chatbots', data).then(r => r.data),
+
+ update: (id: string, data: Partial) =>
+ api.put(`/chatbots/${id}`, data).then(r => r.data),
+
+ delete: (id: string) => api.delete(`/chatbots/${id}`).then(r => r.data),
+
+ publish: (id: string) => api.post(`/chatbots/${id}/publish`).then(r => r.data),
+
+ unpublish: (id: string) => api.post(`/chatbots/${id}/unpublish`).then(r => r.data),
+
+ getPublic: (id: string) => api.get(`/chatbots/${id}/public`).then(r => r.data),
+
+ getEmbed: (id: string) => api.get(`/chatbots/${id}/embed`).then(r => r.data),
+};
+
+// ─── Documents ───────────────────────────────────────────────────────────────
+
+export const documentsAPI = {
+ list: (chatbotId: string) =>
+ api.get(`/chatbots/${chatbotId}/documents`).then(r => r.data),
+
+ upload: (chatbotId: string, formData: FormData) =>
+ api.post(`/chatbots/${chatbotId}/documents`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }).then(r => r.data),
+
+ delete: (chatbotId: string, docId: string) =>
+ api.delete(`/chatbots/${chatbotId}/documents/${docId}`).then(r => r.data),
+
+ retry: (chatbotId: string, docId: string) =>
+ api.post(`/chatbots/${chatbotId}/documents/${docId}/retry`).then(r => r.data),
+};
+
+// ─── URL Sources ──────────────────────────────────────────────────────────────
+
+export const urlSourcesAPI = {
+ list: (chatbotId: string) =>
+ api.get(`/chatbots/${chatbotId}/url-sources`).then(r => r.data),
+
+ add: (chatbotId: string, url: string) =>
+ api.post(`/chatbots/${chatbotId}/url-sources`, { url }).then(r => r.data),
+
+ delete: (chatbotId: string, sourceId: string) =>
+ api.delete(`/chatbots/${chatbotId}/url-sources/${sourceId}`).then(r => r.data),
+
+ refresh: (chatbotId: string, sourceId: string) =>
+ api.post(`/chatbots/${chatbotId}/url-sources/${sourceId}/refresh`).then(r => r.data),
+};
+
+// ─── Chat ─────────────────────────────────────────────────────────────────────
+
+export const chatAPI = {
+ sendMessage: (chatbotId: string, message: string, session_id?: string) =>
+ api.post(`/chat/${chatbotId}`, { message, session_id }).then(r => r.data),
+
+ getHistory: (chatbotId: string, sessionId: string) =>
+ api.get(`/chat/${chatbotId}/history/${sessionId}`).then(r => r.data),
+
+ sendFeedback: (chatbotId: string, message_id: string, feedback: 'positive' | 'negative') =>
+ api.post(`/chat/${chatbotId}/feedback`, { message_id, feedback }).then(r => r.data),
+
+ test: (chatbotId: string, questions: string[]) =>
+ api.post(`/chat/${chatbotId}/test`, { questions }).then(r => r.data),
+};
+
+// ─── Marketplace ─────────────────────────────────────────────────────────────
+
+export const marketplaceAPI = {
+ list: (params?: { search?: string; category?: string; industry?: string; page?: number; limit?: number }) =>
+ api.get('/marketplace/chatbots', { params }).then(r => r.data),
+
+ get: (id: string) => api.get(`/marketplace/chatbots/${id}`).then(r => r.data),
+
+ categories: () => api.get('/marketplace/categories').then(r => r.data),
+
+ rate: (id: string, rating: number, comment?: string) =>
+ api.post(`/marketplace/chatbots/${id}/rate`, { rating, comment }).then(r => r.data),
+};
+
+// ─── Analytics ───────────────────────────────────────────────────────────────
+
+export const analyticsAPI = {
+ overview: () => api.get('/analytics/overview').then(r => r.data),
+
+ chatbot: (id: string) => api.get(`/analytics/chatbot/${id}`).then(r => r.data),
+
+ gaps: (id: string) => api.get(`/analytics/chatbot/${id}/gaps`).then(r => r.data),
+};
+
+// ─── Leads ───────────────────────────────────────────────────────────────────
+
+export const leadsAPI = {
+ list: (params?: { chatbot_id?: string; page?: number; limit?: number }) =>
+ api.get('/leads', { params }).then(r => r.data),
+
+ update: (id: string, data: Record) =>
+ api.patch(`/leads/${id}`, data).then(r => r.data),
+
+ submit: (chatbotId: string, data: Record) =>
+ api.post(`/chatbots/${chatbotId}/leads`, data).then(r => r.data),
+};
+
+// ─── Inbox ───────────────────────────────────────────────────────────────────
+
+export const inboxAPI = {
+ listConversations: (params?: { chatbot_id?: string; page?: number; limit?: number }) =>
+ api.get('/inbox/conversations', { params }).then(r => r.data),
+
+ getConversation: (id: string) =>
+ api.get(`/inbox/conversations/${id}`).then(r => r.data),
+
+ updateStatus: (id: string, status: string) =>
+ api.patch(`/inbox/conversations/${id}`, { status }).then(r => r.data),
+
+ reply: (id: string, message: string) =>
+ api.post(`/inbox/conversations/${id}/reply`, { message }).then(r => r.data),
+
+ deleteConversation: (id: string) =>
+ api.delete(`/inbox/conversations/${id}`).then(r => r.data),
+};
+
+// ─── Billing ─────────────────────────────────────────────────────────────────
+
+export const billingAPI = {
+ subscription: () => api.get('/billing/subscription').then(r => r.data),
+
+ portal: (return_url?: string) =>
+ api.post('/billing/portal', { return_url }).then(r => r.data),
+
+ checkout: (plan: string, success_url: string, cancel_url: string) =>
+ api.post('/billing/checkout', { plan, success_url, cancel_url }).then(r => r.data),
+};
+
+// ─── Models ──────────────────────────────────────────────────────────────────
+
+export const modelsAPI = {
+ available: () => api.get('/models/available').then(r => r.data),
+};
+
+// ─── Channels ────────────────────────────────────────────────────────────────
+
+export const channelsAPI = {
+ list: (chatbotId: string) =>
+ api.get('/channels', { params: { chatbot_id: chatbotId } }).then(r => r.data),
+
+ connectTelegram: (chatbot_id: string, bot_token: string) =>
+ api.post('/channels/telegram', { chatbot_id, bot_token }).then(r => r.data),
+
+ connectWhatsApp: (chatbot_id: string, wa_keyword?: string) =>
+ api.post('/channels/whatsapp', { chatbot_id, wa_keyword }).then(r => r.data),
+
+ disconnect: (connectionId: string) =>
+ api.delete(`/channels/${connectionId}`).then(r => r.data),
+};
+
+// ─── Campaigns ───────────────────────────────────────────────────────────────
+
+export const campaignsAPI = {
+ list: (params?: { chatbot_id?: string; page?: number }) =>
+ api.get('/campaigns', { params }).then(r => r.data),
+
+ create: (data: { chatbot_id: string; title: string; message: string }) =>
+ api.post('/campaigns', data).then(r => r.data),
+
+ send: (id: string) =>
+ api.post(`/campaigns/${id}/send`).then(r => r.data),
+
+ delete: (id: string) =>
+ api.delete(`/campaigns/${id}`).then(r => r.data),
+};
+
+// ─── Appointments ─────────────────────────────────────────────────────────────
+
+export const appointmentsAPI = {
+ list: (params?: { chatbot_id?: string; status?: string; page?: number }) =>
+ api.get('/appointments', { params }).then(r => r.data),
+
+ updateStatus: (id: string, status: string) =>
+ api.patch(`/appointments/${id}`, { status }).then(r => r.data),
+
+ getHours: (chatbotId: string) =>
+ api.get(`/appointments/chatbot/${chatbotId}/hours`).then(r => r.data),
+
+ saveHours: (chatbotId: string, hours: import('../types').BusinessHoursEntry[]) =>
+ api.put(`/appointments/chatbot/${chatbotId}/hours`, { hours }).then(r => r.data),
+};
+
+// ─── Upload ──────────────────────────────────────────────────────────────────
+
+export const uploadAPI = {
+ logo: (formData: FormData) =>
+ api.post('/upload/logo', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ }).then(r => r.data),
+};
+
+export default api;
diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts
new file mode 100644
index 0000000..49b6361
--- /dev/null
+++ b/src/stores/authStore.ts
@@ -0,0 +1,38 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { User } from '../types';
+
+interface AuthState {
+ user: User | null;
+ token: string | null;
+ isAuthenticated: boolean;
+ setAuth: (user: User, token: string) => void;
+ updateUser: (partial: Partial) => void;
+ logout: () => void;
+}
+
+export const useAuthStore = create()(
+ persist(
+ set => ({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+
+ setAuth: (user, token) =>
+ set({ user, token, isAuthenticated: true }),
+
+ updateUser: partial =>
+ set(state => ({
+ user: state.user ? { ...state.user, ...partial } : null,
+ })),
+
+ logout: () =>
+ set({ user: null, token: null, isAuthenticated: false }),
+ }),
+ {
+ name: 'contexta-auth',
+ storage: createJSONStorage(() => AsyncStorage),
+ },
+ ),
+);
diff --git a/src/stores/languageStore.ts b/src/stores/languageStore.ts
new file mode 100644
index 0000000..465a822
--- /dev/null
+++ b/src/stores/languageStore.ts
@@ -0,0 +1,23 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export type AppLanguage = 'en' | 'fr';
+
+interface LanguageState {
+ language: AppLanguage;
+ setLanguage: (lang: AppLanguage) => void;
+}
+
+export const useLanguageStore = create()(
+ persist(
+ set => ({
+ language: 'fr',
+ setLanguage: lang => set({ language: lang }),
+ }),
+ {
+ name: 'contexta-language',
+ storage: createJSONStorage(() => AsyncStorage),
+ },
+ ),
+);
diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts
new file mode 100644
index 0000000..c1458a4
--- /dev/null
+++ b/src/stores/themeStore.ts
@@ -0,0 +1,23 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export type ThemeMode = 'system' | 'light' | 'dark';
+
+interface ThemeState {
+ mode: ThemeMode;
+ setMode: (mode: ThemeMode) => void;
+}
+
+export const useThemeStore = create()(
+ persist(
+ set => ({
+ mode: 'system',
+ setMode: mode => set({ mode }),
+ }),
+ {
+ name: 'contexta-theme',
+ storage: createJSONStorage(() => AsyncStorage),
+ },
+ ),
+);
diff --git a/src/theme/index.ts b/src/theme/index.ts
new file mode 100644
index 0000000..f8814bc
--- /dev/null
+++ b/src/theme/index.ts
@@ -0,0 +1,158 @@
+import { useColorScheme, Platform } from 'react-native';
+import { useThemeStore } from '../stores/themeStore';
+
+// ─── Brand colors ─────────────────────────────────────────────────────────────
+export const COLORS = {
+ primary: '#6366f1',
+ primaryDark: '#4f46e5',
+ primaryLight: '#818cf8',
+ primaryUltraLight:'#eef2ff',
+ primaryMid: '#c7d2fe',
+
+ success: '#10b981',
+ successBg:'#d1fae5',
+ warning: '#f59e0b',
+ warningBg:'#fef3c7',
+ error: '#ef4444',
+ errorBg: '#fee2e2',
+ info: '#3b82f6',
+ infoBg: '#dbeafe',
+ purple: '#8b5cf6',
+ purpleBg: '#ede9fe',
+
+ white: '#ffffff',
+ black: '#000000',
+
+ light: {
+ bg: '#f8fafc',
+ bgSecondary: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceHover: '#f8fafc',
+ border: '#e2e8f0',
+ borderLight: '#f1f5f9',
+ text: '#0f172a',
+ textSecondary:'#475569',
+ textMuted: '#94a3b8',
+ inputBg: '#ffffff',
+ tabBar: '#ffffff',
+ tabBarBorder: '#f1f5f9',
+ placeholder: '#94a3b8',
+ icon: '#64748b',
+ overlay: 'rgba(15,23,42,0.4)',
+ },
+
+ dark: {
+ bg: '#0a0f1e',
+ bgSecondary: '#0f172a',
+ surface: '#151f32',
+ surfaceHover: '#1e293b',
+ border: '#1e293b',
+ borderLight: '#162033',
+ text: '#f1f5f9',
+ textSecondary:'#94a3b8',
+ textMuted: '#475569',
+ inputBg: '#151f32',
+ tabBar: '#0a0f1e',
+ tabBarBorder: '#151f32',
+ placeholder: '#475569',
+ icon: '#64748b',
+ overlay: 'rgba(0,0,0,0.6)',
+ },
+};
+
+// ─── Spacing ──────────────────────────────────────────────────────────────────
+export const SPACING = {
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 20,
+ xxl: 24,
+ xxxl: 32,
+ huge: 48,
+};
+
+// ─── Border radius ────────────────────────────────────────────────────────────
+export const RADIUS = {
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 20,
+ xxl: 24,
+ full: 9999,
+};
+
+// ─── Typography ───────────────────────────────────────────────────────────────
+export const FONT_SIZE = {
+ xs: 11,
+ sm: 13,
+ md: 15,
+ lg: 17,
+ xl: 20,
+ xxl: 24,
+ xxxl: 30,
+ huge: 38,
+};
+
+export const FONT = {
+ regular: '400' as const,
+ medium: '500' as const,
+ semibold: '600' as const,
+ bold: '700' as const,
+ extrabold:'800' as const,
+};
+
+// Pre-built text style objects (use with spread)
+export const TEXT = {
+ h1: { fontSize: FONT_SIZE.xxxl, fontWeight: FONT.bold, lineHeight: 38 },
+ h2: { fontSize: FONT_SIZE.xxl, fontWeight: FONT.bold, lineHeight: 32 },
+ h3: { fontSize: FONT_SIZE.xl, fontWeight: FONT.semibold, lineHeight: 28 },
+ h4: { fontSize: FONT_SIZE.lg, fontWeight: FONT.semibold, lineHeight: 24 },
+ body: { fontSize: FONT_SIZE.md, fontWeight: FONT.regular, lineHeight: 22 },
+ bodyM: { fontSize: FONT_SIZE.md, fontWeight: FONT.medium, lineHeight: 22 },
+ small: { fontSize: FONT_SIZE.sm, fontWeight: FONT.regular, lineHeight: 18 },
+ smallM: { fontSize: FONT_SIZE.sm, fontWeight: FONT.medium, lineHeight: 18 },
+ caption: { fontSize: FONT_SIZE.xs, fontWeight: FONT.regular, lineHeight: 16 },
+ captionM:{ fontSize: FONT_SIZE.xs, fontWeight: FONT.medium, lineHeight: 16 },
+ overline:{ fontSize: FONT_SIZE.xs, fontWeight: FONT.bold, letterSpacing: 1, lineHeight: 16 },
+} as const;
+
+// ─── Shadows ──────────────────────────────────────────────────────────────────
+export const SHADOWS = {
+ none: {},
+ xs: Platform.select({
+ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.04, shadowRadius: 3 },
+ android: { elevation: 1 },
+ }) ?? {},
+ sm: Platform.select({
+ ios: { shadowColor: '#0f172a', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, shadowRadius: 8 },
+ android: { elevation: 2 },
+ }) ?? {},
+ md: Platform.select({
+ ios: { shadowColor: '#0f172a', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.09, shadowRadius: 16 },
+ android: { elevation: 4 },
+ }) ?? {},
+ lg: Platform.select({
+ ios: { shadowColor: '#0f172a', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.12, shadowRadius: 24 },
+ android: { elevation: 8 },
+ }) ?? {},
+ xl: Platform.select({
+ ios: { shadowColor: '#0f172a', shadowOffset: { width: 0, height: 16 }, shadowOpacity: 0.18, shadowRadius: 40 },
+ android: { elevation: 16 },
+ }) ?? {},
+ // Tinted primary shadow
+ primary: Platform.select({
+ ios: { shadowColor: '#6366f1', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.3, shadowRadius: 16 },
+ android: { elevation: 6 },
+ }) ?? {},
+};
+
+// ─── Theme hook ───────────────────────────────────────────────────────────────
+export function useTheme() {
+ const scheme = useColorScheme();
+ const mode = useThemeStore(s => s.mode);
+ const isDark = mode === 'system' ? scheme === 'dark' : mode === 'dark';
+ const theme = isDark ? COLORS.dark : COLORS.light;
+ return { isDark, colors: COLORS, theme, shadows: SHADOWS };
+}
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..3f5902e
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,233 @@
+export interface User {
+ id: string;
+ email: string;
+ company_name: string;
+ plan: 'free' | 'starter' | 'business' | 'agency' | 'enterprise';
+ is_admin: boolean;
+ chatbot_count?: number;
+ conversations_used?: number;
+}
+
+export interface Subscription {
+ plan: string;
+ status: string;
+ current_period_end?: string;
+ chatbots_published: number;
+ conversations_used: number;
+}
+
+export interface Chatbot {
+ id: string;
+ company_id: string;
+ name: string;
+ description: string;
+ system_prompt: string;
+ model: string;
+ temperature: number;
+ max_tokens: number;
+ primary_color: string;
+ welcome_message: string;
+ logo_url?: string;
+ category?: string;
+ industry?: string;
+ languages: string[];
+ visibility: string;
+ is_published: boolean;
+ average_rating: number;
+ show_branding: boolean;
+ lead_capture_enabled: boolean;
+ lead_capture_fields: string[];
+ lead_capture_trigger: string;
+ handoff_enabled: boolean;
+ handoff_message: string;
+ handoff_email: string;
+ handoff_keywords: string[];
+ booking_enabled?: boolean;
+ document_count?: number;
+ conversation_count?: number;
+ created_at?: string;
+}
+
+export interface Document {
+ id: string;
+ chatbot_id: string;
+ file_name: string;
+ file_type: string;
+ file_size: number;
+ chunk_count: number;
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ error_message?: string;
+ created_at?: string;
+}
+
+export interface URLSource {
+ id: string;
+ chatbot_id: string;
+ url: string;
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ page_title?: string;
+ chunk_count?: number;
+ error_message?: string;
+}
+
+export interface Message {
+ id: string;
+ conversation_id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ sources?: MessageSource[];
+ model?: string;
+ confidence_score?: number;
+ is_handoff?: boolean;
+ created_at?: string;
+}
+
+export interface MessageSource {
+ document_name: string;
+ chunk_text: string;
+ score: number;
+ page_number?: number;
+}
+
+export interface Conversation {
+ id: string;
+ chatbot_id: string;
+ session_id: string;
+ language?: string;
+ message_count: number;
+ created_at?: string;
+ last_message?: string;
+ chatbot_name?: string;
+}
+
+export interface Lead {
+ id: string;
+ chatbot_id: string;
+ conversation_id?: string;
+ email?: string;
+ name?: string;
+ phone?: string;
+ company?: string;
+ created_at?: string;
+ chatbot_name?: string;
+}
+
+export interface MarketplaceChatbot {
+ id: string;
+ name: string;
+ description: string;
+ category?: string;
+ industry?: string;
+ languages: string[];
+ average_rating: number;
+ logo_url?: string;
+ primary_color: string;
+ conversation_count?: number;
+ company_name?: string;
+}
+
+export interface AnalyticsOverview {
+ total_conversations: number;
+ total_messages: number;
+ total_chatbots: number;
+ published_chatbots: number;
+ unique_sessions: number;
+ conversations_this_month: number;
+ avg_messages_per_conversation: number;
+ average_rating: number | null;
+ plan: string;
+ conversations_limit: number;
+ conversations_used: number;
+ chatbots: ChatbotAnalytics[];
+}
+
+export interface DailyConversation {
+ date: string;
+ count: number;
+}
+
+export interface TopQuery {
+ query: string;
+ count: number;
+}
+
+export interface ChatbotAnalytics {
+ chatbot_id: string;
+ chatbot_name: string;
+ conversations: number;
+ messages: number;
+ avg_confidence: number;
+ total_conversations: number;
+ unique_sessions: number;
+ total_messages: number;
+ average_rating: number | null;
+ total_ratings: number;
+ conversations_today: number;
+ conversations_this_week: number;
+ conversations_this_month: number;
+ daily_conversations: DailyConversation[];
+ top_queries: TopQuery[];
+ languages_used: Record;
+ peak_hour: number | null;
+ unanswered_count: number;
+ unanswered_queries: TopQuery[];
+ feedback_positive: number;
+ feedback_negative: number;
+}
+
+export interface Campaign {
+ id: string;
+ chatbot_id: string;
+ title: string;
+ message: string;
+ status: 'draft' | 'sending' | 'sent' | 'failed';
+ recipients_count: number;
+ sent_count: number;
+ created_at?: string;
+ sent_at?: string;
+}
+
+export interface Appointment {
+ id: string;
+ chatbot_id: string;
+ customer_name: string;
+ customer_contact: string;
+ service?: string;
+ notes?: string;
+ slot_start: string;
+ slot_end: string;
+ status: 'pending' | 'confirmed' | 'cancelled' | 'completed';
+ created_at?: string;
+}
+
+export interface BusinessHoursEntry {
+ day_of_week: number;
+ is_open: boolean;
+ open_time: string;
+ close_time: string;
+ slot_duration_minutes: number;
+}
+
+export interface ChannelConnection {
+ id: string;
+ chatbot_id: string;
+ channel: 'telegram' | 'whatsapp';
+ bot_username?: string;
+ wa_keyword?: string;
+ is_active: boolean;
+}
+
+export interface AIModel {
+ id: string;
+ name: string;
+ provider: string;
+ available: boolean;
+ upgrade_required?: string;
+}
+
+export interface PaginatedResponse {
+ items: T[];
+ total: number;
+ page: number;
+ limit: number;
+}