commit 8e0deae6d08014bd5e16243c58484194adb35646 Author: test Date: Fri Jan 16 00:22:35 2026 +0300 first ever agent push diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ef0eaca Binary files /dev/null and b/.DS_Store differ diff --git a/.expo/README.md b/.expo/README.md new file mode 100644 index 0000000..f7eb5fe --- /dev/null +++ b/.expo/README.md @@ -0,0 +1,8 @@ +> Why do I have a folder named ".expo" in my project? +The ".expo" folder is created when an Expo project is started using "expo start" command. +> What do the files contain? +- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. +- "settings.json": contains the server configuration that is used to serve the application manifest. +> Should I commit the ".expo" folder? +No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. +Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/.expo/devices.json b/.expo/devices.json new file mode 100644 index 0000000..c3540e1 --- /dev/null +++ b/.expo/devices.json @@ -0,0 +1,8 @@ +{ + "devices": [ + { + "installationId": "5eab1301-ca81-48d8-bdaa-42f65cae7ad8", + "lastUsed": 1768508821071 + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist new file mode 100644 index 0000000..add78be --- /dev/null +++ b/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 613864011564-atsg9nau8hicla4td6dedcab15g7qr04.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.613864011564-atsg9nau8hicla4td6dedcab15g7qr04 + ANDROID_CLIENT_ID + 613864011564-2h1fb41f6conaabli0uq7scacpfmvuiq.apps.googleusercontent.com + API_KEY + AIzaSyCquhCKEsKmvZ5_JzqyWXGoImBF5L2Xlbc + GCM_SENDER_ID + 613864011564 + PLIST_VERSION + 1 + BUNDLE_ID + com.ambapays.app + PROJECT_ID + ambapaydemo + STORAGE_BUCKET + ambapaydemo.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:613864011564:ios:79336eea584d1ba8249e89 + DATABASE_URL + https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3bbe7da --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +# ===== Android Targets ===== +run: + ANDROID_SERIAL=RF8T90SNT7W npx expo start --dev-client --clear --android + +run_dev: + ANDROID_SERIAL=RF8T90SNT7W npx expo run:android + +run_reinstall: + ANDROID_SERIAL=RF8T90SNT7W npx expo run:android + ANDROID_SERIAL=RF8T90SNT7W npx expo start --dev-client --clear --android + +regen_android: + npx expo prebuild --platform android --clean + +# ===== iOS Targets ===== +run_ios: + npx expo start --dev-client --clear --ios + +run_dev_ios: + npx expo run:ios + +run_reinstall_ios: + npx expo run:ios + npx expo start --dev-client --clear --ios + +regen_ios: + npx expo prebuild --platform ios --clean + +# ===== Web Targets ===== +# Run local web development server +web: + npx expo start --web --clear + +# Build static web export for production (outputs to dist/) +web_build: + npx expo export --platform web + +# Serve the built web app locally for testing (requires npx serve) +web_serve: web_build + npx serve dist + +# Clean web build artifacts +web_clean: + rm -rf dist + +# Deploy to Firebase Hosting (requires firebase CLI: npm i -g firebase-tools) +# Note: Run 'firebase init hosting' first and set public directory to 'dist' +web_deploy_firebase: web_build + firebase deploy --only hosting \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..960a37d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +* Backlog +- [X] Add Biometric locks on the app +- [X] Move Authentication to Prod +- [X] Improve the speed of the app +- [X] Improve UX of app via loader +- [X] Configure Firebase Rules +- [] Twilio SMS for SMS Notifications +- [] Resend for Email Notifications +- [] FCM Notifications +- [] Reduced but did not elmintate the lag when switching tabs \ No newline at end of file diff --git a/agent-app-ui-plan.md b/agent-app-ui-plan.md new file mode 100644 index 0000000..5e9a4a0 --- /dev/null +++ b/agent-app-ui-plan.md @@ -0,0 +1,540 @@ +# Amba Agent App – UI Plan (Phase 1, UI-Only) + +## 1. Purpose + +The Amba Agent app provides agents with a platform to: + +- Manage multiple clients’ payment accounts. +- Schedule reminders and manage payment-related events. +- Communicate and act on client payments through a streamlined UI. + +This phase focuses **only on UI/flows**, with dummy data and no real backend integration. + +--- + +## 2. Scope (Phase 1) + +From the SRS: + +- **Multi-client management** + + - Each agent manages multiple client profiles. + - Each client can have multiple linked payment accounts. + +- **Transaction management** + + - Agents can initiate payments on behalf of clients. + - Transactions are displayed and tracked in a list view. + +- **Scheduling & reminders** + + - Simple UI to show upcoming reminders. + - Scheduling controls are UI-only for now. + +- **Notifications** + + - UI toggles for SMS, in-app, email notifications (no real sending yet). + +- **Localization** + + - English-first UI with a language selector prepared for Arabic and others. + - No full RTL logic in this phase, only the settings hooks. + +- **Security** + + - Login screen + simulated biometric toggle (UI-only). + +- **Reporting** + - Transactions list and basic summary cards (dummy data). + +--- + +## 3. Navigation Structure + +### Bottom Tabs (Agent App) + +1. **Home** +2. **Recipients** +3. **Requests** +4. **Transactions** +5. **Profile** + +From these tabs we navigate to: + +- Recipient Detail +- Pay (Payment initiation) Screen +- Request Detail (bottom sheet) +- Optional “Reports” placeholder (or reuse Transactions) + +--- + +## 4. Screens and UI Details + +### 4.1 Login (Agent Login) + +**Goal:** Basic auth + prompt for biometrics after first login. + +- Fields: + - Email / Phone + - Password +- Buttons: + - Primary: **Sign In** + - Secondary: **Forgot password?** +- After first successful login (simulated): + - Show a card or bottom sheet: + - Title: “Enable biometric login” + - Buttons: **Enable Face ID / Fingerprint**, **Not now** + - For Phase 1, this just flips a local toggle (no OS integration). + +--- + +### 4.2 Home + +**From flow:** + +- Login → Home → Decision (to Profile / Recipients / Transactions / Requests / Pay). +- “On this page he can show the amount of credits he has on the application.” + +**UI Sections:** + +- **TopBar** + + - Greeting (“Hi, [Agent Name]”) + - Notification bell + - Agent avatar + +- **Credits Card** + + - Large amount: “Available Credits” + - Subtitle: “Balance usable for client payments” + - Optional “Last updated” label (dummy) + +- **Quick Actions Row** + + - Buttons to navigate: + - Recipients + - Requests + - Transactions + - Pay + +- **Upcoming Reminders** + - Vertical list with: + - Client name + - Due date / time + - Small status pill (e.g. “Upcoming”) + - All populated with dummy items. + +--- + +### 4.3 Recipients (Client Management) + +**From doc:** + +- “Recipient list including account associated with detail page also showing the schedule for when they want payments.” +- “Multi-client management; associate multiple accounts per client.” + +#### Recipients List Screen + +- **Search bar**: “Search recipients” +- Optional filter icon on the right. +- **Recipient Card** per client: + - Name (Individual / Business) + - Tag pill: `Individual` or `Business` + - Short line: e.g. “3 accounts” or main account alias + - Small “Next payment” label if schedule exists + - On tap → Recipient Detail + +#### Recipient Detail Screen + +- **Header** + + - Name, avatar/initials + - Client type pill + +- **Accounts Section** + + - Card or list: bank name + account number for each linked account + - “Add Account” button: + - Opens bottom sheet similar to Edit Profile’s multi-account UI: + - Bank selector + - Account number input + - Save (dummy only) + +- **Schedules Section** + + - List of schedules: + - e.g. “Every Monday 10:00”, “Monthly, 1st at 09:00” + - Button: **Manage Schedules**: + - Opens UI-only bottom sheet with: + - Frequency selector (Daily / Weekly / Monthly / Custom) + - Time selector (simple text field for now) + - Save (no backend) + +- **Actions** + - **Pay Now** → navigates to Pay screen with this recipient preselected. + - **View Transactions** → navigates to Transactions with a client filter preset (UI-only). + +--- + +### 4.4 Transactions + +**From doc:** + +- “All the transaction he has processed.” +- Reporting & tracking. + +#### Transactions Screen + +- **Filter/Search Bar** + + - Text input: “Search by client or reference” + - Filter chips: + - All + - Success + - Pending + - Failed + +- **Summary Row (Optional)** + + - Today’s total + - This week’s total + - Total transactions + - All dummy data. + +- **Transactions List** + - Each row: + - Amount + currency + - Status pill: Success/Pending/Failed + - Client name + - Date and time + - Small method icon (Telebirr/Chapa/etc., static icon) + +For Phase 1, the list is fed by a mocked array. + +--- + +### 4.5 Requests + +**From flow:** + +- “Every book now request for the agent himself stands here. With the person, time, and date.” + +#### Requests Screen + +- List of **Request Cards**: + + - Client name + - Scheduled date & time + - Requested amount (optional) + - Status: Pending / Accepted / Completed (local state only) + - Buttons: + - **View Details** + - **Pay Now** + +- **Request Detail Bottom Sheet** + + - Client info + - Requested schedule + - Notes/description (dummy) + - Actions: + - **Mark as Accepted** + - **Mark as Done** + - All actions affect only in-memory state. + +- **Pay Now** from a request: + - Opens Pay screen with recipient + amount pre-filled (no real payment). + +--- + +### 4.6 Pay + +**From flow:** + +- “Where he can make the payment same flow as per usual.” + +#### Pay Screen + +- **Context** + + - If navigated from Recipient: + - Recipient field is pre-filled and locked. + - If from Home: + - Recipient selector row: “Select Recipient” → bottom-sheet list. + +- **Fields** + + - Amount input + - Payment method dropdown: + - Telebirr + - Chapa + - Other methods (dummy options) + - Optional note / description + +- **Actions** + - Primary: **Review & Confirm** + - Opens bottom sheet: + - Shows summary: recipient, amount, method + - Button: **Confirm (UI only)**: + - Closes sheet and maybe sets a local “Success” toast. + - No network calls in Phase 1. + +--- + +### 4.7 Profile (Agent Profile) + +**From doc:** + +- Multi-level access, notification channels, localization, etc. + +#### Profile Screen + +- **Agent Info Card** + + - Name + - Role: Agent + - Agent ID or code + +- **Sections** + + 1. **Account & Security** + + - `Change Password` row (placeholder screen) + - `Biometric Login` toggle (ties to login biometric prompt state) + + 2. **Notifications** + + - Toggles: + - SMS notifications + - In-app notifications + - Email notifications + - All stored in local state only. + + 3. **Localization** + + - Language selector row: + - English + - Arabic + - Other supported languages (am, fr, om, ti) + - For Phase 1, dont do anything on localization. + + 4. **Reports** + + - Row: **View Reports** + - Navigates to Transactions screen or a placeholder “Reports coming soon” screen. + + 5. **Logout** + - Simple button that routes back to Login screen (no token logic yet). + +--- + +## 5. Implementation Notes (Phase 1) + +- **No backend calls**: + + - All screens use mocked arrays/state. + - No SMS, email, or payment gateway calls. + +- **Biometrics** + + - Only UI affordances (prompt, toggle). + - Underlying OS integration is future work. + +- **Localization** + + - This file is stored under [locales/en](cci:7://file:///Users/user/PROJECTS/NOVEMBER/amba/locales/en:0:0-0:0) as a reference for future translation. + - Actual text keys (i18n) can be extracted later. + +- **Design** + - Reuse existing Amba components where possible: + - `ScreenWrapper`, [TopBar](cci:1://file:///Users/user/PROJECTS/NOVEMBER/amba/components/ui/topBar.tsx:10:0-55:1), `Input`, `Button`, `BottomSheet`, cards, etc. + - Stick to the existing Amba color palette and typography. + +--- + +Overall structure +Goal: Agent can log in, see credits, manage recipients/clients, see transactions, handle “book now” requests, and trigger payments. + +For this phase we: + +Keep Amba look & feel (colors, typography, cards). +Build new agent-focused screens and navigation. +Use static/dummy data only, no real payment/notification backend yet. 2. Navigation proposal +Bottom tabs for Agent app: + +Home +Recipients +Requests +Transactions +Profile +From these, we can navigate to: + +Recipient Detail (per client) +Pay flow (can be reached from Home or Recipient Detail) +Maybe a Calendar/Reminders view later (for now: simple section in Home / Recipient Detail). 3. Screen-by-screen UI plan +3.1 Login (Agent Login) +From doc: + +“Login should promote for biometric access as well rather than password after first login.” +UI changes: + +Login Screen +Email / Phone input. +Password field. +Primary CTA: Sign In. +Secondary: Forgot password?. +After first successful login (simulated via local state for now), show: +Bottom sheet or inline card: +“Enable biometric login” with Enable Face ID / Fingerprint button and Not now. +Next phase we only simulate biometrics (no real OS-level integration yet, just toggle state). +3.2 Home +From doc + flow: + +Show agent credits. +Entry point to other flows. +UI: + +Top bar: +TopBar +-style with: +Greeting (“Hi, [Agent Name]”). +Notification bell. +Agent avatar. +Credits Card: +Big number: “Available Credits”. +Subtitle: “Balance usable for client payments”. +Quick Actions Row (buttons/cards): +Recipients +Requests +Transactions +Pay +Upcoming Reminders (for scheduling feature, UI-only): +List of “Next payment reminders” with: +Client name. +Due date/time. +Small status pill (e.g. “Pending”). +All dummy data for now. + +3.3 Recipients (Client Management) +From doc + post-it: + +“This will have the recipient list including account associated with detail page also showing the schedule for when they want payments.” +“Multi-client management, multiple accounts per client.” +List Screen UI: + +Search bar: Search recipients. +Filters (optional for now, icon only). +List of Recipient Cards: +Name (person or business). +Tags: Individual / Business. +Short info: main account or “3 accounts”. +Next scheduled payment date (if any). +Tap → Recipient Detail. +Recipient Detail UI: + +Header: +Name, avatar/initial icon. +Client type pill. +Accounts section: +Card listing linked accounts (bank name, account number). +“Add Account” button (UI only; bottom sheet like Edit Profile multi-account). +Schedule section (reminders): +List of schedules: “Every Monday 10:00”, “Monthly 1st at 9:00”. +Button: Manage Schedules (for now shows bottom sheet with dummy options). +Actions: +Pay Now button (goes to Pay UI with recipient preselected). +View Transactions (filter Transactions screen by this recipient – UI-only filter). +3.4 Transactions +From doc: + +“All the transaction he has processed.” +Reporting & tracking. +UI: + +Search / filter bar: +Search by client. +Filter chip row: All, Success, Pending, Failed. +Transactions list: +Each row: amount, status pill, client name, date/time, small method icon. +Optional summary at top: +Today’s total, This week, etc. (dummy numbers). +No real data, just static mocked list. + +3.5 Requests +From flow note: + +“Every book now request for the agent himself stands here. With the person, time, and date.” +This is basically a queue of incoming scheduled “Book Now” / payment requests. + +UI: + +List of Request Cards: +Client name. +Requested date & time. +Amount (if part of the request). +Status: Pending, Accepted, Completed (UI-only). +CTA buttons: +View Details → Request Detail bottom sheet. +Pay Now → Pay screen with details pre-filled. +Request Detail (BottomSheet): +Client info. +Requested schedule. +Notes. +UI-only actions: Mark as Accepted, Mark as Done (just local state change). +3.6 Pay +From flow note: + +“Where he can make the payment same flow as per usual.” +This is the payment initiation screen. + +UI: + +If navigated from Recipient / Request: +Recipient pre-filled and locked. +If from Home: +Recipient picker at top (Select Recipient row → bottom sheet list). +Fields: +Amount. +Payment method (dropdown: Telebirr, Chapa, etc). +Optional note / description. +CTA: +Review & Confirm (UI-only): +Opens confirmation bottom sheet showing summary, “Confirm (UI only)” button. +We won’t hit any gateway APIs yet; we just simulate the flow. + +3.7 Profile (Agent Profile) +From doc: + +Multi-level access, localization, notification channels, etc. +UI: + +Agent info card: name, role (Agent), ID. +Sections: +Account & Security: +Change password (UI-only). +Biometric login toggle (syncs with Login prompt state). +Notifications: +Toggles for SMS, In-app, Email (UI-only). +Localization: +Language selector (English / Arabic; no real RTL yet in phase 1, just selection). +Reports: +“View Reports” → for now goes to Transactions screen pre-filtered or a placeholder “coming soon” page. +Logout button. 4. Phase 1 implementation plan (UI-only) +Once you confirm the above direction, I’ll: + +Set up Agent navigation: +New or modified tabs for +Home +, Recipients, Requests, Transactions, Profile. +Create minimal versions of: +AgentHomeScreen +AgentRecipientsScreen + RecipientDetail +AgentTransactionsScreen +AgentRequestsScreen +AgentPayScreen +AgentProfileScreen +Wire in dummy data for: +Recipients (with multiple accounts & schedules). +Transactions. +Requests (bookings). +Keep everything UI-only: +No real payment, no SMS/email sending, no real biometrics or gateway calls. diff --git a/amba-about.html b/amba-about.html new file mode 100644 index 0000000..f73f8ee --- /dev/null +++ b/amba-about.html @@ -0,0 +1,352 @@ + + + + + + + Amba · Purposeful Money Movement + + + + + +
+
+ Amba logo +

Purposeful Money Movement

+

+ Amba keeps giving frictionless. Manage donors and recipients, launch + crowdfunding drives, request or send funds, and stay in sync with + realtime notifications—all inside one secure, biometric-ready + experience. +

+
+ +
+
+

Cash Flow

+

+ Request funds, send money instantly, or route cash to vetted + recipients straight from the Send or Request tab. +

+
+
+

Community

+

+ Build campaigns in Crowdfunding, select trusted donors, and track + their impact with transparent progress. +

+
+
+

Control

+

+ Toggle notification preferences, manage cards, and confirm transfers + with biometric protection and PIN flows. +

+
+
+ +
+
Why Amba
+

A finance hub built for giving

+

+ At its core, Amba is a human-first finance assistant. We focus on + purposeful flows—donations, community payouts, top ups, and transfers + between trusted people. Every screen you see in the app (Select Donor, + Add Recipient, Notification Options, Card Management, Crowdfunding + overviews) is optimized for clarity, speed, and accountability. +

+

+ With native contacts access, tailored notifications, and biometric + locks, Amba keeps every transaction transparent without compromise on + security. +

+ +
+
+

Intuitive Flows

+

+ Guided steps across add-recipient, select-donor, and request + flows prevent costly mistakes and keep donations intentional. +

+
+
+

Smart Notifications

+

+ Pick when and how you get alerted—from instant pushes to curated + summaries—so you always know when money moves. +

+
+
+

Secure Wallet

+

+ Manage cards, confirm transfers with biometric unlocks, and rely + on trusted routing rails for every payout. +

+
+
+ +
+
+

Engage donors

+

+ Use Select Donor flows to curate contributors, send contextual + updates, and keep everyone aligned on progress. +

+
+
+

Disburse with confidence

+

+ Choose recipients, confirm payout routes, and see confirmation + screens that capture every detail for audit-readiness. +

+
+
+
+ +
+ Designed with Amba’s green & gold palette. Learn more in the app or + reach the team at hello@amba.app. +
+
+ + + \ No newline at end of file diff --git a/amba_release.keystore b/amba_release.keystore new file mode 100644 index 0000000..0aed46f Binary files /dev/null and b/amba_release.keystore differ diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..8a6be07 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,16 @@ +# OSX +# +.DS_Store + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ + +# Bundle artifacts +*.jsbundle diff --git a/android/.kotlin/errors/errors-1765842676555.log b/android/.kotlin/errors/errors-1765842676555.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/android/.kotlin/errors/errors-1765842676555.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/android/.kotlin/errors/errors-1766617533941.log b/android/.kotlin/errors/errors-1766617533941.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/android/.kotlin/errors/errors-1766617533941.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/android/.kotlin/errors/errors-1766791234477.log b/android/.kotlin/errors/errors-1766791234477.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/android/.kotlin/errors/errors-1766791234477.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/android/.kotlin/errors/errors-1768389090763.log b/android/.kotlin/errors/errors-1768389090763.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/android/.kotlin/errors/errors-1768389090763.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..441c3df --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,179 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean() + // Use Expo CLI to bundle the app, this ensures the Metro config + // works correctly with Expo projects. + cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace 'com.ambapay.ambaagent' + defaultConfig { + applicationId 'com.ambapay.ambaagent' + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) + } + } + packagingOptions { + jniLibs { + useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) + } + } + androidResources { + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' + } +} + +// Apply static values from `gradle.properties` to the `android.packagingOptions` +// Accepts values in comma delimited lists, example: +// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini +["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> + // Split option: 'foo,bar' -> ['foo', 'bar'] + def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); + // Trim all elements in place. + for (i in 0.. 0) { + println "android.packagingOptions.$prop += $options ($options.length)" + // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' + options.each { + android.packagingOptions[prop] += it + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; + def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; + def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + + if (isGifEnabled) { + // For animated gif support + implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}") + } + + if (isWebpEnabled) { + // For webp support + implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}") + if (isWebpAnimatedEnabled) { + // Animated webp support + implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}") + } + } + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} + +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/debug.keystore b/android/app/debug.keystore new file mode 100644 index 0000000..364e105 Binary files /dev/null and b/android/app/debug.keystore differ diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..72c2b1d --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,107 @@ +{ + "project_info": { + "project_number": "613864011564", + "firebase_url": "https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app", + "project_id": "ambapaydemo", + "storage_bucket": "ambapaydemo.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:613864011564:android:fd35c9ac4fc05b38249e89", + "android_client_info": { + "package_name": "com.amba" + } + }, + "oauth_client": [ + { + "client_id": "613864011564-2h1fb41f6conaabli0uq7scacpfmvuiq.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.amba", + "certificate_hash": "e5160567582b67483fec269eadfc9a286a2e14b0" + } + }, + { + "client_id": "613864011564-c8hfpatgmgvse0a1hdhho3ipj3ddrut3.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.amba", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + } + }, + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDTSIxujhye42iB8bDDvUR7jjRLo7Et3CU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "613864011564-4mof6e14th4jdo4nrqe4qocntpovgof2.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.ambapay.ambaagent" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:613864011564:android:7aad206fe697dc32249e89", + "android_client_info": { + "package_name": "com.ambapay.ambaagent" + } + }, + "oauth_client": [ + { + "client_id": "613864011564-tnj0afut9mmg7vk7u2v6qhenel2auq7i.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.ambapay.ambaagent", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + } + }, + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDTSIxujhye42iB8bDDvUR7jjRLo7Et3CU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "613864011564-4mof6e14th4jdo4nrqe4qocntpovgof2.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.ambapay.ambaagent" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..551eb41 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,14 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# react-native-reanimated +-keep class com.swmansion.reanimated.** { *; } +-keep class com.facebook.react.turbomodule.** { *; } + +# Add any project specific keep options here: diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3ec2507 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eacc8e6 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/fonts/DMSans-Black.ttf b/android/app/src/main/assets/fonts/DMSans-Black.ttf new file mode 100644 index 0000000..56e9bac Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Black.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-Bold.ttf b/android/app/src/main/assets/fonts/DMSans-Bold.ttf new file mode 100644 index 0000000..4f5412d Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Bold.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-ExtraBold.ttf b/android/app/src/main/assets/fonts/DMSans-ExtraBold.ttf new file mode 100644 index 0000000..1d43afd Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-ExtraBold.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-ExtraLight.ttf b/android/app/src/main/assets/fonts/DMSans-ExtraLight.ttf new file mode 100644 index 0000000..73f17d1 Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-ExtraLight.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-Light.ttf b/android/app/src/main/assets/fonts/DMSans-Light.ttf new file mode 100644 index 0000000..250517c Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Light.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-Medium.ttf b/android/app/src/main/assets/fonts/DMSans-Medium.ttf new file mode 100644 index 0000000..841d31d Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Medium.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-Regular.ttf b/android/app/src/main/assets/fonts/DMSans-Regular.ttf new file mode 100644 index 0000000..07266ae Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-SemiBold.ttf b/android/app/src/main/assets/fonts/DMSans-SemiBold.ttf new file mode 100644 index 0000000..afa79e3 Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-SemiBold.ttf differ diff --git a/android/app/src/main/assets/fonts/DMSans-Thin.ttf b/android/app/src/main/assets/fonts/DMSans-Thin.ttf new file mode 100644 index 0000000..0c02db0 Binary files /dev/null and b/android/app/src/main/assets/fonts/DMSans-Thin.ttf differ diff --git a/android/app/src/main/java/com/amba/MainActivity.kt b/android/app/src/main/java/com/amba/MainActivity.kt new file mode 100644 index 0000000..7d092e8 --- /dev/null +++ b/android/app/src/main/java/com/amba/MainActivity.kt @@ -0,0 +1,61 @@ +package com.ambapay.ambaagent + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/android/app/src/main/java/com/amba/MainApplication.kt b/android/app/src/main/java/com/amba/MainApplication.kt new file mode 100644 index 0000000..f8fa5ca --- /dev/null +++ b/android/app/src/main/java/com/amba/MainApplication.kt @@ -0,0 +1,61 @@ +package com.ambapay.ambaagent +import com.facebook.react.common.assets.ReactFontManager + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List { + val packages = PackageList(this).packages + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(MyReactNativePackage()) + return packages + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + // @generated begin xml-fonts-init - expo prebuild (DO NOT MODIFY) sync-da39a3ee5e6b4b0d3255bfef95601890afd80709 + + // @generated end xml-fonts-init + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..31df827 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..ef243aa Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..e9d5474 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..d61da15 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..4aeed11 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..883b2a0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000..5c25e72 --- /dev/null +++ b/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..7fae0cc Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ac03dbf Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..afa0a4e Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..78aaf45 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..e1173a9 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c4f6e10 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..7a0f085 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ff086fd Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6c2d40b Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..730e3fa Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f7f1d06 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..3452615 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..b11a322 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..49a464e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b51fd15 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f387b90 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #ffffff + #ffffff + #023c69 + #ffffff + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..39128af --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + ambaagent + contain + false + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00ab510 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..e28e8be --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,38 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.google.gms:google-services:4.4.1' + classpath('com.android.tools.build:gradle') + classpath('com.facebook.react:react-native-gradle-plugin') + classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') + } +} + +def reactNativeAndroidDir = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('react-native/package.json')") + }.standardOutput.asText.get().trim(), + "../android" +) + +allprojects { + repositories { + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url(reactNativeAndroidDir) + } + + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + } +} + +apply plugin: "expo-root-project" +apply plugin: "com.facebook.react.rootproject" diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..9f8da22 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,59 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Enable AAPT2 PNG crunching +android.enablePngCrunchInReleaseBuilds=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Enable GIF support in React Native images (~200 B increase) +expo.gif.enabled=true +# Enable webp support in React Native images (~85 KB increase) +expo.webp.enabled=true +# Enable animated webp support (~3.4 MB increase) +# Disabled by default because iOS doesn't support animated webp +expo.webp.animated=false + +# Enable network inspector +EX_DEV_CLIENT_NETWORK_INSPECTOR=true + +# Use legacy packaging to compress native libraries in the resulting APK. +expo.useLegacyPackaging=false + +# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin +expo.edgeToEdgeEnabled=false \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..f3b75f3 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..0fa3671 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,39 @@ +pluginManagement { + def reactNativeGradlePlugin = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") + }.standardOutput.asText.get().trim() + ).getParentFile().absolutePath + includeBuild(reactNativeGradlePlugin) + + def expoPluginsPath = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") + }.standardOutput.asText.get().trim(), + "../android/expo-gradle-plugin" + ).absolutePath + includeBuild(expoPluginsPath) +} + +plugins { + id("com.facebook.react.settings") + id("expo-autolinking-settings") +} + +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { + ex.autolinkLibrariesFromCommand() + } else { + ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) + } +} +expoAutolinking.useExpoModules() + +rootProject.name = 'amba' + +expoAutolinking.useExpoVersionCatalog() + +include ':app' +includeBuild(expoAutolinking.reactNativeGradlePlugin) diff --git a/app.json b/app.json new file mode 100644 index 0000000..d121b35 --- /dev/null +++ b/app.json @@ -0,0 +1,94 @@ +{ + "expo": { + "name": "ambaagent", + "slug": "ambaagent", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.ambapay.ambaagent", + "buildNumber": "1", + "infoPlist": { + "NSContactsUsageDescription": "Allow AmbaPay to access your contacts to find friends and recipients for money transfers.", + "NSFaceIDUsageDescription": "Allow AmbaPay to use Face ID for secure authentication.", + "NSCalendarsUsageDescription": "Allow Amba Pay to add your payment schedules to your calendar.", + "ITSAppUsesNonExemptEncryption": false, + "CFBundleURLTypes": [ + { + "CFBundleTypeRole": "Editor", + "CFBundleURLSchemes": [ + "com.googleusercontent.apps.613864011564-atsg9nau8hicla4td6dedcab15g7qr04" + ] + } + ] + }, + "googleServicesFile": "./GoogleService-Info.plist", + "entitlements": { + "aps-environment": "production" + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "com.ambapay.ambaagent", + "googleServicesFile": "./google-services.json", + "permissions": [ + "READ_CONTACTS", + "USE_BIOMETRIC", + "USE_FINGERPRINT", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.POST_NOTIFICATIONS", + "android.permission.READ_CALENDAR", + "android.permission.WRITE_CALENDAR", + "READ_CALENDAR", + "WRITE_CALENDAR" + ] + }, + "web": { + "favicon": "./assets/favicon.png", + "bundler": "metro" + }, + "plugins": [ + "expo-router", + [ + "expo-font", + { + "fonts": [ + "./assets/fonts/DMSans-Regular.ttf", + "./assets/fonts/DMSans-Bold.ttf", + "./assets/fonts/DMSans-Medium.ttf", + "./assets/fonts/DMSans-Black.ttf", + "./assets/fonts/DMSans-SemiBold.ttf", + "./assets/fonts/DMSans-Light.ttf", + "./assets/fonts/DMSans-Thin.ttf", + "./assets/fonts/DMSans-ExtraLight.ttf", + "./assets/fonts/DMSans-ExtraBold.ttf" + ] + } + ], + [ + "expo-contacts", + { + "contactsPermission": "Allow $(PRODUCT_NAME) to access your contacts to find friends and recipients for money transfers." + } + ], + "@react-native-firebase/app", + "@react-native-google-signin/google-signin" + ], + "autolinking": { + "exclude": ["expo-firebase-core"] + }, + "scheme": "ambaagent" + } +} diff --git a/app/(root)/(screens)/addcard.tsx b/app/(root)/(screens)/addcard.tsx new file mode 100644 index 0000000..a45f2c2 --- /dev/null +++ b/app/(root)/(screens)/addcard.tsx @@ -0,0 +1,424 @@ +import React, { useState, useEffect } from "react"; +import { ScrollView, View, TouchableOpacity, Keyboard } from "react-native"; +import { LucideEye, EyeOff, Trash2, CreditCard } from "lucide-react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router, useLocalSearchParams } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import { CreditCard as CreditCardType } from "~/lib/services/walletService"; +import { useTabStore } from "~/lib/stores"; +import { addCardSchema, validate } from "~/lib/utils/validationSchemas"; + +import BackButton from "~/components/ui/backButton"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +// Credit Card Component +const CreditCardComponent = ({ + card, + onRemove, +}: { + card: CreditCardType; + onRemove: () => void; +}) => { + const getCardColor = (cardType: string) => { + switch (cardType?.toLowerCase()) { + case "visa": + return "bg-blue-700"; + case "mastercard": + return "bg-red-600"; + case "american express": + return "bg-green-700"; + case "discover": + return "bg-orange-600"; + default: + return "bg-gray-700"; + } + }; + + const getCardIcon = (cardType: string) => { + switch (cardType?.toLowerCase()) { + case "visa": + return "VISA"; + case "mastercard": + return "MC"; + case "american express": + return "AMEX"; + case "discover": + return "DISC"; + default: + return "CARD"; + } + }; + + return ( + + {/* Card Brand and Remove Button */} + + + + {getCardIcon(card.cardType || "")} + + + { + console.log("Remove button pressed for card:", card.id); + onRemove(); + }} + className="bg-gray-500/80 p-3 rounded-full min-w-10 min-h-10 flex items-center justify-center" + activeOpacity={0.7} + style={{ zIndex: 10 }} + > + + + + + {/* Card Number */} + + + {card.cardNumber} + + + + {/* Card Details */} + + + + Valid Thru + + + {card.expiryDate} + + + + + Card Type + + + {card.cardType || "Unknown"} + + + + + {/* Decorative Elements */} + + + + + + + + ); +}; + +export default function AddCard() { + const { user } = useAuthWithProfile(); + const { addCreditCard, loading, error } = useUserWallet(user); + const { setLastVisitedTab } = useTabStore(); + const { from } = useLocalSearchParams<{ from?: string }>(); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Set the tab state when component mounts based on where user came from + useEffect(() => { + if (from === "addcash") { + setLastVisitedTab("/(tabs)/cardmang"); + } else { + setLastVisitedTab("/"); // Default to home if no specific from parameter + } + }, [setLastVisitedTab, from]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Form state + const [cardNumber, setCardNumber] = useState(""); + const [expiryDate, setExpiryDate] = useState(""); + const [cvv, setCvv] = useState(""); + const [showCvv, setShowCvv] = useState(false); + + // Format card number as user types (add spaces every 4 digits) + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = (matches && matches[0]) || ""; + const parts = []; + + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + + if (parts.length) { + return parts.join(" "); + } else { + return v; + } + }; + + // Format expiry date as MM/YY + const formatExpiryDate = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; + } + return v; + }; + + const handleCardNumberChange = (value: string) => { + const formatted = formatCardNumber(value); + if (formatted.length <= 19) { + // 16 digits + 3 spaces + setCardNumber(formatted); + } + }; + + const handleExpiryDateChange = (value: string) => { + const formatted = formatExpiryDate(value); + if (formatted.length <= 5) { + // MM/YY + setExpiryDate(formatted); + } + }; + + const handleCvvChange = (value: string) => { + const v = value.replace(/[^0-9]/gi, ""); + if (v.length <= 4) { + setCvv(v); + } + }; + + const handleAddCard = async () => { + Keyboard.dismiss(); + + // Basic field validation + if (!cardNumber.trim()) { + showToast( + t("addcard.validationErrorTitle"), + t("addcard.validationCardNumberRequired"), + "error" + ); + return; + } + + if (!expiryDate.trim()) { + showToast( + t("addcard.validationErrorTitle"), + t("addcard.validationExpiryRequired"), + "error" + ); + return; + } + + if (!cvv.trim()) { + showToast( + t("addcard.validationErrorTitle"), + t("addcard.validationCvvRequired"), + "error" + ); + return; + } + + // Validate form using valibot + const validationResult = validate(addCardSchema, { + cardNumber, + expiryDate, + cvv, + }); + + if (!validationResult.success) { + showToast( + t("addcard.validationErrorTitle"), + validationResult.error || t("addcard.validationInvalidCard"), + "error" + ); + return; + } + + try { + await addCreditCard({ + cardNumber: validationResult.data.cardNumber.replace(/\s/g, ""), // Remove spaces + expiryDate: validationResult.data.expiryDate, + cvv: validationResult.data.cvv, + }); + + router.replace(ROUTES.CARD_ADDED); + } catch (err) { + showToast( + t("addcard.toastErrorTitle"), + t("addcard.toastAddFailed"), + "error" + ); + } + }; + + // Show errors + useEffect(() => { + if (error) { + showToast(t("addcard.toastErrorTitle"), error, "error"); + } + }, [error]); + + return ( + + + + + + + {t("addcard.title")} + + + + + + + + {t("addcard.sectionCardTitle")} + + + + + + {t("addcard.sectionCardSubtitle")} + + + + + + + + + + + + + + + + + + + setShowCvv(!showCvv)}> + {showCvv ? ( + + ) : ( + + )} + + } + /> + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/addcash.tsx b/app/(root)/(screens)/addcash.tsx new file mode 100644 index 0000000..006ad99 --- /dev/null +++ b/app/(root)/(screens)/addcash.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { View, InteractionManager, ActivityIndicator } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useFocusEffect } from "expo-router"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { + parseDisplayToCents, + formatDisplayAmount, +} from "~/lib/utils/monetaryUtils"; +import { Big } from "big.js"; +import { PinConfirmationModal } from "~/components/ui/pinConfirmationModal"; +import { amountSchema, validate } from "~/lib/utils/validationSchemas"; +import BackButton from "~/components/ui/backButton"; +import { showAlert } from "~/lib/utils/alertUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useTabStore } from "~/lib/stores"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +export default function AddCash() { + const [amount, setAmount] = useState(""); + const [showPinModal, setShowPinModal] = useState(false); + const [isSecurityVerified, setIsSecurityVerified] = useState(false); + const { setLastVisitedTab } = useTabStore(); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Set the tab state when component mounts (defer non-critical work) + useEffect(() => { + const task = InteractionManager.runAfterInteractions(() => { + if (__DEV__) { + console.log("ADD CASH PAGE MOUNTED"); + } + setLastVisitedTab("/"); + }); + return () => task.cancel(); + }, [setLastVisitedTab]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Reset state and show PIN modal when screen comes into focus + // useFocusEffect only runs when screen is focused, so we can safely show modal here + useFocusEffect( + React.useCallback(() => { + setIsSecurityVerified(false); + setShowPinModal(true); + setAmount(""); + + // Cleanup: hide modal when screen loses focus + return () => { + setShowPinModal(false); + setIsSecurityVerified(false); + }; + }, []) + ); + + // Handle number input and special actions + const handleNumberPress = (input: string) => { + if (input === "clear") { + handleClear(); + return; + } + + if (input === "backspace") { + handleBackspace(); + return; + } + + // Handle decimal point + if (input === ".") { + // Prevent multiple decimals + if (amount.includes(".")) return; + + // If empty, start with "0." + if (amount === "") { + setAmount("0."); + return; + } + + // Add decimal point + setAmount(amount + "."); + return; + } + + // Handle digit input (0-9) + if (!/^[0-9]$/.test(input)) return; + + // Handle leading zeros + if (amount === "0" && input !== ".") { + setAmount(input); // Replace leading zero + return; + } + + const newAmount = amount + input; + + // Check decimal places limit (max 2 decimal places) + if (newAmount.includes(".")) { + const [whole, decimal] = newAmount.split("."); + if (decimal && decimal.length > 2) return; + } + + // Check maximum amount (max $999.99) + try { + const numValue = new Big(newAmount); + if (numValue.gt(999.99)) return; + } catch (error) { + return; + } + + // Check total length to prevent very long inputs + if (newAmount.length > 6) return; // Max: 999.99 + + setAmount(newAmount); + }; + + // Handle backspace + const handleBackspace = () => { + if (amount.length === 0) return; + + // Remove last character + const newAmount = amount.slice(0, -1); + setAmount(newAmount); + }; + + // Clear all input + const handleClear = () => { + setAmount(""); + }; + + // Validate if amount is valid for submission + const isValidAmount = () => { + if ( + !amount || + amount === "" || + amount === "0" || + amount === "0." || + amount === "0.00" + ) { + return false; + } + + const amountInCents = parseDisplayToCents(amount); + return amountInCents >= 1000 && amountInCents <= 99999; // $10.00 to $999.99 + }; + + // Handle PIN confirmation success + const handlePinSuccess = () => { + setShowPinModal(false); + setIsSecurityVerified(true); + }; + + // Handle add cash action (called after PIN is verified) + const handleAddCash = () => { + // Validate amount using valibot (minimum $10.00 = 1000 cents) + const amountValidationResult = validate( + amountSchema({ min: 1000, max: 99999, minDisplay: "$10.00" }), + amount + ); + if (!amountValidationResult.success) { + showToast( + t("addcash.validationErrorTitle"), + t("addcash.validationEnterAmount"), + "error" + ); + return; + } + + const amountInCents = parseDisplayToCents(amount); + + if (amountInCents < 1000) { + // $10.00 minimum + showToast( + t("addcash.validationErrorTitle"), + t("addcash.validationMinAmount"), + "error" + ); + return; + } + + if (amountInCents > 99999) { + // $999.99 maximum + showToast( + t("addcash.validationErrorTitle"), + t("addcash.validationMaxAmount"), + "error" + ); + return; + } + + console.log("Adding cash:", amountInCents, "cents"); + + // Navigate into the add-cash donation + checkout flow + router.push({ + pathname: ROUTES.DONATION, + params: { + amount: amountInCents.toString(), + type: "add_cash", + }, + }); + }; + + return ( + + {!isSecurityVerified ? ( + + + + + {t("addcash.verifyingSecurity")} + + + ) : ( + <> + + + + + + {t("addcash.title")} + + + + + + {formatDisplayAmount(amount)} + + + + + + + + + + + + + + + + )} + + setShowPinModal(false)} + onSuccess={handlePinSuccess} + title={t("addcash.pinModalTitle")} + /> + + + ); +} diff --git a/app/(root)/(screens)/addcashcomp.tsx b/app/(root)/(screens)/addcashcomp.tsx new file mode 100644 index 0000000..b5d3b75 --- /dev/null +++ b/app/(root)/(screens)/addcashcomp.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { View, Text } from "react-native"; +import LottieView from "lottie-react-native"; +import { Button } from "~/components/ui/button"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useTranslation } from "react-i18next"; + +export default function AddCashComp() { + const params = useLocalSearchParams<{ + amount: string; // required + }>(); + const { t } = useTranslation(); + + const handleAddCashAgain = () => { + router.replace(ROUTES.HOME); + router.push(ROUTES.ADD_CASH); + }; + + return ( + + + {/* Centered success content */} + + + + {params.amount && ( + + {String(params.amount)} + + )} + + + {t("addcashcomp.successNote")} + + + + {/* Bottom buttons */} + + + + + + + + ); +} diff --git a/app/(root)/(screens)/addrecipient.tsx b/app/(root)/(screens)/addrecipient.tsx new file mode 100644 index 0000000..d064e77 --- /dev/null +++ b/app/(root)/(screens)/addrecipient.tsx @@ -0,0 +1,728 @@ +import React, { useState, useEffect } from "react"; +import { + View, + ScrollView, + Keyboard, + TouchableOpacity, + Platform, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { ArrowLeftIcon, ChevronLeft, ChevronRight } from "lucide-react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import Dropdown from "~/components/ui/dropdown"; +import { router } from "expo-router"; +import BackButton from "~/components/ui/backButton"; +import { useRecipientsStore } from "~/lib/stores"; +import { useTabStore } from "~/lib/stores"; +import { ROUTES } from "~/lib/routes"; +import { formatPhoneNumber } from "~/lib/utils/phoneUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { awardPoints } from "~/lib/services/pointsService"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { Picker } from "react-native-wheel-pick"; + +const PROFILE_BANK_OPTIONS: { id: string; name: string }[] = [ + { id: "cbe", name: "Commercial Bank of Ethiopia" }, + { id: "dashen", name: "Dashen Bank" }, + { id: "abay", name: "Abay Bank" }, + { id: "awash", name: "Awash Bank" }, + { id: "hibret", name: "Hibret Bank" }, + { id: "telebirr", name: "Ethio Telecom (Telebirr)" }, + { id: "safaricom", name: "Safaricom M-PESA" }, +]; + +export default function AddRecpi() { + const { addRecipient, loading, addError, clearAddError } = + useRecipientsStore(); + const { setLastVisitedTab } = useTabStore(); + const [fullName, setFullName] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false); + const [clientType, setClientType] = useState<"Individual" | "Business">( + "Individual" + ); + const [selectedBank, setSelectedBank] = useState(null); + const [accountNumberInput, setAccountNumberInput] = useState(""); + const [accountSheetVisible, setAccountSheetVisible] = useState(false); + const [scheduleSheetVisible, setScheduleSheetVisible] = useState(false); + const [scheduleFrequency, setScheduleFrequency] = useState(""); + const [scheduleHour, setScheduleHour] = useState("00"); + const [scheduleMinute, setScheduleMinute] = useState("00"); + const [schedulePeriod, setSchedulePeriod] = useState<"AM" | "PM">("AM"); + const [scheduleTime, setScheduleTime] = useState(""); + const [scheduleDate, setScheduleDate] = useState(""); + const { t } = useTranslation(); + + const isTelecomWallet = + selectedBank === "telebirr" || selectedBank === "safaricom"; + const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number"; + const accountPlaceholder = isTelecomWallet + ? "Enter phone number" + : "Enter account number"; + + // Time wheel options + const HOURS = Array.from({ length: 24 }, (_, i) => + String(i).padStart(2, "0") + ); + const MINUTES = Array.from({ length: 60 }, (_, i) => + String(i).padStart(2, "0") + ); + const PERIODS: ("AM" | "PM")[] = ["AM", "PM"]; + + // Calendar state for month navigation (UI-only) + const today = new Date(); + const [calendarCursor, setCalendarCursor] = useState(() => { + const start = new Date(); + start.setDate(1); + return start; + }); + + const cursorYear = calendarCursor.getFullYear(); + const cursorMonth = calendarCursor.getMonth(); + const firstOfMonth = new Date(cursorYear, cursorMonth, 1); + const lastOfMonth = new Date(cursorYear, cursorMonth + 1, 0); + const firstWeekday = firstOfMonth.getDay(); // 0 = Sun + + const calendarDays = [ + // Leading empty slots before the 1st + ...Array.from({ length: firstWeekday }, () => null), + // Actual days + ...Array.from({ length: lastOfMonth.getDate() }, (_, idx) => { + const day = idx + 1; + const key = `${cursorYear}-${cursorMonth + 1}-${day}`; + const isToday = + today.getFullYear() === cursorYear && + today.getMonth() === cursorMonth && + today.getDate() === day; + return { key, label: String(day), isToday }; + }), + ]; + + const MONTH_NAMES = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + // Keep a combined time string for potential future summary usage + useEffect(() => { + setScheduleTime(`${scheduleHour}:${scheduleMinute} ${schedulePeriod}`); + }, [scheduleHour, scheduleMinute, schedulePeriod]); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Set the tab state when component mounts + useEffect(() => { + setLastVisitedTab("/(tabs)/listrecipient"); + }, [setLastVisitedTab]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handlePhoneChange = (value: string) => { + const formatted = formatPhoneNumber(value); + setPhoneNumber(formatted); + }; + + const validateForm = () => { + if (!fullName.trim()) { + showToast( + t("addrecipient.validationErrorTitle"), + t("addrecipient.validationFullNameRequired"), + "error" + ); + return false; + } + + if (!phoneNumber.trim()) { + showToast( + t("addrecipient.validationErrorTitle"), + t("addrecipient.validationPhoneRequired"), + "error" + ); + return false; + } + + const cleanPhone = phoneNumber.replace(/[^+\d]/g, ""); + if (cleanPhone.length < 7) { + showToast( + t("addrecipient.validationErrorTitle"), + t("addrecipient.validationPhoneInvalid"), + "error" + ); + return false; + } + + return true; + }; + + const handleAddRecipient = async () => { + Keyboard.dismiss(); + + if (!validateForm()) return; + + setIsSubmitting(true); + setHasAttemptedSubmit(true); + + await addRecipient({ + fullName: fullName.trim(), + phoneNumber: phoneNumber.trim(), + }); + + setIsSubmitting(false); + }; + + // Show error if any + useEffect(() => { + if (addError) { + showToast( + t("addrecipient.toastErrorTitle"), + addError || t("addrecipient.toastAddError"), + "error" + ); + // Clear the error after showing it + clearAddError(); + // Reset submission state + setHasAttemptedSubmit(false); + } + }, [addError, clearAddError]); + + // Show success when operation completes without error + useEffect(() => { + if (hasAttemptedSubmit && !loading && !addError && !isSubmitting) { + // Operation completed successfully + router.replace(ROUTES.RECIPIENT_ADDED); + + awardPoints("add_recipient").catch((error) => { + console.warn( + "[AddRecipient] Failed to award add recipient points", + error + ); + }); + } + }, [loading, addError, isSubmitting, hasAttemptedSubmit]); + + const isFormValid = + fullName.trim() && + phoneNumber.trim() && + phoneNumber.replace(/[^+\d]/g, "").length >= 7; + + return ( + + + + {/* Header */} + + + + + + + {t("addrecipient.title")} + + + {t("addrecipient.sectionSubtitle")} + + + + {/* Client details card */} + + + {/* Client type toggle */} + + + {t("addrecipient.clientTypeLabel", "Client type")} + + + {(["Individual", "Business"] as const).map((type) => { + const isActive = clientType === type; + return ( + setClientType(type)} + > + + {type} + + + ); + })} + + + + + + + + + + + + + + {/* Actions for account & schedule (UI-only) */} + + + + + + + + + + + + + + + {/* Bottom sheet: Add Account (UI-only, matches Edit Profile add account) */} + setAccountSheetVisible(false)} + maxHeightRatio={0.9} + > + + + {t("addrecipient.accountSheetTitle", "Add Account")} + + + + + + {t("addrecipient.accountBankLabel", "Bank")} + + + {PROFILE_BANK_OPTIONS.map((bank) => { + const isSelected = selectedBank === bank.id; + const initials = bank.name + .split(" ") + .map((part) => part[0]) + .join("") + .toUpperCase() + .slice(0, 2); + + return ( + setSelectedBank(bank.id)} + className={`items-center justify-between px-3 py-4 mb-3 rounded-2xl border ${ + isSelected + ? "border-primary bg-primary/5" + : "border-gray-200 bg-white" + }`} + style={{ width: "30%" }} + > + + + {initials} + + + + {bank.name} + + + ); + })} + + + + + + {accountLabel} + + + setAccountNumberInput(text.replace(/[^0-9]/g, "")) + } + containerClassName="w-full mb-4" + borderClassName="border-[#E5E7EB] bg-white rounded-[4px]" + placeholderColor="#9CA3AF" + textClassName="text-[#111827] text-sm" + keyboardType="number-pad" + /> + + + + + + + + {/* Bottom sheet: Set Schedule (UI-only with dropdowns & calendar) */} + setScheduleSheetVisible(false)} + maxHeightRatio={0.95} + > + + + {t("addrecipient.scheduleSheetTitle", "Set Schedule")} + + + {t( + "addrecipient.scheduleSheetSubtitle", + "Choose a simple reminder schedule for this client. This is UI-only for now." + )} + + + {/* Frequency dropdown */} + + + setScheduleFrequency(val)} + placeholder={t( + "addrecipient.scheduleFrequencyPlaceholder", + "Daily / Weekly / Monthly / Custom" + )} + /> + + + {/* Time-of-day selector: 3-column wheel (hour, minute, AM/PM) */} + + + + {/* Hour wheel */} + + + {t("addrecipient.scheduleHourLabel", "Hour")} + + setScheduleHour(val)} + textColor="#9CA3AF" + selectTextColor="#16A34A" + isCyclic + /> + + + {/* Minute wheel */} + + + {t("addrecipient.scheduleMinuteLabel", "Min")} + + setScheduleMinute(val)} + textColor="#9CA3AF" + selectTextColor="#16A34A" + isCyclic + /> + + + {/* AM/PM wheel */} + + + {t("addrecipient.schedulePeriodLabel", "AM/PM")} + + + setSchedulePeriod(val as "AM" | "PM") + } + textColor="#9CA3AF" + selectTextColor="#16A34A" + /> + + + + + {/* Date selector - calendar with header & weekdays */} + + + {/* Month header */} + + { + setCalendarCursor( + (prev) => + new Date(prev.getFullYear(), prev.getMonth() - 1, 1) + ); + }} + className="p-1" + > + + + + {MONTH_NAMES[cursorMonth]} {cursorYear} + + { + setCalendarCursor( + (prev) => + new Date(prev.getFullYear(), prev.getMonth() + 1, 1) + ); + }} + className="p-1" + > + + + + + {/* Weekday labels */} + + {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((d) => ( + + + {d} + + + ))} + + + {/* Day grid: 7 columns (Sun-Sat) */} + + {Array.from( + { length: Math.ceil(calendarDays.length / 7) }, + (_, rowIndex) => { + const row = calendarDays.slice( + rowIndex * 7, + rowIndex * 7 + 7 + ); + return ( + + {row.map((day, idx) => { + if (!day) { + return ( + + ); + } + + const safeDay = day as { + key: string; + label: string; + isToday: boolean; + }; + + const isSelected = scheduleDate === safeDay.key; + const isToday = safeDay.isToday; + + return ( + + setScheduleDate(safeDay.key)} + className={`items-center justify-center rounded-full border ${ + isSelected + ? "bg-primary border-primary" + : "bg-white border-transparent" + }`} + style={{ width: 32, height: 32 }} + > + + {safeDay.label} + + + {isToday && !isSelected && ( + + {t( + "addrecipient.scheduleTodayLabel", + "Today" + )} + + )} + + ); + })} + + ); + } + )} + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/cardaddedcomp.tsx b/app/(root)/(screens)/cardaddedcomp.tsx new file mode 100644 index 0000000..738db0b --- /dev/null +++ b/app/(root)/(screens)/cardaddedcomp.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { View, Text, ScrollView, Share } from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { SuccessIconNewCard } from "~/components/ui/icons"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +export default function CardAddedComp() { + const params = useLocalSearchParams<{ + message?: string; + cardName?: string; + cardNumber?: string; + }>(); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleAddAnother = () => { + // Navigate to add card page + router.replace(ROUTES.ADD_CARD); + }; + + const handleGoToCards = () => { + // Navigate to list cards page + router.replace(ROUTES.LIST_CARD); + }; + + const handleShare = async () => { + try { + const shareMessage = params.message + ? t("cardaddedcomp.shareMessageWithParam", { + message: params.message, + }) + : t("cardaddedcomp.shareMessageDefault"); + + const result = await Share.share({ + message: shareMessage, + title: t("cardaddedcomp.shareTitle"), + }); + + if (result.action === Share.sharedAction) { + // Content was shared + console.log("Content shared successfully"); + } else if (result.action === Share.dismissedAction) { + // Share dialog was dismissed + console.log("Share dialog dismissed"); + } + } catch (error) { + console.error("Error sharing:", error); + showToast( + t("cardaddedcomp.toastErrorTitle"), + t("cardaddedcomp.toastShareError"), + "error" + ); + } + }; + + return ( + + + {/* Center content */} + + + + + {t("cardaddedcomp.title")} + + + + + {t("cardaddedcomp.description")} + + + + + {/* Bottom buttons */} + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/cardmang.tsx b/app/(root)/(screens)/cardmang.tsx new file mode 100644 index 0000000..956ce17 --- /dev/null +++ b/app/(root)/(screens)/cardmang.tsx @@ -0,0 +1,302 @@ +import React from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { LucideCreditCard, LucidePlus, LucideTrash } from "lucide-react-native"; +import { ROUTES } from "~/lib/routes"; +import { router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import { CreditCard } from "~/lib/services/walletService"; +import TopBar from "~/components/ui/topBar"; +import { + ApplePayIcon, + CreditDebitCardIcon, + GooglePayIcon, +} from "~/components/ui/icons"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Input } from "~/components/ui/input"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import PermissionAlertModal from "~/components/ui/permissionAlertModal"; + +// Individual Card Component +const CardItem = ({ + card, + onRemove, +}: { + card: CreditCard; + onRemove: (card: CreditCard) => void; +}) => { + const getCardColor = (cardType: string) => { + switch (cardType?.toLowerCase()) { + case "visa": + return "bg-blue-50"; + case "mastercard": + return "bg-green-50"; + case "american express": + return "bg-green-50"; + case "discover": + return "bg-orange-50"; + default: + return "bg-gray-50"; + } + }; + + const handleRemovePress = (card: CreditCard) => { + setSelectedCard(card); + setRemoveModalVisible(true); + }; + + const handleRemove = () => { + onRemove(card); + }; + + return ( + + + + + + + + + {card.cardType || "Card"} + + + {card.cardNumber} + + + Expires {card.expiryDate} + + + + + + + + + + + ); +}; + +export default function ListCard() { + const { user } = useAuthWithProfile(); + const { wallet, loading, error, removeCreditCard } = useUserWallet(user); + const [searchQuery, setSearchQuery] = React.useState(""); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const [removeModalVisible, setRemoveModalVisible] = React.useState(false); + const [selectedCard, setSelectedCard] = React.useState( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + React.useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleRemoveCard = async (cardId: string) => { + try { + await removeCreditCard(cardId); + showToast( + t("cards.toastRemoveSuccessTitle"), + t("cards.toastRemoveSuccess"), + "success" + ); + } catch (error) { + showToast( + t("cards.toastRemoveErrorTitle"), + t("cards.toastRemoveError"), + "error" + ); + } + }; + + const renderCards = () => { + if (loading) { + return ( + + + {t("cardmang.loading")} + + + ); + } + + if (error) { + return ( + + + {t("cardmang.errorTitle")} + + + {error} + + + ); + } + + if (!wallet?.cards || wallet.cards.length === 0) { + return ( + + + + {t("cardmang.emptyTitle")} + + + {t("cardmang.emptySubtitle")} + + + ); + } + + return ( + item.id + String(index)} + scrollEnabled={false} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + )} + /> + ); + }; + + return ( + + + + + + + {t("cardmang.title")} + + + + {t("cardmang.subtitle")} + + + + + + + {/* Add Card Button */} + + + + + + + {/* Cards List */} + + + {t("cardmang.paymentOptionsTitle")} + + {renderCards()} + + + + + + + { + if (selectedCard) { + await handleRemoveCard(selectedCard.id); + } + setRemoveModalVisible(false); + setSelectedCard(null); + }} + onSecondary={() => { + setRemoveModalVisible(false); + setSelectedCard(null); + }} + /> + + ); +} diff --git a/app/(root)/(screens)/cashout.tsx b/app/(root)/(screens)/cashout.tsx new file mode 100644 index 0000000..a91c5df --- /dev/null +++ b/app/(root)/(screens)/cashout.tsx @@ -0,0 +1,340 @@ +import React, { useState, useEffect } from "react"; +import { View, ScrollView, InteractionManager } from "react-native"; +import { useFocusEffect } from "expo-router"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import { + parseDisplayToCents, + formatDisplayAmount, +} from "~/lib/utils/monetaryUtils"; +import { Big } from "big.js"; +import BackButton from "~/components/ui/backButton"; +import { PinConfirmationModal } from "~/components/ui/pinConfirmationModal"; +import { amountSchema, validate } from "~/lib/utils/validationSchemas"; +import { showAlert } from "~/lib/utils/alertUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import FourDotLoader from "~/components/ui/FourDotLoader"; + +export default function CashOut() { + const { user } = useAuthWithProfile(); + const { wallet } = useUserWallet(user); + const [amount, setAmount] = useState(""); + const [showPinModal, setShowPinModal] = useState(false); + const [isSecurityVerified, setIsSecurityVerified] = useState(false); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + const task = InteractionManager.runAfterInteractions(() => { + if (__DEV__) { + console.log("CASHOUT PAGE MOUNTED"); + } + }); + return () => task.cancel(); + }, []); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Reset state and show PIN modal when screen comes into focus + // useFocusEffect only runs when screen is focused, so we can safely show modal here + useFocusEffect( + React.useCallback(() => { + setIsSecurityVerified(false); + setShowPinModal(true); + setAmount(""); + + // Cleanup: hide modal when screen loses focus + return () => { + setShowPinModal(false); + setIsSecurityVerified(false); + }; + }, []) + ); + + // Handle number input and special actions (copied from addcash.tsx) + const handleNumberPress = (input: string) => { + if (input === "clear") { + handleClear(); + return; + } + + if (input === "backspace") { + handleBackspace(); + return; + } + + // Handle decimal point + if (input === ".") { + // Prevent multiple decimals + if (amount.includes(".")) return; + + // If empty, start with "0." + if (amount === "") { + setAmount("0."); + return; + } + + // Add decimal point + setAmount(amount + "."); + return; + } + + // Handle digit input (0-9) + if (!/^[0-9]$/.test(input)) return; + + // Handle leading zeros + if (amount === "0" && input !== ".") { + setAmount(input); // Replace leading zero + return; + } + + const newAmount = amount + input; + + // Check decimal places limit (max 2 decimal places) + if (newAmount.includes(".")) { + const [whole, decimal] = newAmount.split("."); + if (decimal && decimal.length > 2) return; + } + + // Check maximum amount (max $999.99) + try { + const numValue = new Big(newAmount); + if (numValue.gt(999.99)) return; + } catch (error) { + return; + } + + // Check total length to prevent very long inputs + if (newAmount.length > 6) return; // Max: 999.99 + + setAmount(newAmount); + }; + + // Handle backspace + const handleBackspace = () => { + if (amount.length === 0) return; + + // Remove last character + const newAmount = amount.slice(0, -1); + setAmount(newAmount); + }; + + // Clear all input + const handleClear = () => { + setAmount(""); + }; + + // Validate if amount is valid for submission + const isValidAmount = () => { + if ( + !amount || + amount === "" || + amount === "0" || + amount === "0." || + amount === "0.00" + ) { + return false; + } + + const amountInCents = parseDisplayToCents(amount); + if (amountInCents < 1 || amountInCents > 99999) { + // 1 cent to $999.99 + return false; + } + + // Check if amount is within wallet balance (no processing fee for cashout) + const currentBalance = wallet?.balance || 0; // Balance in cents + + return currentBalance >= amountInCents; + }; + + // Get validation error message + const getValidationError = () => { + // Validate basic amount using valibot + const amountValidationResult = validate( + amountSchema({ min: 1, max: 99999 }), + amount + ); + if (!amountValidationResult.success) { + return amountValidationResult.error; + } + + const amountInCents = parseDisplayToCents(amount); + if (amountInCents < 1) { + return t("cashout.validationMinAmount"); + } + + if (amountInCents > 99999) { + // $999.99 + return t("cashout.validationMaxAmount"); + } + + // Check if amount exceeds wallet balance (no processing fee for cashout) + const currentBalance = wallet?.balance || 0; // Balance in cents + + if (currentBalance < amountInCents) { + const balanceInDollars = currentBalance / 100; + const requiredInDollars = amountInCents / 100; + return t("cashout.validationInsufficientBalance", { + required: requiredInDollars.toFixed(2), + available: balanceInDollars.toFixed(2), + }); + } + + return null; + }; + + // Handle PIN confirmation success + const handlePinSuccess = () => { + setShowPinModal(false); + setIsSecurityVerified(true); + }; + + // Handle cash out action (called after PIN is verified) + const handleCashOut = async () => { + const validationError = getValidationError(); + if (validationError) { + showToast(t("cashout.validationErrorTitle"), validationError, "error"); + return; + } + + const amountInCents = parseDisplayToCents(amount); + console.log("Cashing out:", amountInCents, "cents"); + + router.push({ + pathname: ROUTES.SEND_BANK, + params: { + amount: amountInCents.toString(), // Pass cents as string + recipientName: user?.displayName || "Self", + recipientPhoneNumber: user?.phoneNumber || "", + recipientType: "saved", + recipientId: user?.uid || "", + note: "Cash out to bank account", + transactionType: "cashout", + }, + }); + }; + + return ( + + {!isSecurityVerified && !showPinModal ? ( + + + {t("cashout.verifyingSecurity")} + + + + ) : ( + + + + + + {/* Wallet Balance Display */} + + + + {t("cashout.availableBalanceLabel")} + + + + ${wallet ? (wallet.balance / 100).toFixed(2) : "0.00"} + + + + + + + ${formatDisplayAmount(amount)} + + + + + + + + + + + + + + + + )} + + {/* PIN Confirmation Modal */} + setShowPinModal(false)} + onSuccess={handlePinSuccess} + title={t("cashout.pinModalTitle")} + /> + + + ); +} diff --git a/app/(root)/(screens)/cashoutcomp.tsx b/app/(root)/(screens)/cashoutcomp.tsx new file mode 100644 index 0000000..8d5d942 --- /dev/null +++ b/app/(root)/(screens)/cashoutcomp.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import LottieView from "lottie-react-native"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { showAlert } from "~/lib/utils/alertUtils"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; + +export default function CashOutComp() { + const params = useLocalSearchParams<{ + amount: string; // required + note?: string; // optional + }>(); + + const { wallet } = useAuthWithProfile(); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleCashOutAgain = () => { + const balance = wallet?.balance; + if (balance === undefined) { + showToast( + t("cashoutcomp.toastErrorTitle"), + t("cashoutcomp.toastNoBalance"), + "error" + ); + return; + } + if (balance < 1000) { + showToast( + t("cashoutcomp.toastErrorTitle"), + t("cashoutcomp.toastMinError"), + "error" + ); + return; + } + + router.replace(ROUTES.HOME); + router.push(ROUTES.CASH_OUT); + }; + + const note = + params.note && String(params.note).trim().length > 0 + ? String(params.note) + : t("cashoutcomp.successNote"); + + return ( + + {/* Main content */} + + + + {/* */} + {params.amount && ( + + {String(params.amount)} + + )} + + + {note} + + + + + {/* Bottom actions */} + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/changepin.tsx b/app/(root)/(screens)/changepin.tsx new file mode 100644 index 0000000..4e3796a --- /dev/null +++ b/app/(root)/(screens)/changepin.tsx @@ -0,0 +1,305 @@ +import React, { useState, useRef, useEffect } from "react"; +import { View, TouchableOpacity } from "react-native"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import BackButton from "~/components/ui/backButton"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; +import { router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import ModalToast from "~/components/ui/toast"; +import { X } from "lucide-react-native"; + +type Step = "old" | "new" | "confirm"; + +export default function ChangePin() { + const { user } = useAuthWithProfile(); + const [currentStep, setCurrentStep] = useState("old"); + const [oldPin, setOldPin] = useState(""); + const [newPin, setNewPin] = useState(""); + const [confirmPin, setConfirmPin] = useState(""); + const [loading, setLoading] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const getCurrentPin = () => { + switch (currentStep) { + case "old": + return oldPin; + case "new": + return newPin; + case "confirm": + return confirmPin; + } + }; + + const handleKeyPress = (key: string) => { + const currentPin = getCurrentPin(); + + if (key === "clear") { + switch (currentStep) { + case "old": + setOldPin(""); + break; + case "new": + setNewPin(""); + break; + case "confirm": + setConfirmPin(""); + break; + } + } else if (key === "backspace") { + switch (currentStep) { + case "old": + setOldPin((prev) => prev.slice(0, -1)); + break; + case "new": + setNewPin((prev) => prev.slice(0, -1)); + break; + case "confirm": + setConfirmPin((prev) => prev.slice(0, -1)); + break; + } + } else if (currentPin.length < 6) { + switch (currentStep) { + case "old": + setOldPin((prev) => prev + key); + break; + case "new": + setNewPin((prev) => prev + key); + break; + case "confirm": + setConfirmPin((prev) => prev + key); + break; + } + } + }; + + const handleContinue = async () => { + const currentPin = getCurrentPin(); + + if (currentPin.length !== 6) { + showToast("Error", "Please enter a 6-digit PIN", "error"); + return; + } + + if (currentStep === "old") { + // TODO: Verify old PIN with backend + // For now, just move to next step + setLoading(true); + setTimeout(() => { + setLoading(false); + setCurrentStep("new"); + }, 500); + } else if (currentStep === "new") { + if (newPin === oldPin) { + showToast("Error", "New PIN must be different from old PIN", "error"); + return; + } + setCurrentStep("confirm"); + } else if (currentStep === "confirm") { + if (confirmPin !== newPin) { + showToast("Error", "PINs do not match", "error"); + setConfirmPin(""); + return; + } + + // TODO: Update PIN in backend + setLoading(true); + setTimeout(() => { + setLoading(false); + showToast("Success", "PIN changed successfully", "success"); + setTimeout(() => { + router.back(); + }, 1500); + }, 1000); + } + }; + + const getStepTitle = () => { + switch (currentStep) { + case "old": + return "Enter Old PIN"; + case "new": + return "Enter New PIN"; + case "confirm": + return "Confirm New PIN"; + } + }; + + const getStepDescription = () => { + switch (currentStep) { + case "old": + return "Enter your current PIN"; + case "new": + return "Enter your new PIN"; + case "confirm": + return "Re-enter your new PIN to confirm"; + } + }; + + const renderPinDots = () => { + const currentPin = getCurrentPin(); + return ( + + + {getStepDescription()} + + + {[0, 1, 2, 3, 4, 5].map((index) => ( + + ))} + + + ); + }; + + return ( + + + + + {/* Header */} + + + Change PIN + + + {getStepDescription()} + + + + {/* Step Indicator */} + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + {/* Spacer */} + + + {/* PIN Dots - positioned right above keypad */} + {renderPinDots()} + + {/* Keypad */} + + + + + {/* Continue Button */} + + + + + + + + ); +} diff --git a/app/(root)/(screens)/checkout.tsx b/app/(root)/(screens)/checkout.tsx new file mode 100644 index 0000000..ea47f6b --- /dev/null +++ b/app/(root)/(screens)/checkout.tsx @@ -0,0 +1,657 @@ +import React, { useEffect, useRef, useState } from "react"; +import { View, ScrollView, Image, TouchableOpacity } from "react-native"; +import { useLocalSearchParams, router } from "expo-router"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { TransactionService } from "~/lib/services/transactionService"; +import { GoogleIcon } from "~/components/ui/icons"; +import { Icons } from "~/assets/icons"; +import { WalletService } from "~/lib/services/walletService"; +import { ROUTES } from "~/lib/routes"; +import { + calculateTotalAmountForSending, + calculateProcessingFee, +} from "~/lib/utils/feeUtils"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +type PaymentOptionCardProps = { + label: string; + icon: React.ReactNode; + selected?: boolean; + onPress?: () => void; +}; + +const PaymentOptionCard = ({ + label, + icon, + selected, + onPress, +}: PaymentOptionCardProps) => { + return ( + + + + {icon} + {selected && ( + + + + )} + + {label} + + + ); +}; + +export default function CheckoutScreen() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + amount?: string; + type?: string; + recipientName?: string; + recipientPhoneNumber?: string; + recipientType?: string; + recipientId?: string; + note?: string; + donationSkipped?: string; + donationType?: string; + donationAmount?: string; + donateAnonymously?: string; + donationCampaignId?: string; + donationCampaignTitle?: string; + ticketTierName?: string; + ticketTierPrice?: string; + eventName?: string; + eventId?: string; + ticketTierId?: string; + ticketCount?: string; + }>(); + + const { user, profile, wallet, refreshWallet } = useAuthWithProfile(); + const [selectedPayment, setSelectedPayment] = useState< + "card" | "apple" | "google" + >("card"); + const [email, setEmail] = useState(""); + const [nameOnCard, setNameOnCard] = useState(""); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvv, setCvv] = useState(""); + const [showCvv, setShowCvv] = useState(false); + const [address, setAddress] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const isAddCashFlow = params.type === "add_cash"; + const isEventTicketFlow = params.type === "event_ticket"; + + const headerTitle = isEventTicketFlow + ? params.ticketTierName || "-" + : isAddCashFlow + ? profile?.fullName || user?.email || "-" + : params.recipientName || "-"; + + const headerSubtitle = isEventTicketFlow + ? params.eventName || "" + : isAddCashFlow + ? profile?.phoneNumber || profile?.email || "" + : params.recipientPhoneNumber || ""; + + const amountInCents = parseInt(params.amount || "0"); + const amountInDollars = amountInCents / 100; + + const hasDonation = + params.donationSkipped === "false" && !!params.donationAmount; + + const donationAmountNumber = params.donationAmount + ? Number(params.donationAmount) + : NaN; + const donationAmountDollars = + !isNaN(donationAmountNumber) && hasDonation ? donationAmountNumber : 0; + + const processingFeeInCents = Math.round(amountInCents * 0.0125); + const processingFeeInDollars = processingFeeInCents / 100; + + const subtotalInDollars = amountInDollars + donationAmountDollars; + const totalInDollars = subtotalInDollars + processingFeeInDollars; + + // Card input helpers (mirrored from AddCard screen) + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = (matches && matches[0]) || ""; + const parts: string[] = []; + + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + + if (parts.length) { + return parts.join(" "); + } else { + return v; + } + }; + + const formatExpiry = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; + } + return v; + }; + + const handleCardNumberChange = (value: string) => { + const formatted = formatCardNumber(value); + if (formatted.length <= 19) { + // 16 digits + 3 spaces + setCardNumber(formatted); + } + }; + + const handleExpiryChange = (value: string) => { + const formatted = formatExpiry(value); + if (formatted.length <= 5) { + // MM/YY + setExpiry(formatted); + } + }; + + const handleCvvChange = (value: string) => { + const v = value.replace(/[^0-9]/gi, ""); + if (v.length <= 4) { + setCvv(v); + } + }; + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + useEffect(() => { + if (!profile) return; + + if (!email && profile.email) { + setEmail(profile.email); + } + + if (!nameOnCard && profile.fullName) { + setNameOnCard(profile.fullName); + } + + if (!address && profile.address) { + setAddress(profile.address); + } + }, [profile, email, nameOnCard, address]); + + const handlePay = async () => { + if (isAddCashFlow) { + if (!params.amount || !user?.uid || !wallet) { + showToast( + t("selectacc.toastErrorTitle"), + t("selectacc.toastMissingInfo"), + "error" + ); + return; + } + + const amountInCents = parseInt(params.amount); + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("addcash.validationErrorTitle"), + t("addcash.validationEnterAmount"), + "error" + ); + return; + } + + setIsProcessing(true); + try { + const currentBalance = wallet.balance || 0; + const newBalance = currentBalance + amountInCents; + + const result = await WalletService.updateWalletBalance( + user.uid, + newBalance + ); + + if (result.success) { + await refreshWallet(); + + router.replace({ + pathname: ROUTES.ADDCASH_COMPLETION, + params: { + amount: (amountInCents / 100).toFixed(2), + }, + }); + } else { + showToast( + t("selectacc.toastErrorTitle"), + result.error || t("selectacc.toastAddCashFailed"), + "error" + ); + } + } catch (error) { + console.error("Error adding cash from checkout:", error); + showToast( + t("selectacc.toastErrorTitle"), + t("selectacc.toastAddCashFailedWithRetry"), + "error" + ); + } finally { + setIsProcessing(false); + } + + return; + } + + if (params.type === "event_ticket") { + if (!params.amount || !user?.uid) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastMissingDetails"), + "error" + ); + return; + } + + const amountInCents = parseInt(params.amount); + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastInvalidAmount"), + "error" + ); + return; + } + + setIsProcessing(true); + try { + router.push({ + pathname: ROUTES.TRANSACTION_DETAIL, + params: { + amount: params.amount, + type: "event_ticket", + recipientName: params.eventName || "", + date: new Date().toISOString(), + status: "Completed", + note: params.ticketTierName || "", + flowType: "event_ticket", + ticketTierId: params.ticketTierId || "", + ticketCount: params.ticketCount || "1", + eventId: params.eventId || "", + }, + }); + } finally { + setIsProcessing(false); + } + + return; + } + + if (!params.amount || !user?.uid) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastMissingDetails"), + "error" + ); + return; + } + + const amountInCents = parseInt(params.amount); + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastInvalidAmount"), + "error" + ); + return; + } + + const totalRequired = calculateTotalAmountForSending(amountInCents); + if (!wallet) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastWalletNotFound"), + "error" + ); + return; + } + if (wallet.balance < totalRequired) { + const processingFee = calculateProcessingFee(amountInCents); + const required = (totalRequired / 100).toFixed(2); + const fee = (processingFee / 100).toFixed(2); + const available = (wallet.balance / 100).toFixed(2); + showToast( + t("transconfirm.toastInsufficientBalanceTitle"), + t("transconfirm.toastInsufficientBalanceDescription", { + required, + fee, + available, + }), + "error" + ); + return; + } + + if ( + !params.recipientName || + !params.recipientPhoneNumber || + !params.recipientType || + !params.recipientId + ) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastRecipientMissing"), + "error" + ); + return; + } + + setIsProcessing(true); + + try { + const result = await TransactionService.sendMoney(user.uid, { + amount: amountInCents, + recipientName: params.recipientName, + recipientPhoneNumber: params.recipientPhoneNumber, + recipientType: params.recipientType as "saved" | "contact", + recipientId: params.recipientId, + note: params.note?.trim() || "", + }); + + if (result.success) { + await refreshWallet(); + + router.replace({ + pathname: "/(screens)/taskcomp", + params: { + message: `Transaction completed on your end. $${( + amountInCents / 100 + ).toFixed(2)} will be claimed by ${ + params.recipientName + } within 7 days, or the money will revert to your wallet.`, + amount: (amountInCents / 100).toFixed(2), + recipientName: params.recipientName, + recipientPhoneNumber: params.recipientPhoneNumber, + }, + }); + } else { + showToast( + t("transconfirm.toastErrorTitle"), + result.error || t("transconfirm.toastSendFailed"), + "error" + ); + } + } catch (error) { + console.error("Error sending money:", error); + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastSendFailedWithRetry"), + "error" + ); + } finally { + setIsProcessing(false); + } + }; + + return ( + + + + + + + + + {t("checkout.title")} + + + + + + {isEventTicketFlow ? "Ticket" : t("checkout.recipientLabel")} + + + {headerTitle} + + + {headerSubtitle} + + + + + {t("checkout.totalLabel")} + + + ${totalInDollars.toFixed(2)} + + + + + {t("checkout.subtitle")} + + + {/* Payment options */} + + + {t("checkout.paymentOptionsTitle")} + + + + } + selected={selectedPayment === "card"} + onPress={() => setSelectedPayment("card")} + /> + + } + selected={selectedPayment === "apple"} + onPress={() => setSelectedPayment("apple")} + /> + } + selected={selectedPayment === "google"} + onPress={() => setSelectedPayment("google")} + /> + + + + {selectedPayment === "card" ? ( + + + {t("checkout.cardInfoTitle")} + + + + + + + + + + + + + + + ) : ( + + + {selectedPayment === "apple" + ? t("checkout.appleIdTitle") + : t("checkout.paymentEmailTitle")} + + + + )} + + + + {/* Contact information */} + + + {t("checkout.contactInfoTitle")} + + + + + {/* Billing address */} + + + {t("checkout.billingAddressTitle")} + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/crowdfunding.tsx b/app/(root)/(screens)/crowdfunding.tsx new file mode 100644 index 0000000..ed5a613 --- /dev/null +++ b/app/(root)/(screens)/crowdfunding.tsx @@ -0,0 +1,218 @@ +import * as React from "react"; +import { Image, View, ScrollView } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Progress } from "~/components/ui/progress"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { Text } from "~/components/ui/text"; +import { + ArrowLeftIcon, + CircleDollarSign, + LucideEye, +} from "lucide-react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useTranslation } from "react-i18next"; + +export default function CrowdFund() { + const [value, setValue] = React.useState("overview"); + const { t } = useTranslation(); + return ( + + + + + + + + + + + + {t("crowdfunding.tabsOverview")} + + + + + {t("crowdfunding.tabsCampaign")} + + + + {t("crowdfunding.tabsFaq")} + + + + + + {t("crowdfunding.title")} + + + {t("crowdfunding.description")} + + + + + + + + + {t("crowdfunding.pledgedAmount")} + + + {t("crowdfunding.pledgedOf", { target: "1,000,000" })} + + + + + + 1,000 + + + {t("crowdfunding.backersCountLabel")} + + + + + 32 + + + {t("crowdfunding.daysToGoLabel")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/donation.tsx b/app/(root)/(screens)/donation.tsx new file mode 100644 index 0000000..fa6bde0 --- /dev/null +++ b/app/(root)/(screens)/donation.tsx @@ -0,0 +1,322 @@ +import React, { useState } from "react"; +import { View, ScrollView, Image, TouchableOpacity } from "react-native"; +import { router, useLocalSearchParams } from "expo-router"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import { ROUTES } from "~/lib/routes"; +import { Icons } from "~/assets/icons"; +import { Check } from "lucide-react-native"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useTranslation } from "react-i18next"; + +const campaigns = [ + { + id: "1", + title: "Fresh Fruits for Improving Children’s Health", + image: Icons.profileImage, + progress: 0.8, + raised: "$54,090", + timeLeft: "8 Hours left", + }, + { + id: "2", + title: "Support Local Farmers Market Initiative", + image: Icons.mainBG, + progress: 0.45, + raised: "$21,400", + timeLeft: "2 Days left", + }, + { + id: "3", + title: "School Meal Program for Kids", + image: Icons.qrImage, + progress: 0.62, + raised: "$33,250", + timeLeft: "1 Day left", + }, +]; + +export default function DonationScreen() { + const { t } = useTranslation(); + const params = useLocalSearchParams>(); + const { profile } = useAuthWithProfile(); + const amountInCents = Number(params.amount ?? "0"); + const baseAmount = isNaN(amountInCents) + ? "0.00" + : (amountInCents / 100).toFixed(2); + + const [donationType, setDonationType] = useState<"one-time" | "monthly">( + "monthly" + ); + const [donationAmount, setDonationAmount] = useState(baseAmount); + const [donateAnonymously, setDonateAnonymously] = useState(false); + const [selectedCampaignId, setSelectedCampaignId] = useState( + null + ); + + const isCampaignSelected = !!selectedCampaignId; + + const displayName = donateAnonymously + ? t("donation.anonymousLabel") + : profile?.fullName || t("donation.displayNameFallback"); + + const goToConfirm = (options?: { skipped?: boolean }) => { + const selectedCampaign = campaigns.find((c) => c.id === selectedCampaignId); + + router.push({ + pathname: ROUTES.TRANSACTION_CONFIRM, + params: { + ...params, + donationSkipped: options?.skipped ? "true" : "false", + donationType, + donationAmount, + donateAnonymously: donateAnonymously ? "true" : "false", + donationCampaignId: selectedCampaign?.id ?? "", + donationCampaignTitle: selectedCampaign?.title ?? "", + fromSelectRecipientFlow: "true", + }, + }); + }; + + const handleSkip = () => { + goToConfirm({ skipped: true }); + }; + + const handleDonate = () => { + goToConfirm({ skipped: false }); + }; + + return ( + + + + + + + + {t("donation.title")} + + + {t("donation.subtitle")} + + + + + {campaigns.map((campaign) => { + const isSelected = selectedCampaignId === campaign.id; + return ( + setSelectedCampaignId(campaign.id)} + > + + + + {/* Content */} + + + {campaign.title} + + + {/* Progress bar */} + + + + + + + {t("donation.donationRaisedLabel")} + + + {Math.round(campaign.progress * 100)}% + + + + + + {campaign.raised} + + + {campaign.timeLeft} + + + + + + ); + })} + + + + + + {t("donation.chooseAmountTitle")} + + + {/* One-Time / Monthly toggle */} + + setDonationType("one-time")} + > + + {t("donation.donationTypeOneTime")} + + + setDonationType("monthly")} + > + + {t("donation.donationTypeMonthly")} + + + + + {/* Amount row */} + + + {donationAmount} + + + + {/* Quick amount chips */} + + {["5", "10", "25", "50", "100"].map((value) => { + const isActive = donationAmount === value; + return ( + setDonationAmount(value)} + > + + + ${value} + + + + ); + })} + + + setDonateAnonymously(!donateAnonymously)} + > + + {donateAnonymously && } + + + {t("donation.donateAnonymouslyLabel")} + + + + + + {displayName} + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/editprofile.tsx b/app/(root)/(screens)/editprofile.tsx new file mode 100644 index 0000000..471cdba --- /dev/null +++ b/app/(root)/(screens)/editprofile.tsx @@ -0,0 +1,896 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + ScrollView, + View, + Keyboard, + TouchableOpacity, + Image, + Alert, + ActivityIndicator, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { ROUTES } from "~/lib/routes"; +import { profileUpdateSchema, validate } from "~/lib/utils/validationSchemas"; +import BackButton from "~/components/ui/backButton"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import Dropdown, { DropdownOption } from "~/components/ui/dropdown"; +import { useLangStore } from "~/lib/stores"; +import { Copy, Plus, ChevronDown } from "lucide-react-native"; +import * as Clipboard from "expo-clipboard"; +import * as ImagePicker from "expo-image-picker"; +import { Icons } from "~/assets/icons"; +import { AuthService } from "~/lib/services/authServices"; +import { uploadProfileImage } from "~/lib/services/profileImageService"; +import BottomSheet from "~/components/ui/bottomSheet"; + +type ProfileLinkedAccount = { + id: string; + bankId: string; + bankName: string; + accountNumber: string; + isDefault: boolean; +}; + +const PROFILE_BANK_OPTIONS: { id: string; name: string }[] = [ + { id: "cbe", name: "Commercial Bank of Ethiopia" }, + { id: "dashen", name: "Dashen Bank" }, + { id: "abay", name: "Abay Bank" }, + { id: "awash", name: "Awash Bank" }, + { id: "hibret", name: "Hibret Bank" }, + { id: "telebirr", name: "Ethio Telecom (Telebirr)" }, + { id: "safaricom", name: "Safaricom M-PESA" }, +]; + +export default function EditProfile() { + const { t } = useTranslation(); + const language = useLangStore((state) => state.language); + const setLanguage = useLangStore((state) => state.setLanguage); + const { user, profile, wallet, profileLoading, profileError } = + useAuthWithProfile(); + const [updateLoading, setUpdateLoading] = useState(false); + const [updateError, setUpdateError] = useState(null); + + const [profileImage, setProfileImage] = useState(null); + const [imagePicking, setImagePicking] = useState(false); + + // Editable state for form fields + const [editedProfile, setEditedProfile] = useState({ + fullName: "", + phoneNumber: "", + email: "", + address: "", + }); + + const [profileAccounts, setProfileAccounts] = useState< + ProfileLinkedAccount[] + >([]); + const [isSelectAccountSheetVisible, setIsSelectAccountSheetVisible] = + useState(false); + const [isAddingAccount, setIsAddingAccount] = useState(false); + const [selectedBank, setSelectedBank] = useState(null); + const [accountNumberInput, setAccountNumberInput] = useState(""); + const [savingAccount, setSavingAccount] = useState(false); + const [pendingDefaultAccountId, setPendingDefaultAccountId] = useState< + string | null + >(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Update local state when profile loads + useEffect(() => { + if (profile) { + setEditedProfile({ + fullName: profile.fullName || "", + phoneNumber: profile.phoneNumber || "", + email: profile.email || "", + address: profile.address || "", + }); + // Only sync image from profile if user hasn't picked a local file + setProfileImage((prev) => { + if (prev && prev.startsWith("file:")) { + return prev; + } + return profile.photoUrl || null; + }); + + const linkedFromProfile: any = (profile as any).linkedAccounts; + if (Array.isArray(linkedFromProfile)) { + const mapped: ProfileLinkedAccount[] = linkedFromProfile.map( + (acc: any) => ({ + id: String(acc.id), + bankId: String(acc.bankId || ""), + bankName: String(acc.bankName || ""), + accountNumber: String(acc.accountNumber || ""), + isDefault: !!acc.isDefault, + }) + ); + setProfileAccounts(mapped); + } else { + setProfileAccounts([]); + } + } + }, [profile]); + + const handleSelectProfileImage = async () => { + try { + setImagePicking(true); + const permissionResult = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + Alert.alert( + "Permission Required", + "Please allow access to your photo library to select a profile picture." + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + const localUri = result.assets[0].uri; + console.log("[EditProfile] Image selected locally", { localUri }); + // Only update local preview here. Actual upload happens on Save. + setProfileImage(localUri); + } + } catch (e) { + console.error("[EditProfile] Error while selecting profile image", e); + showToast("Error", "Failed to select image", "error"); + } finally { + setImagePicking(false); + } + }; + + const handleUpdateProfile = async () => { + Keyboard.dismiss(); + + if (!user?.uid) { + showToast( + t("profile.toastErrorTitle"), + t("profile.toastUserNotFound"), + "error" + ); + return; + } + + // Prepare data for validation based on signup type + const validationData: any = { + fullName: editedProfile.fullName, + address: editedProfile.address || undefined, + }; + + // Only validate phone number if user didn't sign up with phone + if (profile?.signupType !== "phone") { + validationData.phoneNumber = editedProfile.phoneNumber || undefined; + } + + // Only validate email if user signed up with phone + if (profile?.signupType === "phone") { + validationData.email = editedProfile.email || undefined; + } + + // Validate form using valibot + const validationResult = validate(profileUpdateSchema, validationData); + if (!validationResult.success) { + showToast("Error", validationResult.error, "warning"); + return; + } + + try { + setUpdateLoading(true); + setUpdateError(null); + + // If the user picked a new local image (file://), upload it first + let finalPhotoUrl: string | undefined = profile?.photoUrl || undefined; + if ( + profileImage && + profileImage !== profile?.photoUrl && + profileImage.startsWith("file:") + ) { + try { + console.log("[EditProfile] Uploading profile image on save", { + uid: user.uid, + localUri: profileImage, + }); + const uploadedUrl = await uploadProfileImage(user.uid, profileImage); + console.log( + "[EditProfile] Profile image uploaded on save, url:", + uploadedUrl + ); + finalPhotoUrl = uploadedUrl; + setProfileImage(uploadedUrl); + } catch (e) { + console.error( + "[EditProfile] Failed to upload profile image on save", + e + ); + throw new Error( + e instanceof Error ? e.message : "Failed to upload profile image" + ); + } + } + + // Prepare update data based on signup type + const updateData: any = { + fullName: validationResult.data.fullName, + address: validationResult.data.address || undefined, + }; + + if (finalPhotoUrl) { + updateData.photoUrl = finalPhotoUrl; + } + + // Only allow phone number update if user didn't sign up with phone + if ( + profile?.signupType !== "phone" && + validationResult.data.phoneNumber + ) { + updateData.phoneNumber = validationResult.data.phoneNumber; + } + + // Only allow email update if user signed up with phone + if (profile?.signupType === "phone" && validationResult.data.email) { + updateData.email = validationResult.data.email; + } + + const result = await AuthService.updateUserProfile(user.uid, updateData); + if (!result.success) { + throw new Error(result.error || "Failed to update profile"); + } + + showToast( + t("profile.toastProfileUpdatedTitle"), + t("profile.toastProfileUpdatedDescription"), + "success" + ); + + // Navigate back to profile after successful update + setTimeout(() => { + router.back(); + }, 1000); + } catch (error) { + setUpdateError( + error instanceof Error ? error.message : "Failed to update profile" + ); + showToast( + t("profile.toastErrorTitle"), + t("profile.toastUpdateErrorDescription"), + "error" + ); + } finally { + setUpdateLoading(false); + } + }; + + const handleCancel = () => { + router.back(); + }; + + const handleCopyUsername = async () => { + if ( + !usernameDisplay || + usernameDisplay === t("profile.usernamePlaceholder") + ) { + showToast("Error", "No username to copy", "error"); + return; + } + + await Clipboard.setStringAsync(usernameDisplay); + showToast("Copied", "Username copied to clipboard", "success"); + }; + + const handleStartAddAccount = () => { + if (profileAccounts.length >= 5) { + showToast("Limit reached", "You can link up to 5 accounts.", "warning"); + return; + } + setIsSelectAccountSheetVisible(false); + setSelectedBank(null); + setAccountNumberInput(""); + setIsAddingAccount(true); + }; + + const handleSelectBank = (bankId: string) => { + setSelectedBank(bankId); + }; + + const handleSaveAccount = async () => { + if (!selectedBank || !accountNumberInput.trim()) { + return; + } + + if (profileAccounts.length >= 5) { + showToast("Limit reached", "You can link up to 5 accounts.", "warning"); + return; + } + + const bank = PROFILE_BANK_OPTIONS.find((b) => b.id === selectedBank); + if (!bank) return; + + const newAccount: ProfileLinkedAccount = { + id: `${selectedBank}-${Date.now()}`, + bankId: selectedBank, + bankName: bank.name, + accountNumber: accountNumberInput.trim(), + isDefault: profileAccounts.length === 0, + }; + + const updatedAccounts = [...profileAccounts, newAccount]; + setProfileAccounts(updatedAccounts); + + if (!user?.uid) { + showToast( + t("profile.toastErrorTitle"), + t("profile.toastUserNotFound"), + "error" + ); + return; + } + + try { + setSavingAccount(true); + const result = await AuthService.updateUserProfile(user.uid, { + linkedAccounts: updatedAccounts, + }); + if (!result.success) { + throw new Error(result.error || "Failed to save account"); + } + } catch (error) { + showToast( + t("profile.toastErrorTitle"), + error instanceof Error ? error.message : "Failed to save account", + "error" + ); + } finally { + setSavingAccount(false); + } + + setAccountNumberInput(""); + setSelectedBank(null); + setIsAddingAccount(false); + }; + + const handleConfirmDefaultAccount = async () => { + if (!user?.uid) { + showToast( + t("profile.toastErrorTitle"), + t("profile.toastUserNotFound"), + "error" + ); + return; + } + + if (!pendingDefaultAccountId) { + setIsSelectAccountSheetVisible(false); + return; + } + + const updatedAccounts = profileAccounts.map((account) => ({ + ...account, + isDefault: account.id === pendingDefaultAccountId, + })); + + setProfileAccounts(updatedAccounts); + + try { + const result = await AuthService.updateUserProfile(user.uid, { + linkedAccounts: updatedAccounts, + }); + if (!result.success) { + throw new Error(result.error || "Failed to update default account"); + } + } catch (error) { + showToast( + t("profile.toastErrorTitle"), + error instanceof Error + ? error.message + : "Failed to update default account", + "error" + ); + } finally { + setIsSelectAccountSheetVisible(false); + setPendingDefaultAccountId(null); + } + }; + + // Show update error + useEffect(() => { + if (updateError) { + showToast(t("profile.toastUpdateErrorTitle"), updateError, "error"); + } + }, [updateError]); + + const languageOptions: DropdownOption[] = [ + { value: "en", label: t("profile.languageOptionEnglish") }, + { value: "am", label: t("profile.languageOptionAmharic") }, + { value: "fr", label: t("profile.languageOptionFrench") }, + { value: "ti", label: t("profile.languageOptionTigrinya") }, + { value: "om", label: t("profile.languageOptionOromo") }, + ]; + + const isTelecomWallet = + selectedBank === "telebirr" || selectedBank === "safaricom"; + const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number"; + const accountPlaceholder = isTelecomWallet + ? "Enter phone number" + : "Enter account number"; + + const defaultAccount = + profileAccounts.find((account) => account.isDefault) || null; + + const usernameDisplay = profile?.fullName + ? `@${profile.fullName.split(" ")[0]}` + : user?.email + ? `@${user.email.split("@")[0]}` + : t("profile.usernamePlaceholder"); + + return ( + + + + + + + Edit Profile + + + + + + + {profileImage ? ( + + ) : ( + + )} + {(updateLoading || imagePicking) && ( + + + + )} + + + + + + + + + + {profileLoading ? t("profile.loadingProfile") : ""} + + {profileError && ( + + {t("profile.errorWithMessage", { error: profileError })} + + )} + + + + + + setEditedProfile((prev) => ({ ...prev, fullName: text })) + } + containerClassName="w-full" + borderClassName="border-[#D9DBE9] bg-white" + placeholderColor="#7E7E7E" + textClassName="text-[#000] text-sm" + /> + + + + + + + setEditedProfile((prev) => ({ ...prev, address: text })) + } + containerClassName="w-full" + borderClassName="border-[#D9DBE9] bg-white" + placeholderColor="#7E7E7E" + textClassName="text-[#000] text-sm" + /> + + + + + + + setEditedProfile((prev) => ({ ...prev, phoneNumber: text })) + } + editable={profile?.signupType !== "phone"} + containerClassName="w-full" + borderClassName="border-[#D9DBE9] bg-white" + placeholderColor="#7E7E7E" + textClassName="text-[#000] text-sm" + keyboardType="phone-pad" + /> + + + + + + + setEditedProfile((prev) => ({ ...prev, email: text })) + } + editable={profile?.signupType === "phone"} + containerClassName="w-full" + borderClassName="border-[#D9DBE9] bg-white" + placeholderColor="#7E7E7E" + textClassName="text-[#000] text-sm" + /> + + + + {/* Language Dropdown */} + + + setLanguage(value as any)} + placeholder={t("profile.languagePlaceholder")} + /> + + + + {/* Account Number (read-only) */} + + + + { + setPendingDefaultAccountId(defaultAccount?.id || null); + setIsSelectAccountSheetVisible(true); + }} + className={`flex-row items-center justify-between ${ + defaultAccount ? "py-1 px-4" : "py-3 px-4" + } rounded-md border border-[#D9DBE9]`} + > + + + {defaultAccount + ? defaultAccount.bankName + : "Choose account"} + + {defaultAccount && ( + + {defaultAccount.accountNumber} + + )} + + + + + {/* {profileAccounts.length > 0 && ( + + Tap to change default account or add another. + + )} */} + + + + + {/* Username (read-only) with copy icon */} + + + + + + + + + + + + + + + + + + + + + + + + setIsSelectAccountSheetVisible(false)} + maxHeightRatio={0.9} + > + + + Choose Account + + + + {profileAccounts.length === 0 ? ( + + + You have not added any accounts yet. + + + ) : ( + + {profileAccounts.map((account) => ( + { + setPendingDefaultAccountId(account.id); + }} + className={`flex-row items-center justify-between py-1 px-4 rounded-md mb-2 border ${ + ( + pendingDefaultAccountId + ? pendingDefaultAccountId === account.id + : account.isDefault + ) + ? "border-primary bg-primary/5" + : "border-[#D9DBE9] bg-white" + }`} + > + + + {account.bankName} + + + {account.accountNumber} + + + {account.isDefault && ( + + + Default + + + )} + + ))} + + )} + + {profileAccounts.length > 0 && ( + + )} + + + + {profileAccounts.length >= 5 && ( + + You can link up to 5 accounts. + + )} + + + setIsAddingAccount(false)} + maxHeightRatio={0.9} + > + + + Add Account + + + + + Bank + + {PROFILE_BANK_OPTIONS.map((bank) => { + const isSelected = selectedBank === bank.id; + const initials = bank.name + .split(" ") + .map((part) => part[0]) + .join("") + .toUpperCase() + .slice(0, 2); + + return ( + handleSelectBank(bank.id)} + className={`items-center justify-between px-3 py-4 mb-3 rounded-2xl border ${ + isSelected + ? "border-primary bg-primary/5" + : "border-gray-200 bg-white" + }`} + style={{ width: "30%" }} + > + + + {initials} + + + + {bank.name} + + + ); + })} + + + + + + {accountLabel} + + + setAccountNumberInput(text.replace(/[^0-9]/g, "")) + } + containerClassName="w-full mb-4" + borderClassName="border-[#E5E7EB] bg-white rounded-[4px]" + placeholderColor="#9CA3AF" + textClassName="text-[#111827] text-sm" + keyboardType="number-pad" + /> + + + + + + + + ); +} diff --git a/app/(root)/(screens)/eventdetail.tsx b/app/(root)/(screens)/eventdetail.tsx new file mode 100644 index 0000000..4751e94 --- /dev/null +++ b/app/(root)/(screens)/eventdetail.tsx @@ -0,0 +1,500 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + Image, + ScrollView, + ActivityIndicator, + TouchableOpacity, + Dimensions, +} from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { Icons } from "~/assets/icons"; +import { Share } from "react-native"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { router, useLocalSearchParams } from "expo-router"; +import BackButton from "~/components/ui/backButton"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { EventService, type EventDto } from "~/lib/services/eventService"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { ROUTES } from "~/lib/routes"; +import { awardPoints } from "~/lib/services/pointsService"; + +export default function EventDetailScreen() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ id?: string }>(); + const { user } = useAuthWithProfile(); + const [event, setEvent] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const [selectedTierId, setSelectedTierId] = useState(null); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState< + string | undefined + >(); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + const screenWidth = Dimensions.get("window").width; + const [buySheetVisible, setBuySheetVisible] = useState(false); + const [ticketCount, setTicketCount] = useState(1); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleShare = async () => { + try { + await Share.share({ + message: t("eventdetail.shareMessage"), + }); + + try { + await awardPoints("share_event"); + } catch (error) { + console.warn("[EventDetail] Failed to award share event points", error); + } + } catch (error) { + console.log("Error sharing event:", error); + showToast( + t("eventdetail.toastErrorTitle"), + t("eventdetail.toastShareError"), + "error" + ); + } + }; + + useEffect(() => { + let cancelled = false; + + const loadEvent = async () => { + if (!user || !params.id) return; + setLoading(true); + setError(null); + + try { + const token = await user.getIdToken(); + const eventData = await EventService.getEventById( + token, + String(params.id) + ); + if (!cancelled) { + setEvent(eventData); + // Debug images and description to verify response + console.log("[EventDetail] event images", eventData?.images); + console.log( + "[EventDetail] event description", + eventData?.description + ); + } + } catch (err) { + if (!cancelled) { + setError("Failed to load event details"); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + loadEvent(); + + return () => { + cancelled = true; + }; + }, [user, params.id]); + + const fallbackImage = + "https://images.pexels.com/photos/1190297/pexels-photo-1190297.jpeg?auto=compress&cs=tinysrgb&w=800"; + + const images = + event?.images && event.images.length > 0 ? event.images : [fallbackImage]; + + const startDate = event?.startDate ? new Date(event.startDate) : null; + const endDate = event?.endDate ? new Date(event.endDate) : null; + + const formatEventDateRange = (start: Date, end: Date) => { + // Format like: Sat, Jan 10, 2026 at 6:00 PM – 1:00 AM (EAT) + const datePart = start.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + }); + + const startTime = start.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }); + + const endTime = end.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }); + + return `${datePart} at ${startTime} – ${endTime} (EAT)`; + }; + + const formattedDate = + startDate && endDate + ? formatEventDateRange(startDate, endDate) + : t("eventdetail.dateTime"); + + const handleBuy = () => { + if (!event || !selectedTierId) { + showToast( + "Ticket selection required", + "Please select a ticket tier first", + "error" + ); + return; + } + + const tier = event.ticketTiers?.find((t: any) => t.id === selectedTierId); + if (!tier) { + showToast( + "Ticket selection required", + "Please select a ticket tier first", + "error" + ); + return; + } + + console.log("[EventDetail] Buy pressed", { + eventId: event.id, + selectedTierId, + }); + + setTicketCount(1); + setBuySheetVisible(true); + }; + + const handleConfirmBuy = () => { + if (!event || !selectedTierId) { + showToast( + "Ticket selection required", + "Please select a ticket tier first", + "error" + ); + return; + } + + const tier = event.ticketTiers?.find((t: any) => t.id === selectedTierId); + if (!tier) { + showToast( + "Ticket selection required", + "Please select a ticket tier first", + "error" + ); + return; + } + + const priceNumber = Number(tier.price || 0); + const amountInCents = Math.round(priceNumber * 100 * ticketCount); + + setBuySheetVisible(false); + + router.push({ + pathname: ROUTES.CHECKOUT, + params: { + amount: String(amountInCents), + type: "event_ticket", + ticketTierName: tier.name, + ticketTierPrice: priceNumber.toFixed(2), + eventName: event.name, + eventId: event.id, + ticketTierId: tier.id, + ticketCount: String(ticketCount), + }, + }); + }; + + return ( + + + + + + + {event?.name ?? t("eventdetail.title")} + + + + + {loading && ( + + + + Loading event details... + + + )} + + {!loading && error && ( + + + {error} + + + )} + + {!loading && !error && ( + <> + + {event?.description ?? t("eventdetail.description")} + + + + {event?.venue ?? t("eventdetail.location")} + + + {formattedDate} + + + + { + const { contentOffset, layoutMeasurement } = + e.nativeEvent; + const index = Math.round( + contentOffset.x / layoutMeasurement.width + ); + setCurrentImageIndex(index); + }} + > + {images.map((img, index) => ( + + + + ))} + + + + {images.map((_, index) => ( + + ))} + + + + + + {[0, 1, 2, 3, 4].map((i) => ( + + + + ))} + + + {t("eventdetail.peopleComing")} + + + + {event?.ticketTiers && event.ticketTiers.length > 0 && ( + + {event.ticketTiers.map((tier: any) => { + const isSelected = tier.id === selectedTierId; + return ( + setSelectedTierId(tier.id)} + > + + + + {isSelected && ( + + )} + + + {tier.name} + + + + ${tier.price} + + + + ); + })} + + )} + + )} + + + + + + + + + + + + setBuySheetVisible(false)} + maxHeightRatio={0.5} + > + {event && selectedTierId && ( + + + Confirm tickets + + + {event.name} + + + {(() => { + const tier = event.ticketTiers?.find( + (t: any) => t.id === selectedTierId + ); + if (!tier) return null; + + const priceNumber = Number(tier.price || 0); + const total = priceNumber * ticketCount; + + return ( + <> + + + + {tier.name} + + + ${priceNumber.toFixed(2)} per ticket + + + + + Total + + + ${total.toFixed(2)} + + + + + + + Ticket count + + + + setTicketCount((c) => Math.max(1, c - 1)) + } + > + + - + + + + {ticketCount} + + setTicketCount((c) => c + 1)} + > + + + + + + + + + + ); + })()} + + )} + + + ); +} diff --git a/app/(root)/(screens)/eventqrscreen.tsx b/app/(root)/(screens)/eventqrscreen.tsx new file mode 100644 index 0000000..5414925 --- /dev/null +++ b/app/(root)/(screens)/eventqrscreen.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { View, Text, TouchableOpacity, Image } from "react-native"; +import QRCode from "react-native-qrcode-svg"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { router, useLocalSearchParams } from "expo-router"; +import { useTranslation } from "react-i18next"; + +export default function EventQRScreen() { + const params = useLocalSearchParams<{ + code?: string; + packageName?: string; + qrImage?: string; + }>(); + const { t } = useTranslation(); + + const code = params.code || "AMB-123-2025"; + const packageName = params.packageName || "Package 2"; + const qrImage = params.qrImage as string | undefined; + console.log("qrimage", params.qrImage); + + const handleClose = () => { + router.back(); + }; + + const handlePrint = () => { + router.back(); + }; + + return ( + + + {/* QR image */} + + + {qrImage ? ( + + ) : ( + + )} + + + + + {packageName} + + + + + {/* Bottom buttons */} + + + + + + + + ); +} diff --git a/app/(root)/(screens)/events.tsx b/app/(root)/(screens)/events.tsx new file mode 100644 index 0000000..0893800 --- /dev/null +++ b/app/(root)/(screens)/events.tsx @@ -0,0 +1,388 @@ +import React, { useMemo, useState } from "react"; +import { + View, + Text, + Image, + ScrollView, + TouchableOpacity, + ActivityIndicator, +} from "react-native"; +import { ChevronRight } from "lucide-react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Icons } from "~/assets/icons"; +import TopBar from "~/components/ui/topBar"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useTranslation } from "react-i18next"; +import { useEvents } from "~/lib/hooks/useEvents"; +import Skeleton from "~/components/ui/skeleton"; + +export default function EventsScreen() { + const { t } = useTranslation(); + const { + data: events, + loading, + error, + refetch, + } = useEvents({ + status: "ACTIVE", + limit: 50, + }); + + const [searchQuery, setSearchQuery] = useState(""); + + const [filterVisible, setFilterVisible] = useState(false); + const [filterName, setFilterName] = useState(""); + const [filterLocation, setFilterLocation] = useState(""); + const [dateFilter, setDateFilter] = useState<"all" | "today" | "this_week">( + "all" + ); + + const normalizedQuery = searchQuery.trim().toLowerCase(); + + const normalizedFilterName = filterName.trim().toLowerCase(); + const normalizedFilterLocation = filterLocation.trim().toLowerCase(); + + const filteredEvents = useMemo(() => { + if (!events) return []; + + return events.filter((event) => { + const name = (event as any).name ?? ""; + const venue = (event as any).venue ?? ""; + const haystack = `${name} ${venue}`.toLowerCase(); + + if (normalizedQuery && !haystack.includes(normalizedQuery)) { + return false; + } + + if ( + normalizedFilterName && + !name.toLowerCase().includes(normalizedFilterName) + ) { + return false; + } + + if ( + normalizedFilterLocation && + !venue.toLowerCase().includes(normalizedFilterLocation) + ) { + return false; + } + + if (dateFilter !== "all" && (event as any).startDate) { + const start = new Date((event as any).startDate); + const now = new Date(); + + const startDay = new Date( + start.getFullYear(), + start.getMonth(), + start.getDate() + ); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + + if (dateFilter === "today") { + if (startDay.getTime() !== today.getTime()) return false; + } else if (dateFilter === "this_week") { + const endOfWeek = new Date(today); + endOfWeek.setDate(today.getDate() + 7); + if (startDay < today || startDay > endOfWeek) return false; + } + } + + return true; + }); + }, [ + events, + normalizedQuery, + normalizedFilterName, + normalizedFilterLocation, + dateFilter, + ]); + + return ( + + + + + + + + {t("events.title")} + + + {t("events.subtitle")} + + + + + + + + + + + + + + {t("events.featuredTitle")} + + + {loading && ( + + {Array.from({ length: 3 }).map((_, index) => ( + + + + + + + + + + + + + + ))} + + )} + + {!loading && error && ( + + + Failed to load events + + + + )} + + {!loading && !error && events && events.length === 0 && ( + + + No events found. + + + )} + + {!loading && + !error && + events && + events.length > 0 && + filteredEvents.length === 0 && ( + + + No events match your search. + + + )} + + {!loading && + !error && + filteredEvents && + filteredEvents.length > 0 && ( + + {filteredEvents.map((event) => { + const heroImage = + event.images && event.images.length > 0 + ? event.images[0] + : "https://images.pexels.com/photos/1190297/pexels-photo-1190297.jpeg?auto=compress&cs=tinysrgb&w=800"; + const startDate = new Date(event.startDate); + const formattedDate = startDate.toLocaleDateString(); + + return ( + + router.push({ + pathname: ROUTES.EVENT_DETAIL, + params: { id: event.id }, + }) + } + > + + + + + + + {event.name} + + + {event.venue} + + + + + + {t("events.ticketCountPrefix")} + + {event.organizer?.name || ""} - + {formattedDate} + + + + + + ); + })} + + )} + + + + setFilterVisible(false)} + maxHeightRatio={0.7} + > + + Filter events + + + Filter by date, name and location + + + + Date + + + {[ + { key: "all", label: "All dates" }, + { key: "today", label: "Today" }, + { key: "this_week", label: "This week" }, + ].map((option) => ( + + setDateFilter(option.key as "all" | "today" | "this_week") + } + className={`px-3 py-1 rounded-full mr-2 border ${ + dateFilter === option.key + ? "bg-[#0F7B4A] border-[#0F7B4A]" + : "bg-white border-gray-300" + }`} + > + + {option.label} + + + ))} + + + + Event name + + + + + + + Location + + + + + + + { + setFilterName(""); + setFilterLocation(""); + setDateFilter("all"); + }} + > + Clear + + + + + + ); +} diff --git a/app/(root)/(screens)/helpsupport.tsx b/app/(root)/(screens)/helpsupport.tsx new file mode 100644 index 0000000..aed9589 --- /dev/null +++ b/app/(root)/(screens)/helpsupport.tsx @@ -0,0 +1,181 @@ +import React, { useState } from "react"; +import { ScrollView, View, TouchableOpacity, Linking } from "react-native"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import BackButton from "~/components/ui/backButton"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { ChevronDown, ChevronUp, Mail, Phone } from "lucide-react-native"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; + +interface FAQItem { + question: string; + answer: string; +} + +const faqData: FAQItem[] = [ + { + question: "How do I add money to my wallet?", + answer: + "You can add money to your wallet by linking a debit/credit card or bank account. Go to Home > Add Cash, select your payment method, enter the amount, and confirm the transaction.", + }, + { + question: "How do I send money to someone?", + answer: + "Tap on 'Request' from the home screen, select a recipient from your contacts or enter their phone number/email, enter the amount, add an optional note, and confirm the transfer.", + }, + { + question: "What are the transaction fees?", + answer: + "Standard transfers to other Amba users are free. Cash out to bank accounts may incur a small fee depending on the amount and processing time. Check the transaction details before confirming.", + }, + { + question: "How long do transfers take?", + answer: + "Transfers to other Amba users are instant. Bank transfers typically take 1-3 business days depending on your bank and the transfer method selected.", + }, + { + question: "Is my money safe?", + answer: + "Yes, we use bank-level encryption and security measures to protect your funds and personal information. Your money is held in secure, FDIC-insured accounts.", + }, + { + question: "How do I cash out to my bank?", + answer: + "Go to Home > Cash Out, select your linked bank account, enter the amount you want to withdraw, and confirm. The money will be transferred to your bank within 1-3 business days.", + }, + { + question: "Can I cancel a transaction?", + answer: + "Once a transaction is completed, it cannot be cancelled. However, you can request a refund from the recipient. For disputed transactions, please contact our support team.", + }, + { + question: "How do I update my profile information?", + answer: + "Go to Profile > Edit Profile to update your name, email, phone number, and address. Some fields may be restricted based on how you signed up.", + }, +]; + +export default function HelpSupport() { + const [expandedIndex, setExpandedIndex] = useState(null); + + const toggleAccordion = (index: number) => { + setExpandedIndex(expandedIndex === index ? null : index); + }; + + const handleContactEmail = () => { + Linking.openURL("mailto:support@ambapay.com"); + }; + + const handleContactPhone = () => { + Linking.openURL("tel:+1234567890"); + }; + + const handleViewTerms = () => { + router.push(ROUTES.TERMS); + }; + + return ( + + + + + + + Help & Support + + + Find answers to common questions + + + {/* FAQ Section */} + + {faqData.map((faq, index) => ( + + toggleAccordion(index)} + className="p-4 flex-row items-center justify-between" + activeOpacity={0.7} + > + + {faq.question} + + {expandedIndex === index ? ( + + ) : ( + + )} + + + {expandedIndex === index && ( + + + + {faq.answer} + + + )} + + ))} + + + {/* Contact Section */} + + + Still need help? + + + Our support team is here to assist you + + + + {/* Email Support */} + + + + + + + Email Support + + + support@ambapay.com + + + + + {/* Phone Support */} + + + + + + + Phone Support + + + +1 (234) 567-890 + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/history.tsx b/app/(root)/(screens)/history.tsx new file mode 100644 index 0000000..b2866f6 --- /dev/null +++ b/app/(root)/(screens)/history.tsx @@ -0,0 +1,569 @@ +import React from "react"; +import { + View, + Text, + FlatList, + Image, + ScrollView, + TouchableOpacity, +} from "react-native"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import TransactionCard from "~/components/ui/transactionCard"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useTransactions } from "~/lib/hooks/useTransactions"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Icons } from "~/assets/icons"; +import { Input } from "~/components/ui/input"; +import Skeleton from "~/components/ui/skeleton"; +import BottomSheet from "~/components/ui/bottomSheet"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import BackButton from "~/components/ui/backButton"; + +type TransactionTypeFilter = "all" | "incoming" | "outgoing"; + +interface DateRange { + startDate: Date | null; + endDate: Date | null; +} + +const daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]; + +const stripTime = (date: Date) => { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; +}; + +const isSameDay = (a: Date | null, b: Date | null) => { + if (!a || !b) return false; + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); +}; + +const SimpleCalendarRange: React.FC<{ + value: DateRange; + onChange: (range: DateRange) => void; +}> = ({ value, onChange }) => { + const initialBase = value.startDate || new Date(); + const [currentMonth, setCurrentMonth] = React.useState( + () => new Date(initialBase.getFullYear(), initialBase.getMonth(), 1) + ); + + const monthLabel = currentMonth.toLocaleString("default", { + month: "short", + year: "numeric", + }); + + const days: (Date | null)[] = React.useMemo(() => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const firstDayOfMonth = new Date(year, month, 1); + const firstWeekday = firstDayOfMonth.getDay(); + const numDays = new Date(year, month + 1, 0).getDate(); + + const arr: (Date | null)[] = []; + for (let i = 0; i < firstWeekday; i++) { + arr.push(null); + } + for (let d = 1; d <= numDays; d++) { + arr.push(new Date(year, month, d)); + } + return arr; + }, [currentMonth]); + + const handleSelectDay = (day: Date) => { + const { startDate, endDate } = value; + + if (!startDate || (startDate && endDate)) { + onChange({ startDate: stripTime(day), endDate: null }); + return; + } + + const start = stripTime(startDate); + const selected = stripTime(day); + + if (selected < start) { + onChange({ startDate: selected, endDate: start }); + } else { + onChange({ startDate: start, endDate: selected }); + } + }; + + const inRange = (day: Date) => { + const { startDate, endDate } = value; + if (!startDate || !endDate) return false; + const d = stripTime(day); + const s = stripTime(startDate); + const e = stripTime(endDate); + return d > s && d < e; + }; + + const goMonth = (offset: number) => { + setCurrentMonth( + (prev) => new Date(prev.getFullYear(), prev.getMonth() + offset, 1) + ); + }; + + return ( + + + goMonth(-1)} + className="w-10 h-10 rounded-full bg-white items-center justify-center" + > + {"<"} + + + {monthLabel} + + goMonth(1)} + className="w-10 h-10 rounded-full bg-white items-center justify-center" + > + {">"} + + + + + {daysOfWeek.map((d, idx) => ( + + {d} + + ))} + + + + {days.map((day, index) => { + if (!day) { + return ( + + ); + } + + const selectedStart = isSameDay(day, value.startDate); + const selectedEnd = isSameDay(day, value.endDate); + const between = inRange(day); + + const isSelected = selectedStart || selectedEnd; + const bgColor = isSelected + ? "#0F7B4A" + : between + ? "rgba(15,123,74,0.08)" + : "transparent"; + const textColor = isSelected ? "#ffffff" : "#111827"; + + return ( + handleSelectDay(day)} + style={{ width: `${100 / 7}%`, aspectRatio: 1 }} + className="items-center justify-center" + > + + + {day.getDate()} + + + + ); + })} + + + ); +}; + +export default function History() { + const { user } = useAuthWithProfile(); + const { + transactions, + loading: transactionsLoading, + error: transactionsError, + } = useTransactions(user?.uid); + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = React.useState(""); + const [filterVisible, setFilterVisible] = React.useState(false); + const [dateRange, setDateRange] = React.useState({ + startDate: null, + endDate: null, + }); + const [typeFilter, setTypeFilter] = + React.useState("all"); + + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + React.useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + React.useEffect(() => { + if (transactionsError) { + showToast( + t("history.toastErrorTitle"), + transactionsError || t("history.errorTitle"), + "error" + ); + } + }, [transactionsError, t]); + + const filteredTransactions = React.useMemo(() => { + let data = transactions || []; + + const query = searchQuery.trim().toLowerCase(); + if (query) { + data = data.filter((t) => { + const parts: string[] = []; + if ((t as any).recipientName) + parts.push(String((t as any).recipientName)); + if ((t as any).senderName) parts.push(String((t as any).senderName)); + if ((t as any).note) parts.push(String((t as any).note)); + parts.push(String(t.type)); + const haystack = parts.join(" ").toLowerCase(); + return haystack.includes(query); + }); + } + + if (dateRange.startDate || dateRange.endDate) { + const start = dateRange.startDate ? stripTime(dateRange.startDate) : null; + const end = dateRange.endDate ? stripTime(dateRange.endDate) : null; + + data = data.filter((t) => { + const created = + t.createdAt instanceof Date + ? stripTime(t.createdAt) + : stripTime(new Date(t.createdAt)); + + if (start && created < start) return false; + if (end && created > end) return false; + return true; + }); + } + + if (typeFilter !== "all") { + data = data.filter((t) => { + const txType = t.type; + const isOutgoing = txType === "send" || txType === "cash_out"; + const isIncoming = txType === "receive" || txType === "add_cash"; + + if (typeFilter === "outgoing") return isOutgoing; + if (typeFilter === "incoming") return isIncoming; + return true; + }); + } + + return data; + }, [transactions, searchQuery, dateRange, typeFilter]); + + return ( + + + {transactionsLoading ? ( + + + + + + + + {t("history.title")} + + + {t("history.subtitle")} + + + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + + + ) : transactionsError ? ( + + + + + + + + {t("history.title")} + + + {t("history.subtitle")} + + + + + {t("history.errorTitle")} + + + {transactionsError} + + + + + ) : ( + <> + item.id} + contentContainerStyle={{ paddingBottom: 30 }} + ListHeaderComponent={ + <> + + + + + + + {t("history.title")} + + + {t("history.subtitle")} + + + + setFilterVisible(true)} + className="p-1" + > + + + } + /> + + + + } + ListEmptyComponent={ + + + {t("history.emptyTitle")} + + + {t("history.emptySubtitle")} + + + } + renderItem={({ item: transaction }) => ( + + { + router.push({ + pathname: ROUTES.TRANSACTION_DETAIL, + params: { + transactionId: transaction.id, + amount: transaction.amount.toString(), + type: transaction.type, + recipientName: + transaction.type === "send" + ? transaction.recipientName + : transaction.type === "receive" + ? transaction.senderName + : transaction.type === "add_cash" + ? "Card" + : "Bank", + date: transaction.createdAt.toISOString(), + status: transaction.status, + //@ts-ignore + note: transaction?.note || "", + fromHistory: "true", + }, + }); + }} + /> + + )} + /> + + )} + + + setFilterVisible(false)} + maxHeightRatio={0.9} + > + + {t("history.filterTitle")} + + + {t("history.filterSubtitle")} + + + {/* Date range */} + + {t("history.dateRangeLabel")} + + + + + {t("history.fromLabel")} + + + + {dateRange.startDate + ? dateRange.startDate.toLocaleDateString() + : t("history.selectStart")} + + + + + + {t("history.toLabel")} + + + + {dateRange.endDate + ? dateRange.endDate.toLocaleDateString() + : t("history.selectEnd")} + + + + + + + + + setDateRange({ startDate: null, endDate: null })} + > + + {t("history.clearDates")} + + + + + {/* Type filter */} + + {t("history.typeLabel")} + + + {( + [ + { label: t("history.typeAll"), value: "all" }, + { label: t("history.typeIncoming"), value: "incoming" }, + { label: t("history.typeOutgoing"), value: "outgoing" }, + ] as { label: string; value: TransactionTypeFilter }[] + ).map((option) => { + const active = typeFilter === option.value; + return ( + setTypeFilter(option.value)} + className={`px-4 py-2 rounded-[4px] mr-2 border ${ + active + ? "bg-primary border-primary" + : "bg-white border-gray-200" + }`} + > + + {option.label} + + + ); + })} + + + + setFilterVisible(false)} + className="h-11 rounded-3xl bg-[#FFB668] items-center justify-center" + > + + {t("history.applyFilters")} + + + + + + + ); +} diff --git a/app/(root)/(screens)/kyc.tsx b/app/(root)/(screens)/kyc.tsx new file mode 100644 index 0000000..285e16a --- /dev/null +++ b/app/(root)/(screens)/kyc.tsx @@ -0,0 +1,462 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + Alert, + ActivityIndicator, +} from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { UploadCloud } from "lucide-react-native"; +import Dropdown, { type DropdownOption } from "~/components/ui/dropdown"; +import ModalToast from "~/components/ui/toast"; +import BackButton from "~/components/ui/backButton"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import AuthService from "~/lib/services/authServices"; +import { uploadKycDocument } from "~/lib/services/kycDocumentService"; +import * as ImagePicker from "expo-image-picker"; + +export default function KycScreen() { + const { user, refreshProfile } = useAuthWithProfile(); + const [activeTab, setActiveTab] = useState<"personal" | "business">( + "personal" + ); + const [fanNumber, setFanNumber] = useState(""); + const [tin, setTin] = useState(""); + const [businessType, setBusinessType] = useState(""); + const [nationalIdUri, setNationalIdUri] = useState(null); + const [nationalIdLabel, setNationalIdLabel] = useState(""); + const [businessLicenseUri, setBusinessLicenseUri] = useState( + null + ); + const [businessLicenseLabel, setBusinessLicenseLabel] = useState(""); + const [pickingNationalId, setPickingNationalId] = useState(false); + const [pickingBusinessLicense, setPickingBusinessLicense] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const businessTypeOptions: DropdownOption[] = [ + { label: "Retail & E-commerce", value: "Retail & E-commerce" }, + { label: "Food & Beverage", value: "Food & Beverage" }, + { + label: "Transportation & Logistics", + value: "Transportation & Logistics", + }, + { label: "Finance & Fintech", value: "Finance & Fintech" }, + { + label: "Healthcare & Pharmacy", + value: "Healthcare & Pharmacy", + }, + { label: "Education & Training", value: "Education & Training" }, + { + label: "Construction & Real Estate", + value: "Construction & Real Estate", + }, + { + label: "Technology & Software", + value: "Technology & Software", + }, + { label: "Entertainment & Events", value: "Entertainment & Events" }, + { label: "Agriculture & Farming", value: "Agriculture & Farming" }, + { + label: "Freelancer / Sole Proprietor", + value: "Freelancer / Sole Proprietor", + }, + { label: "Other", value: "Other" }, + ]; + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handlePrimary = async () => { + if (!user?.uid) { + showToast("Error", "You must be logged in to save KYC.", "error"); + return; + } + + try { + setSubmitting(true); + + const updateData: any = {}; + + if (activeTab === "personal") { + if (!fanNumber.trim()) { + showToast("Error", "Please enter your FAN Number.", "error"); + setSubmitting(false); + return; + } + + updateData.fanNumber = fanNumber.trim(); + + if (nationalIdUri) { + try { + const url = await uploadKycDocument( + user.uid, + nationalIdUri, + "personal-id" + ); + updateData.nationalIdUrl = url; + } catch (e) { + console.error("[KYC] Failed to upload national ID", e); + showToast("Error", "Failed to upload National ID.", "error"); + setSubmitting(false); + return; + } + } + } else { + if (!tin.trim()) { + showToast("Error", "Please enter your EIN / TIN.", "error"); + setSubmitting(false); + return; + } + + if (!businessType.trim()) { + showToast("Error", "Please enter your business type.", "error"); + setSubmitting(false); + return; + } + + updateData.tin = tin.trim(); + updateData.businessType = businessType.trim(); + + if (businessLicenseUri) { + try { + const url = await uploadKycDocument( + user.uid, + businessLicenseUri, + "business-license" + ); + updateData.businessLicenseUrl = url; + } catch (e) { + console.error("[KYC] Failed to upload business license", e); + showToast("Error", "Failed to upload Business License.", "error"); + setSubmitting(false); + return; + } + } + } + + const result = await AuthService.updateUserProfile(user.uid, updateData); + if (!result.success) { + console.error("[KYC] updateUserProfile failed", result.error); + showToast("Error", result.error || "Failed to save KYC.", "error"); + return; + } + + try { + await refreshProfile(); + } catch (e) { + console.warn("[KYC] Failed to refresh profile after KYC save", e); + } + + // Successful save: clear all KYC fields + setFanNumber(""); + setTin(""); + setBusinessType(""); + setNationalIdUri(null); + setNationalIdLabel(""); + setBusinessLicenseUri(null); + setBusinessLicenseLabel(""); + + showToast("Success", "KYC information saved.", "success"); + } catch (e) { + console.error("[KYC] Unexpected error while saving KYC", e); + showToast("Error", "Unexpected error while saving KYC.", "error"); + } finally { + setSubmitting(false); + } + }; + + const handlePickNationalId = async () => { + if (pickingNationalId) return; + + try { + setPickingNationalId(true); + + const permissionResult = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + Alert.alert( + "Permission Required", + "Please allow access to your photo library to upload your ID." + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + const uri = result.assets[0].uri; + setNationalIdUri(uri); + const name = uri.split("/").pop() || "Selected file"; + setNationalIdLabel(name); + } + } catch (e) { + console.error("[KYC] Error while selecting National ID", e); + showToast("Error", "Failed to select National ID.", "error"); + } finally { + setPickingNationalId(false); + } + }; + + const handlePickBusinessLicense = async () => { + if (pickingBusinessLicense) return; + + try { + setPickingBusinessLicense(true); + + const permissionResult = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + Alert.alert( + "Permission Required", + "Please allow access to your photo library to upload your Business License." + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + const uri = result.assets[0].uri; + setBusinessLicenseUri(uri); + const name = uri.split("/").pop() || "Selected file"; + setBusinessLicenseLabel(name); + } + } catch (e) { + console.error("[KYC] Error while selecting Business License", e); + showToast("Error", "Failed to select Business License.", "error"); + } finally { + setPickingBusinessLicense(false); + } + }; + + return ( + + + + + + + Information + + + KYC + + + Fill out the information below to add more limits to your account + + {/* tab */} + + setActiveTab("personal")} + className={`flex-1 items-center py-2 rounded-full ${ + activeTab === "personal" ? "bg-white" : "bg-transparent" + }`} + activeOpacity={0.8} + > + + Personal + + + + setActiveTab("business")} + className={`flex-1 items-center py-2 rounded-full ${ + activeTab === "business" ? "bg-white" : "bg-transparent" + }`} + activeOpacity={0.8} + > + + Business + + + + + {activeTab === "personal" ? ( + + + FAN Number + + + + + National ID Upload + + + + ) : ( + + ) + } + /> + + + ) : ( + + + EIN / TIN + + + + + Type of Business + + + setBusinessType(value)} + placeholder="Select type of business" + /> + + + + Business License + + + + ) : ( + + ) + } + /> + + + )} + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/moneydonated.tsx b/app/(root)/(screens)/moneydonated.tsx new file mode 100644 index 0000000..03f356e --- /dev/null +++ b/app/(root)/(screens)/moneydonated.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { View, Text, ScrollView } from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { SuccessIcon } from "~/components/ui/icons"; +import { useTranslation } from "react-i18next"; + +export default function MoneyDonated() { + const params = useLocalSearchParams<{ fullName?: string }>(); + const { t } = useTranslation(); + + const handleDonateAgain = () => { + router.replace(ROUTES.HOME); + router.push(ROUTES.SEND_OR_REQUEST_MONEY); + }; + + const fullName = (params.fullName || "").toString().trim(); + + return ( + + + + + + + + {t("moneydonated.title")} + + + + + {t("moneydonated.description")} + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/moneyrequested.tsx b/app/(root)/(screens)/moneyrequested.tsx new file mode 100644 index 0000000..b897eb7 --- /dev/null +++ b/app/(root)/(screens)/moneyrequested.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { NotificationIcon } from "~/components/ui/icons"; +import { useTranslation } from "react-i18next"; + +export default function MoneyRequested() { + const params = useLocalSearchParams<{ + fullName?: string; + requesteePhoneNumber?: string; + }>(); + const { t } = useTranslation(); + + const handleRequestAgain = () => { + router.replace(ROUTES.HOME); + router.push(ROUTES.SEND_OR_REQUEST_MONEY); + }; + + const fullName = (params.fullName || "").toString().trim(); + const description = fullName + ? t("moneyrequested.descriptionWithName", { fullName }) + : t("moneyrequested.description"); + + return ( + + {/* Main content */} + + + + + + {t("moneyrequested.title")} + + + + + {description} + + + + + + {/* Bottom actions */} + + + + + + + ); +} diff --git a/app/(root)/(screens)/mytickets.tsx b/app/(root)/(screens)/mytickets.tsx new file mode 100644 index 0000000..5ac7d4a --- /dev/null +++ b/app/(root)/(screens)/mytickets.tsx @@ -0,0 +1,323 @@ +import React, { useMemo, useState } from "react"; +import { + View, + Text, + Image, + ScrollView, + TouchableOpacity, + ActivityIndicator, +} from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Icons } from "~/assets/icons"; +import { ChevronRight } from "lucide-react-native"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useTranslation } from "react-i18next"; +import BackButton from "~/components/ui/backButton"; +import { useTickets } from "~/lib/hooks/useTickets"; +import BottomSheet from "~/components/ui/bottomSheet"; + +export default function MyTicketsScreen() { + const { t } = useTranslation(); + const { + data: tickets, + loading, + error, + refetch, + } = useTickets({ status: "ACTIVE", limit: 50, immediate: true }); + + const [searchQuery, setSearchQuery] = useState(""); + const [filterVisible, setFilterVisible] = useState(false); + const [dateFilter, setDateFilter] = useState<"all" | "today" | "this_week">( + "all" + ); + + const normalizedQuery = searchQuery.trim().toLowerCase(); + + const filteredTickets = useMemo(() => { + if (!tickets) return []; + + return tickets.filter((ticket) => { + const anyTicket = ticket as any; + const eventName = (anyTicket.event?.name ?? "").toLowerCase(); + + if (normalizedQuery && !eventName.includes(normalizedQuery)) { + return false; + } + + if (dateFilter !== "all") { + const rawDate = anyTicket.event?.startDate || anyTicket.createdAt; + if (!rawDate) { + return true; + } + + const date = new Date(rawDate); + const now = new Date(); + + const ticketDay = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + ); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + + if (dateFilter === "today") { + if (ticketDay.getTime() !== today.getTime()) return false; + } else if (dateFilter === "this_week") { + const endOfWeek = new Date(today); + endOfWeek.setDate(today.getDate() + 7); + if (ticketDay < today || ticketDay > endOfWeek) return false; + } + } + + return true; + }); + }, [tickets, normalizedQuery, dateFilter]); + return ( + + + + + + + + + + {t("mytickets.title")} + + + {t("mytickets.subtitle")} + + + + + + + + + + + + + {t("mytickets.ticketsTitle")} + + + + {loading && ( + + + + {t("mytickets.loading")} + + + )} + + {!loading && error && ( + + + {t("mytickets.error")} + + + + )} + + {!loading && !error && tickets && tickets.length === 0 && ( + + + + No tickets found + + + You don't have any tickets yet. + + + )} + + {!loading && + !error && + tickets && + filteredTickets.length === 0 && ( + + + No tickets match your search. + + + )} + + {!loading && + !error && + filteredTickets && + filteredTickets.length > 0 && ( + + {filteredTickets.map((ticket) => { + const anyTicket = ticket as any; + const eventName = anyTicket.event?.name || "Ticket"; + const ticketNo = anyTicket.ticketNo || anyTicket.id; + const qr = anyTicket.qr || ticketNo; + const qrImage = anyTicket.qrImage as string | undefined; + + const rawDate = + anyTicket.event?.startDate || anyTicket.createdAt; + const formattedDate = rawDate + ? new Date(rawDate).toLocaleDateString() + : ""; + + return ( + + router.push({ + pathname: ROUTES.EVENT_QR, + params: { + code: qr, + packageName: eventName, + ...(qrImage ? { qrImage } : {}), + }, + }) + } + className="flex-row items-center justify-between bg-[#F3FFF7] rounded-[6px] px-4 py-3" + > + + + + + + + {eventName} + + + {formattedDate} + + + + + + + ); + })} + + )} + + + + + setFilterVisible(false)} + maxHeightRatio={0.5} + > + + Filter tickets + + + Filter by date of event or purchase + + + + Date + + + {[ + { key: "all", label: "All dates" }, + { key: "today", label: "Today" }, + { key: "this_week", label: "This week" }, + ].map((option) => ( + + setDateFilter(option.key as "all" | "today" | "this_week") + } + className={`px-3 py-1 rounded-full mr-2 border ${ + dateFilter === option.key + ? "bg-[#0F7B4A] border-[#0F7B4A]" + : "bg-white border-gray-300" + }`} + > + + {option.label} + + + ))} + + + + { + setSearchQuery(""); + setDateFilter("all"); + }} + > + Clear + + + + + + ); +} diff --git a/app/(root)/(screens)/notification.tsx b/app/(root)/(screens)/notification.tsx new file mode 100644 index 0000000..cbb2f1f --- /dev/null +++ b/app/(root)/(screens)/notification.tsx @@ -0,0 +1,665 @@ +import React from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, +} from "react-native"; +import { + User, + ChevronRight, + ArrowUpRight, + ArrowDownLeft, + UploadCloud, + DollarSign, + Ticket, +} from "lucide-react-native"; +import BackButton from "~/components/ui/backButton"; +import { useNotifications } from "~/lib/hooks/useNotifications"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { NotificationService } from "~/lib/services/notificationService"; +import { RequestService } from "~/lib/services/requestService"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import PermissionAlertModal from "~/components/ui/permissionAlertModal"; + +// Notification Card Component +const NotificationCard = ({ + notification, + onPress, + onMoneyRequestAction, + onMoneyRequestPrompt, +}: { + notification: any; + onPress?: () => void; + onMoneyRequestAction?: ( + requestId: string, + action: "accept" | "decline" + ) => void; + onMoneyRequestPrompt?: (notification: any) => void; +}) => { + console.log("Notification:", notification); + const getNotificationIcon = () => { + const rawType = (notification.type || "").toString().toLowerCase(); + const title = (notification.title || "").toString().toLowerCase(); + const message = (notification.message || "").toString().toLowerCase(); + const transactionSubtype = ( + notification.transactionType || + notification.transaction_type || + notification.subtype || + "" + ) + .toString() + .toLowerCase(); + + // Event notifications (tickets, event reminders, etc.) + if ( + rawType === "event" || + rawType === "event_ticket_purchased" || + rawType === "event_reminder" || + notification.category === "event" + ) { + return ; + } + + // Money request notifications + if ( + rawType === "request_received" || + title.includes("request") || + message.includes("request") + ) { + // Arrow going 45° down for money request + return ; + } + + // Cash out (money icon, red theme handled by color helper) + if ( + rawType === "cash_out" || + title.includes("cash out") || + message.includes("cash out") + ) { + return ; + } + + // Money received (same money icon but green theme) + if ( + rawType === "money_received" || + rawType === "receive" || + title.includes("received") || + message.includes("received") + ) { + return ; + } + + // Money sent (arrow 45° up) + if ( + rawType === "money_sent" || + rawType === "send" || + title.includes("sent") || + message.includes("sent") + ) { + return ; + } + + // Fallback for generic transaction-related notifications + if ( + rawType === "transaction_completed" || + rawType === "transaction" || + title.includes("transaction") || + message.includes("transaction") + ) { + return ; + } + + // Everything else fall back to user icon + return ; + }; + + const getNotificationColor = () => { + const rawType = (notification.type || "").toString().toLowerCase(); + const title = (notification.title || "").toString().toLowerCase(); + const message = (notification.message || "").toString().toLowerCase(); + const transactionSubtype = ( + notification.transactionType || + notification.transaction_type || + notification.subtype || + "" + ) + .toString() + .toLowerCase(); + + // --- Explicit transaction subtypes (preferred) --- + if (transactionSubtype === "cash_out") { + // Cash out → red + return "bg-red-50"; + } + + if (transactionSubtype === "receive") { + // Money received → green + return "bg-green-50"; + } + + if (transactionSubtype === "send") { + // Money sent → blue + return "bg-blue-50"; + } + + if (transactionSubtype === "add_cash") { + // Add cash → blue + return "bg-blue-50"; + } + + // Event notifications + if ( + rawType === "event" || + rawType === "event_ticket_purchased" || + rawType === "event_reminder" || + notification.category === "event" + ) { + return "bg-purple-50"; + } + + // Money request + if ( + rawType === "request_received" || + title.includes("request") || + message.includes("request") + ) { + return "bg-yellow-50"; + } + + // Cash out → red + if ( + rawType === "cash_out" || + title.includes("cash out") || + message.includes("cash out") + ) { + return "bg-red-50"; + } + + // Money received → green + if ( + rawType === "money_received" || + rawType === "receive" || + title.includes("received") || + message.includes("received") + ) { + return "bg-green-50"; + } + + // Money sent → blue + if ( + rawType === "money_sent" || + rawType === "send" || + title.includes("sent") || + message.includes("sent") + ) { + return "bg-blue-50"; + } + + // Generic transaction + if ( + rawType === "transaction_completed" || + rawType === "transaction" || + title.includes("transaction") || + message.includes("transaction") + ) { + return "bg-blue-50"; + } + + // Default + return "bg-gray-50"; + }; + + const getIconBackgroundColor = () => { + const rawType = (notification.type || "").toString().toLowerCase(); + const title = (notification.title || "").toString().toLowerCase(); + const message = (notification.message || "").toString().toLowerCase(); + const transactionSubtype = ( + notification.transactionType || + notification.transaction_type || + notification.subtype || + "" + ) + .toString() + .toLowerCase(); + + // --- Explicit transaction subtypes (preferred) --- + if (transactionSubtype === "cash_out") { + // Cash out → red + return "bg-red-500"; + } + + if (transactionSubtype === "receive") { + // Money received → green + return "bg-green-500"; + } + + if (transactionSubtype === "send") { + // Money sent → blue + return "bg-blue-500"; + } + + if (transactionSubtype === "add_cash") { + // Add cash → blue + return "bg-blue-500"; + } + + // Event notifications + if ( + rawType === "event" || + rawType === "event_ticket_purchased" || + rawType === "event_reminder" || + notification.category === "event" + ) { + return "bg-purple-500"; + } + + // Money request → yellow + if ( + rawType === "request_received" || + title.includes("request") || + message.includes("request") + ) { + return "bg-yellow-500"; + } + + // Cash out → red + if ( + rawType === "cash_out" || + title.includes("cash out") || + message.includes("cash out") + ) { + return "bg-red-500"; + } + + // Money received → green + if ( + rawType === "money_received" || + rawType === "receive" || + title.includes("received") || + message.includes("received") + ) { + return "bg-green-500"; + } + + // Money sent → blue + if ( + rawType === "money_sent" || + rawType === "send" || + title.includes("sent") || + message.includes("sent") + ) { + return "bg-blue-500"; + } + + // Generic transaction → blue + if ( + rawType === "transaction_completed" || + rawType === "transaction" || + title.includes("transaction") || + message.includes("transaction") + ) { + return "bg-blue-500"; + } + + // Default + return "bg-orange-200"; + }; + + const handleMoneyRequestPress = () => { + console.log("NOTIFICATION", notification); + //@ts-ignore + if (notification.type === "request_received" && notification.requestId) { + onMoneyRequestPrompt?.(notification); + } else { + onPress?.(); + } + }; + + return ( + + + {/* Icon Avatar */} + + {getNotificationIcon()} + + + {/* Notification Content */} + + + {notification.title} + + + {notification.message} + + + + {/* Chevron for money requests */} + {notification.type === "request_received" && ( + + )} + + + ); +}; + +export default function Notification() { + const { user } = useAuthWithProfile(); + const { t } = useTranslation(); + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const [requestModalVisible, setRequestModalVisible] = React.useState(false); + const [activeRequestNotification, setActiveRequestNotification] = + React.useState(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + const { + notifications, + loading, + error, + unreadCount, + markAsRead, + markAllAsRead, + } = useNotifications(user?.uid || null); + + const handleMarkAsRead = async () => { + await markAllAsRead(); + }; + + const handleNotificationPress = async (notification: any) => { + if (notification?.id) { + await markAsRead(notification.id); + } + + const transactionId = + notification?.transactionId || notification?.transaction_id; + + const transactionSubtype = + notification?.transactionType || + notification?.transaction_type || + notification?.subtype; + + if ( + transactionId && + (notification?.type === "transaction_completed" || + notification?.type === "money_received" || + notification?.type === "money_sent" || + notification?.category === "transaction") + ) { + router.push({ + pathname: ROUTES.TRANSACTION_DETAIL, + params: { + transactionId, + type: transactionSubtype || "", + amount: + notification?.amount !== undefined && notification?.amount !== null + ? String(notification.amount) + : "", + recipientName: + notification?.recipientName || notification?.counterpartyName || "", + date: + (notification?.createdAt && + typeof notification.createdAt === "string" && + notification.createdAt) || + (notification?.date && typeof notification.date === "string" + ? notification.date + : ""), + status: notification?.status || "", + note: notification?.note || "", + }, + }); + return; + } + + // For other notification types (e.g. money requests), we rely on their specific handlers + }; + + const handleMoneyRequestAction = async ( + requestId: string, + action: "accept" | "decline" + ) => { + try { + // Get the request details + const requestResult = await RequestService.getRequestById(requestId); + if (!requestResult.success || !requestResult.request) { + console.error("Failed to get request:", requestResult.error); + return; + } + + const request = requestResult.request; + + if (request.status !== "pending") { + console.error("Request is not pending"); + showToast( + t("notification.toastErrorTitle"), + t("notification.toastRequestNotPending"), + "error" + ); + return; + } + + // Get requestor details (the person who made the request) + const requestorResult = await RequestService.getUserByPhoneNumber( + request.requestorPhoneNumber + ); + if (!requestorResult.success || !requestorResult.user) { + console.error( + "Failed to get requestor details:", + requestorResult.error + ); + return; + } + + // Get requestee details (the person receiving the request - current user) + const requesteeResult = await RequestService.getUserByPhoneNumber( + request.requesteePhoneNumber + ); + if (!requesteeResult.success || !requesteeResult.user) { + console.error( + "Failed to get requestee details:", + requesteeResult.error + ); + return; + } + + const result = await NotificationService.acceptDeclineMoneyRequest( + requestId, + requesteeResult.user.uid, // requesteeUid (current user) + requestorResult.user.uid, // requestorUid (person who made the request) + requestorResult.user.displayName, // requestorName + requesteeResult.user.displayName, // requesteeName + request.amount, // actual amount from request + action + ); + + if (result.success) { + console.log(`Request ${action}ed successfully`); + + if (action === "accept") { + // Navigate to success screen only for accepted requests + router.replace({ + pathname: ROUTES.MONEY_DONATED, + params: { + message: `Congratulations! Transaction completed on your end.`, + amount: (request.amount / 100).toFixed(2), + recipientName: requestorResult.user!.displayName, + }, + }); + } else { + // For declined requests, show a simple confirmation toast + showToast( + t("notification.toastInfoTitle", "Request Declined"), + t( + "notification.toastRequestDeclined", + `You have declined the money request from ${ + requestorResult.user!.displayName + }.` + ), + "info" + ); + } + } else { + console.error(`Failed to ${action} request:`, result.error); + showToast( + t("notification.toastErrorTitle"), + t("notification.toastRequestActionFailed", { action }), + "error" + ); + } + } catch (error) { + console.error(`Error ${action}ing request:`, error); + showToast( + t("notification.toastErrorTitle"), + t("notification.toastRequestActionFailed", { action }), + "error" + ); + } + }; + + const handleMoneyRequestPrompt = (notification: any) => { + setActiveRequestNotification(notification); + setRequestModalVisible(true); + }; + + return ( + + + + + {t("notification.title")} + + + + + {/* Today Section */} + + + + + {t("notification.sectionToday")} + + + + + {loading ? ( + + + {t("notification.loading")} + + + ) : error ? ( + + + {t("notification.errorWithMessage", { error })} + + + ) : ( + item.id} + scrollEnabled={false} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + handleNotificationPress(item)} + onMoneyRequestAction={handleMoneyRequestAction} + onMoneyRequestPrompt={handleMoneyRequestPrompt} + /> + )} + /> + )} + + + {/* Empty state if no notifications */} + {!loading && !error && notifications.length === 0 && ( + + + + + + {t("notification.emptyTitle")} + + + {t("notification.emptySubtitle")} + + + )} + + + + { + if (activeRequestNotification?.requestId) { + handleMoneyRequestAction( + activeRequestNotification.requestId, + "accept" + ); + } + setRequestModalVisible(false); + setActiveRequestNotification(null); + }} + onSecondary={() => { + if (activeRequestNotification?.requestId) { + handleMoneyRequestAction( + activeRequestNotification.requestId, + "decline" + ); + } + setRequestModalVisible(false); + setActiveRequestNotification(null); + }} + /> + + ); +} diff --git a/app/(root)/(screens)/notificationOption.tsx b/app/(root)/(screens)/notificationOption.tsx new file mode 100644 index 0000000..9032dff --- /dev/null +++ b/app/(root)/(screens)/notificationOption.tsx @@ -0,0 +1,277 @@ +import React, { useState, useRef } from "react"; +import { View, Text, TouchableOpacity, ScrollView } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import BackButton from "~/components/ui/backButton"; +import { router, useLocalSearchParams } from "expo-router"; +import { SMSIcon, WhatsappIcon } from "~/components/ui/icons"; +import { RequestService } from "~/lib/services/requestService"; +import { useAuthStore } from "~/lib/stores"; +import { ROUTES } from "~/lib/routes"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import PermissionAlertModal from "~/components/ui/permissionAlertModal"; + +export default function NotificationOption() { + const params = useLocalSearchParams<{ + amount: string; + recipientName: string; + recipientPhoneNumber: string; + recipientType: string; + recipientId: string; + note: string; + requestorName: string; + requestorPhoneNumber: string; + requestorType: string; + type: string; + provider?: string; + }>(); + + const [selectedMethod, setSelectedMethod] = useState< + "SMS" | "WhatsApp" | null + >(null); + const { user } = useAuthStore(); + const isMoneyRequest = params.type === "request"; + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const [confirmVisible, setConfirmVisible] = useState(false); + const [confirmTitle, setConfirmTitle] = useState(""); + const [confirmMessage, setConfirmMessage] = useState(""); + const confirmActionRef = useRef<() => void>(() => {}); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleMoneyRequest = async (notificationMethod: "SMS" | "WhatsApp") => { + if (!user?.uid) { + showToast( + t("notificationOption.toastErrorTitle"), + t("notificationOption.toastAuthRequired"), + "error" + ); + return; + } + + if (!params.amount || !params.recipientName || !params.requestorName) { + showToast( + t("notificationOption.toastErrorTitle"), + t("notificationOption.toastMissingInfo"), + "error" + ); + return; + } + + const amountInCents = parseInt(params.amount); + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("notificationOption.toastErrorTitle"), + t("notificationOption.toastInvalidAmount"), + "error" + ); + return; + } + + try { + const result = await RequestService.createRequest(user.uid, { + requestorName: params.requestorName, + requesteeName: params.recipientName, + requestorPhoneNumber: params.requestorPhoneNumber || "", + requesteePhoneNumber: params.recipientPhoneNumber, + requestorType: params.requestorType as "saved" | "contact", + amount: amountInCents, + note: params.note || "", + status: "pending" as const, + notificationMethod, + }); + + if (result.success) { + // Navigate to success screen + router.replace({ + pathname: ROUTES.MONEY_REQUESTED, + params: { + fullName: params.recipientName, + requesteePhoneNumber: params.recipientPhoneNumber, + }, + }); + } else { + showToast( + t("notificationOption.toastErrorTitle"), + result.error || t("notificationOption.toastCreateFailed"), + "error" + ); + } + } catch (error) { + console.error("Error requesting money:", error); + showToast( + t("notificationOption.toastErrorTitle"), + t("notificationOption.toastRequestFailed"), + "error" + ); + } + }; + + const handleMethodSelection = (method: "SMS" | "WhatsApp") => { + setSelectedMethod(method); + }; + + const handleProceed = () => { + if (!selectedMethod) { + showToast( + t("notificationOption.toastErrorTitle"), + t("notificationOption.toastSelectMethod"), + "error" + ); + return; + } + + const description = `Your money request will be sent to the selected donor and will be notified via ${selectedMethod}.`; + const title = `${selectedMethod} Notification`; + + setConfirmTitle(title); + setConfirmMessage(description); + + if (isMoneyRequest) { + confirmActionRef.current = () => handleMoneyRequest(selectedMethod); + } else { + confirmActionRef.current = () => { + console.log(`${selectedMethod} notification selected`); + router.back(); + }; + } + + setConfirmVisible(true); + }; + + return ( + + {/* Header with close button */} + + + + + {/* Main content */} + + {/* Title */} + + + {t("notificationOption.title")} + + + + + {/* Notification Options Section */} + + + {t("notificationOption.sectionTitle")} + + + {t("notificationOption.sectionSubtitle")} + + + + {/* SMS Notification Option */} + handleMethodSelection("SMS")} + className={`rounded-lg p-4 mb-4 flex-row items-center ${ + selectedMethod === "SMS" + ? "bg-green-50 border-2 border-primary" + : "bg-green-50 border border-gray-200" + }`} + > + + + + + + {t("notificationOption.smsLabel")} + + + + + {/* WhatsApp Notification Option */} + handleMethodSelection("WhatsApp")} + className={`rounded-lg p-4 flex-row items-center ${ + selectedMethod === "WhatsApp" + ? "bg-green-50 border-2 border-primary" + : "bg-green-50 border border-gray-200" + }`} + > + + + + + + {t("notificationOption.whatsappLabel")} + + + + + + {/* Proceed Button - Only visible when a method is selected */} + {selectedMethod && ( + + + + {t("notificationOption.continueButton")} + + + + )} + + + + { + setConfirmVisible(false); + confirmActionRef.current && confirmActionRef.current(); + }} + onSecondary={() => { + setConfirmVisible(false); + }} + /> + + ); +} diff --git a/app/(root)/(screens)/points.tsx b/app/(root)/(screens)/points.tsx new file mode 100644 index 0000000..22238ea --- /dev/null +++ b/app/(root)/(screens)/points.tsx @@ -0,0 +1,237 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + ScrollView, + Image, + TouchableOpacity, + Share, +} from "react-native"; +import { router } from "expo-router"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Button } from "~/components/ui/button"; +import { Icons } from "~/assets/icons"; +import { ROUTES } from "~/lib/routes"; +import { useTranslation } from "react-i18next"; +import { getPointsState, type PointsState } from "~/lib/services/pointsService"; +import * as Clipboard from "expo-clipboard"; +import ModalToast from "~/components/ui/toast"; + +// Placeholder referral link +const REFERRAL_LINK = "ambapay.com/frars"; + +export default function PointsScreen() { + const { t } = useTranslation(); + const [pointsState, setPointsState] = useState(null); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const state = await getPointsState(); + if (!cancelled) { + setPointsState(state); + } + } catch (error) { + if (__DEV__) { + console.warn("[PointsScreen] Failed to load points state", error); + } + } + })(); + + return () => { + cancelled = true; + }; + }, []); + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleCopyLink = async () => { + try { + await Clipboard.setStringAsync(REFERRAL_LINK); + showToast("Copied", "Referral link copied to clipboard", "success"); + } catch (error) { + showToast("Error", "Failed to copy referral link", "error"); + } + }; + + const handleShare = async () => { + try { + await Share.share({ + message: `Use my Amba referral link: ${REFERRAL_LINK}`, + }); + } catch (error) { + // Optional: only show error toast if share actually fails (not cancelled) + showToast("Error", "Failed to open share options", "error"); + } + }; + + const handleActivityPress = () => { + router.push(ROUTES.POINTS_ACTIVITY); + }; + + const handleHowToEarnPress = () => { + // Navigate to how-to-earn details when available + }; + + const handleRedeem = (rewardId: string) => { + // Integrate redeem API later + }; + + return ( + + + + + + + {t("points.title")} + + + {/* Total Points Card */} + + + + Your Points + + + {pointsState?.total ?? 0} + + + + + + + + {t("points.referTitle")} + + + {t("points.earnSubtitle")} + + + + + + + {REFERRAL_LINK} + + + + {t("points.copyButton")} + + + + + + + + + + + + + {t("points.activityButton")} + + + + + + + {t("points.howToEarnButton")} + + + + + {/* Rewards section can be wired to real redemption in the future */} + + + + + + ); +} diff --git a/app/(root)/(screens)/pointsactivity.tsx b/app/(root)/(screens)/pointsactivity.tsx new file mode 100644 index 0000000..9aa8c95 --- /dev/null +++ b/app/(root)/(screens)/pointsactivity.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from "react"; +import { View, Text, ScrollView, Image } from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Button } from "~/components/ui/button"; +import { Icons } from "~/assets/icons"; +import { useTranslation } from "react-i18next"; +import { + getPointsState, + type PointsActivityEntry, +} from "~/lib/services/pointsService"; + +export default function PointsActivityScreen() { + const { t } = useTranslation(); + const [activities, setActivities] = useState([]); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const state = await getPointsState(); + if (!cancelled) { + setActivities(state.activities); + } + } catch (error) { + if (__DEV__) { + console.warn("[PointsActivity] Failed to load points state", error); + } + } + })(); + + return () => { + cancelled = true; + }; + }, []); + const renderPointsPill = (points: number) => { + const isPositive = points >= 0; + const backgroundColor = isPositive ? "#0F7B4A" : "#EF4444"; + const sign = isPositive ? "+" : "-"; + const absPoints = Math.abs(points); + + return ( + + + {t("pointsactivity.pointsPill", { sign, points: absPoints })} + + + ); + }; + + return ( + + + + + + + {t("pointsactivity.title")} + + + + {activities.map((item) => { + const date = new Date(item.timestamp); + const formattedDate = `${date.toLocaleDateString()} • ${date.toLocaleTimeString( + [], + { + hour: "numeric", + minute: "2-digit", + } + )}`; + + let titleKey: string; + switch (item.type) { + case "contact_sync": + titleKey = "Contact Sync"; + break; + case "send_money": + titleKey = "Send Money"; + break; + case "login": + titleKey = "Login"; + break; + case "purchase_ticket": + titleKey = "Purchase Ticket"; + break; + case "add_recipient": + titleKey = "Add Recipient"; + break; + case "share_event": + titleKey = "Share Event"; + break; + case "make_request": + titleKey = "Make Request"; + break; + case "referral_link": + titleKey = "Referral Link"; + break; + default: + titleKey = item.type; + } + + return ( + + + + + + + + {titleKey} + + + {formattedDate} + + + + + {renderPointsPill(item.points)} + + ); + })} + + + + + ); +} diff --git a/app/(root)/(screens)/profile.tsx b/app/(root)/(screens)/profile.tsx new file mode 100644 index 0000000..b3cf1f4 --- /dev/null +++ b/app/(root)/(screens)/profile.tsx @@ -0,0 +1,538 @@ +import React, { useState, useEffect, useRef } from "react"; +import { ScrollView, View, Image, TouchableOpacity } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Icons } from "~/assets/icons"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { + ChevronRight, + Store, + LifeBuoy, + Bell, + ScanFace, + Grid3x3, + LogOut, + Book, + Award, + Settings, +} from "lucide-react-native"; +import Toggle from "~/components/ui/toggle"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { useLangStore } from "~/lib/stores"; +import { getPointsState } from "~/lib/services/pointsService"; +import BackButton from "~/components/ui/backButton"; + +export default function Profile() { + const { t } = useTranslation(); + const { signOut, user, profile, profileLoading } = useAuthWithProfile(); + const language = useLangStore((state) => state.language); + const setLanguage = useLangStore((state) => state.setLanguage); + + // Preferences state + const [pushNotifications, setPushNotifications] = useState(true); + const [smsNotifications, setSmsNotifications] = useState(true); + const [emailNotifications, setEmailNotifications] = useState(true); + const [faceID, setFaceID] = useState(true); + const [profileImage, setProfileImage] = useState(null); + const [languageSheetVisible, setLanguageSheetVisible] = useState(false); + const [pointsTotal, setPointsTotal] = useState(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + useEffect(() => { + if (profile?.photoUrl) { + setProfileImage(profile.photoUrl); + } else { + setProfileImage(null); + } + }, [profile?.photoUrl]); + + useEffect(() => { + (async () => { + try { + const state = await getPointsState(); + setPointsTotal(state.total); + } catch (error) { + if (__DEV__) { + console.warn("[Profile] Failed to load points state", error); + } + } + })(); + }, []); + + const handleLogout = async () => { + try { + await signOut(); + showToast( + t("profile.toastLoggedOutTitle"), + t("profile.toastLoggedOutDescription"), + "success" + ); + router.replace(ROUTES.SIGNIN); + } catch (error) { + console.log(error); + showToast( + t("profile.toastErrorTitle"), + t("profile.toastLogoutFailed"), + "error" + ); + } + }; + + const handleEditProfile = () => { + router.push(ROUTES.EDIT_PROFILE); + }; + + const handleMyStores = () => { + // TODO: Navigate to My Stores screen + showToast("My stores", "Coming soon", "info"); + }; + + const handleSupport = () => { + router.push(ROUTES.HELP_SUPPORT); + }; + + const handleKyc = () => { + router.push(ROUTES.KYC); + }; + + const handlePoints = () => { + router.push(ROUTES.POINTS); + }; + + const handleHistory = () => { + router.push(ROUTES.HISTORY); + }; + + const handleChangePassword = () => { + // Placeholder screen for Change Password + showToast("Change Password", "Coming soon", "info"); + }; + + const handlePINCode = () => { + router.push(ROUTES.CHANGE_PIN); + }; + + const displayName = profile?.fullName || user?.displayName || "User"; + const displayEmail = profile?.email || user?.email || ""; + const agentId = user?.uid + ? `AGENT-${user.uid.slice(-6).toUpperCase()}` + : "AGENT-001"; + + const languageOptions = [ + { value: "en", label: t("profile.languageOptionEnglish") }, + { value: "am", label: t("profile.languageOptionAmharic") }, + { value: "fr", label: t("profile.languageOptionFrench") }, + { value: "ti", label: t("profile.languageOptionTigrinya") }, + { value: "om", label: t("profile.languageOptionOromo") }, + ]; + + const initialLetter = displayName?.trim().charAt(0).toUpperCase() || "U"; + + return ( + + + + {/* Profile Header */} + + {/* Avatar with + icon */} + + + {profileImage ? ( + + ) : ( + + )} + + + + {/* Agent Info Card: Name, Role, Agent ID, Email */} + + {profileLoading ? "..." : displayName} + + + Role: Agent + + + Agent ID: {agentId} + + + {profileLoading ? "..." : displayEmail} + + + {/* Edit Profile Button */} + + + Edit profile + + + + + + + Inventories + + + {/* Inventories Card - grouped items */} + + {/* Points */} + + + + + + + Points + + + + {pointsTotal ?? 0} + + + + + + + + + {/* Help and Support */} + + + + + + + Help & Support + + + + + + + + {/* Terms and Conditions */} + router.push(ROUTES.TERMS)} + className="p-4 flex-row items-center justify-between" + activeOpacity={0.7} + > + + + + + + Terms & Conditions + + + + + + + + + + + + Information + + + + + + + + + + {/* Preferences Section */} + + + Preferences + + + {/* Preferences Card - grouped items */} + + {/* SMS notifications */} + + + + + + + SMS notifications + + + + + + {/* Divider */} + + + {/* In-app notifications */} + + + + + + + In-app notifications + + + + + + {/* Divider */} + + + {/* Email notifications */} + + + + + + + Email notifications + + + + + + {/* Divider */} + + + {/* Biometric Login (Face ID) */} + + + + + + + Biometric Login + + + + + + {/* Divider */} + + + {/* Language */} + setLanguageSheetVisible(true)} + className="p-4 flex-row items-center justify-between" + activeOpacity={0.7} + > + + + + + + + {t("profile.languageLabel")} + + + { + languageOptions.find((opt) => opt.value === language) + ?.label + } + + + + + + + {/* Divider */} + + + {/* Reports / Transaction history */} + + + + + + + View Reports + + + + + + {/* Divider */} + + + {/* Change Password (placeholder) */} + + + + + + + Change Password + + + + + + {/* Divider */} + + + {/* PIN Code */} + + + + + + + PIN Code + + + + + + + {/* Logout - separate card */} + + + + + + Logout + + + + + + + + setLanguageSheetVisible(false)} + maxHeightRatio={0.4} + > + + {languageOptions.map((opt) => { + const selected = language === opt.value; + return ( + { + setLanguage(opt.value as any); + setLanguageSheetVisible(false); + }} + className="py-3 flex-row items-center border-b border-gray-100" + > + + {selected ? ( + + ) : ( + + )} + + + {opt.label} + + + ); + })} + + + + ); +} diff --git a/app/(root)/(screens)/qrscreen.tsx b/app/(root)/(screens)/qrscreen.tsx new file mode 100644 index 0000000..c2afb13 --- /dev/null +++ b/app/(root)/(screens)/qrscreen.tsx @@ -0,0 +1,364 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + TouchableOpacity, + Share, + ActivityIndicator, + Platform, +} from "react-native"; +import { ArrowLeft } from "lucide-react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import { router } from "expo-router"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import QRCode from "react-native-qrcode-svg"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { UserQrService } from "~/lib/services/userQrService"; +import { + CameraView, + useCameraPermissions, + type BarcodeScanningResult, +} from "expo-camera"; +import { ROUTES } from "~/lib/routes"; +import BackButton from "~/components/ui/backButton"; + +export default function QRScreen() { + const { t } = useTranslation(); + const { user, profile, wallet } = useAuthWithProfile(); + const [activeTab, setActiveTab] = useState<"scan" | "my">("scan"); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + // Scanner state + const [hasPermission, setHasPermission] = useState(null); + const [scanned, setScanned] = useState(false); + const [permission, requestPermission] = useCameraPermissions(); + + const displayName = + profile?.fullName || user?.displayName || t("profile.usernamePlaceholder"); + const phoneNumber = profile?.phoneNumber || (user as any)?.phoneNumber || ""; + const accountId = wallet?.uid || user?.uid || ""; + + const [qrPayload, setQrPayload] = useState(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Sync camera permission state for Scan tab + useEffect(() => { + if (Platform.OS === "web") { + setHasPermission(false); + return; + } + + if (!permission) { + return; + } + + setHasPermission(permission.granted); + }, [permission]); + + useEffect(() => { + let isMounted = true; + + const loadQr = async () => { + if (!user?.uid) return; + + try { + const payload = await UserQrService.getOrCreateProfileQr({ + uid: user.uid, + accountId, + name: displayName, + phoneNumber, + }); + + if (isMounted) { + setQrPayload(payload); + } + } catch (error) { + console.error("[QRScreen] Failed to load/create profile QR", error); + } + }; + + loadQr(); + + return () => { + isMounted = false; + }; + }, [user?.uid, accountId, displayName, phoneNumber]); + + const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => { + if (scanned) return; + + try { + setScanned(true); + + const parsed = JSON.parse(data); + if (!parsed || parsed.type !== "AMBA_PROFILE") { + showToast( + "Invalid QR", + "This is not a valid Amba profile QR.", + "error" + ); + setScanned(false); + return; + } + + const accountIdFromQr: string | undefined = parsed.accountId; + const nameFromQr: string | undefined = parsed.name; + const phoneNumberFromQr: string | undefined = parsed.phoneNumber; + + if (!phoneNumberFromQr) { + showToast( + "Invalid QR", + "This profile QR does not contain a phone number.", + "error" + ); + setScanned(false); + return; + } + + router.push({ + pathname: ROUTES.SEND_OR_REQUEST_MONEY, + params: { + selectedContactId: accountIdFromQr || phoneNumberFromQr, + selectedContactName: nameFromQr || "User", + selectedContactPhone: phoneNumberFromQr, + }, + }); + } catch (error) { + console.warn("[QRScreen] Failed to parse QR payload", error); + showToast("Scan failed", "Could not read this QR code.", "error"); + setScanned(false); + } + }; + + const handleShare = async () => { + try { + await Share.share({ + message: t("qrscreen.shareMessage"), + }); + } catch (error) { + console.log("Error sharing QR:", error); + showToast( + t("qrscreen.toastErrorTitle"), + t("qrscreen.toastShareError"), + "error" + ); + } + }; + + const handleClose = () => { + router.back(); + }; + + return ( + + + {/* Top back button */} + + + + + {/* Tabs - match KYC style */} + + { + setActiveTab("scan"); + setScanned(false); + }} + className={`flex-1 items-center py-2 rounded-full ${ + activeTab === "scan" ? "bg-white" : "bg-transparent" + }`} + activeOpacity={0.8} + > + + {t("qrscreen.scanTabLabel", "Scan QR")} + + + + setActiveTab("my")} + className={`flex-1 items-center py-2 rounded-full ${ + activeTab === "my" ? "bg-white" : "bg-transparent" + }`} + activeOpacity={0.8} + > + + {t("qrscreen.myTabLabel", "My QR")} + + + + + {/* Tab content */} + {activeTab === "my" ? ( + <> + + {qrPayload ? ( + + ) : ( + + + + + {t("qrscreen.loadingLabel", "Preparing your QR code...")} + + + )} + + + + + + + + + ) : ( + + {Platform.OS === "web" ? ( + + + QR scanning is not supported on web. Please use a mobile + device. + + + ) : hasPermission === null ? ( + + + + + Requesting camera permission... + + + ) : hasPermission === false ? ( + + + Camera permission needed + + + Please grant camera access to scan profile QR codes. + + + + ) : ( + + + + + + + Scan profile QR code + + + Align the QR code inside the frame. We will automatically + select the account and take you to the amount screen. + + + )} + + )} + + + + ); +} diff --git a/app/(root)/(screens)/recipaddedcomp.tsx b/app/(root)/(screens)/recipaddedcomp.tsx new file mode 100644 index 0000000..a2b5679 --- /dev/null +++ b/app/(root)/(screens)/recipaddedcomp.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useRef, useState } from "react"; +import { View, Text, ScrollView, Share } from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import LottieView from "lottie-react-native"; + +export default function RecipAddedComp() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + message?: string; + recipientName?: string; + recipientPhone?: string; + }>(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleAddAnother = () => { + // Navigate to add recipient page + router.replace(ROUTES.ADD_RECIPIENT); + }; + + const handleGoToRecipients = () => { + // Navigate to list recipients page + router.replace(ROUTES.LIST_RECIPIENTS); + }; + + const handleShare = async () => { + try { + const shareMessage = params.message + ? t("recipaddedcomp.shareMessageWithParam", { + message: params.message, + }) + : t("recipaddedcomp.shareMessageDefault"); + + const result = await Share.share({ + message: shareMessage, + title: t("recipaddedcomp.shareTitle"), + }); + + if (result.action === Share.sharedAction) { + // Content was shared + console.log("Content shared successfully"); + } else if (result.action === Share.dismissedAction) { + // Share dialog was dismissed + console.log("Share dialog dismissed"); + } + } catch (error) { + console.error("Error sharing:", error); + showToast( + t("recipaddedcomp.toastErrorTitle"), + t("recipaddedcomp.toastShareError"), + "error" + ); + } + }; + + return ( + + + {/* Center content */} + + + + + {t("recipaddedcomp.title")} + + + + {t("recipaddedcomp.description")} + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/recipdetail.tsx b/app/(root)/(screens)/recipdetail.tsx new file mode 100644 index 0000000..4c42d2b --- /dev/null +++ b/app/(root)/(screens)/recipdetail.tsx @@ -0,0 +1,262 @@ +import React, { useMemo, useState } from "react"; +import { View, Text, TouchableOpacity, ScrollView } from "react-native"; +import { useLocalSearchParams, router } from "expo-router"; +import { useRecipientsStore } from "~/lib/stores"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Button } from "~/components/ui/button"; +import { ROUTES } from "~/lib/routes"; +import { + LucideUser, + LucideCreditCard, + LucideCalendarClock, + LucideChevronRight, +} from "lucide-react-native"; + +function getInitials(name: string) { + return name + .split(" ") + .map((word) => word.charAt(0).toUpperCase()) + .slice(0, 2) + .join(""); +} + +export default function RecipDetail() { + const { recipientId } = useLocalSearchParams<{ recipientId?: string }>(); + const { recipients } = useRecipientsStore(); + + const recipient = useMemo( + () => recipients.find((r) => r.id === recipientId), + [recipients, recipientId] + ); + + const initials = recipient ? getInitials(recipient.fullName) : "?"; + const lowerName = recipient?.fullName.toLowerCase() ?? ""; + const isBusiness = + lowerName.includes("ltd") || + lowerName.includes("plc") || + lowerName.includes("inc") || + lowerName.includes("company"); + const clientType = isBusiness ? "Business" : "Individual"; + + // Dummy accounts for UI-only + const accounts = useMemo( + () => [ + { + id: "acc-1", + bank: "Bank of Abyssinia", + number: "***1234", + primary: true, + }, + { + id: "acc-2", + bank: "Dashen Bank", + number: "***5678", + primary: false, + }, + ], + [] + ); + + // Dummy schedules for UI-only + const schedules = useMemo( + () => [ + { id: "sch-1", label: "Every Monday · 10:00" }, + { id: "sch-2", label: "Monthly, 1st · 09:00" }, + ], + [] + ); + + const [showManageSchedules, setShowManageSchedules] = useState(false); + + const handlePayNow = () => { + if (!recipient) return; + router.push({ + pathname: ROUTES.SEND_OR_REQUEST_MONEY, + params: { recipientId: recipient.id, recipientName: recipient.fullName }, + }); + }; + + const handleViewTransactions = () => { + if (!recipient) return; + router.push({ + pathname: ROUTES.HISTORY, + params: { recipientId: recipient.id, recipientName: recipient.fullName }, + }); + }; + + if (!recipient) { + return ( + + + + Recipient not found. + + + + + ); + } + + return ( + + + + + + + + {/* Header */} + + + + {initials} + + + + + {recipient.fullName} + + + + + {clientType} + + + + {recipient.phoneNumber} + + + + + + {/* Accounts Section */} + + + Linked Accounts + + {accounts.map((acc) => ( + + + + + + + + {acc.bank} + + + {acc.number} + + + + {acc.primary && ( + + Primary + + )} + + ))} + + {}} + > + + Add Account + + + + + {/* Schedules Section */} + + + Payment Schedules + + {schedules.map((sch) => ( + + + + + + + {sch.label} + + + + + ))} + + setShowManageSchedules(true)} + > + + Manage Schedules + + + + + + {/* Actions */} + + + + + + + {/* Simple placeholder bottom sheet for Manage Schedules */} + {showManageSchedules && ( + + + + + + Manage Schedules (UI only) + + + This is a placeholder UI. Editing schedules will be wired to backend + in a later phase. + + + + )} + + ); +} diff --git a/app/(root)/(screens)/requestprovider.tsx b/app/(root)/(screens)/requestprovider.tsx new file mode 100644 index 0000000..4cedfb5 --- /dev/null +++ b/app/(root)/(screens)/requestprovider.tsx @@ -0,0 +1,203 @@ +import React, { useState, useRef } from "react"; +import { View, Text, ScrollView, TouchableOpacity } from "react-native"; +import { router, useLocalSearchParams } from "expo-router"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { AwashIcon, TeleBirrIcon } from "~/components/ui/icons"; +import { LucideChevronRightCircle } from "lucide-react-native"; +import { Button } from "~/components/ui/button"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { ROUTES } from "~/lib/routes"; +import { awardPoints } from "~/lib/services/pointsService"; + +export default function RequestProvider() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + amount: string; + recipientName: string; + recipientPhoneNumber: string; + recipientType: string; + recipientId: string; + note: string; + requestorName: string; + requestorPhoneNumber: string; + requestorType: string; + type: string; + }>(); + + const [selectedProvider, setSelectedProvider] = useState< + "awash" | "telebirr" | null + >(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleContinue = () => { + if (!selectedProvider) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastNoMethod"), + "error" + ); + return; + } + + awardPoints("make_request").catch((error) => { + console.warn( + "[RequestProvider] Failed to award make request points", + error + ); + }); + + router.push({ + pathname: ROUTES.NOTIFICATION_OPTION, + params: { + ...params, + provider: selectedProvider, + } as any, + }); + }; + + return ( + + + + + + + + + {t("sendbank.paymentOptionsTitle")} + + + {selectedProvider + ? t("sendbank.paymentOptionsSelected", { + providerName: + selectedProvider === "awash" + ? t("sendbank.awashName") + : t("sendbank.telebirrName"), + }) + : t("sendbank.paymentOptionsUnselected")} + + + + {/* Telebirr Section */} + + + {t("sendbank.telebirrName")} + + setSelectedProvider("telebirr")} + className={`flex flex-row w-full justify-between items-center py-4 rounded-md px-3 mb-4 ${ + selectedProvider === "telebirr" + ? "bg-orange-100 border-2 border-orange-300" + : "bg-green-50 border border-gray-200" + }`} + > + + + + + + + + {t("sendbank.telebirrName")} + + + {t("sendbank.telebirrSubtitle")} + + + + + + + + + + {/* Bank Section with Awash */} + + + Bank + + setSelectedProvider("awash")} + className={`flex flex-row w-full justify-between items-center py-4 rounded-md px-3 ${ + selectedProvider === "awash" + ? "bg-blue-100 border-2 border-blue-300" + : "bg-green-50 border border-gray-200" + }`} + > + + + + + + + {t("sendbank.awashName")} + + + {t("sendbank.awashSubtitle")} + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/scanprofileqr.tsx b/app/(root)/(screens)/scanprofileqr.tsx new file mode 100644 index 0000000..5d1c1d4 --- /dev/null +++ b/app/(root)/(screens)/scanprofileqr.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + TouchableOpacity, + ActivityIndicator, + Platform, +} from "react-native"; +import { ArrowLeft } from "lucide-react-native"; +import { router } from "expo-router"; +import { + CameraView, + useCameraPermissions, + type BarcodeScanningResult, +} from "expo-camera"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Button } from "~/components/ui/button"; +import ModalToast from "~/components/ui/toast"; +import { ROUTES } from "~/lib/routes"; + +export default function ScanProfileQR() { + const [hasPermission, setHasPermission] = useState(null); + const [scanned, setScanned] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + const [permission, requestPermission] = useCameraPermissions(); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + useEffect(() => { + if (Platform.OS === "web") { + setHasPermission(false); + return; + } + + if (!permission) { + return; + } + + setHasPermission(permission.granted); + }, [permission]); + + const handleClose = () => { + router.back(); + }; + + const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => { + if (scanned) return; + + try { + setScanned(true); + + const parsed = JSON.parse(data); + if (!parsed || parsed.type !== "AMBA_PROFILE") { + showToast( + "Invalid QR", + "This is not a valid Amba profile QR.", + "error" + ); + setScanned(false); + return; + } + + const accountId: string | undefined = parsed.accountId; + const name: string | undefined = parsed.name; + const phoneNumber: string | undefined = parsed.phoneNumber; + + if (!phoneNumber) { + showToast( + "Invalid QR", + "This profile QR does not contain a phone number.", + "error" + ); + setScanned(false); + return; + } + + router.push({ + pathname: ROUTES.SEND_OR_REQUEST_MONEY, + params: { + selectedContactId: accountId || phoneNumber, + selectedContactName: name || "User", + selectedContactPhone: phoneNumber, + }, + }); + } catch (error) { + console.warn("[ScanProfileQR] Failed to parse QR payload", error); + showToast("Scan failed", "Could not read this QR code.", "error"); + setScanned(false); + } + }; + + return ( + + + {/* Top back button */} + + + + + + + {Platform.OS === "web" ? ( + + + QR scanning is not supported on web. Please use a mobile device. + + + ) : hasPermission === null ? ( + + + + + Requesting camera permission... + + + ) : hasPermission === false ? ( + + + Camera permission needed + + + Please grant camera access to scan profile QR codes. + + + + ) : ( + + + + + + + Scan profile QR code + + + Align the QR code inside the frame. We will automatically select + the account and take you to the amount screen. + + + )} + + + + ); +} diff --git a/app/(root)/(screens)/schedules.tsx b/app/(root)/(screens)/schedules.tsx new file mode 100644 index 0000000..c1b73e9 --- /dev/null +++ b/app/(root)/(screens)/schedules.tsx @@ -0,0 +1,637 @@ +import React, { useState, useRef, useEffect } from "react"; +import { View, ScrollView, TouchableOpacity, Platform } from "react-native"; +import { Text } from "~/components/ui/text"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { Button } from "~/components/ui/button"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import * as Calendar from "expo-calendar"; +import ModalToast from "~/components/ui/toast"; +import Skeleton from "~/components/ui/skeleton"; +import { collection, doc, FieldValue } from "~/lib/firebase"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; + +type ScheduleStatus = "active" | "paused" | "stopped"; + +type Schedule = { + id: string; + recipientName: string; + label: string; + amount: string; + currency: string; + nextRunDate: string; + nextRunLabel: string; + frequency: string; + status: ScheduleStatus; +}; + +const toISODate = (date: Date) => date.toISOString().split("T")[0]; + +const TODAY = new Date(); + +const addDays = (date: Date, days: number) => { + const d = new Date(date); + d.setDate(date.getDate() + days); + return d; +}; + +const START_OF_WEEK = new Date(TODAY); +START_OF_WEEK.setDate(TODAY.getDate() - TODAY.getDay()); + +const WEEK_DAYS = Array.from({ length: 7 }).map((_, index) => { + const day = new Date(START_OF_WEEK); + day.setDate(START_OF_WEEK.getDate() + index); + const labels = ["S", "M", "T", "W", "T", "F", "S"]; + return { + key: toISODate(day), + label: labels[day.getDay()], + dateNumber: day.getDate(), + isToday: day.toDateString() === TODAY.toDateString(), + }; +}); + +const MOCK_SCHEDULES: Schedule[] = [ + { + id: "sch-1", + recipientName: "Abebe Kebede", + label: "Monthly support", + amount: "4,500", + currency: "ETB", + nextRunDate: toISODate(TODAY), + nextRunLabel: "Today · 4:00 PM", + frequency: "Every month", + status: "active", + }, + { + id: "sch-2", + recipientName: "Sara Alemu", + label: "Weekly allowance", + amount: "1,000", + currency: "ETB", + nextRunDate: toISODate(addDays(TODAY, 1)), + nextRunLabel: "Tomorrow · 9:00 AM", + frequency: "Every week", + status: "paused", + }, + { + id: "sch-3", + recipientName: "Hope Community Fund", + label: "Quarterly disbursement", + amount: "32,000", + currency: "ETB", + nextRunDate: toISODate(addDays(TODAY, 2)), + nextRunLabel: "In 2 days · 2:15 PM", + frequency: "Every 3 months", + status: "stopped", + }, +]; + +const getStatusPillClasses = (status: ScheduleStatus) => { + switch (status) { + case "active": + return "bg-emerald-100 text-emerald-700"; + case "paused": + return "bg-yellow-100 text-yellow-700"; + case "stopped": + default: + return "bg-gray-100 text-gray-600"; + } +}; + +const isSameDay = (a: Date, b: Date) => + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate(); + +const buildFrequencyLabel = ( + repeatType?: string, + interval?: number +): string => { + if (!repeatType || repeatType === "none") return "One-time"; + if (repeatType === "every_x_days" && interval) { + return `Every ${interval} days`; + } + return repeatType; +}; + +const buildNextRunLabel = (dateValue?: any, time?: string): string => { + if (!dateValue && !time) return ""; + + const today = TODAY; + const tomorrow = addDays(TODAY, 1); + + let dateObj: Date | null = null; + + if (dateValue instanceof Date) { + dateObj = dateValue; + } else if (typeof dateValue === "string") { + const parsed = new Date(dateValue); + if (!isNaN(parsed.getTime())) { + dateObj = parsed; + } + } + + let dayLabel = ""; + if (dateObj) { + if (isSameDay(dateObj, today)) dayLabel = "Today"; + else if (isSameDay(dateObj, tomorrow)) dayLabel = "Tomorrow"; + else { + dayLabel = dateObj.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + } + } + + if (dayLabel && time) return `${dayLabel} · ${time}`; + return dayLabel || time || ""; +}; + +const parseTimeFromLabel = (label: string) => { + const parts = label.split("·"); + const rawTime = parts[1]?.trim(); + if (!rawTime) { + return { hours: 9, minutes: 0 }; + } + + const match = rawTime.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); + if (!match) { + return { hours: 9, minutes: 0 }; + } + + let hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + const period = match[3].toUpperCase(); + + if (period === "PM" && hours < 12) hours += 12; + if (period === "AM" && hours === 12) hours = 0; + + return { hours, minutes }; +}; + +const getDateTimesForSchedule = (schedule: Schedule) => { + const [year, month, day] = schedule.nextRunDate + .split("-") + .map((v) => parseInt(v, 10)); + const { hours, minutes } = parseTimeFromLabel(schedule.nextRunLabel); + + const startDate = new Date(year, month - 1, day, hours, minutes); + const endDate = new Date(startDate.getTime() + 60 * 60 * 1000); + + return { startDate, endDate }; +}; + +const syncScheduleToCalendar = async ( + schedule: Schedule, + showToast: ( + title: string, + description?: string, + variant?: "success" | "error" | "warning" | "info" + ) => void +) => { + try { + const { status } = await Calendar.requestCalendarPermissionsAsync(); + if (status !== "granted") { + showToast( + "Calendar permission needed", + "Allow calendar access to sync schedules.", + "warning" + ); + return; + } + + let calendarId: string | null = null; + + if (Platform.OS === "ios") { + const defaultCal = await Calendar.getDefaultCalendarAsync(); + calendarId = defaultCal?.id ?? null; + } else { + const calendars = await Calendar.getCalendarsAsync( + Calendar.EntityTypes.EVENT + ); + const editable = calendars.find( + (c: Calendar.Calendar) => c.allowsModifications + ); + calendarId = editable?.id ?? calendars[0]?.id ?? null; + } + + if (!calendarId) { + showToast( + "No calendar found", + "We couldn't find an editable calendar on this device.", + "error" + ); + return; + } + + const { startDate, endDate } = getDateTimesForSchedule(schedule); + + await Calendar.createEventAsync(calendarId, { + title: schedule.label || "Scheduled payment", + notes: `${schedule.recipientName} • ${schedule.currency} ${schedule.amount}`, + startDate, + endDate, + timeZone: undefined, + }); + showToast( + "Added to calendar", + "This schedule was added as an event.", + "success" + ); + } catch (error) { + console.warn("[Schedules] Failed to sync schedule to calendar", error); + showToast( + "Calendar error", + "We couldn't add this schedule to your calendar.", + "error" + ); + } +}; + +export default function SchedulesScreen() { + const [schedules, setSchedules] = useState([]); + const [selected, setSelected] = useState(null); + const [statusFilter, setStatusFilter] = useState<"all" | ScheduleStatus>( + "all" + ); + const [selectedDateKey, setSelectedDateKey] = useState( + WEEK_DAYS.find((d) => d.isToday)?.key || WEEK_DAYS[0].key + ); + const [loading, setLoading] = useState(true); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + const { user } = useAuthWithProfile(); + + useEffect(() => { + const loadSchedules = async () => { + if (!user?.uid) { + setSchedules([]); + setLoading(false); + return; + } + + try { + setLoading(true); + const appointmentsCol: any = collection("appointments"); + const snap = await appointmentsCol + .where("agentId", "==", user.uid) + .where("acceptance", "==", "accepted") + .get(); + + const loaded: Schedule[] = snap.docs.map((docSnap: any) => { + const data = docSnap.data() || {}; + + const nextRunSource = + data.nextRunDate || + (data.date && data.time + ? `${data.date}T${data.time}:00` + : undefined); + + const nextRun = nextRunSource ? new Date(nextRunSource) : TODAY; + const nextRunDateKey = toISODate(nextRun); + const nextRunLabel = buildNextRunLabel(nextRun, data.time); + + const frequency = buildFrequencyLabel(data.repeatType, data.interval); + + const status: ScheduleStatus = + data.scheduleStatus === "paused" || + data.scheduleStatus === "stopped" + ? data.scheduleStatus + : "active"; + + return { + id: String(docSnap.id), + recipientName: + data.fullName || data.email || data.phoneNumber || "Unknown", + label: data.notes || "", + amount: + typeof data.amount === "number" + ? data.amount.toString() + : String(data.amount ?? ""), + currency: "ETB", + nextRunDate: nextRunDateKey, + nextRunLabel, + frequency, + status, + }; + }); + + setSchedules(loaded); + } catch (error) { + console.error( + "[Schedules] Failed to load appointments as schedules", + error + ); + } finally { + setLoading(false); + } + }; + + loadSchedules(); + }, [user]); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleUpdateStatus = async (id: string, next: ScheduleStatus) => { + try { + const ref = doc("appointments", id); + await ref.update({ + scheduleStatus: next, + updatedAt: FieldValue.serverTimestamp(), + }); + + setSchedules((prev) => + prev.map((sch) => (sch.id === id ? { ...sch, status: next } : sch)) + ); + setSelected((prev) => + prev && prev.id === id ? { ...prev, status: next } : prev + ); + } catch (error) { + console.error("[Schedules] Failed to update scheduleStatus", id, error); + } + }; + + const activeCount = schedules.filter((s) => s.status === "active").length; + const pausedCount = schedules.filter((s) => s.status === "paused").length; + const stoppedCount = schedules.filter((s) => s.status === "stopped").length; + + const dateFiltered = schedules.filter( + (s) => s.nextRunDate === selectedDateKey + ); + + const filteredSchedules = + statusFilter === "all" + ? dateFiltered + : dateFiltered.filter((s) => s.status === statusFilter); + + return ( + + + + + + + Schedules + + + View and manage your recurring payment schedules. + + + {/* Week day selector */} + + {WEEK_DAYS.map((day) => { + const isSelected = day.key === selectedDateKey; + const hasSchedulesForDay = schedules.some( + (s) => s.nextRunDate === day.key + ); + + return ( + setSelectedDateKey(day.key)} + > + + {day.label} + + + {day.dateNumber} + + + {hasSchedulesForDay && ( + + )} + + + ); + })} + + {loading && ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + )} + + {!loading && filteredSchedules.length === 0 && ( + + + No schedules match your filters. + + + )} + + {!loading && filteredSchedules.length > 0 && ( + + {filteredSchedules.map((sch) => { + return ( + + + + + + {sch.recipientName} + + + {sch.label} + + + + + + + + Next run + + + {sch.nextRunLabel} + + + + + Amount + + + {sch.currency} {sch.amount} + + + + + + + + Frequency + + + {sch.frequency} + + + + setSelected(sch)} + > + + View details + + + + + + + ); + })} + + )} + + + + + + setSelected(null)} + maxHeightRatio={0.45} + > + {selected && ( + + + Schedule Details + + + {selected.recipientName} + + + {selected.label} + + + + + + Next run + + + {selected.nextRunLabel} + + + + + Amount + + + {selected.currency} {selected.amount} + + + + + Frequency + + + {selected.frequency} + + + + + + router.push(ROUTES.SEND_OR_REQUEST_MONEY)} + > + + Pay now + + + + + + + )} + + + + ); +} diff --git a/app/(root)/(screens)/schedulesall.tsx b/app/(root)/(screens)/schedulesall.tsx new file mode 100644 index 0000000..83e23fc --- /dev/null +++ b/app/(root)/(screens)/schedulesall.tsx @@ -0,0 +1,539 @@ +import React, { useState, useRef, useEffect } from "react"; +import { View, ScrollView, TouchableOpacity, Platform } from "react-native"; +import { Text } from "~/components/ui/text"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { Button } from "~/components/ui/button"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import * as Calendar from "expo-calendar"; +import ModalToast from "~/components/ui/toast"; +import Skeleton from "~/components/ui/skeleton"; +import { collection } from "~/lib/firebase"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; + +type ScheduleStatus = "active" | "paused" | "stopped"; + +type Schedule = { + id: string; + recipientName: string; + label: string; + amount: string; + currency: string; + nextRunDate: string; + nextRunLabel: string; + frequency: string; + status: ScheduleStatus; +}; + +const toISODate = (date: Date) => date.toISOString().split("T")[0]; + +const TODAY = new Date(); + +const addDays = (date: Date, days: number) => { + const d = new Date(date); + d.setDate(date.getDate() + days); + return d; +}; + +const MOCK_SCHEDULES: Schedule[] = [ + { + id: "sch-1", + recipientName: "Abebe Kebede", + label: "Monthly support", + amount: "4,500", + currency: "ETB", + nextRunDate: toISODate(TODAY), + nextRunLabel: "Today · 4:00 PM", + frequency: "Every month", + status: "active", + }, + { + id: "sch-2", + recipientName: "Sara Alemu", + label: "Weekly allowance", + amount: "1,000", + currency: "ETB", + nextRunDate: toISODate(addDays(TODAY, 1)), + nextRunLabel: "Tomorrow · 9:00 AM", + frequency: "Every week", + status: "paused", + }, + { + id: "sch-3", + recipientName: "Hope Community Fund", + label: "Quarterly disbursement", + amount: "32,000", + currency: "ETB", + nextRunDate: toISODate(addDays(TODAY, 2)), + nextRunLabel: "In 2 days · 2:15 PM", + frequency: "Every 3 months", + status: "stopped", + }, +]; + +const getStatusPillClasses = (status: ScheduleStatus) => { + switch (status) { + case "active": + return "bg-emerald-100 text-emerald-700"; + case "paused": + return "bg-yellow-100 text-yellow-700"; + case "stopped": + default: + return "bg-gray-100 text-gray-600"; + } +}; + +const isSameDay = (a: Date, b: Date) => + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate(); + +const buildFrequencyLabel = ( + repeatType?: string, + interval?: number +): string => { + if (!repeatType || repeatType === "none") return "One-time"; + if (repeatType === "every_x_days" && interval) { + return `Every ${interval} days`; + } + return repeatType; +}; + +const buildNextRunLabel = (dateValue?: any, time?: string): string => { + if (!dateValue && !time) return ""; + + const today = TODAY; + const tomorrow = addDays(TODAY, 1); + + let dateObj: Date | null = null; + + if (dateValue instanceof Date) { + dateObj = dateValue; + } else if (typeof dateValue === "string") { + const parsed = new Date(dateValue); + if (!isNaN(parsed.getTime())) { + dateObj = parsed; + } + } + + let dayLabel = ""; + if (dateObj) { + if (isSameDay(dateObj, today)) dayLabel = "Today"; + else if (isSameDay(dateObj, tomorrow)) dayLabel = "Tomorrow"; + else { + dayLabel = dateObj.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + } + } + + if (dayLabel && time) return `${dayLabel} · ${time}`; + return dayLabel || time || ""; +}; + +const parseTimeFromLabel = (label: string) => { + const parts = label.split("·"); + const rawTime = parts[1]?.trim(); + if (!rawTime) { + return { hours: 9, minutes: 0 }; + } + + const match = rawTime.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i); + if (!match) { + return { hours: 9, minutes: 0 }; + } + + let hours = parseInt(match[1], 10); + const minutes = parseInt(match[2], 10); + const period = match[3].toUpperCase(); + + if (period === "PM" && hours < 12) hours += 12; + if (period === "AM" && hours === 12) hours = 0; + + return { hours, minutes }; +}; + +const getDateTimesForSchedule = (schedule: Schedule) => { + const [year, month, day] = schedule.nextRunDate + .split("-") + .map((v) => parseInt(v, 10)); + const { hours, minutes } = parseTimeFromLabel(schedule.nextRunLabel); + + const startDate = new Date(year, month - 1, day, hours, minutes); + const endDate = new Date(startDate.getTime() + 60 * 60 * 1000); + + return { startDate, endDate }; +}; + +const syncScheduleToCalendar = async ( + schedule: Schedule, + showToast: ( + title: string, + description?: string, + variant?: "success" | "error" | "warning" | "info" + ) => void +) => { + try { + const { status } = await Calendar.requestCalendarPermissionsAsync(); + if (status !== "granted") { + showToast( + "Calendar permission needed", + "Allow calendar access to sync schedules.", + "warning" + ); + return; + } + + let calendarId: string | null = null; + + if (Platform.OS === "ios") { + const defaultCal = await Calendar.getDefaultCalendarAsync(); + calendarId = defaultCal?.id ?? null; + } else { + const calendars = await Calendar.getCalendarsAsync( + Calendar.EntityTypes.EVENT + ); + const editable = calendars.find( + (c: Calendar.Calendar) => c.allowsModifications + ); + calendarId = editable?.id ?? calendars[0]?.id ?? null; + } + + if (!calendarId) { + showToast( + "No calendar found", + "We couldn't find an editable calendar on this device.", + "error" + ); + return; + } + + const { startDate, endDate } = getDateTimesForSchedule(schedule); + + await Calendar.createEventAsync(calendarId, { + title: schedule.label || "Scheduled payment", + notes: `${schedule.recipientName} • ${schedule.currency} ${schedule.amount}`, + startDate, + endDate, + timeZone: undefined, + }); + showToast( + "Added to calendar", + "This schedule was added as an event.", + "success" + ); + } catch (error) { + console.warn("[AllSchedules] Failed to sync schedule to calendar", error); + showToast( + "Calendar error", + "We couldn't add this schedule to your calendar.", + "error" + ); + } +}; + +export default function AllSchedulesScreen() { + const [schedules, setSchedules] = useState([]); + const [selected, setSelected] = useState(null); + const [statusFilter, setStatusFilter] = useState<"all" | ScheduleStatus>( + "all" + ); + const [loading, setLoading] = useState(true); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + const { user } = useAuthWithProfile(); + + useEffect(() => { + const loadSchedules = async () => { + if (!user?.uid) { + setSchedules([]); + setLoading(false); + return; + } + + try { + setLoading(true); + const appointmentsCol: any = collection("appointments"); + const snap = await appointmentsCol + .where("agentId", "==", user.uid) + .where("acceptance", "==", "accepted") + .get(); + + const loaded: Schedule[] = snap.docs.map((docSnap: any) => { + const data = docSnap.data() || {}; + + const nextRunSource = + data.nextRunDate || + (data.date && data.time + ? `${data.date}T${data.time}:00` + : undefined); + + const nextRun = nextRunSource ? new Date(nextRunSource) : TODAY; + const nextRunDateKey = toISODate(nextRun); + const nextRunLabel = buildNextRunLabel(nextRun, data.time); + + const frequency = buildFrequencyLabel(data.repeatType, data.interval); + + const status: ScheduleStatus = + data.scheduleStatus === "paused" || + data.scheduleStatus === "stopped" + ? data.scheduleStatus + : "active"; + + return { + id: String(docSnap.id), + recipientName: + data.fullName || data.email || data.phoneNumber || "Unknown", + label: data.notes || "", + amount: + typeof data.amount === "number" + ? data.amount.toString() + : String(data.amount ?? ""), + currency: "ETB", + nextRunDate: nextRunDateKey, + nextRunLabel, + frequency, + status, + }; + }); + + setSchedules(loaded); + } catch (error) { + console.error( + "[AllSchedules] Failed to load appointments as schedules", + error + ); + } finally { + setLoading(false); + } + }; + + loadSchedules(); + }, [user]); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const activeCount = schedules.filter((s) => s.status === "active").length; + const pausedCount = schedules.filter((s) => s.status === "paused").length; + const stoppedCount = schedules.filter((s) => s.status === "stopped").length; + + const filteredSchedules = + statusFilter === "all" + ? schedules + : schedules.filter((s) => s.status === statusFilter); + + return ( + + + + + + + All Schedules + + + Full list of your recurring payment schedules. + + {loading && ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + )} + + {!loading && filteredSchedules.length === 0 && ( + + + No schedules match your filters. + + + )} + + {!loading && filteredSchedules.length > 0 && ( + + {filteredSchedules.map((sch) => { + return ( + + + + + + {sch.recipientName} + + + {sch.label} + + + + + + + + Next run + + + {sch.nextRunLabel} + + + + + Amount + + + {sch.currency} {sch.amount} + + + + + + + + Frequency + + + {sch.frequency} + + + + setSelected(sch)} + > + + View details + + + + + + + ); + })} + + )} + + + + setSelected(null)} + maxHeightRatio={0.6} + > + {selected && ( + + + Schedule Details + + + {selected.recipientName} + + + {selected.label} + + + + + + Next run + + + {selected.nextRunLabel} + + + + + Amount + + + {selected.currency} {selected.amount} + + + + + Frequency + + + {selected.frequency} + + + + + + router.push(ROUTES.SEND_OR_REQUEST_MONEY)} + > + + Pay now + + + + + + + )} + + + + ); +} diff --git a/app/(root)/(screens)/selectacc.tsx b/app/(root)/(screens)/selectacc.tsx new file mode 100644 index 0000000..e6f4ca9 --- /dev/null +++ b/app/(root)/(screens)/selectacc.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, +} from "react-native"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { Filter, LucideX, LucideCreditCard } from "lucide-react-native"; +import AccountCard from "~/components/ui/accCard"; +import BackButton from "~/components/ui/backButton"; +import { useLocalSearchParams, router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import { CreditCard, WalletService } from "~/lib/services/walletService"; +import { ROUTES } from "~/lib/routes"; +import { showAlert } from "~/lib/utils/alertUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +export default function SelectAccount() { + const { t } = useTranslation(); + const { amount } = useLocalSearchParams<{ amount: string }>(); + const { user } = useAuthWithProfile(); + const { wallet, loading, error, refreshWallet } = useUserWallet(user); + const [selectedCardId, setSelectedCardId] = React.useState( + null + ); + const [isProcessing, setIsProcessing] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleCardSelect = (card: CreditCard) => { + setSelectedCardId(card.id); + console.log("Selected card:", card.id); + }; + + const renderCards = () => { + if (loading) { + return ( + + + {t("selectacc.loadingCards")} + + + ); + } + + if (error) { + return ( + + + {t("selectacc.errorTitle")} + + + {t("selectacc.errorWithMessage", { error })} + + + ); + } + + if (!wallet?.cards || wallet.cards.length === 0) { + return ( + + + + {t("selectacc.emptyTitle")} + + + {t("selectacc.emptySubtitle")} + + + ); + } + + return ( + item.id} + scrollEnabled={false} + ItemSeparatorComponent={() => } + renderItem={({ item: card }) => ( + handleCardSelect(card)} + className="w-full" + > + + + )} + /> + ); + }; + + return ( + + + + + + {t("selectacc.title")} + + {amount && ( + + {t("selectacc.addingAmount", { + amount: (parseInt(amount) / 100).toFixed(2), + })} + + )} + + + + + {t("selectacc.accountsTitle")} + + + {selectedCardId + ? t("selectacc.accountsDescriptionSelected") + : t("selectacc.accountsDescriptionUnselected")} + + + + + {renderCards()} + + + + {selectedCardId && ( + + + + )} + + + ); +} diff --git a/app/(root)/(screens)/selectdonor.tsx b/app/(root)/(screens)/selectdonor.tsx new file mode 100644 index 0000000..f2cb1d2 --- /dev/null +++ b/app/(root)/(screens)/selectdonor.tsx @@ -0,0 +1,500 @@ +import React, { + useState, + useEffect, + useMemo, + useCallback, + useRef, +} from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, +} from "react-native"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { LucideUser } from "lucide-react-native"; +import BackButton from "~/components/ui/backButton"; +import { useContactsStore, useRecipientsStore } from "~/lib/stores"; +import { useLocalSearchParams, router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +const DonorCard = React.memo( + ({ + name, + phoneNumber, + selected, + onPress, + }: { + name: string; + phoneNumber: string; + selected: boolean; + onPress: () => void; + }) => { + const borderClass = selected + ? "border-2 border-primary" + : "border border-gray-200"; + + return ( + + + + + + + + {name} + + {phoneNumber} + + + + + ); + }, + (prevProps, nextProps) => { + // Custom comparison for better performance + return ( + prevProps.name === nextProps.name && + prevProps.phoneNumber === nextProps.phoneNumber && + prevProps.selected === nextProps.selected + ); + } +); + +DonorCard.displayName = "DonorCard"; + +// Selected Donor Pill Component - Memoized for performance +const SelectedDonorPill = React.memo( + ({ + name, + phoneNumber, + onRemove, + }: { + name: string; + phoneNumber: string; + onRemove: () => void; + }) => { + return ( + + + {name} + + + × + + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.name === nextProps.name && + prevProps.phoneNumber === nextProps.phoneNumber + ); + } +); + +SelectedDonorPill.displayName = "SelectedDonorPill"; + +export default function SelectDonor() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + amount: string; + selectedContactId?: string; + selectedContactName?: string; + selectedContactPhone?: string; + }>(); + const { user, profile } = useAuthWithProfile(); + const { contacts, loading, error, hasPermission, requestPermission } = + useContactsStore(); + const { + recipients, + loading: recipientsLoading, + error: recipientsError, + } = useRecipientsStore(); + const [selectedRecipient, setSelectedRecipient] = useState( + null + ); + const [note, setNote] = useState(""); + const [search, setSearch] = useState(""); + const [isRequesting, setIsRequesting] = useState(false); + const [hasInitialized, setHasInitialized] = useState(false); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState< + string | undefined + >(); + const toastTimeoutRef = useRef | null>(null); + + const showToast = useCallback((title: string, description?: string) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }, []); + + // Combine contacts and recipients into a single list - Memoized for performance + const allRecipients = useMemo(() => { + const allRecipientsList: Array<{ + id: string; + name: string; + phoneNumber: string; + type: "saved" | "contact"; + }> = []; + + // Add saved recipients + recipients.forEach((recipient) => { + allRecipientsList.push({ + id: recipient.id, + name: recipient.fullName, + phoneNumber: recipient.phoneNumber, + type: "saved", + }); + }); + + // Add contacts (only if permission granted) + if (hasPermission && contacts) { + contacts.forEach((contact) => { + allRecipientsList.push({ + id: contact.id, + name: contact.name, + phoneNumber: contact.phoneNumbers?.[0]?.number || "No phone number", + type: "contact", + }); + }); + } + + // If a contact was selected from the modal, prioritize it + if (params.selectedContactId) { + // Find the selected contact and move it to the top + const selectedContactIndex = allRecipientsList.findIndex( + (recipient) => recipient.id === params.selectedContactId + ); + + if (selectedContactIndex > -1) { + const selectedContact = allRecipientsList.splice( + selectedContactIndex, + 1 + )[0]; + allRecipientsList.unshift(selectedContact); + } + } + + return allRecipientsList; + }, [recipients, contacts, hasPermission, params.selectedContactId]); + + // Handle initial selection from contact modal + useEffect(() => { + if ( + params.selectedContactId && + !hasInitialized && + allRecipients.length > 0 + ) { + // Find the selected contact in the combined list + const selectedContact = allRecipients.find( + (recipient) => recipient.id === params.selectedContactId + ); + + if (selectedContact) { + setSelectedRecipient(`${selectedContact.type}-${selectedContact.id}`); + setHasInitialized(true); + } + } + }, [params.selectedContactId, hasInitialized, allRecipients]); + + // Memoized handler for recipient selection + const handleRecipientSelect = useCallback((id: string) => { + setSelectedRecipient(id); + if (__DEV__) { + console.log("Selected donor:", id); + } + }, []); + + // Memoized handler for removing selection + const handleRemoveSelected = useCallback(() => { + setSelectedRecipient(null); + setSearch(""); + }, []); + + // Get selected donor data - Memoized for performance + const selectedDonorData = useMemo(() => { + if (!selectedRecipient) return null; + + return ( + allRecipients.find( + (recipient) => `${recipient.type}-${recipient.id}` === selectedRecipient + ) || null + ); + }, [selectedRecipient, allRecipients]); + + const handleRequestMoney = useCallback(async () => { + if (!selectedRecipient || !params.amount || !user?.uid || !profile) { + showToast( + t("selectdonor.toastErrorTitle"), + t("selectdonor.toastMissingInfo") + ); + return; + } + + const amountInCents = parseInt(params.amount); // Amount is already in cents + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("selectdonor.toastErrorTitle"), + t("selectdonor.toastInvalidAmount") + ); + return; + } + + // Use already computed selectedDonorData + if (!selectedDonorData) { + showToast( + t("selectdonor.toastErrorTitle"), + t("selectdonor.toastDonorNotFound") + ); + return; + } + + // Navigate directly to payment options (sendbank) with recipient data + router.push({ + pathname: ROUTES.SEND_BANK, + params: { + amount: params.amount, + recipientName: selectedDonorData.name, + recipientPhoneNumber: selectedDonorData.phoneNumber, + recipientType: selectedDonorData.type, + recipientId: selectedDonorData.id, + note: note.trim() || "", + transactionType: "send" as const, + }, + }); + + // Clear the note and selected recipient after navigation + setNote(""); + setSelectedRecipient(null); + }, [ + selectedRecipient, + params.amount, + user?.uid, + profile, + selectedDonorData, + note, + showToast, + t, + ]); + + // Filter recipients based on search input - Memoized for performance + const filteredRecipients = useMemo(() => { + if (!search.trim()) return allRecipients; + + const searchTerm = search.toLowerCase().trim(); + return allRecipients.filter((recipient) => { + const nameMatch = recipient.name.toLowerCase().includes(searchTerm); + const phoneMatch = recipient.phoneNumber + .toLowerCase() + .includes(searchTerm); + return nameMatch || phoneMatch; + }); + }, [allRecipients, search]); + + // Memoized renderItem function for FlatList performance + const renderDonorItem = useCallback( + ({ item }: { item: (typeof allRecipients)[0] }) => { + const itemId = `${item.type}-${item.id}`; + return ( + handleRecipientSelect(itemId)} + /> + ); + }, + [selectedRecipient, handleRecipientSelect] + ); + + // Memoized keyExtractor + const donorKeyExtractor = useCallback( + (item: (typeof allRecipients)[0]) => `${item.type}-${item.id}`, + [] + ); + + // Memoized ItemSeparator + const ItemSeparator = useCallback(() => , []); + + return ( + + + + + + + + + + + + + + + + {/* Horizontal Divider */} + + + + {t("selectdonor.toLabel")} + + + {selectedDonorData ? ( + + + + ) : ( + + )} + + + + + + + {t("selectdonor.forLabel")} + + + + + + + + {/* Combined Recipients List */} + + + {t("selectdonor.donorsTitle", { + count: filteredRecipients.length, + })} + + + {/* Loading State */} + {loading || recipientsLoading ? ( + + + {t("selectdonor.loadingDonors")} + + + ) : error || recipientsError ? ( + /* Error State */ + + + {t("selectdonor.errorTitle")} + + + {t("selectdonor.errorWithMessage", { + error: error || recipientsError, + })} + + + ) : !hasPermission ? ( + /* Permission Required */ + + + + {t("selectdonor.contactsPermissionTitle")} + + + {t("selectdonor.contactsPermissionSubtitle")} + + + + ) : filteredRecipients.length === 0 ? ( + /* Empty State */ + + + + {search.trim() + ? t("selectdonor.emptyTitleSearch") + : t("selectdonor.emptyTitleDefault")} + + + {search.trim() + ? t("selectdonor.emptySubtitleSearch") + : t("selectdonor.emptySubtitleDefault")} + + + ) : ( + /* Recipients List */ + + )} + + + + + + ); +} diff --git a/app/(root)/(screens)/selectrecip.tsx b/app/(root)/(screens)/selectrecip.tsx new file mode 100644 index 0000000..651c35b --- /dev/null +++ b/app/(root)/(screens)/selectrecip.tsx @@ -0,0 +1,571 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + FlatList, + ActivityIndicator, +} from "react-native"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { LucideUser } from "lucide-react-native"; +import BackButton from "~/components/ui/backButton"; +import { useContactsStore, useRecipientsStore } from "~/lib/stores"; +import { useLocalSearchParams, router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { + calculateTotalAmountForSending, + calculateProcessingFee, +} from "~/lib/utils/feeUtils"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { UserSearchService } from "~/lib/services/userSearchService"; + +const RecipientCard = ({ + name, + phoneNumber, + selected, + onPress, +}: { + name: string; + phoneNumber: string; + selected: boolean; + onPress: () => void; +}) => { + return ( + + + + + + + + {name} + + {phoneNumber} + + + + + ); +}; + +// Selected Recipient Pill Component +const SelectedRecipientPill = ({ + name, + phoneNumber, + onRemove, +}: { + name: string; + phoneNumber: string; + onRemove: () => void; +}) => { + return ( + + {name} + + × + + + ); +}; + +export default function SelectRecip() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + amount: string; + selectedContactId?: string; + selectedContactName?: string; + selectedContactPhone?: string; + }>(); + const { user, wallet, refreshWallet } = useAuthWithProfile(); + const { contacts, loading, error, hasPermission, requestPermission } = + useContactsStore(); + const { + recipients, + loading: recipientsLoading, + error: recipientsError, + } = useRecipientsStore(); + const [selectedRecipient, setSelectedRecipient] = useState( + null + ); + const [note, setNote] = useState(""); + const [search, setSearch] = useState(""); + const [isSending, setIsSending] = useState(false); + const [hasInitialized, setHasInitialized] = useState(false); + const [remoteRecipient, setRemoteRecipient] = useState<{ + id: string; + name: string; + phoneNumber: string; + type: "saved" | "contact"; + } | null>(null); + const remoteSearchTimeoutRef = useRef | null>( + null + ); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState< + string | undefined + >(); + const toastTimeoutRef = useRef | null>(null); + + const showToast = (title: string, description?: string) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Remote user search by email/username (users collection) + useEffect(() => { + if (remoteSearchTimeoutRef.current) { + clearTimeout(remoteSearchTimeoutRef.current); + remoteSearchTimeoutRef.current = null; + } + + const term = search.trim(); + if (!term || !term.includes("@")) { + setRemoteRecipient(null); + return; + } + + remoteSearchTimeoutRef.current = setTimeout(async () => { + const profile = await UserSearchService.findUserByEmail( + term.toLowerCase() + ); + + if (!profile) { + setRemoteRecipient(null); + return; + } + + setRemoteRecipient({ + id: profile.uid, + name: profile.fullName || profile.email || "User", + phoneNumber: profile.phoneNumber || "No phone number", + // Treat remote user as a saved recipient-type for transaction metadata + type: "saved", + }); + }, 500); + + return () => { + if (remoteSearchTimeoutRef.current) { + clearTimeout(remoteSearchTimeoutRef.current); + remoteSearchTimeoutRef.current = null; + } + }; + }, [search]); + + // Combine contacts and recipients into a single list + const getAllRecipients = () => { + const allRecipients: Array<{ + id: string; + name: string; + phoneNumber: string; + type: "saved" | "contact"; + }> = []; + + // Add saved recipients + recipients.forEach((recipient) => { + allRecipients.push({ + id: recipient.id, + name: recipient.fullName, + phoneNumber: recipient.phoneNumber, + type: "saved", + }); + }); + + // Add contacts (only if permission granted) + if (hasPermission && contacts) { + contacts.forEach((contact) => { + allRecipients.push({ + id: contact.id, + name: contact.name, + phoneNumber: contact.phoneNumbers?.[0]?.number || "No phone number", + type: "contact", + }); + }); + } + + // Include remote user search result (from users collection) if present + if (remoteRecipient) { + const alreadyExists = allRecipients.some( + (recipient) => recipient.id === remoteRecipient.id + ); + if (!alreadyExists) { + allRecipients.unshift(remoteRecipient); + } + } + + // If a contact was selected from the modal or QR scan, prioritize it + if (params.selectedContactId) { + const existingIndex = allRecipients.findIndex( + (recipient) => recipient.id === params.selectedContactId + ); + + if (existingIndex > -1) { + const selectedContact = allRecipients.splice(existingIndex, 1)[0]; + allRecipients.unshift(selectedContact); + } else if (params.selectedContactName && params.selectedContactPhone) { + // Synthetic recipient from QR scan or external source + allRecipients.unshift({ + id: params.selectedContactId, + name: params.selectedContactName, + phoneNumber: params.selectedContactPhone, + type: "contact", + }); + } + } + + return allRecipients; + }; + + // Handle initial selection from contact modal + useEffect(() => { + if (params.selectedContactId && !hasInitialized && contacts && recipients) { + // Find the selected contact in the combined list + const allRecipients = getAllRecipients(); + const selectedContact = allRecipients.find( + (recipient) => recipient.id === params.selectedContactId + ); + + if (selectedContact) { + setSelectedRecipient(`${selectedContact.type}-${selectedContact.id}`); + setHasInitialized(true); + } + } + }, [ + params.selectedContactId, + hasInitialized, + contacts, + recipients, + remoteRecipient, + ]); + + const handleRecipientSelect = (id: string) => { + setSelectedRecipient(id); + console.log("Selected recipient:", id); + }; + + const handleRemoveSelected = () => { + setSelectedRecipient(null); + setSearch(""); + }; + + // Get selected recipient data + const getSelectedRecipientData = () => { + if (!selectedRecipient) return null; + + const allRecipients = getAllRecipients(); + return allRecipients.find( + (recipient) => `${recipient.type}-${recipient.id}` === selectedRecipient + ); + }; + + const selectedRecipientData = getSelectedRecipientData(); + + const handleSendMoney = async () => { + if (!selectedRecipient || !params.amount || !user?.uid) { + showToast( + t("selectrecip.toastErrorTitle"), + t("selectrecip.toastMissingInfo") + ); + return; + } + + setIsSending(true); + + // Let the UI update (show spinner) before running validation and navigation + await new Promise((resolve) => setTimeout(resolve, 0)); + + let didNavigate = false; + + try { + const amountInCents = parseInt(params.amount); // Amount is already in cents + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("selectrecip.toastErrorTitle"), + t("selectrecip.toastInvalidAmount") + ); + return; + } + + // Check if user has sufficient balance (including processing fee) + const totalRequired = calculateTotalAmountForSending(amountInCents); + if (!wallet) { + showToast( + t("selectrecip.toastErrorTitle"), + t("selectrecip.toastWalletNotFound") + ); + return; + } + if (wallet.balance < totalRequired) { + const processingFee = calculateProcessingFee(amountInCents); + const required = (totalRequired / 100).toFixed(2); + const fee = (processingFee / 100).toFixed(2); + const available = (wallet.balance / 100).toFixed(2); + showToast( + t("selectrecip.toastInsufficientBalanceTitle"), + t("selectrecip.toastInsufficientBalanceDescription", { + required, + fee, + available, + }) + ); + return; + } + + // Find the selected recipient details + const allRecipients = getAllRecipients(); + const selectedRecipientData = allRecipients.find( + (recipient) => `${recipient.type}-${recipient.id}` === selectedRecipient + ); + + if (!selectedRecipientData) { + showToast( + t("selectrecip.toastErrorTitle"), + t("selectrecip.toastRecipientNotFound") + ); + return; + } + + // Navigate to donation screen first, then continue to confirmation from there + router.push({ + pathname: ROUTES.DONATION, + params: { + amount: params.amount, + recipientName: selectedRecipientData.name, + recipientPhoneNumber: selectedRecipientData.phoneNumber, + recipientType: selectedRecipientData.type, + recipientId: selectedRecipientData.id, + note: note.trim() || "", + type: "send", + }, + }); + + didNavigate = true; + + // Clear the note and selected recipient after navigation + setNote(""); + setSelectedRecipient(null); + } finally { + // If we didn't navigate (validation error), re-enable the button + if (!didNavigate) { + setIsSending(false); + } + // If we did navigate, keep isSending=true; this screen will unmount + } + }; + + const allRecipients = getAllRecipients(); + + // Filter recipients based on search input + const filteredRecipients = allRecipients.filter((recipient) => { + if (!search.trim()) return true; + + const searchTerm = search.toLowerCase().trim(); + const nameMatch = recipient.name.toLowerCase().includes(searchTerm); + const phoneMatch = recipient.phoneNumber.toLowerCase().includes(searchTerm); + + return nameMatch || phoneMatch; + }); + + return ( + + + + + + + + + + + + + + + + {/* Horizontal Divider */} + + + + {t("selectrecip.toLabel")} + + + {selectedRecipientData ? ( + + + + ) : ( + + )} + + + + + + + {t("selectrecip.forLabel")} + + + + + + + + {/* Combined Recipients List */} + + + {t("selectrecip.recipientsTitle", { + count: filteredRecipients.length, + })} + + + {/* Loading State */} + {loading || recipientsLoading ? ( + + + {t("selectrecip.loadingRecipients")} + + + ) : error || recipientsError ? ( + /* Error State */ + + + {t("selectrecip.errorTitle")} + + + {t("selectrecip.errorWithMessage", { + error: error || recipientsError, + })} + + + ) : !hasPermission ? ( + /* Permission Required */ + + + + {t("selectrecip.contactsPermissionTitle")} + + + {t("selectrecip.contactsPermissionSubtitle")} + + + + ) : filteredRecipients.length === 0 ? ( + /* Empty State */ + + + + {search.trim() + ? t("selectrecip.emptyTitleSearch") + : t("selectrecip.emptyTitleDefault")} + + + {search.trim() + ? t("selectrecip.emptySubtitleSearch") + : t("selectrecip.emptySubtitleDefault")} + + + ) : ( + /* Recipients List */ + `${item.type}-${item.id}`} + scrollEnabled={false} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + handleRecipientSelect(`${item.type}-${item.id}`) + } + /> + )} + /> + )} + + + + + + ); +} diff --git a/app/(root)/(screens)/sendbank.tsx b/app/(root)/(screens)/sendbank.tsx new file mode 100644 index 0000000..63dfe5e --- /dev/null +++ b/app/(root)/(screens)/sendbank.tsx @@ -0,0 +1,498 @@ +import React, { useEffect, useRef, useState } from "react"; +import { View, Text, ScrollView, Pressable } from "react-native"; +import Svg, { G, Path } from "react-native-svg"; +import { Button } from "~/components/ui/button"; +import { LucideChevronRightCircle } from "lucide-react-native"; +import { useLocalSearchParams, router } from "expo-router"; +import { TransactionService } from "~/lib/services/transactionService"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import BackButton from "~/components/ui/backButton"; +import { AwashIcon, TeleBirrIcon } from "~/components/ui/icons"; +import { + calculateTotalAmountForSending, + calculateProcessingFee, +} from "~/lib/utils/feeUtils"; +import { showAlert } from "~/lib/utils/alertUtils"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { useTranslation } from "react-i18next"; + +export default function SendBank() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + amount: string; + recipientName: string; + recipientPhoneNumber: string; + recipientType: "saved" | "contact"; + recipientId: string; + note: string; + transactionType?: "send" | "cashout"; + }>(); + + const { user } = useAuthWithProfile(); + const { wallet, refreshWallet } = useUserWallet(user); + const [selectedBank, setSelectedBank] = useState<"awash" | "telebirr" | null>( + null + ); + const [isAccountSheetVisible, setIsAccountSheetVisible] = useState(false); + const [selectedAccountId, setSelectedAccountId] = useState( + null + ); + const [isProcessing, setIsProcessing] = useState(false); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleBankSelection = (bankProvider: "awash" | "telebirr") => { + setSelectedBank(bankProvider); + if (bankProvider !== "awash") { + setSelectedAccountId(null); + setIsAccountSheetVisible(false); + } + }; + + const handleSendOrCashOutTransaction = async () => { + if (!selectedBank) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastNoMethod"), + "error" + ); + return; + } + + if (!params.amount || !user?.uid) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastMissingInfo"), + "error" + ); + return; + } + + if (selectedBank === "awash" && wallet?.cards && wallet.cards.length > 0) { + if (!selectedAccountId) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastNoAccount"), + "error" + ); + return; + } + } + + const amountInCents = parseInt(params.amount); // Amount is already in cents + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastInvalidAmount"), + "error" + ); + return; + } + + // Check if user has sufficient balance + if (params.transactionType === "cashout") { + // For cashout, no processing fee + if (!wallet || wallet.balance < amountInCents) { + const required = (amountInCents / 100).toFixed(2); + const available = ((wallet?.balance ?? 0) / 100).toFixed(2); + showToast( + t("sendbank.toastInsufficientBalanceTitle"), + t("sendbank.toastInsufficientBalanceCashoutDescription", { + required, + available, + }), + "error" + ); + return; + } + } else { + // For send money, include processing fee + const totalRequired = calculateTotalAmountForSending(amountInCents); + if (!wallet || wallet.balance < totalRequired) { + const processingFee = calculateProcessingFee(amountInCents); + const required = (totalRequired / 100).toFixed(2); + const fee = (processingFee / 100).toFixed(2); + const available = ((wallet?.balance ?? 0) / 100).toFixed(2); + showToast( + t("sendbank.toastInsufficientBalanceTitle"), + t("sendbank.toastInsufficientBalanceSendDescription", { + required, + fee, + available, + }), + "error" + ); + return; + } + } + + setIsProcessing(true); + + try { + let result; + let successMessage; + + console.log("PARAMS TRANSACTION TYPE", params.transactionType); + + if (params.transactionType === "cashout") { + // Handle cash out transaction + result = await TransactionService.cashOut(user.uid, { + amount: amountInCents, // Amount is already in cents + bankProvider: selectedBank, + note: params.note || "Cash out to bank account", + }); + successMessage = `Successfully cashed out $${( + amountInCents / 100 + ).toFixed(2)} to your ${selectedBank} account`; + } else { + // Handle send money transaction + if (!params.recipientName) { + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastMissingRecipient"), + "error" + ); + setIsProcessing(false); + return; + } + + result = await TransactionService.sendMoney(user.uid, { + amount: amountInCents, // Amount is already in cents + recipientName: params.recipientName, + recipientPhoneNumber: params.recipientPhoneNumber, + recipientType: params.recipientType, + recipientId: params.recipientId, + note: params.note || "", + }); + successMessage = `Successfully transferred $${( + amountInCents / 100 + ).toFixed(2)} to ${params.recipientName}`; + } + + if (result.success) { + await refreshWallet(); + + if (params.transactionType === "cashout") { + router.replace({ + pathname: ROUTES.CASHOUT_COMPLETION, + params: { + note: successMessage, + amount: (amountInCents / 100).toFixed(2), + }, + }); + } else { + router.replace({ + pathname: ROUTES.TASK_COMPLETION, + params: { + message: successMessage, + amount: (amountInCents / 100).toFixed(2), + recipientName: params.recipientName, + bankProvider: selectedBank, + }, + }); + } + } else { + showToast( + t("sendbank.toastErrorTitle"), + result.error || t("sendbank.toastProcessFailed"), + "error" + ); + } + } catch (error) { + console.error("Error processing transaction:", error); + showToast( + t("sendbank.toastErrorTitle"), + t("sendbank.toastProcessFailedWithRetry"), + "error" + ); + } finally { + setIsProcessing(false); + } + }; + + const handleClose = () => { + router.back(); + }; + + return ( + + + + + {params.amount && ( + + + + ${(parseInt(params.amount) / 100).toFixed(2)}{" "} + {params.transactionType === "cashout" + ? t("sendbank.amountTitleCashOut") + : t("sendbank.amountTitleToRecipient", { + recipientName: params.recipientName, + })} + + + + {params.note && ( + + {t("sendbank.noteWithText", { note: params.note })} + + )} + + setIsAccountSheetVisible(false)} + > + + + {t("sendbank.chooseAccountTitle")} + + + + {wallet?.cards && wallet.cards.length > 0 ? ( + <> + {wallet.cards.map((card) => ( + setSelectedAccountId(card.id)} + > + + + + + + + {t("sendbank.accountLabel")} + + + {card.cardNumber} + + + + + + ))} + + + + + + ) : ( + + {t("sendbank.noAccounts")} + + )} + + + + )} + + + + + + + {t("sendbank.paymentOptionsTitle")} + + + {selectedBank + ? t("sendbank.paymentOptionsSelected", { + providerName: + selectedBank === "awash" + ? t("sendbank.awashName") + : t("sendbank.telebirrName"), + }) + : t("sendbank.paymentOptionsUnselected")} + + + + + + {/* Telebirr Section */} + + {t("sendbank.telebirrName")} + + handleBankSelection("telebirr")} + disabled={isProcessing} + > + + + + + + + + + {t("sendbank.telebirrName")} + + + {t("sendbank.telebirrSubtitle")} + + + + + + + + + + + + {/* Bank Section with Awash */} + + Bank + + { + handleBankSelection("awash"); + setIsAccountSheetVisible(true); + }} + disabled={isProcessing} + > + + + + + + + + {t("sendbank.awashName")} + + + {t("sendbank.awashSubtitle")} + + + + + + + + + + + + + {/* Send Button */} + {selectedBank && !isProcessing && ( + + + + )} + + + + {t("sendbank.poweredBy")} + + + + + + ); +} diff --git a/app/(root)/(screens)/sendnotification.tsx b/app/(root)/(screens)/sendnotification.tsx new file mode 100644 index 0000000..e9a1848 --- /dev/null +++ b/app/(root)/(screens)/sendnotification.tsx @@ -0,0 +1,300 @@ +import React, { useMemo, useRef, useState } from "react"; +import { View, ScrollView, TouchableOpacity } from "react-native"; +import { Text } from "~/components/ui/text"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import BackButton from "~/components/ui/backButton"; +import { Input } from "~/components/ui/input"; +import ModalToast from "~/components/ui/toast"; +import { useRecipientsStore } from "~/lib/stores"; +import { + BellIcon, + MessageCircle, + LucideSlidersHorizontal, +} from "lucide-react-native"; +import BottomSheet from "~/components/ui/bottomSheet"; + +const getInitials = (name: string) => { + return name + .split(" ") + .map((word) => word.charAt(0).toUpperCase()) + .slice(0, 2) + .join(""); +}; + +export default function SendNotificationScreen() { + const { recipients } = useRecipientsStore(); + const [searchQuery, setSearchQuery] = useState(""); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + const [sheetVisible, setSheetVisible] = useState(false); + const [selectedRecipient, setSelectedRecipient] = useState(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const normalizedSearch = searchQuery.trim().toLowerCase(); + + const filteredRecipients = useMemo(() => { + const base = recipients || []; + + const bySearch = !normalizedSearch + ? base + : base.filter((recipient) => { + const name = recipient.fullName.toLowerCase(); + const phone = recipient.phoneNumber.toLowerCase(); + return ( + name.includes(normalizedSearch) || phone.includes(normalizedSearch) + ); + }); + return bySearch; + }, [recipients, normalizedSearch]); + + const handleSendInApp = (name: string) => { + showToast( + "In-app notification", + `We'd notify ${name} in the app (stub).`, + "success" + ); + }; + + const handleSendWhatsApp = (name: string) => { + showToast( + "WhatsApp notification", + `We'd open WhatsApp to message ${name} (stub).`, + "info" + ); + }; + + return ( + + + + + + + Send notification + + + Pick who you want to notify about upcoming or recent payments. + + + + + } + /> + + + {filteredRecipients.length === 0 && ( + + + No clients found. Add recipients first to send notifications. + + + )} + + {filteredRecipients.length > 0 && ( + + {filteredRecipients.map((recipient, index) => { + const initials = getInitials(recipient.fullName); + const lowerName = recipient.fullName.toLowerCase(); + const isBusiness = + lowerName.includes("ltd") || + lowerName.includes("plc") || + lowerName.includes("inc") || + lowerName.includes("company"); + const clientType = isBusiness ? "Business" : "Individual"; + + const hasSchedule = index % 2 === 1; + + return ( + + + + + + {initials} + + + + + + {recipient.fullName} + + + + {clientType} + + + + + {recipient.phoneNumber} + + + + + + + + Notification context + + + {hasSchedule + ? "Upcoming scheduled payment this week" + : "One-off payment reminder"} + + + {hasSchedule && ( + + + Has schedules + + + )} + + + + { + setSelectedRecipient(recipient); + setSheetVisible(true); + }} + > + + + Send notification + + + + + + ); + })} + + )} + + + + { + setSheetVisible(false); + setSelectedRecipient(null); + }} + maxHeightRatio={0.4} + > + {selectedRecipient && ( + + + Choose notification type + + + {`Who: ${selectedRecipient.fullName}`} + + + + { + handleSendInApp(selectedRecipient.fullName); + setSheetVisible(false); + setSelectedRecipient(null); + }} + > + + + + + In-app notification + + + Show inside Amba when they open the app. + + + + + + { + handleSendWhatsApp(selectedRecipient.fullName); + setSheetVisible(false); + setSelectedRecipient(null); + }} + > + + + + + WhatsApp message + + + Open WhatsApp to send them a quick update. + + + + + + + )} + + + + ); +} diff --git a/app/(root)/(screens)/sendorrequestmoney.tsx b/app/(root)/(screens)/sendorrequestmoney.tsx new file mode 100644 index 0000000..5587770 --- /dev/null +++ b/app/(root)/(screens)/sendorrequestmoney.tsx @@ -0,0 +1,431 @@ +import React, { useState, useEffect } from "react"; +import { + View, + ScrollView, + InteractionManager, + ActivityIndicator, +} from "react-native"; +import { useFocusEffect } from "expo-router"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserWallet } from "~/lib/hooks/useUserWallet"; +import { + validateBalanceWithFee, + getFeeInformation, + calculateTotalAmountForSending, +} from "~/lib/utils/feeUtils"; +import { + parseDisplayToCents, + formatDisplayAmount, +} from "~/lib/utils/monetaryUtils"; +import SendMoneyBar from "~/components/ui/sendMoneyBar"; +import { Big } from "big.js"; +import { PinConfirmationModal } from "~/components/ui/pinConfirmationModal"; +import { amountSchema, validate } from "~/lib/utils/validationSchemas"; +import { showAlert } from "~/lib/utils/alertUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +export default function SendOrRequestMoney() { + const { user } = useAuthWithProfile(); + const { wallet } = useUserWallet(user); + const [amount, setAmount] = useState(""); + const [showPinModal, setShowPinModal] = useState(false); + const [pendingAction, setPendingAction] = useState<"send" | "request" | null>( + null + ); + const [isSecurityVerified, setIsSecurityVerified] = useState(false); + const { t } = useTranslation(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + const task = InteractionManager.runAfterInteractions(() => { + if (__DEV__) { + console.log("SEND OR REQUEST MONEY PAGE MOUNTED"); + } + }); + return () => task.cancel(); + }, []); + + // Get contact params from navigation + const params = useLocalSearchParams<{ + selectedContactId?: string; + selectedContactName?: string; + selectedContactPhone?: string; + }>(); + + // Reset state and show PIN modal when screen comes into focus + // useFocusEffect only runs when screen is focused, so we can safely show modal here + useFocusEffect( + React.useCallback(() => { + setIsSecurityVerified(false); + setShowPinModal(true); + setAmount(""); + setPendingAction(null); + + // Cleanup: hide modal when screen loses focus + return () => { + setShowPinModal(false); + setIsSecurityVerified(false); + }; + }, []) + ); + + // Handle number input and special actions (copied from addcash.tsx) + const handleNumberPress = (input: string) => { + if (input === "clear") { + handleClear(); + return; + } + + if (input === "backspace") { + handleBackspace(); + return; + } + + // Handle decimal point + if (input === ".") { + // Prevent multiple decimals + if (amount.includes(".")) return; + + // If empty, start with "0." + if (amount === "") { + setAmount("0."); + return; + } + + // Add decimal point + setAmount(amount + "."); + return; + } + + // Handle digit input (0-9) + if (!/^[0-9]$/.test(input)) return; + + // Handle leading zeros + if (amount === "0" && input !== ".") { + setAmount(input); // Replace leading zero + return; + } + + const newAmount = amount + input; + + // Check decimal places limit (max 2 decimal places) + if (newAmount.includes(".")) { + const [whole, decimal] = newAmount.split("."); + if (decimal && decimal.length > 2) return; + } + + // Check maximum amount (max $999.99) + try { + const numValue = new Big(newAmount); + if (numValue.gt(999.99)) return; + } catch (error) { + return; + } + + // Check total length to prevent very long inputs + if (newAmount.length > 6) return; // Max: 999.99 + + setAmount(newAmount); + }; + + // Handle backspace + const handleBackspace = () => { + if (amount.length === 0) return; + + // Remove last character + const newAmount = amount.slice(0, -1); + setAmount(newAmount); + }; + + // Clear all input + const handleClear = () => { + setAmount(""); + }; + + // Validate if amount is valid for submission + const isValidAmount = () => { + if ( + !amount || + amount === "" || + amount === "0" || + amount === "0." || + amount === "0.00" + ) { + return false; + } + + const amountInCents = parseDisplayToCents(amount); + if (amountInCents < 1 || amountInCents > 99999) { + // 1 cent to $999.99 + return false; + } + + // Check if amount is within wallet balance (including processing fee) + const currentBalance = wallet?.balance || 0; // Balance in cents + const totalRequired = calculateTotalAmountForSending(amountInCents); + + return currentBalance >= totalRequired; + }; + + // Get validation error message + const getValidationError = () => { + // Validate basic amount using valibot + const amountValidationResult = validate( + amountSchema({ min: 1, max: 99999 }), + amount + ); + if (!amountValidationResult.success) { + return amountValidationResult.error; + } + + const amountInCents = parseDisplayToCents(amount); + if (amountInCents < 1) { + return t("sendorrequestmoney.validationMinAmount"); + } + + if (amountInCents > 99999) { + // $999.99 + return t("sendorrequestmoney.validationMaxAmount"); + } + + // Check if amount exceeds wallet balance (including processing fee) + const currentBalance = wallet?.balance || 0; // Balance in cents + const balanceValidation = validateBalanceWithFee( + currentBalance, + amountInCents + ); + + if (!balanceValidation.hasSufficientBalance) { + const balanceInDollars = currentBalance / 100; + const requiredInDollars = balanceValidation.requiredBalance / 100; + return t("sendorrequestmoney.validationInsufficientBalance", { + required: requiredInDollars.toFixed(2), + fee: balanceValidation.feeInfo.formatted.fee, + available: balanceInDollars.toFixed(2), + }); + } + + return null; + }; + + // Handle PIN confirmation success + const handlePinSuccess = () => { + setShowPinModal(false); + setIsSecurityVerified(true); + }; + + // Handle send money action (called after PIN is verified) + const handleSendMoney = async () => { + const validationError = getValidationError(); + if (validationError) { + showToast( + t("sendorrequestmoney.validationErrorTitle"), + validationError, + "error" + ); + return; + } + + const amountInCents = parseDisplayToCents(amount); + console.log("Sending money:", amountInCents, "cents"); + + router.push({ + pathname: ROUTES.SELECT_RECIPIENT, + params: { + amount: amountInCents.toString(), // Pass cents as string + selectedContactId: params.selectedContactId, + selectedContactName: params.selectedContactName, + selectedContactPhone: params.selectedContactPhone, + }, + }); + }; + + const handleRequestMoney = async () => { + const validationError = getValidationError(); + if (validationError) { + showToast( + t("sendorrequestmoney.validationErrorTitle"), + validationError, + "error" + ); + return; + } + + const amountInCents = parseDisplayToCents(amount); + console.log("Requesting money:", amountInCents, "cents"); + + router.push({ + pathname: ROUTES.SELECT_DONOR, + params: { + amount: amountInCents.toString(), // Pass cents as string + selectedContactId: params.selectedContactId, + selectedContactName: params.selectedContactName, + selectedContactPhone: params.selectedContactPhone, + }, + }); + }; + + return ( + + + {!isSecurityVerified && !showPinModal ? ( + + + + + {t("sendorrequestmoney.verifyingSecurity")} + + + ) : ( + + + + + + + + {t("sendorrequestmoney.availableBalanceLabel")} + + + + ${wallet ? (wallet.balance / 100).toFixed(2) : "0.00"} + + + + + {/* Big amount */} + + + {formatDisplayAmount(amount)} + + + + {/* Fee Information Display */} + + {amount && parseDisplayToCents(amount) > 0 && ( + + {(() => { + const amountInCents = parseDisplayToCents(amount); + const feeInfo = getFeeInformation(amountInCents); + return ( + + + {t("sendorrequestmoney.processingFee", { + fee: feeInfo.formatted.fee, + percent: "1.25", + })} + + + {t("sendorrequestmoney.totalLabel", { + total: feeInfo.formatted.total, + })} + + + ); + })()} + + )} + + + + + + + + + + + + + + + + + + + )} + + + {/* PIN Confirmation Modal */} + { + setShowPinModal(false); + }} + onSuccess={handlePinSuccess} + title={t("sendorrequestmoney.pinModalTitle")} + /> + + + ); +} diff --git a/app/(root)/(screens)/taskcomp.tsx b/app/(root)/(screens)/taskcomp.tsx new file mode 100644 index 0000000..2a04024 --- /dev/null +++ b/app/(root)/(screens)/taskcomp.tsx @@ -0,0 +1,390 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + View, + Text, + Image, + ScrollView, + Share, + TouchableOpacity, + TextInput, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { router, useLocalSearchParams } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { SuccessIcon } from "~/components/ui/icons"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import LottieView from "lottie-react-native"; +import { Icons } from "~/assets/icons"; + +export default function TaskComp() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + message?: string; + amount?: string; + recipientName?: string; + recipientPhoneNumber?: string; + flowType?: string; + }>(); + + const { wallet } = useAuthWithProfile(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const isEventTicketFlow = params.flowType === "event_ticket"; + + const [ratingSheetVisible, setRatingSheetVisible] = useState(false); + const [rating, setRating] = useState(0); + const [selectedIssues, setSelectedIssues] = useState([]); + const [comments, setComments] = useState(""); + const [selectedPurpose, setSelectedPurpose] = useState(null); + const [isPurposeDropdownOpen, setIsPurposeDropdownOpen] = useState(false); + const [otherPurpose, setOtherPurpose] = useState(""); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const toggleIssue = (key: string) => { + setSelectedIssues((prev) => + prev.includes(key) ? prev.filter((i) => i !== key) : [...prev, key] + ); + }; + + const handleSendAgain = () => { + const balance = wallet?.balance; + if (balance === undefined) { + showToast( + t("taskcomp.toastErrorTitle"), + t("taskcomp.toastNoBalance"), + "error" + ); + return; + } + if (balance < 1000) { + showToast( + t("taskcomp.toastErrorTitle"), + t("taskcomp.toastMinError"), + "error" + ); + return; + } + + // Navigate to home first, then to send money to prevent going back to success page + router.replace(ROUTES.HOME); + // Use setTimeout to ensure home navigation completes before send money navigation + router.push(ROUTES.SEND_OR_REQUEST_MONEY); + }; + + const handleSubmitRating = () => { + setRatingSheetVisible(false); + router.replace(ROUTES.HOME); + }; + + const handleShare = async () => { + try { + const shareMessage = params.message + ? t("taskcomp.shareMessageWithParam", { message: params.message }) + : t("taskcomp.shareMessageDefault"); + + const result = await Share.share({ + message: shareMessage, + title: t("taskcomp.shareTitle"), + }); + + if (result.action === Share.sharedAction) { + // Content was shared + console.log("Content shared successfully"); + } else if (result.action === Share.dismissedAction) { + // Share dialog was dismissed + console.log("Share dialog dismissed"); + } + } catch (error) { + console.error("Error sharing:", error); + showToast( + t("taskcomp.toastErrorTitle"), + t("taskcomp.toastShareError"), + "error" + ); + } + }; + return ( + + + + + {/* */} + + + {/* */} + {params.amount ? ( + + + {params.amount} + + + ) : ( + <> + )} + + + + {params.message || t("taskcomp.successDescription")} + + + + + + + {/* Bottom action buttons */} + + {isEventTicketFlow ? ( + <> + + + + + ) : ( + <> + + + + + + + )} + + + {!isEventTicketFlow && ratingSheetVisible && ( + + setRatingSheetVisible(false)} + /> + + + + + + + {t("taskcomp.ratingTitle")} + + + {t("taskcomp.ratingSubtitle")} + + + + + + {t("taskcomp.ratingOverallLabel")} + + + {[1, 2, 3, 4, 5].map((star) => ( + setRating(star)} + activeOpacity={0.8} + > + + ★ + + + ))} + + + + + + {t("taskcomp.ratingPurposeLabel")} + + + setIsPurposeDropdownOpen((prev) => !prev)} + className="border border-gray-300 rounded-2xl px-3 py-2 flex-row items-center justify-between" + > + + {selectedPurpose + ? t( + { + family: "taskcomp.ratingPurposeFamily", + medical: "taskcomp.ratingPurposeMedical", + loan: "taskcomp.ratingPurposeLoan", + purchase: "taskcomp.ratingPurposePurchase", + other: "taskcomp.ratingPurposeOther", + }[ + selectedPurpose as + | "family" + | "medical" + | "loan" + | "purchase" + | "other" + ] + ) + : t("taskcomp.ratingPurposePlaceholder")} + + + + {isPurposeDropdownOpen && ( + + {[ + { + key: "family", + labelKey: "taskcomp.ratingPurposeFamily", + }, + { + key: "medical", + labelKey: "taskcomp.ratingPurposeMedical", + }, + { key: "loan", labelKey: "taskcomp.ratingPurposeLoan" }, + { + key: "purchase", + labelKey: "taskcomp.ratingPurposePurchase", + }, + { key: "other", labelKey: "taskcomp.ratingPurposeOther" }, + ].map((option) => ( + { + setSelectedPurpose(option.key); + setIsPurposeDropdownOpen(false); + }} + > + + {t(option.labelKey as any)} + + + ))} + + )} + + + + {selectedPurpose === "other" && ( + + + {t("taskcomp.ratingOtherLabel")} + + + + + + )} + + + + + )} + + + ); +} diff --git a/app/(root)/(screens)/terms.tsx b/app/(root)/(screens)/terms.tsx new file mode 100644 index 0000000..adcdb42 --- /dev/null +++ b/app/(root)/(screens)/terms.tsx @@ -0,0 +1,284 @@ +import React, { useState } from "react"; +import { ScrollView, View, TouchableOpacity } from "react-native"; +import { Text } from "~/components/ui/text"; +import BackButton from "~/components/ui/backButton"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; + +type TabType = "terms" | "privacy"; + +export default function Terms() { + const [activeTab, setActiveTab] = useState("terms"); + + return ( + + + + + {/* Header */} + + Legal + + + {/* Tabs */} + + setActiveTab("terms")} + className="flex-1 mr-2" + activeOpacity={0.7} + > + + + Terms & Conditions + + + + + setActiveTab("privacy")} + className="flex-1 ml-2" + activeOpacity={0.7} + > + + + Privacy Policy + + + + + + {/* Content */} + + {activeTab === "terms" ? : } + + + + ); +} + +function TermsContent() { + return ( + + + Terms & Conditions + + + + + 1. Usage Rights + + + You are granted a limited, non-transferable license to use the app for + personal or commercial reading purposes. Unauthorized reproduction or + distribution of content is prohibited. + + + + + + 2. User Responsibilities + + + You agree to use the app ethically, without engaging in activities + that disrupt or harm the platform, including unauthorized access or + data scraping. + + + + + + 3. Purchases & Subscriptions + + + Some content may require a paid subscription or one-time purchase. + Payments are processed securely, and refunds will be handled according + to our refund policy. + + + + + + 4. Account Security + + + You are responsible for maintaining the confidentiality of your + account credentials. Notify us immediately of any unauthorized access + or security breaches. + + + + + + 5. Service Availability + + + We strive to maintain service availability but do not guarantee + uninterrupted access. Scheduled maintenance and updates may + temporarily affect service. + + + + + + 6. Limitation of Liability + + + Amba Pay is not liable for any indirect, incidental, or consequential + damages arising from your use of the service. Our total liability is + limited to the amount you paid for the service. + + + + + + 7. Modifications to Terms + + + We reserve the right to modify these terms at any time. Continued use + of the service after changes constitutes acceptance of the modified + terms. + + + + + + 8. Termination + + + We may suspend or terminate your account for violations of these + terms. You may also close your account at any time through the app + settings. + + + + + + Last updated: December 2024 + + + + ); +} + +function PrivacyContent() { + return ( + + + Privacy Policy + + + + + 1. Information We Collect + + + We collect information you provide directly, such as your name, email, + phone number, and payment information. We also collect usage data, + device information, and location data when you use our services. + + + + + + 2. How We Use Your Information + + + Your information is used to provide and improve our services, process + transactions, send notifications, prevent fraud, and comply with legal + obligations. We do not sell your personal information to third + parties. + + + + + + 3. Data Security + + + We implement industry-standard security measures to protect your data, + including encryption, secure servers, and regular security audits. + However, no method of transmission over the internet is 100% secure. + + + + + + 4. Information Sharing + + + We may share your information with service providers who help us + operate our platform, comply with legal requirements, or protect our + rights. We require these parties to maintain confidentiality. + + + + + + 5. Your Rights + + + You have the right to access, correct, or delete your personal + information. You can also opt out of marketing communications and + request data portability. Contact us to exercise these rights. + + + + + + 6. Cookies & Tracking + + + We use cookies and similar technologies to enhance your experience, + analyze usage patterns, and deliver personalized content. You can + manage cookie preferences in your device settings. + + + + + + 7. Children's Privacy + + + Our services are not intended for children under 13. We do not + knowingly collect personal information from children. If we discover + such data, we will delete it promptly. + + + + + + 8. Changes to Privacy Policy + + + We may update this privacy policy periodically. We will notify you of + significant changes via email or in-app notification. Your continued + use constitutes acceptance of the updated policy. + + + + + + Last updated: December 2024 + + + + ); +} diff --git a/app/(root)/(screens)/transconfirm.tsx b/app/(root)/(screens)/transconfirm.tsx new file mode 100644 index 0000000..18aa7ea --- /dev/null +++ b/app/(root)/(screens)/transconfirm.tsx @@ -0,0 +1,521 @@ +import React, { useEffect, useRef, useState } from "react"; +import { + Text, + View, + ScrollView, + TextInput, + ActivityIndicator, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { useLocalSearchParams, router } from "expo-router"; +import BackButton from "~/components/ui/backButton"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { TransactionService } from "~/lib/services/transactionService"; +import { + calculateTotalAmountForSending, + calculateProcessingFee, +} from "~/lib/utils/feeUtils"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { ROUTES } from "~/lib/routes"; +import { applyReferral } from "~/lib/services/referralService"; +import { awardPoints } from "~/lib/services/pointsService"; + +export default function TransConfirm() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + transactionId?: string; + amount?: string; + type?: string; + recipientName?: string; + recipientPhoneNumber?: string; + recipientType?: string; + recipientId?: string; + note?: string; + donationSkipped?: string; + donationType?: string; + donationAmount?: string; + donateAnonymously?: string; + donationCampaignId?: string; + donationCampaignTitle?: string; + fromSelectRecipientFlow?: string; + }>(); + + // Parse amount from cents to dollars + const amountInCents = parseInt(params.amount || "0"); + const amountInDollars = amountInCents / 100; + + // Donation amount (entered on donation screen) + const hasDonation = + params.donationSkipped === "false" && !!params.donationAmount; + + const donationAmountNumber = params.donationAmount + ? Number(params.donationAmount) + : NaN; + const donationAmountDisplay = isNaN(donationAmountNumber) + ? null + : donationAmountNumber.toFixed(2); + + const donationAmountDollars = + !isNaN(donationAmountNumber) && hasDonation ? donationAmountNumber : 0; + + // Calculate processing fee (1.25% of the amount) + const processingFeeInCents = Math.round(amountInCents * 0.0125); + const processingFeeInDollars = processingFeeInCents / 100; + + // Subtotal = main amount + donation + const subtotalInDollars = amountInDollars + donationAmountDollars; + + // Total = subtotal + processing fee + const totalInDollars = subtotalInDollars + processingFeeInDollars; + + const donationTypeLabel = + params.donationType === "monthly" ? "Monthly" : "One-Time"; + + const transactionDate = new Date(); + const formattedDate = transactionDate.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + + const { user, wallet, refreshWallet } = useAuthWithProfile(); + const [isSending, setIsSending] = useState(false); + const [referralCode, setReferralCode] = useState(""); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleConfirm = async () => { + const isAddCashFlow = params.type === "add_cash"; + + if (isAddCashFlow) { + // If a referral code is provided in the add-cash flow, apply it here + if (referralCode.trim() && user?.uid) { + try { + setIsSending(true); + const referralResult = await applyReferral({ + referralCode: referralCode.trim(), + uid: user.uid, + referralReason: "transaction", + contextId: params.transactionId || "add_cash", + }); + + console.log( + "[TransConfirm] Add-cash referral apply result", + referralResult + ); + + if (!referralResult.success) { + showToast( + t("transconfirm.toastErrorTitle"), + referralResult.error || "Failed to apply referral code", + "error" + ); + } + } catch (referralError) { + console.error( + "[TransConfirm] Error while applying add-cash referral code", + referralError + ); + showToast( + t("transconfirm.toastErrorTitle"), + "Failed to apply referral code", + "error" + ); + } finally { + setIsSending(false); + } + } + + router.push({ + pathname: ROUTES.CHECKOUT, + params: { + ...params, + }, + }); + return; + } + + if (!params.amount || !user?.uid) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastMissingDetails"), + "error" + ); + return; + } + + const amountInCents = parseInt(params.amount); + if (isNaN(amountInCents) || amountInCents <= 0) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastInvalidAmount"), + "error" + ); + return; + } + + // Check if user has sufficient balance (including processing fee) + const totalRequired = calculateTotalAmountForSending(amountInCents); + if (!wallet) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastWalletNotFound"), + "error" + ); + return; + } + if (wallet.balance < totalRequired) { + const processingFee = calculateProcessingFee(amountInCents); + const required = (totalRequired / 100).toFixed(2); + const fee = (processingFee / 100).toFixed(2); + const available = (wallet.balance / 100).toFixed(2); + showToast( + t("transconfirm.toastInsufficientBalanceTitle"), + t("transconfirm.toastInsufficientBalanceDescription", { + required, + fee, + available, + }), + "error" + ); + return; + } + + if ( + !params.recipientName || + !params.recipientPhoneNumber || + !params.recipientType || + !params.recipientId + ) { + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastRecipientMissing"), + "error" + ); + return; + } + + const isFromSelectRecipientFlow = params.fromSelectRecipientFlow === "true"; + + if (isFromSelectRecipientFlow) { + router.push({ + pathname: "checkout", + params: { + ...params, + }, + }); + return; + } + + setIsSending(true); + + try { + const result = await TransactionService.sendMoney(user.uid, { + amount: amountInCents, + recipientName: params.recipientName, + recipientPhoneNumber: params.recipientPhoneNumber, + recipientType: params.recipientType as "saved" | "contact", + recipientId: params.recipientId, + note: params.note?.trim() || "", + }); + + if (result.success) { + try { + await awardPoints("send_money"); + } catch (error) { + console.warn( + "[TransConfirm] Failed to award send money points", + error + ); + } + + await refreshWallet(); + + if (referralCode.trim() && result.transactionId) { + try { + const referralResult = await applyReferral({ + referralCode: referralCode.trim(), + uid: user.uid, + referralReason: "transaction", + contextId: result.transactionId, + }); + + console.log("[TransConfirm] Referral apply result", referralResult); + + if (!referralResult.success) { + console.warn( + "[TransConfirm] Failed to apply referral:", + referralResult.error + ); + showToast( + t("transconfirm.toastErrorTitle"), + referralResult.error || "Failed to apply referral code", + "error" + ); + } + } catch (referralError) { + console.error( + "[TransConfirm] Error while applying referral code", + referralError + ); + showToast( + t("transconfirm.toastErrorTitle"), + "Failed to apply referral code", + "error" + ); + } + } + + // Navigate to success screen with message + router.replace({ + pathname: "/(screens)/taskcomp", + params: { + message: `Transaction completed on your end. $${( + amountInCents / 100 + ).toFixed(2)} will be claimed by ${ + params.recipientName + } within 7 days, or the money will revert to your wallet.`, + amount: (amountInCents / 100).toFixed(2), + recipientName: params.recipientName, + recipientPhoneNumber: params.recipientPhoneNumber, + }, + }); + } else { + showToast( + t("transconfirm.toastErrorTitle"), + result.error || t("transconfirm.toastSendFailed"), + "error" + ); + } + } catch (error) { + console.error("Error sending money:", error); + showToast( + t("transconfirm.toastErrorTitle"), + t("transconfirm.toastSendFailedWithRetry"), + "error" + ); + } finally { + setIsSending(false); + } + }; + + return ( + + + + + + + + + {/* Centered content: title + card */} + + + + {t("transconfirm.title")} + + + + + + + ${amountInDollars.toFixed(2)} + + + {params.type === "add_cash" + ? "You are planning to add money to your wallet." + : t("transconfirm.planningDescription", { + recipientName: params.recipientName, + })} + + + + + {t("transconfirm.sectionTitle")} + + + {params.note && ( + + + {t("transconfirm.noteLabel")} + + + {params.note} + + + )} + + {hasDonation && donationAmountDisplay && ( + <> + + Donation + + ${donationAmountDisplay} + + + + + Donation Type + + {donationTypeLabel} + + + + )} + + + Date + + {formattedDate} + + + + + + {t("transconfirm.processingFeeLabel")} + + + ${processingFeeInDollars.toFixed(2)} + + + + + + {t("transconfirm.subtotalLabel")} + + + ${subtotalInDollars.toFixed(2)} + + + + + + {t("transconfirm.totalLabel")} + + + ${totalInDollars.toFixed(2)} + + + + + + + + {t("transconfirm.referralCodeLabel", "Referral Code")} + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/(screens)/transdetail.tsx b/app/(root)/(screens)/transdetail.tsx new file mode 100644 index 0000000..be6d622 --- /dev/null +++ b/app/(root)/(screens)/transdetail.tsx @@ -0,0 +1,433 @@ +import React, { useRef, useState } from "react"; +import { + Text, + View, + ScrollView, + TextInput, + ActivityIndicator, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { router, useLocalSearchParams } from "expo-router"; +import BackButton from "~/components/ui/backButton"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import useTransactions from "~/lib/hooks/useTransactions"; +import { ROUTES } from "~/lib/routes"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useTranslation } from "react-i18next"; +import ModalToast from "~/components/ui/toast"; +import { TicketService } from "~/lib/services/ticketService"; +import { awardPoints } from "~/lib/services/pointsService"; +import { applyReferral } from "~/lib/services/referralService"; + +export default function TransDetail() { + const { t } = useTranslation(); + const params = useLocalSearchParams<{ + transactionId?: string; + amount?: string; + type?: string; + recipientName?: string; + date?: string; + status?: string; + note?: string; + flowType?: string; + ticketTierId?: string; + eventId?: string; + ticketCount?: string; + fromHistory?: string; + }>(); + + const { user } = useAuthWithProfile(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const [isProcessing, setIsProcessing] = useState(false); + const [referralCode, setReferralCode] = useState(""); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Parse amount from cents to dollars + const amountInCents = parseInt(params.amount || "0"); + const amountInDollars = amountInCents / 100; + + // Calculate processing fee (1.25% of the amount) + const processingFeeInCents = Math.round(amountInCents * 0.0125); + const processingFeeInDollars = processingFeeInCents / 100; + + // Calculate total (amount + processing fee) + const totalInCents = amountInCents + processingFeeInCents; + const totalInDollars = totalInCents / 100; + + const isEventTicket = params.type === "event_ticket"; + const fromHistory = params.fromHistory === "true"; + + // Format date + const formatDate = (dateString?: string) => { + if (!dateString) return t("transdetail.dateUnknown"); + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + month: "2-digit", + day: "2-digit", + year: "numeric", + }); + }; + + // Get transaction description based on type + const getTransactionDescription = () => { + switch (params.type) { + case "send": + return t("transdetail.descriptionSend", { + recipientName: params.recipientName, + }); + case "receive": + return t("transdetail.descriptionReceive", { + recipientName: params.recipientName, + }); + case "add_cash": + return t("transdetail.descriptionAddCash"); + case "cash_out": + return t("transdetail.descriptionCashOut"); + case "event_ticket": + return `Ticket purchase for ${params.recipientName || "event"}`; + default: + return t("transdetail.descriptionDefault"); + } + }; + + const handlePrimaryAction = async () => { + if (isEventTicket) { + if (!user) { + showToast( + t("transconfirm.toastErrorTitle"), + "You must be logged in to buy tickets.", + "error" + ); + return; + } + + const ticketTierId = params.ticketTierId; + const eventId = params.eventId; + const ticketCount = params.ticketCount ? parseInt(params.ticketCount) : 1; + + if (!ticketTierId || !eventId || !ticketCount || isNaN(ticketCount)) { + showToast( + t("transconfirm.toastErrorTitle"), + "Missing ticket information.", + "error" + ); + return; + } + + try { + setIsProcessing(true); + const token = await user.getIdToken(); + await TicketService.buyTicket(token, { + ticketTierId, + eventId, + ticketCount, + }); + + try { + await awardPoints("purchase_ticket"); + } catch (error) { + console.warn( + "[TransDetail] Failed to award purchase ticket points", + error + ); + } + + // Apply referral for event purchase if a code was provided + if (referralCode.trim() && eventId) { + try { + const referralResult = await applyReferral({ + referralCode: referralCode.trim(), + uid: user.uid, + referralReason: "event", + contextId: eventId, + }); + + console.log( + "[TransDetail] Event referral apply result", + referralResult + ); + + if (!referralResult.success) { + console.warn( + "[TransDetail] Failed to apply event referral:", + referralResult.error + ); + showToast( + t("transconfirm.toastErrorTitle"), + referralResult.error || "Failed to apply referral code", + "error" + ); + } + } catch (referralError) { + console.error( + "[TransDetail] Error while applying event referral code", + referralError + ); + showToast( + t("transconfirm.toastErrorTitle"), + "Failed to apply referral code", + "error" + ); + } + } + + router.replace({ + pathname: "/(screens)/taskcomp", + params: { + amount: amountInDollars.toFixed(2), + message: `Ticket for ${ + params.recipientName || "event" + } purchased successfully.`, + flowType: "event_ticket", + }, + }); + } catch (error) { + console.error("[TransDetail] Error buying ticket", error); + + let apiMessage: string | undefined; + if (error instanceof Error) { + try { + const parsed = JSON.parse(error.message); + if (parsed && typeof parsed.message === "string") { + apiMessage = parsed.message; + } + } catch {} + } + + showToast( + t("transconfirm.toastErrorTitle"), + apiMessage || "Could not complete ticket purchase. Please try again.", + "error" + ); + } finally { + setIsProcessing(false); + } + + return; + } + + if (!fromHistory && referralCode.trim() && user && params.transactionId) { + try { + setIsProcessing(true); + const referralResult = await applyReferral({ + referralCode: referralCode.trim(), + uid: user.uid, + referralReason: "transaction", + contextId: params.transactionId, + }); + + console.log( + "[TransDetail] Transaction referral apply result", + referralResult + ); + + if (!referralResult.success) { + console.warn( + "[TransDetail] Failed to apply transaction referral:", + referralResult.error + ); + showToast( + t("transconfirm.toastErrorTitle"), + referralResult.error || "Failed to apply referral code", + "error" + ); + } + } catch (referralError) { + console.error( + "[TransDetail] Error while applying transaction referral code", + referralError + ); + showToast( + t("transconfirm.toastErrorTitle"), + "Failed to apply referral code", + "error" + ); + } finally { + setIsProcessing(false); + } + } + + router.replace(ROUTES.HOME); + router.push(ROUTES.SEND_OR_REQUEST_MONEY); + }; + + return ( + + + + + + + + + + {t("transdetail.title")} + + + + + + + ${amountInDollars.toFixed(2)} + + + {getTransactionDescription()} + + + + + {t("transdetail.sectionTitle")} + + + + {t("transdetail.dateLabel")} + + + {formatDate(params.date)} + + + + + + {t("transdetail.statusLabel")} + + + {params.status || t("transdetail.statusUnknown")} + + + + {params.note && ( + + + {t("transdetail.noteLabel")} + + + {params.note} + + + )} + + + + {t("transdetail.processingFeeLabel")} + + + ${processingFeeInDollars.toFixed(2)} + + + + + + {t("transdetail.subtotalLabel")} + + + ${amountInDollars.toFixed(2)} + + + + + + {t("transdetail.totalLabel")} + + + ${totalInDollars.toFixed(2)} + + + + + + {/* Referral code input (only shown when not coming from History) */} + {!fromHistory && ( + + + {t("transdetail.referralCodeLabel", "Referral Code")} + + + + )} + + + + + + + + + ); +} diff --git a/app/(root)/(tabs)/_layout.tsx b/app/(root)/(tabs)/_layout.tsx new file mode 100644 index 0000000..d325078 --- /dev/null +++ b/app/(root)/(tabs)/_layout.tsx @@ -0,0 +1,238 @@ +import React, { useEffect, useState } from "react"; +import { + createMaterialTopTabNavigator, + MaterialTopTabNavigationOptions, + MaterialTopTabNavigationEventMap, + MaterialTopTabBarProps, +} from "@react-navigation/material-top-tabs"; +import { withLayoutContext } from "expo-router"; +import { ParamListBase, TabNavigationState } from "@react-navigation/native"; +import { useAuthStore } from "~/lib/stores/authStore"; +import { useUserProfileStore } from "~/lib/stores/userProfileStore"; +import { router } from "expo-router"; +import { View, Pressable, StyleSheet, Image, Platform } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useTabStore } from "~/lib/stores"; +import { Icons } from "~/assets/icons"; +import { + History, + CalendarClock, + ListChecks, + CalendarRange, + ListChecksIcon, +} from "lucide-react-native"; + +const { Navigator } = createMaterialTopTabNavigator(); + +// Wrap the navigator with expo-router's layout context +export const MaterialTopTabs = withLayoutContext< + MaterialTopTabNavigationOptions, + typeof Navigator, + TabNavigationState, + MaterialTopTabNavigationEventMap +>(Navigator); + +// Screen title mapping - automatically includes all screens in (tabs) folder +// Add new screens here as you create them in the (tabs) folder +const screenTitles: Record = { + index: "Home", + schedules: "Schedules", + requests: "Requests", + listrecipient: "Recipients", + history: "Transactions", +}; + +const TAB_BAR_HEIGHT = 72; + +const tabIcons: Record React.ReactNode> = { + index: (color: string) => ( + + ), + schedules: (color: string) => , + requests: (color: string) => , + listrecipient: (color: string) => ( + + ), + history: (color: string) => , +}; + +const getHrefForRoute = (routeName: string) => + routeName === "index" ? "/" : `/(tabs)/${routeName}`; + +// Primary green color used across the app (matches app's light theme) +const PRIMARY_COLOR = "hsl(147,55%,28%)"; +const INACTIVE_COLOR = "#d1d5db"; + +function CustomTabBar({ state, navigation }: MaterialTopTabBarProps) { + const { setLastVisitedTab } = useTabStore(); + const [selectedIndex, setSelectedIndex] = React.useState(state.index); + const insets = useSafeAreaInsets(); + + React.useEffect(() => { + setSelectedIndex(state.index); + }, [state.index]); + + // App uses light theme - use primary green color + const activeColor = PRIMARY_COLOR; + const inactiveColor = INACTIVE_COLOR; + + // Calculate bottom padding: use reduced safe area inset on iOS, or 16px on Android + const bottomPadding = + Platform.OS === "ios" ? Math.max(insets.bottom * 0.6, 8) : 16; + + return ( + + {state.routes.map((route, index) => { + const focused = selectedIndex === index; + const href = getHrefForRoute(route.name); + const Icon = tabIcons[route.name]; + + const onPress = () => { + setSelectedIndex(index); + const event = navigation.emit({ + type: "tabPress", + target: route.key, + canPreventDefault: true, + }); + + if (!focused && !event.defaultPrevented) { + setLastVisitedTab(href); + navigation.navigate(route.name); + } + }; + + return ( + + {Icon ? Icon(focused ? activeColor : inactiveColor) : null} + + ); + })} + + ); +} + +export default function TabLayout() { + const { user, loading } = useAuthStore(); + const { profiles } = useUserProfileStore(); + const [isAgent, setIsAgent] = React.useState(null); + const [checkingAgent, setCheckingAgent] = React.useState(false); + + // Get the current user's profile entry + const profileEntry = user?.uid ? profiles[user.uid] : null; + const profile = profileEntry?.profile; + const profileLoading = profileEntry?.loading ?? false; + + const isDevEmulatorUser = __DEV__ && user?.uid === "dev-emulator-user"; + + // Check if user is an agent when profile is not available + useEffect(() => { + if (!user || profile || checkingAgent || isAgent !== null) { + return; + } + + const checkAgent = async () => { + setCheckingAgent(true); + try { + const { AuthService } = await import("~/lib/services/authServices"); + const agentExists = await AuthService.checkAgentExists(user.uid); + setIsAgent(agentExists); + } catch (error) { + console.error('TabLayout - error checking agent:', error); + setIsAgent(false); + } finally { + setCheckingAgent(false); + } + }; + + checkAgent(); + }, [user, profile, checkingAgent, isAgent]); + + // Redirect to agent signup if not authenticated or no profile/agent + useEffect(() => { + if (!loading && !profileLoading && !checkingAgent) { + // In dev, allow the fake emulator user even without a profile + if (isDevEmulatorUser) { + return; + } + + if (!user || (!profile && isAgent === false)) { + console.log( + "TabLayout - redirecting to agent signin, user:", + !!user, + "profile:", + !!profile, + "isAgent:", + isAgent + ); + router.replace("/auth/agent-signin"); + } + } + }, [user, loading, profile, profileLoading, isDevEmulatorUser, isAgent, checkingAgent]); + + // Don't render tabs if not authenticated or no profile/agent + // In dev, still render for the emulator user even without profile + if (!isDevEmulatorUser && (loading || profileLoading || checkingAgent || !user || (!profile && isAgent !== true))) { + return null; + } + + return ( + } + > + {Object.entries(screenTitles).map(([name, title]) => ( + + ))} + + ); +} + +const styles = StyleSheet.create({ + tabBarContainer: { + position: "absolute", + left: 0, + right: 0, + bottom: 0, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-around", + paddingTop: 16, + paddingHorizontal: 4, + backgroundColor: "#ffffff", // tailwind gray-50 + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: "#e5e7eb", // tailwind gray-200 + zIndex: 10, + }, + tabButton: { + flex: 1, + alignItems: "center", + }, +}); diff --git a/app/(root)/(tabs)/history.tsx b/app/(root)/(tabs)/history.tsx new file mode 100644 index 0000000..1b8b9b7 --- /dev/null +++ b/app/(root)/(tabs)/history.tsx @@ -0,0 +1,577 @@ +import React from "react"; +import { View, Text, ScrollView, Image, TouchableOpacity } from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Input } from "~/components/ui/input"; +import { Icons } from "~/assets/icons"; +import TopBar from "~/components/ui/topBar"; +import BottomSheet from "~/components/ui/bottomSheet"; + +type TransactionStatus = "success" | "pending" | "failed"; + +type AgentTransaction = { + id: string; + clientName: string; + amount: string; + currency: string; + status: TransactionStatus; + date: string; + time: string; + method: "telebirr" | "chapa" | "bank" | "cash"; + reference?: string; +}; + +const MOCK_TRANSACTIONS: AgentTransaction[] = [ + { + id: "tx-1", + clientName: "Abebe Kebede", + amount: "4,500", + currency: "ETB", + status: "success", + date: "Today", + time: "09:32 AM", + method: "telebirr", + reference: "REF-938201", + }, + { + id: "tx-2", + clientName: "Sara Alemu", + amount: "250", + currency: "USD", + status: "pending", + date: "Today", + time: "01:15 PM", + method: "chapa", + reference: "REF-938202", + }, + { + id: "tx-3", + clientName: "Hope Community Fund", + amount: "32,000", + currency: "ETB", + status: "success", + date: "Yesterday", + time: "05:47 PM", + method: "bank", + reference: "REF-937812", + }, + { + id: "tx-4", + clientName: "Kidus Tadesse", + amount: "900", + currency: "ETB", + status: "failed", + date: "Mon, 06 Jan", + time: "10:02 AM", + method: "telebirr", + reference: "REF-937111", + }, + { + id: "tx-5", + clientName: "Blue Nile Traders", + amount: "12,750", + currency: "ETB", + status: "success", + date: "Sun, 05 Jan", + time: "03:28 PM", + method: "cash", + reference: "REF-936444", + }, +]; + +const getStatusPillClasses = (status: TransactionStatus) => { + switch (status) { + case "success": + return "bg-green-100 text-green-700"; + case "pending": + return "bg-yellow-100 text-yellow-700"; + case "failed": + default: + return "bg-red-100 text-red-700"; + } +}; + +export default function AgentTab() { + const [search, setSearch] = React.useState(""); + const [statusFilter, setStatusFilter] = React.useState< + "all" | TransactionStatus + >("all"); + const [methodFilter, setMethodFilter] = React.useState< + "all" | "telebirr" | "chapa" | "bank" | "cash" + >("all"); + const [selectedTx, setSelectedTx] = React.useState( + null + ); + const [filterSheetVisible, setFilterSheetVisible] = React.useState(false); + + const normalizedSearch = search.trim().toLowerCase(); + + const filteredTransactions = React.useMemo(() => { + return MOCK_TRANSACTIONS.filter((tx) => { + const matchesStatus = + statusFilter === "all" || tx.status === statusFilter; + + const matchesMethod = + methodFilter === "all" || tx.method === methodFilter; + + const matchesSearch = + !normalizedSearch || + tx.clientName.toLowerCase().includes(normalizedSearch) || + (tx.reference && tx.reference.toLowerCase().includes(normalizedSearch)); + + return matchesStatus && matchesMethod && matchesSearch; + }); + }, [statusFilter, methodFilter, normalizedSearch]); + + const { + todayTotalEtb, + weekTotalEtb, + totalTxns, + successCount, + pendingCount, + failedCount, + } = React.useMemo(() => { + const parseAmount = (amount: string) => + parseInt(amount.replace(/,/g, ""), 10) || 0; + + let todayTotal = 0; + let weekTotal = 0; + let success = 0; + let pending = 0; + let failed = 0; + + MOCK_TRANSACTIONS.forEach((tx) => { + if (tx.status === "success") success += 1; + if (tx.status === "pending") pending += 1; + if (tx.status === "failed") failed += 1; + + if (tx.currency === "ETB") { + const val = parseAmount(tx.amount); + weekTotal += val; + if (tx.date === "Today") { + todayTotal += val; + } + } + }); + + return { + todayTotalEtb: todayTotal, + weekTotalEtb: weekTotal, + totalTxns: MOCK_TRANSACTIONS.length, + successCount: success, + pendingCount: pending, + failedCount: failed, + }; + }, []); + + const groupedTransactions = React.useMemo(() => { + const groups: Record = {}; + filteredTransactions.forEach((tx) => { + if (!groups[tx.date]) { + groups[tx.date] = []; + } + groups[tx.date].push(tx); + }); + return Object.entries(groups); + }, [filteredTransactions]); + + return ( + + + + + + + + Transactions + + + All the transactions you have processed. UI-only reporting & + tracking. + + + Last updated: Just now (mock data) + + + + {/* Search / filter bar */} + + setFilterSheetVisible(true)} + > + + + } + /> + + + {/* Optional summary row (dummy numbers) */} + + + + Today + + + ETB {todayTotalEtb.toLocaleString("en-ET")} + + + + + This week + + + ETB {weekTotalEtb.toLocaleString("en-ET")} + + + + + Total txns + + + {totalTxns} + + + + + + Success: {successCount} · Pending: {pendingCount} · Failed:{" "} + {failedCount} + + + {/* Transactions list */} + {groupedTransactions.map(([dateLabel, txs]) => ( + + + {dateLabel} + + {txs.map((tx) => { + const pillClasses = getStatusPillClasses(tx.status); + + return ( + setSelectedTx(tx)} + > + + + + + {tx.currency} {tx.amount} + + + + {tx.status === "success" + ? "Success" + : tx.status === "pending" + ? "Pending" + : "Failed"} + + + + + + + {tx.clientName} + + + {tx.time} + + + + + + + + + + {tx.method === "telebirr" + ? "Telebirr" + : tx.method === "chapa" + ? "Chapa" + : tx.method === "bank" + ? "Bank transfer" + : "Cash"} + + + + {tx.reference && ( + + {tx.reference} + + )} + + + + + ); + })} + + ))} + + {groupedTransactions.length === 0 && ( + + + No transactions match your filters. + + + )} + + + {/* Filter BottomSheet */} + setFilterSheetVisible(false)} + maxHeightRatio={0.5} + > + + + Filters + + + {/* Status filter chips */} + + Status + + + {["all", "success", "pending", "failed"].map((status) => { + const isActive = statusFilter === status; + const label = + status === "all" + ? "All" + : status === "success" + ? "Success" + : status === "pending" + ? "Pending" + : "Failed"; + return ( + + setStatusFilter(status as "all" | TransactionStatus) + } + > + + {label} + + + ); + })} + + + {/* Method filter chips */} + + Method + + + {["all", "telebirr", "chapa", "bank", "cash"].map((method) => { + const isActive = methodFilter === method; + const label = + method === "all" + ? "All methods" + : method === "telebirr" + ? "Telebirr" + : method === "chapa" + ? "Chapa" + : method === "bank" + ? "Bank" + : "Cash"; + return ( + + setMethodFilter( + method as "all" | "telebirr" | "chapa" | "bank" | "cash" + ) + } + > + + {label} + + + ); + })} + + + + { + setStatusFilter("all"); + setMethodFilter("all"); + }} + > + + Reset + + + setFilterSheetVisible(false)} + > + + Apply + + + + + + setSelectedTx(null)} + maxHeightRatio={0.6} + > + {selectedTx && ( + + + Transaction Details + + + {selectedTx.currency} {selectedTx.amount} + + + + {selectedTx.status === "success" + ? "Success" + : selectedTx.status === "pending" + ? "Pending" + : "Failed"} + + + + + + + Client + + + {selectedTx.clientName} + + + + + Date & time + + + {selectedTx.date} · {selectedTx.time} + + + + + Method + + + + + + + {selectedTx.method === "telebirr" + ? "Telebirr" + : selectedTx.method === "chapa" + ? "Chapa" + : selectedTx.method === "bank" + ? "Bank transfer" + : "Cash"} + + + + {selectedTx.reference && ( + + + Reference + + + {selectedTx.reference} + + + )} + + + + + + Repeat Payment + + + + + Share Receipt + + + + + )} + + + ); +} diff --git a/app/(root)/(tabs)/index.tsx b/app/(root)/(tabs)/index.tsx new file mode 100644 index 0000000..39a3ae3 --- /dev/null +++ b/app/(root)/(tabs)/index.tsx @@ -0,0 +1,644 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + View, + Text, + ScrollView, + TouchableOpacity, + Pressable, + FlatList, + Platform, + ImageBackground, + Image, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import ProfileCard from "~/components/ui/profileCard"; +import ContactModal from "~/components/ui/contactModal"; +import ProtectedRoute from "~/components/other/protectedRoute"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { + Users, + AlertCircle, + BellIcon, + Plus, + ScanBarcode, + LucideScan, + ScanIcon, + ScanBarcodeIcon, + ScanLine, +} from "lucide-react-native"; +import { Link, router, useFocusEffect } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { Contact, useContactsStore } from "~/lib/stores"; +import { useTransactions } from "~/lib/hooks/useTransactions"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Icons } from "~/assets/icons"; +import Skeleton from "~/components/ui/skeleton"; +import { getAuthInstance } from "~/lib/firebase"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +// Contacts are only available on native platforms +const isContactsSupported = Platform.OS !== "web"; + +const UPCOMING_REMINDERS = [ + { + id: "rem-1", + clientName: "Abebe Kebede", + datetimeLabel: "Today · 4:00 PM", + status: "Upcoming", + }, + { + id: "rem-2", + clientName: "Sara Alemu", + datetimeLabel: "Tomorrow · 9:30 AM", + status: "Upcoming", + }, + { + id: "rem-3", + clientName: "Hope Community Fund", + datetimeLabel: "Fri, 12 Jan · 2:15 PM", + status: "Scheduled", + }, +]; + +const EXCHANGE_RATES = [ + { code: "USD", name: "US Dollar", rateToETB: 57.2 }, + { code: "AUD", name: "Australian Dollar", rateToETB: 38.4 }, + { code: "EUR", name: "Euro", rateToETB: 62.9 }, + { code: "GBP", name: "British Pound", rateToETB: 73.5 }, +]; + +const getHomeTxStatusPillClasses = (status: string) => { + switch (status) { + case "completed": + return "bg-emerald-100 text-emerald-700"; + case "pending": + return "bg-yellow-100 text-yellow-700"; + case "failed": + default: + return "bg-red-100 text-red-700"; + } +}; + +const formatHomeTxAmount = (amountInCents: number) => { + return `$${(amountInCents / 100).toFixed(2)}`; +}; + +const formatHomeTxTime = (createdAt: Date) => { + const d = createdAt instanceof Date ? createdAt : new Date(createdAt); + return d.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }); +}; + +const getHomeTxClientName = (tx: any) => { + if (tx.type === "send") return tx.recipientName || "Client"; + if (tx.type === "receive") return tx.senderName || "Client"; + if (tx.type === "add_cash") return "Card top up"; + if (tx.type === "cash_out") return "Cash out"; + return "Transaction"; +}; + +const getHomeTxMethodLabel = (tx: any) => { + if (tx.type === "send") return "Telebirr"; + if (tx.type === "receive") return "Wallet"; + if (tx.type === "add_cash") return "Card"; + if (tx.type === "cash_out") { + const provider = tx.bankProvider + ? tx.bankProvider.charAt(0).toUpperCase() + tx.bankProvider.slice(1) + : "Bank"; + return provider === "Telebirr" ? "Telebirr" : "Bank transfer"; + } + return "Wallet"; +}; + +export default function Home() { + const { user, wallet, walletLoading, walletError } = useAuthWithProfile(); + const { t } = useTranslation(); + const { profile, profileLoading } = useAuthWithProfile(); + const insets = useSafeAreaInsets(); + const fullName = profile?.fullName; + const firstName = fullName?.split(" ")[0]; + const { + contacts, + loading: contactsLoading, + error: contactsError, + hasPermission, + requestPermission, + } = useContactsStore(); + const { + transactions, + loading: transactionsLoading, + error: transactionsError, + } = useTransactions(user?.uid); + const scrollRef = useRef(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const displayName = user?.displayName || ""; + const avatarSource = profile?.photoUrl + ? { uri: profile.photoUrl } + : Icons.avatar; + + // Log Firebase ID token when Home mounts (for debugging) + useEffect(() => { + const logIdToken = async () => { + try { + const auth = getAuthInstance(); + const currentUser = auth.currentUser; + if (!currentUser) { + console.log("HOME: No current Firebase user, cannot get ID token"); + return; + } + + const idToken = await currentUser.getIdToken(); + console.log("HOME SCREEN ID TOKEN:", idToken); + } catch (error) { + console.warn("HOME: Failed to get ID token:", error); + } + }; + + logIdToken(); + }, []); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Modal state + const [selectedContact, setSelectedContact] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + + const handleContactPress = (contact: Contact) => { + setSelectedContact(contact); + setModalVisible(true); + }; + + const handleCloseModal = () => { + setModalVisible(false); + setSelectedContact(null); + }; + + // Scroll to top whenever Home tab regains focus + useFocusEffect( + React.useCallback(() => { + if (scrollRef.current) { + scrollRef.current.scrollTo({ y: 0, animated: false }); + } + }, []) + ); + + const handleCashOut = () => { + const balance = wallet?.balance; + if (balance === undefined) { + showToast( + t("home.cashoutErrorTitle"), + t("home.cashoutNoBalance"), + "error" + ); + return; + } + if (balance < 1000) { + showToast( + t("home.cashoutErrorTitle"), + t("home.cashoutMinError"), + "warning" + ); + return; + } + + router.push(ROUTES.CASH_OUT); + }; + + const handleAddCash = () => { + router.push(ROUTES.ADD_CASH); + }; + + const handleAddCard = () => { + router.push(ROUTES.ADD_CARD); + }; + + return ( + + + + + + + + + + + + {t("components.topbar.greeting")} + + + {profileLoading + ? "..." + : firstName && firstName.length > 8 + ? firstName.substring(0, 8) + : firstName} + + + + + + + + + {/* profile */} + + + + + + + + + + + + Available Credits + + + {walletLoading + ? "..." + : wallet + ? (wallet.balance / 100).toFixed(2) + : "0.00"} + + + Balance usable for client payments + + + {walletError && ( + + {walletError} + + )} + + + + + {/* Action Pill */} + + + {/* Add Cash */} + router.push(ROUTES.ADD_CASH)} + > + + + Add Cash + + + + + + {/* Pay */} + router.push(ROUTES.SEND_OR_REQUEST_MONEY)} + > + + + Pay + + + + + + {/* Send notification */} + router.push(ROUTES.SEND_NOTIFICATION)} + > + + + {"Send"} + {"\n"} + {"notification"} + + + + + + + + {/* Upcoming Reminders */} + + + Upcoming Reminders + + {UPCOMING_REMINDERS.map((reminder) => ( + + + + {reminder.clientName} + + + {reminder.datetimeLabel} + + + + + {reminder.status} + + + + ))} + + + + + + Exchange Rates + + + + Live preview + + + + + + + {EXCHANGE_RATES.map((item, index) => ( + + {index > 0 && ( + + )} + + + + + {item.code} + + + + + {item.name} + + + 1 {item.code} + + + + + + {item.rateToETB.toFixed(2)} + + + ≈ {item.rateToETB.toFixed(2)} ETB + + + + + ))} + + + + + + {t("home.transactionsTitle")} + + + {transactionsLoading ? ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + ) : transactionsError ? ( + + + {t("home.transactionsError")} + + + {transactionsError} + + + ) : transactions.length === 0 ? ( + + + {t("home.transactionsNoTransactions")} + + + {t("home.transactionsEmptySubtitle")} + + + ) : ( + + {transactions.slice(0, 5).map((transaction) => { + const pillClasses = getHomeTxStatusPillClasses( + transaction.status + ); + const clientName = getHomeTxClientName(transaction); + const timeLabel = formatHomeTxTime(transaction.createdAt); + const methodLabel = getHomeTxMethodLabel(transaction); + const shortRef = transaction.id + ? String(transaction.id).slice(-8) + : ""; + + return ( + { + router.push({ + pathname: ROUTES.TRANSACTION_DETAIL, + params: { + transactionId: transaction.id, + amount: transaction.amount.toString(), + type: transaction.type, + recipientName: clientName, + date: transaction.createdAt.toISOString(), + status: transaction.status, + //@ts-ignore + note: transaction?.note || "", + fromHistory: "true", + }, + }); + }} + > + + + + + {formatHomeTxAmount(transaction.amount)} + + + + {transaction.status === "completed" + ? "Success" + : transaction.status === "pending" + ? "Pending" + : "Failed"} + + + + + + + {clientName} + + + {timeLabel} + + + + + + + + + + {methodLabel} + + + + {shortRef ? ( + + {shortRef} + + ) : null} + + + + + ); + })} + {transactions.length > 5 && ( + router.push(ROUTES.HISTORY)}> + + + {t("home.transactionsMore", { + count: transactions.length - 5, + })} + + + + )} + + )} + + + + + + {/* Contact Modal */} + + + + + ); +} diff --git a/app/(root)/(tabs)/listrecipient.tsx b/app/(root)/(tabs)/listrecipient.tsx new file mode 100644 index 0000000..2f62f3a --- /dev/null +++ b/app/(root)/(tabs)/listrecipient.tsx @@ -0,0 +1,604 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { + View, + Text, + TouchableOpacity, + Image, + Platform, + Pressable, +} from "react-native"; +import { FlashList } from "@shopify/flash-list"; +import { Button } from "~/components/ui/button"; +import { + LucideChevronRight, + LucideUser, + LucidePlus, + LucideSlidersHorizontal, +} from "lucide-react-native"; +import { ROUTES } from "~/lib/routes"; +import { router } from "expo-router"; +import ContactModal from "~/components/ui/contactModal"; +import { Recipient } from "~/lib/services/recipientService"; +import { Contact, useContactsStore, useRecipientsStore } from "~/lib/stores"; +import TopBar from "~/components/ui/topBar"; +import { Input } from "~/components/ui/input"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import Skeleton from "~/components/ui/skeleton"; + +// Contacts are only available on native platforms +const isContactsSupported = Platform.OS !== "web"; + +// Helper function to create initials - moved outside component +const getInitials = (name: string) => { + return name + .split(" ") + .map((word) => word.charAt(0).toUpperCase()) + .slice(0, 2) + .join(""); +}; + +// Individual Contact Card Component - Memoized for performance +const ContactCard = React.memo( + ({ contact, onPress }: { contact: Contact; onPress: () => void }) => { + const { t } = useTranslation(); + const primaryPhone = useMemo( + () => + contact.phoneNumbers?.find((phone) => phone.isPrimary) || + contact.phoneNumbers?.[0], + [contact.phoneNumbers] + ); + + const initials = useMemo(() => getInitials(contact.name), [contact.name]); + + return ( + + + + {contact.imageAvailable && contact.image ? ( + + ) : ( + + {initials} + + )} + + + + + {contact.name} + + + {primaryPhone?.number || t("listrecipient.contactNoPhone")} + + + + + + + + + ); + }, + (prevProps, nextProps) => { + // Custom comparison function for better performance + return ( + prevProps.contact.id === nextProps.contact.id && + prevProps.contact.name === nextProps.contact.name && + prevProps.contact.imageAvailable === nextProps.contact.imageAvailable && + prevProps.contact.image?.uri === nextProps.contact.image?.uri && + prevProps.contact.phoneNumbers?.length === + nextProps.contact.phoneNumbers?.length + ); + } +); + +// Individual Saved Recipient (Client) Card Component - Memoized for performance +const SavedRecipientCard = React.memo( + ({ + recipient, + onPress, + clientType, + accountsLabel, + nextPaymentLabel, + }: { + recipient: Recipient; + onPress: () => void; + clientType: "Individual" | "Business"; + accountsLabel: string; + nextPaymentLabel?: string; + }) => { + const initials = useMemo( + () => getInitials(recipient.fullName), + [recipient.fullName] + ); + + return ( + + + + + {initials} + + + + + + + {recipient.fullName} + + + + {clientType} + + + + + {accountsLabel} + + {nextPaymentLabel && ( + + {nextPaymentLabel} + + )} + + + + + + + + ); + }, + (prevProps, nextProps) => { + // Custom comparison function for better performance + return ( + prevProps.recipient.id === nextProps.recipient.id && + prevProps.recipient.fullName === nextProps.recipient.fullName && + prevProps.recipient.phoneNumber === nextProps.recipient.phoneNumber + ); + } +); + +export default function ListRecip() { + const { contacts, loading, error, hasPermission, requestPermission } = + useContactsStore(); + const { + recipients, + loading: recipientsLoading, + error: recipientsError, + } = useRecipientsStore(); + const { t } = useTranslation(); + const [selectedContact, setSelectedContact] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + React.useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + React.useEffect(() => { + if (error) { + showToast( + t("listrecipient.toastErrorTitle"), + t("listrecipient.toastContactsError"), + "error" + ); + } else if (recipientsError) { + showToast( + t("listrecipient.toastErrorTitle"), + t("listrecipient.toastRecipientsError"), + "error" + ); + } + }, [error, recipientsError, t]); + + const handleContactPress = useCallback((contact: Contact) => { + setSelectedContact(contact); + setModalVisible(true); + }, []); + + const handleCloseModal = useCallback(() => { + setModalVisible(false); + setSelectedContact(null); + }, []); + + const handleSendMoney = useCallback( + (contact: Contact) => { + // TODO: Navigate to send money screen with selected contact/recipient + if (__DEV__) { + console.log( + "Send money to:", + contact.name, + "Phone:", + contact.phoneNumbers?.[0]?.number + ); + } + // Check if this is a saved recipient or device contact + const isSavedRecipient = recipients.some( + (recipient) => recipient.id === contact.id + ); + if (isSavedRecipient) { + // router.push(`/sendorrequestmoney?recipientId=${contact.id}`); + if (__DEV__) { + console.log("This is a saved recipient"); + } + } else { + // router.push(`/sendorrequestmoney?contactId=${contact.id}`); + if (__DEV__) { + console.log("This is a device contact"); + } + } + }, + [recipients] + ); + + const handleRecipientPress = useCallback((recipient: Recipient) => { + router.push({ + pathname: ROUTES.RECIPIENT_DETAIL, + params: { recipientId: recipient.id }, + }); + }, []); + + // Memoized renderItem function for FlashList performance + const renderContactItem = useCallback( + ({ item }: { item: Contact }) => ( + handleContactPress(item)} /> + ), + [handleContactPress] + ); + + // Memoized renderItem for saved recipients + const renderRecipientItem = useCallback( + ({ item, index }: { item: Recipient; index: number }) => { + // Simple UI-only client typing and schedule data + const lowerName = item.fullName.toLowerCase(); + const isBusiness = + lowerName.includes("ltd") || + lowerName.includes("plc") || + lowerName.includes("inc") || + lowerName.includes("company"); + const clientType: "Individual" | "Business" = isBusiness + ? "Business" + : "Individual"; + const accountsCount = (index % 3) + 1; // 1–3 accounts, UI-only + const accountsLabel = + accountsCount === 1 ? "1 account" : `${accountsCount} accounts`; + // Give every 2nd/3rd client a dummy schedule label + let nextPaymentLabel: string | undefined; + if (index % 3 === 1) { + nextPaymentLabel = "Next payment: Every Monday · 10:00"; + } else if (index % 3 === 2) { + nextPaymentLabel = "Next payment: Monthly, 1st · 09:00"; + } + + return ( + handleRecipientPress(item)} + clientType={clientType} + accountsLabel={accountsLabel} + nextPaymentLabel={nextPaymentLabel} + /> + ); + }, + [handleRecipientPress] + ); + + // Memoized ItemSeparator component + const ItemSeparator = useCallback(() => , []); + + // Memoized keyExtractor + const keyExtractor = useCallback((item: Contact) => item.id, []); + + // Memoized keyExtractor for recipients + const recipientKeyExtractor = useCallback((item: Recipient) => item.id, []); + + const normalizedSearch = searchQuery.trim().toLowerCase(); + + const filteredContacts = useMemo(() => { + if (!normalizedSearch) return contacts; + + return contacts.filter((contact) => { + const name = contact.name?.toLowerCase() ?? ""; + const phones = (contact.phoneNumbers ?? []) + .map((p) => p.number) + .join(" ") + .toLowerCase(); + + return ( + name.includes(normalizedSearch) || phones.includes(normalizedSearch) + ); + }); + }, [contacts, normalizedSearch]); + + const filteredRecipients = useMemo(() => { + if (!normalizedSearch) return recipients; + + return recipients.filter((recipient) => { + const name = recipient.fullName.toLowerCase(); + const phone = recipient.phoneNumber.toLowerCase(); + return ( + name.includes(normalizedSearch) || phone.includes(normalizedSearch) + ); + }); + }, [recipients, normalizedSearch]); + + const renderContent = () => { + if (loading) { + return ( + + {Array.from({ length: 8 }).map((_, index) => ( + + + + + + + + + + ))} + + ); + } + + if (error) { + return ( + + + {t("listrecipient.contactsErrorTitle")} + + + {error} + + + ); + } + + if (!hasPermission) { + return ( + + + + {t("listrecipient.contactsPermissionTitle")} + + + {t("listrecipient.contactsPermissionSubtitle")} + + + + ); + } + + if (contacts.length === 0) { + return ( + + + + {t("listrecipient.contactsEmptyTitle")} + + + {t("listrecipient.contactsEmptySubtitle")} + + + ); + } + + return ( + + ); + }; + + return ( + + + + + {t("listrecipient.title")} + + + + + } + /> + + + {/* Add Recipient Button */} + + + + + {/* Saved Recipients List */} + + + {t("listrecipient.savedRecipientsTitle", { + count: recipients.length, + })} + + {recipientsLoading ? ( + + {Array.from({ length: 4 }).map((_, index) => ( + + + + + + + + + + ))} + + ) : recipientsError ? ( + + + {t("listrecipient.savedRecipientsError")} + + + ) : recipients.length === 0 ? ( + + + {t("listrecipient.savedRecipientsEmpty")} + + + ) : ( + + {filteredRecipients.map((recipient, index) => { + const lowerName = recipient.fullName.toLowerCase(); + const isBusiness = + lowerName.includes("ltd") || + lowerName.includes("plc") || + lowerName.includes("inc") || + lowerName.includes("company"); + const clientType: "Individual" | "Business" = isBusiness + ? "Business" + : "Individual"; + const accountsCount = (index % 3) + 1; + const accountsLabel = + accountsCount === 1 + ? "1 account" + : `${accountsCount} accounts`; + let nextPaymentLabel: string | undefined; + if (index % 3 === 1) { + nextPaymentLabel = "Next payment: Every Monday · 10:00"; + } else if (index % 3 === 2) { + nextPaymentLabel = "Next payment: Monthly, 1st · 09:00"; + } + + return ( + handleRecipientPress(recipient)} + clientType={clientType} + accountsLabel={accountsLabel} + nextPaymentLabel={nextPaymentLabel} + /> + ); + })} + + )} + + + {/* Contacts list hidden for Agent app */} + + + {/* Contact Modal */} + + + + ); +} diff --git a/app/(root)/(tabs)/requests.tsx b/app/(root)/(tabs)/requests.tsx new file mode 100644 index 0000000..769cc5e --- /dev/null +++ b/app/(root)/(tabs)/requests.tsx @@ -0,0 +1,491 @@ +import React, { useState, useEffect } from "react"; +import { View, Text, Image, ScrollView, TouchableOpacity } from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Icons } from "~/assets/icons"; +import TopBar from "~/components/ui/topBar"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { Button } from "~/components/ui/button"; +import Skeleton from "~/components/ui/skeleton"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { doc, FieldValue } from "~/lib/firebase"; + +type RequestStatus = "pending" | "accepted" | "completed" | "rejected"; + +type AgentRequest = { + id: string; + clientName: string; + datetimeLabel: string; + amount?: string; + currency?: string; + status: RequestStatus; + acceptance: RequestStatus; + rawDate: string; + time: string; + notes?: string; + repeatLabel?: string; + destinationAccount?: string; +}; + +const MOCK_REQUESTS: AgentRequest[] = []; + +const getStatusPillClasses = (status: RequestStatus) => { + switch (status) { + case "pending": + return "bg-yellow-100 text-yellow-700"; + case "accepted": + return "bg-blue-100 text-blue-700"; + case "rejected": + return "bg-red-100 text-red-700"; + case "completed": + default: + return "bg-green-100 text-green-700"; + } +}; + +const isSameDay = (a: Date, b: Date) => { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); +}; + +const formatAppointmentDatetime = (item: any): string => { + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + + let dateObj: Date | null = null; + + if (item.nextRunDate) { + const d = new Date(item.nextRunDate); + if (!isNaN(d.getTime())) { + dateObj = d; + } + } + + if (!dateObj && item.date) { + const d = new Date(item.date); + if (!isNaN(d.getTime())) { + dateObj = d; + } + } + + const timeLabel = item.time || ""; + + let dayLabel = ""; + if (dateObj) { + if (isSameDay(dateObj, today)) { + dayLabel = "Today"; + } else if (isSameDay(dateObj, tomorrow)) { + dayLabel = "Tomorrow"; + } else { + dayLabel = dateObj.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + } + } else if (item.date) { + dayLabel = item.date; + } + + if (dayLabel && timeLabel) { + return `${dayLabel} · ${timeLabel}`; + } + + return dayLabel || timeLabel || ""; +}; + +const buildRepeatLabel = ( + repeatType?: string, + interval?: number +): string | undefined => { + if (!repeatType || repeatType === "none") { + return "One-time"; + } + + if (repeatType === "every_x_days" && interval) { + return `Every ${interval} days`; + } + + return repeatType; +}; + +export default function EventsScreen() { + const [requests, setRequests] = useState(MOCK_REQUESTS); + const [selectedRequest, setSelectedRequest] = useState( + null + ); + const [loading, setLoading] = useState(true); + const { user } = useAuthWithProfile(); + + useEffect(() => { + const fetchAgentAppointments = async () => { + try { + if (!user || typeof (user as any).getIdToken !== "function") { + setLoading(false); + console.log( + "[Requests] /api/agents/my-appointments skipping fetch - no auth user or token method" + ); + return; + } + + setLoading(true); + const token = await (user as any).getIdToken(); + + const response = await fetch( + "https://referralapi-fclnigvupq-uc.a.run.app/api/agents/my-appointments", + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + console.log("[Requests] raw fetch Response", response); + + console.log( + "[Requests] /api/agents/my-appointments status", + response.status + ); + + const text = await response.text(); + try { + const json = JSON.parse(text); + console.log("[Requests] /api/agents/my-appointments json", json); + console.log( + "[Requests] /api/agents/my-appointments json data ", + json?.data + ); + + const payload = json?.data; + const items = Array.isArray(payload?.data) + ? payload.data + : Array.isArray(json?.data) + ? json.data + : []; + + if (items.length > 0) { + const mapped: AgentRequest[] = items.map((item: any) => { + const acceptance: RequestStatus = + item.acceptance === "accepted" || item.acceptance === "rejected" + ? item.acceptance + : "pending"; + + const status: RequestStatus = acceptance; + const datetimeLabel = formatAppointmentDatetime(item); + + const repeatLabel = buildRepeatLabel( + item.repeatType, + item.interval + ); + + return { + id: String(item.id), + clientName: + item.fullName || + item.email || + item.phoneNumber || + "Unknown client", + datetimeLabel, + rawDate: item.date || "", + time: item.time || "", + amount: + typeof item.amount === "number" + ? item.amount.toString() + : undefined, + currency: "ETB", + status, + acceptance, + notes: item.notes, + repeatLabel, + }; + }); + + setRequests(mapped); + } + } catch { + console.log("[Requests] /api/agents/my-appointments raw", text); + } + } catch (error) { + console.error("[Requests] /api/agents/my-appointments error", error); + } finally { + setLoading(false); + } + }; + + fetchAgentAppointments(); + }, [user]); + + const handleUpdateStatus = async (id: string, next: RequestStatus) => { + try { + const ref = doc("appointments", id); + await ref.update({ + acceptance: next, + updatedAt: FieldValue.serverTimestamp(), + }); + console.log( + "[Requests] updated appointments acceptance in Firestore", + id, + next + ); + + // Only update local UI once backend write succeeds + setRequests((prev) => + prev.map((req) => + req.id === id ? { ...req, status: next, acceptance: next } : req + ) + ); + setSelectedRequest((prev) => + prev && prev.id === id + ? { ...prev, status: next, acceptance: next } + : prev + ); + } catch (error) { + console.error( + "[Requests] failed to update appointments acceptance", + id, + error + ); + } + }; + + const handlePayNow = (req: AgentRequest) => { + // UI-only: navigate to the main pay/send screen with no real prefill logic + router.push(ROUTES.SEND_OR_REQUEST_MONEY); + }; + + return ( + + + + + + + + Requests + + + Every "Book Now" request for you appears here. + + + {loading && ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + )} + + {!loading && requests.length === 0 && ( + + + No requests right now. + + + )} + + {!loading && requests.length > 0 && ( + + {[...requests] + // Hide accepted requests completely + .filter((req) => req.status !== "accepted") + // Keep rejected requests at the bottom + .sort((a, b) => { + if (a.status === "rejected" && b.status !== "rejected") + return 1; + if (a.status !== "rejected" && b.status === "rejected") + return -1; + return 0; + }) + .map((req) => { + const pillClasses = getStatusPillClasses(req.status); + + return ( + + + + + {req.clientName} + + + + {req.status === "pending" + ? "Pending" + : req.status === "accepted" + ? "Accepted" + : req.status === "rejected" + ? "Rejected" + : "Completed"} + + + + + + {req.datetimeLabel} + + + {req.amount && req.currency && ( + + {req.currency} {req.amount} + + )} + + + setSelectedRequest(req)} + > + + View Details + + + + + + ); + })} + + )} + + + + setSelectedRequest(null)} + maxHeightRatio={0.5} + > + {selectedRequest && ( + + + Request Details + + + {selectedRequest.clientName} + + + {selectedRequest.datetimeLabel} + + + + + + Date + + + {selectedRequest.datetimeLabel} + + + {selectedRequest.amount && selectedRequest.currency && ( + + + Amount + + + {selectedRequest.currency} {selectedRequest.amount} + + + )} + + {selectedRequest.destinationAccount && ( + + + Destination account + + + {selectedRequest.destinationAccount} + + + )} + + + Status + + + {selectedRequest.acceptance === "pending" + ? "Pending" + : selectedRequest.acceptance === "accepted" + ? "Accepted" + : selectedRequest.acceptance === "rejected" + ? "Rejected" + : "Completed"} + + + {selectedRequest.notes && ( + + + Notes + + + {selectedRequest.notes} + + + )} + {selectedRequest.repeatLabel && ( + + + Repeat + + + {selectedRequest.repeatLabel} + + + )} + + + + {selectedRequest.status === "pending" && ( + <> + + handleUpdateStatus(selectedRequest.id, "accepted") + } + > + + Mark as Accepted + + + + handleUpdateStatus(selectedRequest.id, "rejected") + } + > + + Reject Request + + + + )} + + + )} + + + ); +} diff --git a/app/(root)/(tabs)/schedules.tsx b/app/(root)/(tabs)/schedules.tsx new file mode 100644 index 0000000..c595218 --- /dev/null +++ b/app/(root)/(tabs)/schedules.tsx @@ -0,0 +1,3 @@ +import SchedulesScreen from "../(screens)/schedules"; + +export default SchedulesScreen; diff --git a/app/(root)/.DS_Store b/app/(root)/.DS_Store new file mode 100644 index 0000000..c875b78 Binary files /dev/null and b/app/(root)/.DS_Store differ diff --git a/app/(root)/_layout.tsx b/app/(root)/_layout.tsx new file mode 100644 index 0000000..df36b78 --- /dev/null +++ b/app/(root)/_layout.tsx @@ -0,0 +1,176 @@ +import "../global.css"; +import "~/lib/i18n"; +import { Theme, ThemeProvider, DefaultTheme } from "@react-navigation/native"; +import { Stack, SplashScreen, router, usePathname } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import * as React from "react"; +import { useFonts } from "expo-font"; +import { PortalHost } from "@rn-primitives/portal"; +import { useEffect, useRef } from "react"; +import { View } from "react-native"; +import { + useAuthStore, + useContactsStore, + useRecipientsStore, +} from "~/lib/stores"; +import { setGlobalLoading } from "~/lib/stores/uiStore"; +import GlobalLoadingOverlay from "~/components/ui/GlobalLoadingOverlay"; +import ChatwootFloatingButton from "~/components/other/ChatwootFloatingButton"; +import { enableRouterLoader } from "~/lib/navigation/routerLoader"; +import { useFCM } from "~/lib/hooks/useFCM"; +import * as Network from "expo-network"; +import { HOME } from "~/lib/routes"; + +const NAV_THEME = { + light: { + background: "hsl(0 0% 100%)", // background + border: "hsla(30,100%,84%,0.24)", // border uses secondary color (FFB668) with 30% opacity + card: "hsla(145, 45%, 50%, 0.80)", // card uses primary background color (4CD080 with 8% opacity) + notification: "hsl(0 84.2% 60.2%)", // destructive + primary: "hsl(145, 45%, 25%)", // primary color 105D38 + text: "hsl(0, 0%, 100%)", // text is white + }, +}; + +const LIGHT_THEME: Theme = { + ...DefaultTheme, + colors: NAV_THEME.light, +}; + +export { + // Catch any errors thrown by the Layout component. + ErrorBoundary, +} from "expo-router"; + +// Component to initialize stores +function AppContent() { + const { user, loading, initializeAuth } = useAuthStore(); + const { initialize: initializeContacts } = useContactsStore(); + const { initialize: initializeRecipients } = useRecipientsStore(); + const pathname = usePathname(); + + // Track if we've already shown the initial auth loader + const hasInitializedAuth = useRef(false); + + // Initialize FCM (Android only) + useFCM(); + + // Show opaque loader during initial auth check + useEffect(() => { + if (!hasInitializedAuth.current) { + // Show opaque loader on mount (auth is loading by default) + setGlobalLoading(true, { opaque: true }); + hasInitializedAuth.current = true; + } + }, []); + + // Hide loader when auth state is determined + useEffect(() => { + if (!loading && hasInitializedAuth.current) { + setGlobalLoading(false); + } + }, [loading]); + + // Initialize auth listener + useEffect(() => { + console.log("Initializing auth listener"); + const unsubscribe = initializeAuth(); + return unsubscribe; + }, [initializeAuth]); + + useEffect(() => { + enableRouterLoader(); + }, []); + + // Initialize contacts - defer to not block navigation + useEffect(() => { + // Defer contacts initialization to avoid blocking navigation + const initContacts = async () => { + // Wait for navigation to settle first + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("Initializing contacts"); + await initializeContacts(); + }; + initContacts(); + }, [initializeContacts]); + + // Initialize recipients when user changes + useEffect(() => { + if (user && !loading) { + console.log("Initializing recipients for user:", user.uid); + initializeRecipients(user); + } + }, [user, loading, initializeRecipients]); + + useEffect(() => { + let interval: ReturnType | null = null; + + const checkNetwork = async () => { + try { + const state = await Network.getNetworkStateAsync(); + const isOffline = + !state.isConnected || state.isInternetReachable === false; + + const isOnNoInternet = pathname?.includes("nointernet"); + + if (isOffline) { + if (!isOnNoInternet) { + router.replace("/nointernet"); + } + } else { + if (isOnNoInternet) { + router.replace(HOME); + } + } + } catch (error) { + console.log("Network check error", error); + } + }; + + checkNetwork(); + + interval = setInterval(checkNetwork, 10000); + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [pathname]); + + + return ( + + + + + + + + + ); +} + +export default function RootLayout() { + const [fontsLoaded] = useFonts({ + "DMSans-Regular": require("../../assets/fonts/DMSans-Regular.ttf"), + "DMSans-Bold": require("../../assets/fonts/DMSans-Bold.ttf"), + "DMSans-Black": require("../../assets/fonts/DMSans-Black.ttf"), + "DMSans-Medium": require("../../assets/fonts/DMSans-Medium.ttf"), + "DMSans-SemiBold": require("../../assets/fonts/DMSans-SemiBold.ttf"), + "DMSans-Thin": require("../../assets/fonts/DMSans-Thin.ttf"), + "DMSans-ExtraBold": require("../../assets/fonts/DMSans-ExtraBold.ttf"), + "DMSans-ExtraLight": require("../../assets/fonts/DMSans-ExtraLight.ttf"), + "DMSans-Light": require("../../assets/fonts/DMSans-Light.ttf"), + }); + + useEffect(() => { + if (fontsLoaded) { + SplashScreen.hideAsync(); + } + }, [fontsLoaded]); + + if (!fontsLoaded) return null; + + return ; +} diff --git a/app/(root)/auth/_layout.tsx b/app/(root)/auth/_layout.tsx new file mode 100644 index 0000000..c067415 --- /dev/null +++ b/app/(root)/auth/_layout.tsx @@ -0,0 +1,80 @@ +import React, { useEffect, useState } from 'react'; +import { Stack } from 'expo-router'; +import { useAuthWithProfile } from '~/lib/hooks/useAuthWithProfile'; +import { router, usePathname } from 'expo-router'; +import { AuthService } from '~/lib/services/authServices'; + +export default function AuthLayout() { + const { user, loading, profile, profileLoading } = useAuthWithProfile(); + const pathname = usePathname(); + const [checkingAgent, setCheckingAgent] = useState(false); + + // Redirect to agent signin if no user and not on an auth page + useEffect(() => { + if (!loading && !user) { + // If not on any auth page, redirect to agent signin + const authPages = ['/auth/signin', '/auth/phone-setup', '/auth/google-setup', '/auth/otp', '/auth/forgot', '/auth/agent-signin']; + const isOnAuthPage = authPages.some(page => pathname?.includes(page)); + + if (!isOnAuthPage) { + console.log('Auth Layout - no user, redirecting to agent signin'); + router.replace('/auth/agent-signin'); + return; + } + } + }, [user, loading, pathname]); + + // Redirect to home if user is authenticated AND has a complete profile OR is an agent + useEffect(() => { + console.log('Auth Layout - user:', user?.uid, 'loading:', loading, 'profile:', !!profile, 'profileLoading:', profileLoading, 'pathname:', pathname); + + // Setup pages that should not redirect even if user has profile + const setupPages = ['/auth/phone-setup', '/auth/google-setup', '/auth/agent-signin']; + const isOnSetupPage = setupPages.some(page => pathname.includes(page)); + + // Skip if still loading or on setup page + if (loading || profileLoading || checkingAgent || isOnSetupPage || !user) { + return; + } + + const checkAndRedirect = async () => { + // Check if user has a complete profile (regular user) + if (profile && profile.fullName && (profile.phoneNumber || profile.email)) { + console.log('Auth Layout - user has complete profile, redirecting to home'); + router.replace('/'); + return; + } + + // Check if user is an agent (exists in agents collection) + setCheckingAgent(true); + try { + const isAgent = await AuthService.checkAgentExists(user.uid); + if (isAgent) { + console.log('Auth Layout - user is an agent, redirecting to home'); + router.replace('/'); + } + } catch (error) { + console.error('Auth Layout - error checking agent:', error); + } finally { + setCheckingAgent(false); + } + }; + + checkAndRedirect(); + }, [user, loading, profile, profileLoading, pathname, checkingAgent]); + + return ( + + + + + + + + + ); +} diff --git a/app/(root)/auth/agent-signin.tsx b/app/(root)/auth/agent-signin.tsx new file mode 100644 index 0000000..9c7611d --- /dev/null +++ b/app/(root)/auth/agent-signin.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + Keyboard, + Platform, + Image, + TouchableOpacity, + KeyboardAvoidingView, + TouchableWithoutFeedback, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { Icons } from "~/assets/icons"; +import { useGlobalLoading } from "~/lib/hooks/useGlobalLoading"; +import { AuthService } from "~/lib/services/authServices"; +import ModalToast from "~/components/ui/toast"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useAuthStore } from "~/lib/stores/authStore"; + +export default function AgentSignin() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const { withLoading } = useGlobalLoading(); + const { setUser } = useAuthStore(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleSignIn = async () => { + Keyboard.dismiss(); + + // Validation + if (!email.trim()) { + showToast("Error", "Please enter your email", "warning"); + return; + } + + if (!validateEmail(email.trim())) { + showToast("Error", "Please enter a valid email address", "warning"); + return; + } + + if (!password.trim()) { + showToast("Error", "Please enter your password", "warning"); + return; + } + + setLoading(true); + + try { + const result = await withLoading(() => + AuthService.signInAgent(email.trim(), password) + ); + + if (result.error) { + showToast("Sign In Error", result.error, "error"); + return; + } + + if (result.user) { + // Set user in auth store + setUser(result.user); + + // Navigate to home (signInAgent already checks if agent exists) + router.replace(ROUTES.HOME); + } + } catch (error) { + console.error("Agent signin error:", error); + showToast( + "Error", + error instanceof Error ? error.message : "Failed to sign in", + "error" + ); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + return ( + + + + + + + + + + amba + + + pay + + + + Agent Sign In + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/agent-signup.tsx b/app/(root)/auth/agent-signup.tsx new file mode 100644 index 0000000..8ab00f7 --- /dev/null +++ b/app/(root)/auth/agent-signup.tsx @@ -0,0 +1,251 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + Keyboard, + Platform, + Image, + TouchableOpacity, + KeyboardAvoidingView, + TouchableWithoutFeedback, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { Icons } from "~/assets/icons"; +import { useGlobalLoading } from "~/lib/hooks/useGlobalLoading"; +import { AuthService } from "~/lib/services/authServices"; +import ModalToast from "~/components/ui/toast"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useAuthStore } from "~/lib/stores/authStore"; + +export default function AgentSignup() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const { withLoading } = useGlobalLoading(); + const { setUser } = useAuthStore(); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleSignUp = async () => { + Keyboard.dismiss(); + + // Validation + if (!email.trim()) { + showToast("Error", "Please enter your email", "warning"); + return; + } + + if (!validateEmail(email.trim())) { + showToast("Error", "Please enter a valid email address", "warning"); + return; + } + + if (!password.trim()) { + showToast("Error", "Please enter a password", "warning"); + return; + } + + if (password.length < 6) { + showToast("Error", "Password must be at least 6 characters", "warning"); + return; + } + + if (password !== confirmPassword) { + showToast("Error", "Passwords do not match", "warning"); + return; + } + + setLoading(true); + + try { + const result = await withLoading(() => + AuthService.signUpAgent(email.trim(), password) + ); + + if (result.error) { + showToast("Sign Up Error", result.error, "error"); + return; + } + + if (result.user) { + // Set user in auth store + setUser(result.user); + + // Navigate to home + router.replace(ROUTES.HOME); + } + } catch (error) { + console.error("Agent signup error:", error); + showToast( + "Error", + error instanceof Error ? error.message : "Failed to sign up", + "error" + ); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + return ( + + + + + + + + + + amba + + + pay + + + + Agent Sign Up + + + + + + + + + + + + + + + + + + + + + + + + Already have an account?{" "} + + router.push("/auth/agent-signin")} + > + + Sign In + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/forgot.tsx b/app/(root)/auth/forgot.tsx new file mode 100644 index 0000000..4f8f734 --- /dev/null +++ b/app/(root)/auth/forgot.tsx @@ -0,0 +1,259 @@ +import * as React from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { Text } from "~/components/ui/text"; +import { + ArrowLeftIcon, + CircleDollarSign, + LucideEye, +} from "lucide-react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useTranslation } from "react-i18next"; +import ModalToast from "~/components/ui/toast"; + +export default function Forgot() { + const [value, setValue] = React.useState("phone"); + const { t } = useTranslation(); + const [toastVisible, setToastVisible] = React.useState(false); + const [toastTitle, setToastTitle] = React.useState(""); + const [toastDescription, setToastDescription] = React.useState< + string | undefined + >(undefined); + const [toastVariant, setToastVariant] = React.useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = React.useRef | null>( + null + ); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + React.useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + return ( + + + + + + + + + {t("forgot.headerTitle")} + + + + + + + + + + + {" "} + + {t("forgot.title")} + + + {t("forgot.description")} + + + + + + + + {t("forgot.tabsPhone")} + + + {t("forgot.tabsEmail")} + + + + + + + {/* Add a international phone selector here */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/google-setup.tsx b/app/(root)/auth/google-setup.tsx new file mode 100644 index 0000000..6b8c2a9 --- /dev/null +++ b/app/(root)/auth/google-setup.tsx @@ -0,0 +1,450 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + ScrollView, + Keyboard, + Platform, + KeyboardAvoidingView, + TouchableWithoutFeedback, + Image, + TouchableOpacity, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { AuthService } from "~/lib/services/authServices"; +import BackButton from "~/components/ui/backButton"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import WalletService from "~/lib/services/walletService"; +import { + fullNameSchema, + phoneNumberSchema, + pinSchema, + confirmPinSchema, + addressSchema, + validate, +} from "~/lib/utils/validationSchemas"; +import { useAuthStore } from "~/lib/stores/authStore"; +import ModalToast from "~/components/ui/toast"; +import * as ImagePicker from "expo-image-picker"; +import { Icons } from "~/assets/icons"; +import { Plus } from "lucide-react-native"; +import { uploadProfileImage } from "~/lib/services/profileImageService"; + +// Conditionally import FCMService only for native +let FCMService: any = null; +if (Platform.OS !== "web") { + FCMService = require("~/lib/services/fcmService").FCMService; +} + +export default function GoogleSetup() { + const { user, refreshProfile, refreshWallet } = useAuthWithProfile(); + const { signOut } = useAuthStore(); + + // Pre-fill with Google account info if available + const [fullName, setFullName] = useState(user?.displayName || ""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [address, setAddress] = useState(""); + const [pin, setPin] = useState(""); + const [confirmPin, setConfirmPin] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [profileImage, setProfileImage] = useState(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + console.log("GOOGLE SETUP PAGE LOADED, user:", user?.uid); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Redirect if no user is authenticated + useEffect(() => { + if (!user) { + console.log("NO USER FOUND, redirecting to signin"); + router.replace(ROUTES.SIGNIN); + } + }, [user]); + + // Update fullName when user data becomes available + useEffect(() => { + if (user?.displayName && !fullName) { + setFullName(user.displayName); + } + }, [user?.displayName]); + + const handleSelectProfileImage = async () => { + try { + const permissionResult = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + showToast( + "Permission Required", + "Please allow access to your photo library to select a profile picture.", + "error" + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + setProfileImage(result.assets[0].uri); + } + } catch (e) { + showToast("Error", "Failed to select image", "error"); + } + }; + + const handleCompleteSetup = async () => { + Keyboard.dismiss(); + + // Dev-only shortcut: on emulator/dev builds, skip profile setup and go straight home + if (__DEV__) { + console.log( + "DEV google setup bypass: skipping profile creation and navigating to HOME" + ); + router.replace(ROUTES.HOME); + return; + } + + if (!user) { + showToast("Error", "No authenticated user found", "error"); + return; + } + + // Validate full name + const fullNameResult = validate(fullNameSchema, fullName); + if (!fullNameResult.success) { + showToast("Error", fullNameResult.error, "warning"); + return; + } + + // Validate phone number + const phoneResult = validate(phoneNumberSchema, phoneNumber); + if (!phoneResult.success) { + showToast("Error", phoneResult.error, "warning"); + return; + } + + // Validate PIN + const pinResult = validate(pinSchema, pin); + if (!pinResult.success) { + showToast("Error", pinResult.error, "warning"); + return; + } + + // Validate confirm PIN + const confirmPinResult = validate(confirmPinSchema(pin), confirmPin); + if (!confirmPinResult.success) { + showToast("Error", confirmPinResult.error, "warning"); + return; + } + + // Validate address (optional) + const addressResult = validate(addressSchema, address); + if (!addressResult.success) { + showToast("Error", addressResult.error, "warning"); + return; + } + + setLoading(true); + setError(null); + + try { + let photoUrl: string | undefined; + if (profileImage) { + try { + photoUrl = await uploadProfileImage(user.uid, profileImage); + } catch (e) { + console.warn("Failed to upload profile image during setup", e); + } + } + + await AuthService.createUserProfile(user.uid, { + fullName: fullName.trim(), + phoneNumber: phoneNumber.trim(), + address: address.trim() || undefined, + email: user.email || "", + pin: pin.trim(), + signupType: "google", + createdAt: new Date(), + updatedAt: new Date(), + photoUrl, + }); + + // Initialize FCM token for new user (native only) + if (Platform.OS !== "web" && FCMService) { + try { + const fcmResult = await FCMService.initializeTokenForNewUser( + user.uid + ); + if (fcmResult.success) { + console.log("FCM token initialized for new user"); + } else { + console.warn("Failed to initialize FCM token:", fcmResult.error); + } + } catch (fcmError) { + console.error("Error initializing FCM token:", fcmError); + } + } + + // Create wallet for the user + await WalletService.createUserWallet(user.uid); + console.log("User wallet created successfully"); + + // Refresh profile and wallet to update auth context + await refreshProfile(); + await refreshWallet(); + + router.replace(ROUTES.HOME); + } catch (error) { + console.error("Profile setup error:", error); + setError( + error instanceof Error ? error.message : "Failed to complete setup" + ); + showToast( + "Error", + "Failed to complete profile setup. Please try again.", + "error" + ); + } finally { + setLoading(false); + } + }; + + const handleBackPress = () => { + showToast( + "Cancel Setup", + "Are you sure you want to cancel? You will need to sign in again.", + "warning" + ); + setTimeout(async () => { + try { + await signOut(); + router.replace(ROUTES.SIGNIN); + } catch (error) { + console.error("Error signing out:", error); + router.replace(ROUTES.SIGNIN); + } + }, 1500); + }; + + if (!user) { + return null; // Will redirect to signin + } + + return ( + + + + + + + + + + Welcome to AmbaPay! + + + + Just a few more details to get you started. + + + + + + + {profileImage ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/otp.tsx b/app/(root)/auth/otp.tsx new file mode 100644 index 0000000..956d1c1 --- /dev/null +++ b/app/(root)/auth/otp.tsx @@ -0,0 +1,274 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + ScrollView, + Keyboard, + Platform, + KeyboardAvoidingView, + TouchableWithoutFeedback, + type ScrollView as ScrollViewType, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { usePhoneAuth, useAuthState } from "~/lib/hooks/useAuth"; +import { ROUTES } from "~/lib/routes"; +import BackButton from "~/components/ui/backButton"; +import { AuthService } from "~/lib/services/authServices"; +import { useGlobalLoading } from "~/lib/hooks/useGlobalLoading"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; + +export default function OTP() { + const [otpCode, setOtpCode] = useState(""); + const [countdown, setCountdown] = useState(60); + const [otpVerified, setOtpVerified] = useState(false); + const { t } = useTranslation(); + + const { verifyOTP, sendOTP, loading, error } = usePhoneAuth(); + const { user } = useAuthState(); + const { showLoader, hideLoader, isGlobalLoading } = useGlobalLoading(); + const loaderActiveRef = useRef(false); + const scrollViewRef = useRef(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handleVerifyOTP = async () => { + Keyboard.dismiss(); + + if (!otpCode.trim() || otpCode.length !== 6) { + showToast( + t("otp.validationErrorTitle"), + t("otp.validationInvalidCode"), + "warning" + ); + return; + } + + try { + loaderActiveRef.current = true; + showLoader(); + await verifyOTP(otpCode); + console.log("OTP VERIFIED successfully"); + } catch (error) { + if (loaderActiveRef.current) { + hideLoader(); + loaderActiveRef.current = false; + } + showToast(t("otp.toastErrorTitle"), t("otp.toastInvalidCode"), "error"); + } + }; + + const handleResendCode = async () => { + Keyboard.dismiss(); + + // You'll need to store the phone number from the previous screen + // For now, we'll show a message + showToast(t("otp.toastInfoTitle"), t("otp.toastResendInfo"), "info"); + }; + + // Countdown timer for resend button + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000); + return () => clearTimeout(timer); + } + }, [countdown]); + + // Scroll back when keyboard is dismissed + useEffect(() => { + const keyboardDidHide = Keyboard.addListener( + Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide", + () => { + scrollViewRef.current?.scrollTo({ y: 0, animated: true }); + } + ); + + return () => { + keyboardDidHide.remove(); + }; + }, []); + + // Show errors + useEffect(() => { + if (error) { + showToast(t("otp.toastVerificationErrorTitle"), error, "error"); + } + }, [error]); + + // Handle navigation after OTP verification + useEffect(() => { + const checkProfileAndNavigate = async () => { + if (user?.uid && !otpVerified) { + console.log("User authenticated, checking profile existence"); + setOtpVerified(true); // Set flag before navigation + + try { + const profileExists = await AuthService.checkUserProfileExists( + user.uid + ); + console.log("Profile exists:", profileExists); + + if (profileExists) { + console.log("User profile exists, redirecting to home"); + router.replace(ROUTES.HOME); + } else { + console.log( + "User profile does not exist, redirecting to phone setup" + ); + router.replace(ROUTES.PHONE_SETUP); + } + } catch (error) { + console.error("Error checking profile:", error); + // Default to phone setup if there's an error + router.replace(ROUTES.PHONE_SETUP); + } finally { + if (loaderActiveRef.current) { + hideLoader(); + loaderActiveRef.current = false; + } + } + } + }; + + checkProfileAndNavigate(); + }, [user, otpVerified, hideLoader]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + // Note: We don't redirect authenticated users here since they need to complete setup + + return ( + + + + + + { + showToast( + t("otp.toastInfoTitle"), + t("otp.toastBackInfo"), + "info" + ); + router.back(); + }} + /> + + + + {t("otp.title")} + + + + {t("otp.description")} + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/phone-setup.tsx b/app/(root)/auth/phone-setup.tsx new file mode 100644 index 0000000..4e25b4b --- /dev/null +++ b/app/(root)/auth/phone-setup.tsx @@ -0,0 +1,465 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + ScrollView, + Keyboard, + Platform, + KeyboardAvoidingView, + TouchableWithoutFeedback, + Image, + TouchableOpacity, + Alert, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { AuthService } from "~/lib/services/authServices"; +import BackButton from "~/components/ui/backButton"; +import WalletService from "~/lib/services/walletService"; +import { + fullNameSchema, + emailSchema, + pinSchema, + confirmPinSchema, + addressSchema, + validate, +} from "~/lib/utils/validationSchemas"; +import { FCMService } from "~/lib/services/fcmService"; +import { useAuthStore } from "~/lib/stores/authStore"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import ModalToast from "~/components/ui/toast"; +import { useTranslation } from "react-i18next"; +import * as ImagePicker from "expo-image-picker"; +import { Icons } from "~/assets/icons"; +import { Plus } from "lucide-react-native"; +import { uploadProfileImage } from "~/lib/services/profileImageService"; + +export default function PhoneSetup() { + const [fullName, setFullName] = useState(""); + const [email, setEmail] = useState(""); + const [address, setAddress] = useState(""); + const [pin, setPin] = useState(""); + const [confirmPin, setConfirmPin] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const { user, clearPhoneAuth, refreshProfile, refreshWallet } = + useAuthWithProfile(); + const { signOut } = useAuthStore(); + const [profileImage, setProfileImage] = useState(null); + + console.log("PHONE SETUP PAGE LOADED, user:", user?.uid); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + // Redirect if no user is authenticated + useEffect(() => { + // In dev, allow the fake emulator user through without redirecting + if (__DEV__ && user?.uid === "dev-emulator-user") { + return; + } + + if (!user) { + console.log("NO USER FOUND, redirecting to signin"); + router.replace(ROUTES.SIGNIN); + } + }, [user]); + + const handleSelectProfileImage = async () => { + try { + const permissionResult = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + Alert.alert( + "Permission Required", + "Please allow access to your photo library to select a profile picture." + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + setProfileImage(result.assets[0].uri); + } + } catch (e) { + showToast("Error", "Failed to select image", "error"); + } + }; + + const handleCompleteSetup = async () => { + Keyboard.dismiss(); + + // Dev-only shortcut: on emulator/dev builds, skip profile setup and go straight home + if (__DEV__) { + console.log( + "DEV phone setup bypass: skipping profile creation and navigating to HOME" + ); + router.replace(ROUTES.HOME); + return; + } + + // Validate full name + const fullNameResult = validate(fullNameSchema, fullName); + if (!fullNameResult.success) { + showToast("Error", fullNameResult.error, "warning"); + return; + } + + // Validate email + const emailResult = validate(emailSchema, email); + if (!emailResult.success) { + showToast("Error", emailResult.error, "warning"); + return; + } + + // Validate PIN + const pinResult = validate(pinSchema, pin); + if (!pinResult.success) { + showToast("Error", pinResult.error, "warning"); + return; + } + + // Validate confirm PIN + const confirmPinResult = validate(confirmPinSchema(pin), confirmPin); + if (!confirmPinResult.success) { + showToast("Error", confirmPinResult.error, "warning"); + return; + } + + // Validate address (optional) + const addressResult = validate(addressSchema, address); + if (!addressResult.success) { + showToast("Error", addressResult.error, "warning"); + return; + } + + if (!user) { + showToast( + t("phoneSetup.toastNoUserTitle"), + t("phoneSetup.toastNoUser"), + "error" + ); + return; + } + + setLoading(true); + setError(null); + + try { + let photoUrl: string | undefined; + if (profileImage) { + try { + photoUrl = await uploadProfileImage(user.uid, profileImage); + } catch (e) { + console.warn("Failed to upload profile image during setup", e); + } + } + + await AuthService.createUserProfile(user.uid, { + fullName: fullName.trim(), + phoneNumber: user.phoneNumber || undefined, + address: address.trim() || undefined, + email: email.trim(), + pin: pin.trim(), + signupType: "phone", // Track that this user signed up with phone + createdAt: new Date(), + updatedAt: new Date(), + photoUrl, + }); + + // Initialize FCM token for new user (Android only) + if (Platform.OS === "android") { + try { + const fcmResult = await FCMService.initializeTokenForNewUser( + user.uid + ); + if (fcmResult.success) { + console.log("FCM token initialized for new user"); + } else { + console.warn("Failed to initialize FCM token:", fcmResult.error); + // Don't fail the setup if FCM fails + } + } catch (fcmError) { + console.error("Error initializing FCM token:", fcmError); + // Don't fail the setup if FCM fails + } + } + + // Create wallet for the user + await WalletService.createUserWallet(user.uid); + console.log("User wallet created successfully"); + + // Clear phone auth state + clearPhoneAuth(); + + // Refresh profile and wallet to update auth context + await refreshProfile(); + await refreshWallet(); + + router.replace(ROUTES.HOME); + } catch (error) { + console.error("Profile setup error:", error); + setError( + error instanceof Error ? error.message : "Failed to complete setup" + ); + showToast( + t("phoneSetup.validationErrorTitle"), + t("phoneSetup.toastSetupError"), + "error" + ); + } finally { + setLoading(false); + } + }; + + // Show errors + useEffect(() => { + if (error) { + showToast(t("phoneSetup.toastSetupErrorTitle"), error, "error"); + } + }, [error]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + if (!user) { + return null; // Redirect handled in effect above + } + + const handleBackPress = () => { + showToast("Cancel Setup", "Are you sure you want to cancel?", "warning"); + // Give user time to see the toast, then handle navigation + setTimeout(async () => { + try { + clearPhoneAuth(); + await signOut(); + router.replace(ROUTES.SIGNIN); + } catch (error) { + console.error("Error signing out:", error); + router.replace(ROUTES.SIGNIN); + } + }, 1500); + }; + + return ( + + + + + + + + + + {t("phoneSetup.title")} + + + + {t("phoneSetup.subtitle")} + + + + + + + {profileImage ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/(root)/auth/signin.tsx b/app/(root)/auth/signin.tsx new file mode 100644 index 0000000..af5bb76 --- /dev/null +++ b/app/(root)/auth/signin.tsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + View, + Keyboard, + Platform, + Image, + TouchableOpacity, + KeyboardAvoidingView, + TouchableWithoutFeedback, +} from "react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { router } from "expo-router"; +import { usePhoneAuth } from "~/lib/hooks/useAuth"; +import { ROUTES } from "~/lib/routes"; +import { Icons } from "~/assets/icons"; +import { AmbapayIcon, GoogleIcon } from "~/components/ui/icons"; +import { useGlobalLoading } from "~/lib/hooks/useGlobalLoading"; +import { AuthService } from "~/lib/services/authServices"; +import ModalToast from "~/components/ui/toast"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { useLangStore } from "~/lib/stores"; +import { useTranslation } from "react-i18next"; +import { ChevronDown } from "lucide-react-native"; +import CountryPicker, { + Country, + CountryCode, +} from "react-native-country-picker-modal"; + +export default function Signin() { + const [phoneNumber, setPhoneNumber] = useState(""); + const [countryCode, setCountryCode] = useState("ET"); + const [selectedCountryCode, setSelectedCountryCode] = useState("+251"); + const [isCountryPickerVisible, setIsCountryPickerVisible] = useState(false); + const { t } = useTranslation(); + const language = useLangStore((state) => state.language); + const setLanguage = useLangStore((state) => state.setLanguage); + const [googleLoading, setGoogleLoading] = useState(false); + const { + sendOTP, + loading: phoneLoading, + error: phoneError, + confirmationResult, + } = usePhoneAuth(); + const { withLoading } = useGlobalLoading(); + + // Phone auth availability - reCAPTCHA not set up for web yet + const isPhoneAuthAvailable = Platform.OS !== "web"; + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("info"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = ( + title: string, + description?: string, + variant: "success" | "error" | "warning" | "info" = "info" + ) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVariant(variant); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const handlePhoneChange = (text: string) => { + setPhoneNumber(text); + }; + + const handlePhoneSignIn = async () => { + Keyboard.dismiss(); + + if (!phoneNumber.trim()) { + showToast("Error", "Please enter a phone number", "warning"); + return; + } + + // Phone auth not available on web without reCAPTCHA setup + if (Platform.OS === "web") { + showToast( + "Phone Auth Unavailable", + "Phone authentication is not available on web. Please use Google Sign-In instead.", + "warning" + ); + return; + } + + const fullPhoneNumber = phoneNumber.startsWith("+") + ? phoneNumber + : `${selectedCountryCode}${phoneNumber}`; + + // Dev-only shortcut: on emulator/dev builds, skip Firebase phone auth entirely + if (__DEV__) { + console.log( + "DEV phone auth bypass: skipping sendOTP and navigating directly to OTP screen" + ); + router.push(ROUTES.OTP); + return; + } + + try { + await withLoading(() => sendOTP(fullPhoneNumber)); + console.log("REACHED HERE AFTER SEND OTP"); + } catch (error) { + console.error("Phone sign in error:", error); + showToast( + t("signin.toastErrorTitle"), + t("signin.toastOtpFailed"), + "error" + ); + } + }; + + const handleGoogleSignIn = async () => { + setGoogleLoading(true); + try { + const result = await withLoading(() => AuthService.signInWithGoogle()); + + if (result.error) { + showToast("Google Sign-In Error", result.error, "error"); + return; + } + + if (result.user) { + // Check if user has a profile + const hasProfile = await AuthService.checkUserProfileExists( + result.user.uid + ); + + if (hasProfile) { + // Existing user - go to home + router.replace(ROUTES.HOME); + } else { + // New user - go to setup page + router.replace(ROUTES.GOOGLE_SETUP); + } + } + } catch (error) { + console.error("Google sign in error:", error); + showToast("Error", "Failed to sign in with Google.", "error"); + } finally { + setGoogleLoading(false); + } + }; + + // Navigate to OTP page when confirmationResult is available + useEffect(() => { + if (confirmationResult) { + console.log("VERIFICATION STARTED, navigating to OTP"); + router.push(ROUTES.OTP); + } + }, [confirmationResult]); + + useEffect(() => { + if (phoneError) { + console.log(phoneError); + showToast(t("signin.toastAuthErrorTitle"), phoneError, "error"); + } + }, [phoneError]); + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + return ( + + + + + + + + + + amba + + + pay + + + + + + {/* Phone Sign-In - Only show on native */} + {isPhoneAuthAvailable && ( + <> + + + setIsCountryPickerVisible(true)} + > + + {selectedCountryCode} + + + + } + /> + + )} + + {/* Web-specific message */} + {Platform.OS === "web" && ( + + Sign in with your Google account to continue + + )} + + + + + {/* Phone Sign-In Button - Only show on native */} + {isPhoneAuthAvailable && ( + <> + + + {/* Divider */} + + OR + + + )} + + {/* Google Sign-In Button */} + + + {/* reCAPTCHA container for web phone auth (hidden, for future use) */} + {Platform.OS === "web" && ( + + )} + + + + + + setIsCountryPickerVisible(false)} + onSelect={(country: Country) => { + setCountryCode(country.cca2 as CountryCode); + const code = + country.callingCode && country.callingCode.length > 0 + ? country.callingCode[0] + : "251"; + setSelectedCountryCode(`+${code}`); + setIsCountryPickerVisible(false); + }} + /> + + + + ); +} diff --git a/app/(root)/firebaseauth/_layout.tsx b/app/(root)/firebaseauth/_layout.tsx new file mode 100644 index 0000000..164e0e4 --- /dev/null +++ b/app/(root)/firebaseauth/_layout.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { Stack } from 'expo-router'; + +export default function FirebaseAuthLayout() { + return ( + + ); +} + diff --git a/app/(root)/firebaseauth/link.tsx b/app/(root)/firebaseauth/link.tsx new file mode 100644 index 0000000..07f0722 --- /dev/null +++ b/app/(root)/firebaseauth/link.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from 'react'; +import { View, ActivityIndicator } from 'react-native'; +import { router } from 'expo-router'; +import { Text } from '~/components/ui/text'; +import { ROUTES } from '~/lib/routes'; + +/** + * Firebase Auth callback handler + * This route catches the deep link callback from Firebase Phone Auth reCAPTCHA + * and redirects back to the appropriate screen + */ +export default function FirebaseAuthLink() { + useEffect(() => { + // Firebase handles the auth callback internally + // We redirect to signin - the auth state listener will handle navigation + // if the user becomes authenticated + const timer = setTimeout(() => { + // Use replace to avoid navigation stack issues + router.replace(ROUTES.SIGNIN); + }, 100); + + return () => clearTimeout(timer); + }, []); + + return ( + + + Verifying... + + ); +} + diff --git a/app/+not-found.tsx b/app/+not-found.tsx new file mode 100644 index 0000000..3db3036 --- /dev/null +++ b/app/+not-found.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { View } from "react-native"; +import { router } from "expo-router"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import { HOME } from "~/lib/routes"; +import BackButton from "~/components/ui/backButton"; +import LottieView from "lottie-react-native"; + +export default function NotFoundScreen() { + const handleClose = () => { + if (router.canGoBack()) { + router.back(); + } else { + router.replace(HOME); + } + }; + + const handleGoHome = () => { + router.replace(HOME); + }; + + return ( + + + {/* Close icon */} + + + {/* Content */} + + + + + 404 + + + + Page has not been found. + + + + {/* Go Home button */} + + + + + + ); +} diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..bf4030e Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/global.css b/app/global.css new file mode 100644 index 0000000..a17d039 --- /dev/null +++ b/app/global.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 147 55% 28%; + --primary-foreground: 0 0% 98%; + --secondary: 33 100% 71%; + --secondary-foreground: 33, 100%, 71%, 0.1; + --muted: 33 100% 85%; + --muted-foreground: 33 70% 30%; + --accent: 33 100% 85%; + --accent-foreground: 147 55% 28%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 147 40% 45%; + --input: 147 40% 45%; + --ring: 147 55% 28%; + } +} \ No newline at end of file diff --git a/app/nointernet.tsx b/app/nointernet.tsx new file mode 100644 index 0000000..45cb49c --- /dev/null +++ b/app/nointernet.tsx @@ -0,0 +1,110 @@ +import React, { useRef, useState, useEffect } from "react"; +import { View, ActivityIndicator } from "react-native"; +import ScreenWrapper from "~/components/ui/ScreenWrapper"; +import { Text } from "~/components/ui/text"; +import { Button } from "~/components/ui/button"; +import LottieView from "lottie-react-native"; +import * as Network from "expo-network"; +import { router } from "expo-router"; +import { HOME } from "~/lib/routes"; +import ModalToast from "~/components/ui/toast"; + +export default function NoInternetScreen() { + const [checking, setChecking] = useState(false); + const [error, setError] = useState(null); + + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState("Can't connect to network"); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const [toastVariant, setToastVariant] = useState< + "success" | "error" | "warning" | "info" + >("error"); + const toastTimeoutRef = useRef | null>(null); + + const showToast = () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle("Can't connect to network"); + setToastDescription(undefined); + setToastVariant("error"); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + useEffect(() => { + return () => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + }; + }, []); + + const handleRetry = async () => { + setChecking(true); + setError(null); + try { + const state = await Network.getNetworkStateAsync(); + const isOffline = + !state.isConnected || state.isInternetReachable === false; + + if (!isOffline) { + router.replace(HOME); + } else { + setError("Still offline. Please check your connection."); + showToast(); + } + } catch (e) { + setError("Unable to check connection. Please try again."); + showToast(); + } finally { + setChecking(false); + } + }; + + return ( + + + + + + No Internet + + + + Please reconnect the application to the internet. + + + + + + + ); +} diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..ad9a325 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/assets/adaptive-icon.png differ diff --git a/assets/banks/awash.svg b/assets/banks/awash.svg new file mode 100644 index 0000000..42de567 --- /dev/null +++ b/assets/banks/awash.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/banks/telebirr.svg b/assets/banks/telebirr.svg new file mode 100644 index 0000000..d86198b --- /dev/null +++ b/assets/banks/telebirr.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/assets/favicon.png differ diff --git a/assets/fonts/DMSans-Black.ttf b/assets/fonts/DMSans-Black.ttf new file mode 100644 index 0000000..56e9bac Binary files /dev/null and b/assets/fonts/DMSans-Black.ttf differ diff --git a/assets/fonts/DMSans-Bold.ttf b/assets/fonts/DMSans-Bold.ttf new file mode 100644 index 0000000..4f5412d Binary files /dev/null and b/assets/fonts/DMSans-Bold.ttf differ diff --git a/assets/fonts/DMSans-ExtraBold.ttf b/assets/fonts/DMSans-ExtraBold.ttf new file mode 100644 index 0000000..1d43afd Binary files /dev/null and b/assets/fonts/DMSans-ExtraBold.ttf differ diff --git a/assets/fonts/DMSans-ExtraLight.ttf b/assets/fonts/DMSans-ExtraLight.ttf new file mode 100644 index 0000000..73f17d1 Binary files /dev/null and b/assets/fonts/DMSans-ExtraLight.ttf differ diff --git a/assets/fonts/DMSans-Light.ttf b/assets/fonts/DMSans-Light.ttf new file mode 100644 index 0000000..250517c Binary files /dev/null and b/assets/fonts/DMSans-Light.ttf differ diff --git a/assets/fonts/DMSans-Medium.ttf b/assets/fonts/DMSans-Medium.ttf new file mode 100644 index 0000000..841d31d Binary files /dev/null and b/assets/fonts/DMSans-Medium.ttf differ diff --git a/assets/fonts/DMSans-Regular.ttf b/assets/fonts/DMSans-Regular.ttf new file mode 100644 index 0000000..07266ae Binary files /dev/null and b/assets/fonts/DMSans-Regular.ttf differ diff --git a/assets/fonts/DMSans-SemiBold.ttf b/assets/fonts/DMSans-SemiBold.ttf new file mode 100644 index 0000000..afa79e3 Binary files /dev/null and b/assets/fonts/DMSans-SemiBold.ttf differ diff --git a/assets/fonts/DMSans-Thin.ttf b/assets/fonts/DMSans-Thin.ttf new file mode 100644 index 0000000..0c02db0 Binary files /dev/null and b/assets/fonts/DMSans-Thin.ttf differ diff --git a/assets/gif/AmbaPayLogo.gif b/assets/gif/AmbaPayLogo.gif new file mode 100644 index 0000000..c575e79 Binary files /dev/null and b/assets/gif/AmbaPayLogo.gif differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icons/Bill.png b/assets/icons/Bill.png new file mode 100644 index 0000000..80a6fe3 Binary files /dev/null and b/assets/icons/Bill.png differ diff --git a/assets/icons/activityIcon.png b/assets/icons/activityIcon.png new file mode 100644 index 0000000..1e3416a Binary files /dev/null and b/assets/icons/activityIcon.png differ diff --git a/assets/icons/addWallet.png b/assets/icons/addWallet.png new file mode 100644 index 0000000..6085c51 Binary files /dev/null and b/assets/icons/addWallet.png differ diff --git a/assets/icons/ambaLogo.png b/assets/icons/ambaLogo.png new file mode 100644 index 0000000..20030e5 Binary files /dev/null and b/assets/icons/ambaLogo.png differ diff --git a/assets/icons/applePay.png b/assets/icons/applePay.png new file mode 100644 index 0000000..f65d30c Binary files /dev/null and b/assets/icons/applePay.png differ diff --git a/assets/icons/avatar.png b/assets/icons/avatar.png new file mode 100644 index 0000000..1664cea Binary files /dev/null and b/assets/icons/avatar.png differ diff --git a/assets/icons/bottomTransferIcon.png b/assets/icons/bottomTransferIcon.png new file mode 100644 index 0000000..b378cb9 Binary files /dev/null and b/assets/icons/bottomTransferIcon.png differ diff --git a/assets/icons/cardCheck.png b/assets/icons/cardCheck.png new file mode 100644 index 0000000..e0667cf Binary files /dev/null and b/assets/icons/cardCheck.png differ diff --git a/assets/icons/cashout.png b/assets/icons/cashout.png new file mode 100644 index 0000000..71beffb Binary files /dev/null and b/assets/icons/cashout.png differ diff --git a/assets/icons/coinIcon.png b/assets/icons/coinIcon.png new file mode 100644 index 0000000..24043b8 Binary files /dev/null and b/assets/icons/coinIcon.png differ diff --git a/assets/icons/copyIcon.png b/assets/icons/copyIcon.png new file mode 100644 index 0000000..740f68d Binary files /dev/null and b/assets/icons/copyIcon.png differ diff --git a/assets/icons/creditCard.png b/assets/icons/creditCard.png new file mode 100644 index 0000000..5e47f07 Binary files /dev/null and b/assets/icons/creditCard.png differ diff --git a/assets/icons/filterBar.png b/assets/icons/filterBar.png new file mode 100644 index 0000000..d25c016 Binary files /dev/null and b/assets/icons/filterBar.png differ diff --git a/assets/icons/filterIcon.png b/assets/icons/filterIcon.png new file mode 100644 index 0000000..5e74955 Binary files /dev/null and b/assets/icons/filterIcon.png differ diff --git a/assets/icons/historyIcon.png b/assets/icons/historyIcon.png new file mode 100644 index 0000000..a39e78b Binary files /dev/null and b/assets/icons/historyIcon.png differ diff --git a/assets/icons/homeIcon.png b/assets/icons/homeIcon.png new file mode 100644 index 0000000..cd0d149 Binary files /dev/null and b/assets/icons/homeIcon.png differ diff --git a/assets/icons/index.ts b/assets/icons/index.ts new file mode 100644 index 0000000..db6066c --- /dev/null +++ b/assets/icons/index.ts @@ -0,0 +1,35 @@ +import type { ImageSourcePropType } from "react-native"; + +export const Icons: Record = { + transactionCard: require("./transactionCard.png"), + homeIcon: require("./homeIcon.png"), + creditCard: require("./creditCard.png"), + historyIcon: require("./historyIcon.png"), + searchIcon: require("./searchIcon.png"), + bottomTransferIcon: require("./bottomTransferIcon.png"), + filterIcon: require("./filterIcon.png"), + qrIcon: require("./qrIcon.png"), + ambaLogo: require("./ambaLogo.png"), + activityIcon: require("./activityIcon.png"), + coinIcon: require("./coinIcon.png"), + copyIcon: require("./copyIcon.png"), + moneyIcon: require("./moneyIcon.png"), + profileImage: require("../images/profileImage.png"), + qrImage: require("../images/qrImage.png"), + filterBar: require("./filterBar.png"), + ticketIcon: require("./ticketIcon.png"), + mainAmbapayLogo: require("../images/mainAmbapayLogo.png"), + ticketHome: require("./ticketHome.png"), + mainBG: require("../images/mainBG.png"), + addWallet: require("../icons/addWallet.png"), + cashout: require("../icons/cashout.png"), + request: require("../icons/request.png"), + characters: require("../images/Characters.png"), + device: require("../images/Device.png"), + applePay: require("../icons/applePay.png"), + cardCheck: require("../icons/cardCheck.png"), + bill: require("../icons/Bill.png"), + avatar: require("../icons/avatar.png"), +}; + +export type IconName = keyof typeof Icons; diff --git a/assets/icons/moneyIcon.png b/assets/icons/moneyIcon.png new file mode 100644 index 0000000..2f54ca1 Binary files /dev/null and b/assets/icons/moneyIcon.png differ diff --git a/assets/icons/qrIcon.png b/assets/icons/qrIcon.png new file mode 100644 index 0000000..79aacf8 Binary files /dev/null and b/assets/icons/qrIcon.png differ diff --git a/assets/icons/request.png b/assets/icons/request.png new file mode 100644 index 0000000..83473a1 Binary files /dev/null and b/assets/icons/request.png differ diff --git a/assets/icons/searchIcon.png b/assets/icons/searchIcon.png new file mode 100644 index 0000000..68161a4 Binary files /dev/null and b/assets/icons/searchIcon.png differ diff --git a/assets/icons/ticketHome.png b/assets/icons/ticketHome.png new file mode 100644 index 0000000..570b5b0 Binary files /dev/null and b/assets/icons/ticketHome.png differ diff --git a/assets/icons/ticketIcon.png b/assets/icons/ticketIcon.png new file mode 100644 index 0000000..f988aaf Binary files /dev/null and b/assets/icons/ticketIcon.png differ diff --git a/assets/icons/transactionCard.png b/assets/icons/transactionCard.png new file mode 100644 index 0000000..125622e Binary files /dev/null and b/assets/icons/transactionCard.png differ diff --git a/assets/images/Characters.png b/assets/images/Characters.png new file mode 100644 index 0000000..d212e7a Binary files /dev/null and b/assets/images/Characters.png differ diff --git a/assets/images/Device.png b/assets/images/Device.png new file mode 100644 index 0000000..72246bf Binary files /dev/null and b/assets/images/Device.png differ diff --git a/assets/images/mainAmbapayLogo.png b/assets/images/mainAmbapayLogo.png new file mode 100644 index 0000000..3d5d1b2 Binary files /dev/null and b/assets/images/mainAmbapayLogo.png differ diff --git a/assets/images/mainBG.png b/assets/images/mainBG.png new file mode 100644 index 0000000..f5402e8 Binary files /dev/null and b/assets/images/mainBG.png differ diff --git a/assets/images/profileImage.png b/assets/images/profileImage.png new file mode 100644 index 0000000..458f338 Binary files /dev/null and b/assets/images/profileImage.png differ diff --git a/assets/images/qrImage.png b/assets/images/qrImage.png new file mode 100644 index 0000000..004d598 Binary files /dev/null and b/assets/images/qrImage.png differ diff --git a/assets/lottie/404.json b/assets/lottie/404.json new file mode 100644 index 0000000..ab1c4dc --- /dev/null +++ b/assets/lottie/404.json @@ -0,0 +1 @@ +{"v":"5.7.1","fr":25,"ip":0,"op":125,"w":700,"h":400,"nm":"404-error","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"#ground","ln":"ground","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-93.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-193,0]],"o":[[0,0],[0,0],[193,0]],"v":[[292,84],[-283,84],[-9,98]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":12,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[69.5,115.5],[177.5,115.5]],"c":false},"ix":2},"nm":"Tracé 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[252,102.5],[310,102.5]],"c":false},"ix":2},"nm":"Tracé 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-203.5,114.5],[-169,114.5]],"c":false},"ix":2},"nm":"Tracé 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[157,102],[207,102]],"c":false},"ix":2},"nm":"Tracé 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-293.5,96.5],[-177,96.5]],"c":false},"ix":2},"nm":"Tracé 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-283,84],[292,84]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":7,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"#dust3","ln":"dust3","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-223.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-70,1],[-65,-2],[-11.591,41.402],[40.352,-34.064],[-28.61,-12.383],[-66,-72]],"o":[[0,0],[70,-1],[30.936,0.952],[12.763,-45.587],[-34.443,29.076],[35.349,15.3],[66,72]],"v":[[-293,-20],[-144,79],[21,48],[135.613,-25.388],[53,-93],[67.944,-12.87],[276,78]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":67,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.467],"y":[0]},"t":76,"s":[8]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.816],"y":[0]},"t":115,"s":[15]},{"t":123,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":5.455,"ix":1,"x":"var $bm_rt;\n$bm_rt = $bm_sub(content('Forme 1').content('R\\xe9duire les trac\\xe9s 1').end, 0.1);"},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":67,"s":[0]},{"t":123,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":67,"op":124,"st":67,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"#dust2","ln":"dust2","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-143.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-70,1],[-65,-2],[-11.591,41.402],[40.352,-34.064],[-28.61,-12.383],[-66,-72]],"o":[[0,0],[70,-1],[30.936,0.952],[12.763,-45.587],[-34.443,29.076],[35.349,15.3],[66,72]],"v":[[-293,-20],[-144,79],[21,48],[135.613,-25.388],[53,-93],[67.944,-12.87],[276,78]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.794],"y":[1]},"o":{"x":[0.372],"y":[0]},"t":9,"s":[7]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.816],"y":[0]},"t":48,"s":[15]},{"t":56,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":5.455,"ix":1,"x":"var $bm_rt;\n$bm_rt = $bm_sub(content('Forme 1').content('R\\xe9duire les trac\\xe9s 1').end, 0.1);"},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":56,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":57,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"#dust 2","ln":"dust","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[130.5,-143.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-40,65],[38,-20],[-65,-2],[-65,-12],[-69,-7],[0,0]],"o":[[0,0],[40,-65],[-38,20],[65,2],[65,12],[69,7],[0,0]],"v":[[-293,-20],[-128,-79],[-181,-151],[-116,-19],[53,-93],[194,-8],[311,-45]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.0627,0.3647,0.2196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.467],"y":[0]},"t":49,"s":[15]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.816],"y":[0]},"t":88,"s":[11]},{"t":96,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":5.455,"ix":1,"x":"var $bm_rt;\n$bm_rt = $bm_sub(content('Forme 1').content('R\\xe9duire les trac\\xe9s 1').end, 0.1);"},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0]},{"t":96,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":97,"st":40,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"#dust","ln":"dust","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-93.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-40,65],[38,-20],[-65,-2],[-65,-12],[-69,-7],[0,0]],"o":[[0,0],[40,-65],[-38,20],[65,2],[65,12],[69,7],[0,0]],"v":[[-293,-20],[-128,-79],[-181,-151],[-116,-19],[53,-93],[194,-8],[311,-45]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.0627,0.3647,0.2196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.467],"y":[0]},"t":21,"s":[5]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.816],"y":[0]},"t":60,"s":[15]},{"t":68,"s":[0]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":5.455,"ix":1,"x":"var $bm_rt;\n$bm_rt = $bm_sub(content('Forme 1').content('R\\xe9duire les trac\\xe9s 1').end, 0.1);"},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"t":68,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":69,"st":12,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"#page","ln":"page","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-93.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[402,85],[-336,85],[-336,247],[402,247]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16.25,-132],[13.383,-131.312],[11.905,-125.813],[12,157.5]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":69,"s":[0]},{"i":{"x":[1],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":94,"s":[3]},{"t":106,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2,"x":"var $bm_rt;\n$bm_rt = $bm_sum(content('Forme 2').content('R\\xe9duire les trac\\xe9s 1').start, 0.1);"},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.406,0.874,0.5308,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[5]},{"t":100,"s":[8]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16.25,-132],[13.383,-131.312],[11.905,-125.813],[12,157.5]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":13,"s":[0]},{"i":{"x":[1],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":38,"s":[3]},{"t":50,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2,"x":"var $bm_rt;\n$bm_rt = $bm_sum(content('Forme 1').content('R\\xe9duire les trac\\xe9s 1').start, 0.1);"},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.406,0.874,0.5308,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":13,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":34,"s":[5]},{"t":44,"s":[8]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.125,-7.875]],"o":[[0,0],[1.125,7.875]],"v":[[14.375,-121.125],[28,-120.125]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"mouth","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12.333,"s":[{"i":[[0,0],[-4.635,2.748]],"o":[[0,0],[4.376,-2.594]],"v":[[8.305,-147.345],[16.982,-144.766]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":26.359,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40.386,"s":[{"i":[[0,0],[-4.635,2.748]],"o":[[0,0],[4.376,-2.594]],"v":[[8.305,-147.345],[16.982,-144.766]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":54.412,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":68.921,"s":[{"i":[[0,0],[-4.635,2.748]],"o":[[0,0],[4.376,-2.594]],"v":[[8.305,-147.345],[16.982,-144.766]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":82.947,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96.974,"s":[{"i":[[0,0],[-4.635,2.748]],"o":[[0,0],[4.376,-2.594]],"v":[[8.305,-147.345],[16.982,-144.766]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":111,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]},{"t":124,"s":[{"i":[[0,0],[-5.043,0.672]],"o":[[0,0],[5.043,-0.672]],"v":[[8.363,-146.657],[17.04,-144.078]],"c":false}]}],"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[13.125,-138.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[5,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.34,0.34,0.34,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.5,-137],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[33.125,-136.875],"ix":2},"a":{"a":0,"k":[13.125,-140.125],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":10,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"eyes right","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12.333,"s":[{"i":[[0,0],[-1.739,5.855]],"o":[[0,0],[1.333,-4.487]],"v":[[8.442,-143.688],[17.692,-147.938]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":26.359,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40.386,"s":[{"i":[[0,0],[-1.739,5.855]],"o":[[0,0],[1.333,-4.487]],"v":[[8.442,-143.688],[17.692,-147.938]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":54.412,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":68.921,"s":[{"i":[[0,0],[-1.739,5.855]],"o":[[0,0],[1.333,-4.487]],"v":[[8.442,-143.688],[17.692,-147.938]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":82.947,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":96.974,"s":[{"i":[[0,0],[-1.739,5.855]],"o":[[0,0],[1.333,-4.487]],"v":[[8.442,-143.688],[17.692,-147.938]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":111,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]},{"t":124,"s":[{"i":[[0,0],[-2.625,3.875]],"o":[[0,0],[2.625,-3.875]],"v":[[8.5,-143],[17.75,-147.25]],"c":false}]}],"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[13.125,-138.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[5,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.34,0.34,0.34,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.5,-137],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[15.625,-138.875],"ix":2},"a":{"a":0,"k":[13.125,-140.125],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":10,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"eyes left","np":3,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.875,0.875],[-15,-1.75]],"o":[[2.875,-0.875],[13.306,1.552]],"v":[[-3,-99.375],[20.875,-99.375]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"plis","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[8.875,-147.625],[8,-161.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[11.625,-151],[8,-161.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":50,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[8.875,-148.25],[8,-161.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":75,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[11.75,-151],[8,-161.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":100,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[8.75,-147.375],[8,-161.5]],"c":false}]},{"t":124,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3.5,-150.625],[8.875,-147.625],[8,-161.5]],"c":false}]}],"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"angle","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-8.292,3.252],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-12.384,-4.911],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[65.5,-154.5],[30.607,-162.271]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-8.875,0.875],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-9.625,-9.5],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[63.75,-147.875],[29.607,-159.771]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":49,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-1.924,4.606],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-12.049,3.356],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[60.799,-161.356],[29.107,-163.771]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":71,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-8.875,0.875],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-9.625,-9.5],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[63.75,-147.875],[30.107,-160.271]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":95,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-1.924,4.606],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-12.049,3.356],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[60.799,-161.356],[30.607,-162.271]],"c":true}]},{"t":124,"s":[{"i":[[0,0],[0,0],[0,0],[-7.5,-15],[-14.5,-2],[0,0],[-7.688,16.696],[-8.292,3.252],[10.125,0.776]],"o":[[0,0],[0,0],[0,0],[24.5,-4.5],[-1,-14],[0,0],[3.705,-8.046],[-12.384,-4.911],[-13.587,-1.042]],"v":[[7.5,-162],[-4.5,-152],[-13.5,-97],[1,-84],[52,-81.5],[40.5,-98.5],[47.96,-136.143],[65.5,-154.5],[30.607,-162.271]],"c":true}]}],"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"corp","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[26,-122.087],"ix":2},"a":{"a":0,"k":[26,-122.087],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"body","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.125,-10],[0,0]],"o":[[0,0],[5,0.125],[0,0]],"v":[[1.75,-86.5],[4.375,-70.25],[12.375,-69.625]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[50.688,-82.625],"ix":2},"a":{"a":0,"k":[2.563,-85.188],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[-29.222]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":95,"s":[25]},{"t":124,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"foot 2","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.125,-10],[0,0]],"o":[[0,0],[5,0.125],[0,0]],"v":[[1.75,-86.5],[4.375,-70.25],[12.375,-69.625]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[2.563,-85.188],"ix":2},"a":{"a":0,"k":[2.563,-85.188],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[-36]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[10.6]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":95,"s":[-36]},{"t":124,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"foot","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3,-23.25]],"o":[[0,0],[-2.788,21.607]],"v":[[-3,-131.25],[-27,-87.5]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-27.25,-86],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[52.875,-107.875],"ix":2},"a":{"a":0,"k":[52.875,-107.875],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"arm 2","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3,-23.25]],"o":[[0,0],[-2.788,21.607]],"v":[[44,-133.75],[57.5,-87]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8,8],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[57.75,-86],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[52.875,-107.875],"ix":2},"a":{"a":0,"k":[52.875,-107.875],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"arm","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[3.625,-6.75],[0,0]],"o":[[0,0],[0,0],[45.75,-3.625],[0,0]],"v":[[35.625,-97.75],[-12.75,-97.125],[-8.5,-78.625],[46.125,-77.25]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":17,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"shadow","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"#wind5","ln":"wind5","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[290.5,-73.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-256,-130],[60,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[0]},{"t":125,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[0]},{"t":125,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":104,"op":125,"st":104,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"#wind4","ln":"wind4","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-133.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-256,-130],[60,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":81,"s":[0]},{"t":94,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":73,"s":[0]},{"t":94,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":73,"op":94,"st":73,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"#wind 5","ln":"wind","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,6.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-256,-130],[270,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.72,0.72,0.72,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":88,"s":[0]},{"t":105,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":80,"s":[0]},{"t":105,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":80,"op":106,"st":80,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"#wind3","ln":"wind3","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[290.5,-73.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-256,-130],[60,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":54,"s":[0]},{"t":67,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":46,"s":[0]},{"t":67,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":46,"op":67,"st":46,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"#wind 3","ln":"wind","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,-143.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-107,-130],[270,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.72,0.72,0.72,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[0]},{"t":45,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0]},{"t":45,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":20,"op":46,"st":20,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"#wind 2","ln":"wind","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,6.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-256,-130],[270,-130]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.72,0.72,0.72,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[0]},{"t":62,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.197],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":37,"s":[0]},{"t":62,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Réduire les tracés 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":37,"op":63,"st":37,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"shadow","parent":22,"sr":1,"ks":{"o":{"a":0,"k":10,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[202,14.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[212.5,-24],[-26,-24],[-26,39],[212.5,39]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"},{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[37,-26.5],[10.5,-14.5],[0,0],[0,0],[0,0]],"o":[[0,0],[-37,26.5],[-10.5,14.5],[0,0],[0,0],[0,0]],"v":[[155,-235.5],[60.5,-229.5],[-2.5,-122.5],[-26.5,-52],[48.5,-35],[158,-41.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 2"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.106,-212.883],[91.222,-212.883],[91.222,-212.457],[8.383,-79.176],[11.225,-49.906],[91.222,-49.906],[91.222,-6],[139.106,-6],[139.106,-49.906],[160.135,-49.906],[160.135,-86.849],[139.106,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[88.238,-145.674],[91.222,-150.505],[91.222,-86.849],[53.994,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[58.5,3],"ix":2},"a":{"a":0,"k":[91.327,-6.373],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":9,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"4","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"#4last","ln":"4last","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[202,14.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[212.5,-24],[-26,-24],[-26,39],[212.5,39]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.106,-212.883],[91.222,-212.883],[91.222,-212.457],[8.383,-79.176],[11.225,-49.906],[91.222,-49.906],[91.222,-6],[139.106,-6],[139.106,-49.906],[160.135,-49.906],[160.135,-86.849],[139.106,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[88.238,-145.674],[91.222,-150.505],[91.222,-86.849],[53.994,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[58.5,3],"ix":2},"a":{"a":0,"k":[91.327,-6.373],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":9,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"4","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"shadow","parent":22,"sr":1,"ks":{"o":{"a":0,"k":10,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-126,14.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[212.5,-24],[-26,-24],[-26,39],[212.5,39]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"},{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[46,-1.5],[34.5,-13],[-21,-61.5],[0,0],[0,0],[0,0]],"o":[[-46,1.5],[-34.5,13],[21,61.5],[0,0],[0,0],[0,0]],"v":[[78,-236],[1,-228],[-56.5,-151.5],[-21.5,-31.5],[157,-45.5],[141,-145]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 2"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.106,-212.883],[91.222,-212.883],[91.222,-212.457],[8.383,-79.176],[11.225,-49.906],[91.222,-49.906],[91.222,-6],[139.106,-6],[139.106,-49.906],[160.135,-49.906],[160.135,-86.849],[139.106,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[88.238,-145.674],[91.222,-150.505],[91.222,-86.849],[53.994,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104,-5],"ix":2},"a":{"a":0,"k":[91.327,-6.373],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-29,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"4","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"#4","ln":"4","parent":22,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-126,14.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[212.5,-24],[-26,-24],[-26,39],[212.5,39]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.106,-212.883],[91.222,-212.883],[91.222,-212.457],[8.383,-79.176],[11.225,-49.906],[91.222,-49.906],[91.222,-6],[139.106,-6],[139.106,-49.906],[160.135,-49.906],[160.135,-86.849],[139.106,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[88.238,-145.674],[91.222,-150.505],[91.222,-86.849],[53.994,-86.849]],"c":true},"ix":2},"nm":"4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[104,-5],"ix":2},"a":{"a":0,"k":[91.327,-6.373],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-29,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"4","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"shadow","sr":1,"ks":{"o":{"a":0,"k":10,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[241.5,302.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[254.5,-9],[-66.5,-9],[-66.5,63.5],[254.5,63.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"},{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[-1.5,0],[0,0],[-41.5,0],[0,0],[0,0]],"o":[[1.5,0],[0,0],[41.5,0],[0,0],[0,0]],"v":[[6,-263.5],[22.5,-45],[93.5,-17.5],[177,-31],[233.5,-243.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 2"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[12.597,15.252],[22.923,0],[12.551,-15.155],[0,-27.943],[0,0],[-12.599,-15.25],[-23.019,0],[-12.553,15.157],[0,27.945]],"o":[[0,-27.85],[-12.599,-15.25],[-22.925,0],[-12.553,15.157],[0,0],[0,27.756],[12.597,15.252],[22.828,0],[12.551,-15.155],[0,0]],"v":[[156.299,-128.197],[137.401,-192.848],[84.117,-215.725],[30.905,-192.99],[12.078,-128.339],[12.078,-90.543],[30.976,-26.035],[84.401,-3.158],[137.472,-25.893],[156.299,-90.543]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[3.883,-6.962],[8.146,0],[3.883,7.153],[0,15.441],[0,0],[-3.743,6.82],[-8.241,0],[-3.885,-7.34],[0,-14.871]],"o":[[-0.095,14.873],[-3.885,6.962],[-8.432,0],[-3.885,-7.151],[0,0],[0.284,-13.829],[3.741,-6.82],[8.43,0],[3.883,7.342],[0,0]],"v":[[108.415,-83.297],[102.447,-50.545],[84.401,-40.102],[65.93,-50.829],[60.104,-84.718],[60.104,-137.575],[66.143,-168.551],[84.117,-178.781],[102.589,-167.769],[108.415,-134.449]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":13,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"0","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"#0","ln":"0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[241.5,302.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"s","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[254.5,-9],[-66.5,-9],[-66.5,63.5],[254.5,63.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[12.597,15.252],[22.923,0],[12.551,-15.155],[0,-27.943],[0,0],[-12.599,-15.25],[-23.019,0],[-12.553,15.157],[0,27.945]],"o":[[0,-27.85],[-12.599,-15.25],[-22.925,0],[-12.553,15.157],[0,0],[0,27.756],[12.597,15.252],[22.828,0],[12.551,-15.155],[0,0]],"v":[[156.299,-128.197],[137.401,-192.848],[84.117,-215.725],[30.905,-192.99],[12.078,-128.339],[12.078,-90.543],[30.976,-26.035],[84.401,-3.158],[137.472,-25.893],[156.299,-90.543]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[3.883,-6.962],[8.146,0],[3.883,7.153],[0,15.441],[0,0],[-3.743,6.82],[-8.241,0],[-3.885,-7.34],[0,-14.871]],"o":[[-0.095,14.873],[-3.885,6.962],[-8.432,0],[-3.885,-7.151],[0,0],[0.284,-13.829],[3.741,-6.82],[8.43,0],[3.883,7.342],[0,0]],"v":[[108.415,-83.297],[102.447,-50.545],[84.401,-40.102],[65.93,-50.829],[60.104,-84.718],[60.104,-137.575],[66.143,-168.551],[84.117,-178.781],[102.589,-167.769],[108.415,-134.449]],"c":true},"ix":2},"nm":"0","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Fusionner les tracés 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0.1333,0.5725,0.251,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":13,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"0","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lottie/Loading.json b/assets/lottie/Loading.json new file mode 100644 index 0000000..dc155bb --- /dev/null +++ b/assets/lottie/Loading.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"Kuni","k":"","d":"Loading (Bouncy)","tc":""},"fr":60,"ip":0,"op":140,"w":500,"h":500,"nm":"Loading (Bouncy)","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Loading","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[60,60,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.105,0],[0,0],[0,1.105],[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0],[1.105,0],[0,0],[0,1.105]],"v":[[6,8],[-6,8],[-8,6],[-8,-6],[-6,-8],[6,-8],[8,-6],[8,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0.0627,0.3647,0.2196,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[0.207,0.693,0.4581,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[0.207,0.693,0.4581,1]},{"t":86,"s":[0.0627,0.3647,0.2196,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[104,68],"to":[0.5,-3.5],"ti":[1.667,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[107,47],"to":[-1.667,-2.667],"ti":[3.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[94,52],"to":[-3.5,3.5],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[86,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":59,"s":[86,68],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[94,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[94,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":71,"s":[94,52],"to":[2.167,-0.833],"ti":[-1.667,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":81,"s":[107,47],"to":[1.667,2.667],"ti":[0.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":86,"s":[104,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[104,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[104,68],"to":[0,0],"ti":[0,0]},{"t":101,"s":[104,68]}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[-8,8],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":46,"s":[0,-8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":59,"s":[0,-8],"to":[-1.333,2.667],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":71,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"t":101,"s":[-8,8]}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":46,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":53,"s":[120,80]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":70,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":86,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":87,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":94,"s":[120,80]},{"t":101,"s":[100,100]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[-90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":71,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[-90]},{"t":86,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.105,0],[0,0],[0,1.105],[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0],[1.105,0],[0,0],[0,1.105]],"v":[[6,8],[-6,8],[-8,6],[-8,-6],[-6,-8],[6,-8],[8,-6],[8,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0.207,0.693,0.4581,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[0.168,0.792,0.4904,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[0.168,0.792,0.4904,1]},{"t":96,"s":[0.207,0.693,0.4581,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[78,68],"to":[0.5,-3.5],"ti":[1.667,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[81,47],"to":[-1.667,-2.667],"ti":[3.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[68,52],"to":[-3.5,3.5],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[60,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[60,68],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[68,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[68,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":81,"s":[68,52],"to":[2.167,-0.833],"ti":[-1.667,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":91,"s":[81,47],"to":[1.667,2.667],"ti":[0.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":96,"s":[78,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":97,"s":[78,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":110,"s":[78,68],"to":[0,0],"ti":[0,0]},{"t":111,"s":[78,68]}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[-8,8],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[0,-8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":49,"s":[0,-8],"to":[-1.333,2.667],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":50,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":81,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":97,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":110,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"t":111,"s":[-8,8]}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":36,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":43,"s":[120,80]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":80,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":96,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":97,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":104,"s":[120,80]},{"t":111,"s":[100,100]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[-90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":35,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":91,"s":[-90]},{"t":96,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.105,0],[0,0],[0,1.105],[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0],[1.105,0],[0,0],[0,1.105]],"v":[[6,8],[-6,8],[-8,6],[-8,-6],[-6,-8],[6,-8],[8,-6],[8,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0.207,0.693,0.4581,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[0.168,0.792,0.4904,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":91,"s":[0.168,0.792,0.4904,1]},{"t":106,"s":[0.207,0.693,0.4581,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[52,68],"to":[0.5,-3.5],"ti":[1.667,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[55,47],"to":[-1.667,-2.667],"ti":[3.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[42,52],"to":[-3.5,3.5],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[34,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[34,68],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[42,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":90,"s":[42,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":91,"s":[42,52],"to":[2.167,-0.833],"ti":[-1.667,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":101,"s":[55,47],"to":[1.667,2.667],"ti":[0.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":106,"s":[52,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":107,"s":[52,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":120,"s":[52,68],"to":[0,0],"ti":[0,0]},{"t":121,"s":[52,68]}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":25,"s":[-8,8],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26,"s":[0,-8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":39,"s":[0,-8],"to":[-1.333,2.667],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":90,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":91,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":107,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":120,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"t":121,"s":[-8,8]}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":26,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":33,"s":[120,80]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":106,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":107,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":114,"s":[120,80]},{"t":121,"s":[100,100]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[-90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":25,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":91,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":101,"s":[-90]},{"t":106,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.105,0],[0,0],[0,1.105],[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0],[1.105,0],[0,0],[0,1.105]],"v":[[6,8],[-6,8],[-8,6],[-8,-6],[-6,-8],[6,-8],[8,-6],[8,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.168,0.792,0.4904,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[0.2169,0.9631,0.6024,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":101,"s":[0.2169,0.9631,0.6024,1]},{"t":116,"s":[0.168,0.792,0.4904,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[26,68],"to":[0.5,-3.5],"ti":[1.667,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[29,47],"to":[-1.667,-2.667],"ti":[3.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[16,52],"to":[-3.5,3.5],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[8,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":29,"s":[8,68],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[16,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[16,52],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":101,"s":[16,52],"to":[2.167,-0.833],"ti":[-1.667,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":111,"s":[29,47],"to":[1.667,2.667],"ti":[0.5,-3.5]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":116,"s":[26,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":117,"s":[26,68],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[26,68],"to":[0,0],"ti":[0,0]},{"t":131,"s":[26,68]}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[-8,8],"to":[1.333,-2.667],"ti":[-1.333,2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[0,-8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":29,"s":[0,-8],"to":[-1.333,2.667],"ti":[1.333,-2.667]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":101,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":117,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[-8,8],"to":[0,0],"ti":[0,0]},{"t":131,"s":[-8,8]}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":16,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":23,"s":[120,80]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":100,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":116,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":117,"s":[100,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":124,"s":[120,80]},{"t":131,"s":[100,100]}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[-90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":101,"s":[-180]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":111,"s":[-90]},{"t":116,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.105,0],[0,0],[0,1.105],[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0]],"o":[[0,0],[-1.105,0],[0,0],[0,-1.105],[0,0],[1.105,0],[0,0],[0,1.105]],"v":[[6,8],[-6,8],[-8,6],[-8,-6],[-6,-8],[6,-8],[8,-6],[8,6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.2169,0.9631,0.6024,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0.168,0.792,0.4904,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[0.168,0.792,0.4904,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0.207,0.693,0.4581,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[0.0627,0.3647,0.2196,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0.0627,0.3647,0.2196,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":80,"s":[0.236,0.564,0.4055,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.207,0.693,0.4581,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[0.168,0.792,0.4904,1]},{"t":110,"s":[0.2169,0.9631,0.6024,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[8,60],"to":[17.333,0],"ti":[-17.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[112,60],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":70,"s":[112,60],"to":[-17.333,0],"ti":[17.333,0]},{"t":110,"s":[8,60]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0,1,0.5167,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":140,"st":-10,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lottie/NoInternet.json b/assets/lottie/NoInternet.json new file mode 100644 index 0000000..99da786 --- /dev/null +++ b/assets/lottie/NoInternet.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":58,"w":1452,"h":1245,"nm":"no internet","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"no internet Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14,"s":[33]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":27,"s":[-33]},{"t":35,"s":[0]}],"ix":10},"p":{"a":0,"k":[725.057,839.768,0],"ix":2},"a":{"a":0,"k":[741.057,835.768,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-27.537,0.031],[0,0]],"o":[[0,0],[20.96,-0.024],[0,0]],"v":[[-36.6,10.092],[1.72,-10.068],[36.6,7.212]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.0667,0.3137,0.0745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":21,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[742.32,376.628],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.233,0],[0,-11.234],[-11.233,0],[0,11.233]],"o":[[-11.233,0],[0,11.233],[11.233,0],[0,-11.234]],"v":[[0,-20.34],[-20.34,0],[0,20.34],[20.34,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0667,0.3137,0.0745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[792.92,310.34],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[11.233,0],[0,-11.234],[-11.233,0],[0,11.233]],"o":[[-11.233,0],[0,11.233],[11.233,0],[0,-11.234]],"v":[[0,-20.34],[-20.34,0],[0,20.34],[20.34,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0667,0.3137,0.0745,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[691.04,310.34],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[33.994,0],[26.394,-16.971],[13.096,-23.744],[0.082,-1.649],[0,0],[0,0],[0,0],[0,0],[0,0],[-16.416,-26.713],[-13.689,-9.652],[-22.175,0],[-19.45,25.316],[-0.659,2.882],[0,0],[0,0],[0,0],[0,0],[0,0],[21.449,37.189],[20.187,13.028]],"o":[[-36.602,0],[-21.343,13.723],[-21.664,39.277],[0,0],[0,0],[0,0],[0,0],[0,0],[0.238,1.124],[10.078,16.4],[17.861,12.594],[61.265,0],[20.344,-26.48],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.141,-1.563],[-12.974,-22.492],[-25.135,-16.22]],"v":[[3.21,-373.548],[-91.727,-347.972],[-143.628,-291.507],[-167.549,-211.393],[-167.62,-209.944],[-167.51,-208.497],[-131.854,257.738],[-131.737,259.267],[-131.419,260.767],[-108.961,315.309],[-73.143,354.569],[-12.807,373.548],[103.953,318.055],[131.312,262.652],[131.693,260.973],[131.823,259.256],[167.479,-215.894],[167.62,-217.791],[167.45,-219.685],[142.294,-295.573],[92.32,-349.103]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-140.811,0],[0,0],[0,0],[100.465,0],[0,0],[0,0]],"o":[[129.318,0],[0,0],[0,0],[-76.004,0],[0,0],[0,0]],"v":[[3.21,-350.548],[144.542,-217.615],[108.888,257.533],[-12.807,350.548],[-108.922,255.983],[-144.578,-210.251]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[741.057,462.22],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":4,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[56.581,0],[21.21,34.518],[0.221,1.045],[0,0],[0,0],[0,0],[-20.395,36.978],[-78.351,0],[-19.003,-32.947],[-0.132,-1.46],[0,0],[0,0],[0,0],[19.177,-24.962]],"o":[[-35.284,0],[-15.39,-25.042],[0,0],[0,0],[0,0],[0.076,-1.541],[19.147,-34.713],[72.292,0],[20.199,35.022],[0,0],[0,0],[0,0],[-0.239,1.05],[-17.875,23.267]],"v":[[-12.798,362.048],[-99.154,309.286],[-120.16,258.376],[-120.32,257.626],[-156.091,-210.097],[-156.055,-210.822],[-133.549,-285.954],[3.219,-362.048],[132.341,-289.827],[156.005,-218.649],[156.091,-217.702],[120.299,259.254],[120.108,260.093],[94.843,311.047]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[741.049,462.22],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":35,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"no internet Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[710,626.5,0],"ix":2},"a":{"a":0,"k":[726,622.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[18.751,18.751]],"o":[[-18.751,18.751],[0,0]],"v":[[33.951,-9.375],[-33.951,-9.375]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[735.682,993.03],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-18.751,18.751]],"o":[[-18.751,-18.751],[0,0]],"v":[[9.375,33.951],[9.375,-33.951]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[692.355,949.703],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-18.751,-18.75]],"o":[[18.751,-18.75],[0,0]],"v":[[-33.951,9.375],[33.951,9.375]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[735.682,906.377],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[18.75,-18.751]],"o":[[18.75,18.751],[0,0]],"v":[[-9.375,-33.951],[-9.375,33.951]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[779.008,949.703],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-93.605,-93.604]],"o":[[93.604,-93.604],[0,0]],"v":[[-169.757,46.804],[169.757,46.8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[735.68,667.146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-168.485,-168.486]],"o":[[168.488,-168.489],[0,0]],"v":[[-305.56,84.244],[305.561,84.241]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[735.68,493.901],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-243.371,-243.37]],"o":[[243.369,-243.37],[0,0]],"v":[[-441.365,121.686],[441.365,121.683]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2549,0.7098,0.3765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":100,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[735.681,320.655],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":35,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"slowed","refId":"comp_0","sr":1.7,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[726,622.5,0],"ix":2},"a":{"a":0,"k":[726,622.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1452,"h":1245,"ip":0,"op":58,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lottie/Recipient.json b/assets/lottie/Recipient.json new file mode 100644 index 0000000..5be75c7 --- /dev/null +++ b/assets/lottie/Recipient.json @@ -0,0 +1 @@ +{"v":"5.7.8","fr":30,"ip":0,"op":150,"w":400,"h":400,"nm":"anima enviando cadastro","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"user contornos","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[201.433,191.846,0],"ix":2,"l":2},"a":{"a":0,"k":[31,37.109,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.837,0],[0,0],[0,-8.837],[0,0]],"o":[[0,0],[-8.837,0],[0,0],[0,-8.837]],"v":[[0,-8],[0,-8],[-16,8],[16,8]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.18431372549,0.654901960784,0.333333333333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[31,51.218],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":50,"s":[0]},{"t":64,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-5.763],[5.763,0],[0,5.763],[-5.763,0]],"o":[[0,5.763],[-5.763,0],[0,-5.763],[5.763,0]],"v":[[10.435,0],[0,10.435],[-10.435,0],[0,-10.435]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.18431372549,0.654901960784,0.333333333333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":33,"s":[0]},{"t":54,"s":[6]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[30.304,25.435],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"air contornos","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.99]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[1]},{"i":{"x":[0.833],"y":[15.646]},"o":{"x":[0.167],"y":[14.953]},"t":22,"s":[100]},{"i":{"x":[0.833],"y":[0.944]},"o":{"x":[0.167],"y":[0.005]},"t":51,"s":[100]},{"i":{"x":[0.833],"y":[-246.5]},"o":{"x":[0.167],"y":[-166.667]},"t":52,"s":[0]},{"i":{"x":[0.833],"y":[0.99]},"o":{"x":[0.167],"y":[0.167]},"t":82,"s":[1]},{"i":{"x":[0.833],"y":[15.646]},"o":{"x":[0.167],"y":[14.953]},"t":84,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.015]},"t":113,"s":[100]},{"t":116,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.908]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[-125]},{"i":{"x":[0.833],"y":[0.791]},"o":{"x":[0.167],"y":[0.278]},"t":21,"s":[-94]},{"i":{"x":[0.833],"y":[0.84]},"o":{"x":[0.167],"y":[0.159]},"t":23,"s":[-73.4]},{"i":{"x":[0.833],"y":[0.891]},"o":{"x":[0.167],"y":[0.186]},"t":31,"s":[35]},{"i":{"x":[0.833],"y":[0.471]},"o":{"x":[0.167],"y":[-0.275]},"t":34,"s":[70]},{"i":{"x":[0.833],"y":[0.837]},"o":{"x":[0.167],"y":[0.142]},"t":35,"s":[65.4]},{"i":{"x":[0.833],"y":[0.836]},"o":{"x":[0.167],"y":[0.169]},"t":39,"s":[-3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.17]},"t":46,"s":[-119]},{"i":{"x":[0.833],"y":[-1.164]},"o":{"x":[0.167],"y":[-1.117]},"t":51,"s":[-199]},{"i":{"x":[0.833],"y":[0.908]},"o":{"x":[0.167],"y":[0.167]},"t":82,"s":[-125]},{"i":{"x":[0.833],"y":[0.791]},"o":{"x":[0.167],"y":[0.278]},"t":83,"s":[-94]},{"i":{"x":[0.833],"y":[0.84]},"o":{"x":[0.167],"y":[0.159]},"t":85,"s":[-73.4]},{"i":{"x":[0.833],"y":[0.891]},"o":{"x":[0.167],"y":[0.186]},"t":93,"s":[35]},{"i":{"x":[0.833],"y":[0.471]},"o":{"x":[0.167],"y":[-0.275]},"t":96,"s":[70]},{"i":{"x":[0.833],"y":[0.837]},"o":{"x":[0.167],"y":[0.142]},"t":97,"s":[65.4]},{"i":{"x":[0.833],"y":[0.836]},"o":{"x":[0.167],"y":[0.169]},"t":101,"s":[-3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.17]},"t":108,"s":[-119]},{"t":113,"s":[-199]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.915},"o":{"x":0.167,"y":0.167},"t":19,"s":[178.149,221.964,0],"to":[-7.833,0,0],"ti":[14.503,5.608,0]},{"i":{"x":0.833,"y":0.748},"o":{"x":0.167,"y":0.206},"t":21,"s":[131.149,221.964,0],"to":[-12.5,-4.833,0],"ti":[-45.704,15.648,0]},{"i":{"x":0.833,"y":0.814},"o":{"x":0.167,"y":0.11},"t":31,"s":[130.149,141.964,0],"to":[21.837,-18.489,0],"ti":[-35.354,-1.533,0]},{"i":{"x":0.833,"y":0.96},"o":{"x":0.167,"y":0.072},"t":37.5,"s":[265.896,186.467,0],"to":[11.604,0.033,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.832},"o":{"x":0.167,"y":0.208},"t":38,"s":[293.149,177.214,0],"to":[36.851,-50.214,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.844},"o":{"x":0.167,"y":0.164},"t":46,"s":[272.649,105.714,0],"to":[-38.149,-8.214,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.188},"t":51,"s":[224.649,126.714,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.878,"y":0},"o":{"x":0.167,"y":0.55},"t":54,"s":[215.149,153.714,0],"to":[0,0,0],"ti":[14,-11.375,0]},{"i":{"x":0.833,"y":0.918},"o":{"x":0.167,"y":0.16},"t":81,"s":[178.149,221.964,0],"to":[-14,11.375,0],"ti":[14.503,5.608,0]},{"i":{"x":0.833,"y":0.748},"o":{"x":0.167,"y":0.206},"t":83,"s":[131.149,221.964,0],"to":[-12.5,-4.833,0],"ti":[-45.704,15.648,0]},{"i":{"x":0.833,"y":0.814},"o":{"x":0.167,"y":0.11},"t":93,"s":[130.149,141.964,0],"to":[21.837,-18.489,0],"ti":[-35.354,-1.533,0]},{"i":{"x":0.833,"y":0.96},"o":{"x":0.167,"y":0.072},"t":99.5,"s":[265.896,186.467,0],"to":[11.604,0.033,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.832},"o":{"x":0.167,"y":0.208},"t":100,"s":[293.149,177.214,0],"to":[36.851,-50.214,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.844},"o":{"x":0.167,"y":0.164},"t":108,"s":[272.649,105.714,0],"to":[-38.149,-8.214,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.188},"t":113,"s":[224.649,126.714,0],"to":[0,0,0],"ti":[0,0,0]},{"t":116,"s":[215.149,153.714,0]}],"ix":2,"l":2},"a":{"a":0,"k":[18.835,17.661,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.881,0.881,0.707]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.41]},"t":21,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[3.083,3.083,2.872]},"o":{"x":[0.167,0.167,0.167],"y":[1.19,1.19,0.117]},"t":31,"s":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.867,0.867,2.967]},"o":{"x":[0.167,0.167,0.167],"y":[0.083,0.083,-1.872]},"t":35,"s":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,2.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.194,0.194,-3.443]},"t":39,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[-0.881,-0.881,-8.81]},"t":46,"s":[70,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.881,0.881,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":83,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[3.083,3.083,1]},"o":{"x":[0.167,0.167,0.167],"y":[1.19,1.19,0]},"t":93,"s":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.867,0.867,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.083,0.083,0]},"t":97,"s":[125,125,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.194,0.194,0]},"t":101,"s":[100,100,100]},{"t":108,"s":[70,70,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-11.011,2.759],[-6.591,11.126],[-5.234,5.586],[11.011,-11.126]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[20.024,18.805],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-16.335,1.158],[16.335,-15.161],[9.309,15.161],[-4.045,6.73],[-5.375,12.587],[-9.823,3.903]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.160784313725,0.160784313725,0.160784313725,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333393172,0.741176470588,0.321568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.835,17.661],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-7.514,9.007],[7.638,-9.007],[-2.83,-5.062],[-7.638,3.796]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[20.974,21.241],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"live contornos","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[208.919,158.737,0],"ix":2,"l":2},"a":{"a":0,"k":[108.081,66.652,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[50.497,122.041],[50.497,123.13],[50.497,123.362],[50.497,123.838],[50.497,124.462],[50.497,124.711],[50.497,125.603],[50.497,125.815],[50.497,126.06],[50.497,126.296],[50.497,128.521],[50.497,131.744],[50.563,131.761],[63.844,135.244],[63.344,119.916],[51.005,121.957]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":24,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[11.242,92.366],[10.841,92.302],[10.02,92.171],[8.942,91.999],[8.512,91.93],[6.972,91.685],[6.606,91.626],[6.185,91.559],[5.777,91.494],[1.936,90.882],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[49.782,49.044],[47.4,46.682],[44.275,43.584],[43.028,42.348],[38.562,37.921],[37.5,36.868],[36.277,35.655],[35.094,34.483],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[179.234,100.168],[179.202,94.619],[179.19,92.404],[179.144,84.474],[179.134,82.588],[179.121,80.416],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[214.963,39.041],[212.247,39.266],[202.523,40.073],[200.211,40.264],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":47,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[156.463,5.291],[156.785,9.245],[157.937,23.398],[158.211,26.764],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":52,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[152.463,-6.209],[97.359,51.4],[126.773,53.136],[158.211,26.764],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":80,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[50.497,122.041],[50.497,123.13],[50.497,123.362],[50.497,123.838],[50.497,124.462],[50.497,124.711],[50.497,125.603],[50.497,125.815],[50.497,126.06],[50.497,126.296],[50.497,128.521],[50.497,131.744],[50.563,131.761],[63.844,135.244],[63.344,119.916],[51.005,121.957]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[11.242,92.366],[10.841,92.302],[10.02,92.171],[8.942,91.999],[8.512,91.93],[6.972,91.685],[6.606,91.626],[6.185,91.559],[5.777,91.494],[1.936,90.882],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":92,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[49.782,49.044],[47.4,46.682],[44.275,43.584],[43.028,42.348],[38.562,37.921],[37.5,36.868],[36.277,35.655],[35.094,34.483],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[179.234,100.168],[179.202,94.619],[179.19,92.404],[179.144,84.474],[179.134,82.588],[179.121,80.416],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[214.963,39.041],[212.247,39.266],[202.523,40.073],[200.211,40.264],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":109,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[156.463,5.291],[156.785,9.245],[157.937,23.398],[158.211,26.764],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":114,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[152.463,-6.209],[97.359,51.4],[126.773,53.136],[158.211,26.764],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[13.122,92.666],[53.63,52.859],[179.282,108.544],[225.484,38.168],[152.463,-6.209],[97.359,51.4],[126.773,53.136],[158.211,26.764],[195.371,40.666],[179.094,75.733],[22.878,22.372],[-3.628,89.994],[10.531,130.09],[63.844,135.244],[63.344,119.916],[26.287,120.136]],"c":true}]},{"t":127,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-211.878,34.666],[-171.37,-5.141],[-45.718,50.544],[0.484,-19.832],[-72.537,-64.209],[-127.641,-6.6],[-98.227,-4.864],[-66.789,-31.236],[-29.629,-17.334],[-45.906,17.733],[-202.122,-35.628],[-228.628,31.994],[-214.469,72.09],[-161.156,77.244],[-161.656,61.916],[-198.713,62.136]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Máscara 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-6.812,16.494],[-5.08,4.329],[-6.629,0.775],[-36,48],[14.099,17.966],[17.872,-38.464]],"o":[[-36.672,1.601],[2.548,-6.169],[5.08,-4.33],[54.336,-6.355],[0,0],[-14.188,-18.078],[0,0]],"v":[[-66.812,62.551],[-98.768,0.983],[-87.632,-15.431],[-68.323,-21.035],[84.014,10.609],[91.482,-32.203],[15.488,-25.689]],"c":false},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.133333333333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"d":[{"n":"d","nm":"traço","v":{"a":0,"k":8.003,"ix":1}},{"n":"g","nm":"lacuna","v":{"a":0,"k":8.003,"ix":2}},{"n":"o","nm":"deslocamento","v":{"a":0,"k":0,"ix":7}}],"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[108.08,66.653],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"stars contornos","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[0]},{"t":48,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":43,"s":[207.24,204.354,0],"to":[0,-3.333,0],"ti":[0,3.333,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":48,"s":[207.24,184.354,0],"to":[0,0,0],"ti":[0,1.197,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":81,"s":[207.24,189.309,0],"to":[0,-0.966,0],"ti":[0,0,0]},{"t":148,"s":[207.24,184.354,0]}],"ix":2,"l":2},"a":{"a":0,"k":[62.66,75.648,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.632,0],[-5.632,0]],"o":[[-5.623,0],[5.623,0]],"v":[[0,-4.097],[0,4.097]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.276,76.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.144,-0.869],[0,0],[0,0],[0.87,-0.143],[0,0],[0,0],[0.144,0.869],[0,0],[0,0],[-0.87,0.144],[0,0],[0,0]],"o":[[0,0],[0,0],[0.87,0.144],[0,0],[0,0],[-0.144,0.869],[0,0],[0,0],[-0.87,-0.143],[0,0],[0,0],[0.144,-0.869]],"v":[[0.769,-3.831],[1.203,-1.203],[3.831,-0.769],[3.831,0.768],[1.203,1.202],[0.769,3.831],[-0.768,3.831],[-1.202,1.202],[-3.831,0.768],[-3.831,-0.769],[-1.202,-1.203],[-0.768,-3.831]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0.5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[119.37,145.346],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.208,-1.257],[0,0],[0,0],[1.257,-0.207],[0,0],[0,0],[0.208,1.257],[0,0],[0,0],[-1.258,0.207],[0,0],[0,0]],"o":[[0,0],[0,0],[1.257,0.207],[0,0],[0,0],[-0.208,1.257],[0,0],[0,0],[-1.258,-0.207],[0,0],[0,0],[0.208,-1.257]],"v":[[1.112,-5.54],[1.74,-1.739],[5.54,-1.111],[5.54,1.111],[1.74,1.739],[1.112,5.54],[-1.111,5.54],[-1.738,1.739],[-5.539,1.111],[-5.539,-1.111],[-1.738,-1.739],[-1.111,-5.54]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0.75,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[105.37,14.673],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.24,-1.451],[0,0],[0,0],[1.451,-0.239],[0,0],[0,0],[0.24,1.451],[0,0],[0,0],[-1.451,0.239],[0,0],[0,0]],"o":[[0,0],[0,0],[1.451,0.239],[0,0],[0,0],[-0.24,1.451],[0,0],[0,0],[-1.451,-0.239],[0,0],[0,0],[0.24,-1.451]],"v":[[1.283,-6.395],[2.007,-2.007],[6.395,-1.282],[6.395,1.282],[2.007,2.007],[1.283,6.394],[-1.284,6.394],[-2.008,2.007],[-6.395,1.282],[-6.395,-1.282],[-2.008,-2.007],[-1.284,-6.395]],"c":true},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[10.841,14.377],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 4","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"progress contornos","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":1,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199.684,308.089,0],"ix":2,"l":2},"a":{"a":0,"k":[94.316,3,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":0.872},"o":{"x":0.732,"y":0.788},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3,3],[2.633,3.09]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":1,"y":0.391},"t":117,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3,3],[112.633,3.269]],"c":false}]},{"t":149,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3,3],[185.633,3]],"c":false}]}],"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.185627881218,0.65361998315,0.332277933757,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"perfil contornos","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200.433,208.919,0],"ix":2,"l":2},"a":{"a":0,"k":[47.122,66.532,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":31,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.124,111.702],[17.622,111.792]],"c":false}]},{"t":43,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.124,111.702],[79.622,111.702]],"c":false}]}],"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":29,"s":[0]},{"t":31,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[25.311,101.701],[25.122,101.79]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[25.311,101.701],[71.622,101.701]],"c":false}]}],"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[0]},{"t":39,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.444,"y":1},"o":{"x":0.306,"y":0.357},"t":10,"s":[{"i":[[0,0],[0,0],[0,4],[0,0],[2.925,-0.05],[0,0],[0,-2.855],[0,0],[-2.925,0.05],[0,0]],"o":[[0,0],[0,0],[0,0],[0,-2.926],[0,0],[-2.855,0.049],[0,0],[0,2.926],[0,0],[0,0]],"v":[[34,63.427],[39.212,63.341],[43.5,59.427],[43.622,67.251],[38.298,62.018],[-40.478,63.369],[-45.622,68.601],[-44.622,58.603],[-39.298,63.836],[-30.946,64.032]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0,0],[0,4],[0,0],[2.925,-0.05],[0,0],[0,-2.855],[0,0],[-2.925,0.05],[0,0]],"o":[[0,0],[0,0],[0,0],[0,-2.926],[0,0],[-2.855,0.049],[0,0],[0,2.926],[0,0],[0,0]],"v":[[34,63.427],[39.212,63.341],[43.5,59.427],[44.622,-58.749],[39.298,-63.982],[-39.478,-62.631],[-44.622,-57.399],[-44.622,58.603],[-39.298,63.836],[-30.946,64.032]],"c":true}]}],"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.133333333333,0.133333333333,0.129411764706,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[47.122,66.532],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[-4.556]},"o":{"x":[0.167],"y":[0]},"t":9,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.111]},"t":10,"s":[0]},{"t":12,"s":[100]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Grupo 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lottie/Success.json b/assets/lottie/Success.json new file mode 100644 index 0000000..f274c14 --- /dev/null +++ b/assets/lottie/Success.json @@ -0,0 +1 @@ +{"v":"5.0.1","fr":29.9700012207031,"ip":0,"op":45.0000018328876,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":7,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":13,"s":[92.154],"e":[30]},{"t":17.0000006924242}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.588],"y":[-51709.363]},"o":{"x":[0.167],"y":[0.167]},"n":["0p588_-51709p363_0p167_0p167"],"t":7,"s":[0],"e":[0]},{"i":{"x":[0.696],"y":[0.999]},"o":{"x":[0.509],"y":[0.003]},"n":["0p696_0p999_0p509_0p003"],"t":10,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.566],"y":[0.999]},"o":{"x":[0.457],"y":[0.063]},"n":["0p566_0p999_0p457_0p063"],"t":7,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0,0],"y":[0.997,0.997]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0_0p997_0p167_0p167","0_0p997_0p167_0p167"],"t":24,"s":[40,40],"e":[90,90]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.574,0.574],"y":[-0.004,-0.004]},"n":["0p833_0p833_0p574_-0p004","0p833_0p833_0p574_-0p004"],"t":27,"s":[90,90],"e":[18.394,18.394]},{"t":38.0000015477717}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":24,"s":[-181.074,-5.414],"e":[200,-5.414],"to":[34.0465698242188,0],"ti":[-26.72825050354,0]},{"t":38.0000015477717}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":24,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":29,"s":[100],"e":[0]},{"t":38.0000015477717}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":10,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":16,"s":[92.154],"e":[30]},{"t":20.0000008146167}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.588],"y":[-51709.363]},"o":{"x":[0.167],"y":[0.167]},"n":["0p588_-51709p363_0p167_0p167"],"t":10,"s":[0],"e":[0]},{"i":{"x":[0.696],"y":[0.999]},"o":{"x":[0.509],"y":[0.003]},"n":["0p696_0p999_0p509_0p003"],"t":13,"s":[0],"e":[100]},{"t":19.0000007738859}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.566],"y":[0.999]},"o":{"x":[0.457],"y":[0.063]},"n":["0p566_0p999_0p457_0p063"],"t":10,"s":[0],"e":[100]},{"t":19.0000007738859}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[263.334,471.109,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[51.641,253.275,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[266.322,44.315,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[469.91,258.792,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-18.097,"ix":10},"p":{"a":0,"k":[400.635,189.708,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-72.471,"ix":10},"p":{"a":0,"k":[359.413,150.912,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.707,"ix":10},"p":{"a":0,"k":[396.894,150.961,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[30,30,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-135.205,"ix":10},"p":{"a":0,"k":[410.865,406.53,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[-19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.606,"ix":10},"p":{"a":0,"k":[105.535,402.598,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[-19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-135.205,"ix":10},"p":{"a":0,"k":[104.864,111.71,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.606,"ix":10},"p":{"a":0,"k":[416.722,113.206,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[236.888,240.258,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[69.59,69.59,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-76.426,37.999],[12.056,114.074],[169.991,-68.635]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":35,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7,11],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[-2.986]},"o":{"x":[0.167],"y":[0]},"n":["0p833_-2p986_0p167_0"],"t":0,"s":[0],"e":[0]},{"i":{"x":[0],"y":[0.973]},"o":{"x":[0.167],"y":[0.042]},"n":["0_0p973_0p167_0p042"],"t":14.791,"s":[0],"e":[32]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.828],"y":[0.011]},"n":["0p833_0p833_0p828_0p011"],"t":19.791,"s":[32],"e":[100]},{"t":24.7912510097683}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.976,0.976],"y":[0.968,0.968]},"o":{"x":[0.654,0.654],"y":[0.007,0.007]},"n":["0p976_0p968_0p654_0p007","0p976_0p968_0p654_0p007"],"t":0,"s":[0,0],"e":[401.025,401.025]},{"i":{"x":[0.468,0.468],"y":[1.057,1.057]},"o":{"x":[0.346,0.346],"y":[-4.83,-4.83]},"n":["0p468_1p057_0p346_-4p83","0p468_1p057_0p346_-4p83"],"t":7,"s":[401.025,401.025],"e":[372.7,372.7]},{"i":{"x":[0.375,0.375],"y":[1.543,1.543]},"o":{"x":[0.364,0.364],"y":[0.031,0.031]},"n":["0p375_1p543_0p364_0p031","0p375_1p543_0p364_0p031"],"t":12,"s":[372.7,372.7],"e":[401.025,401.025]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.327,0.327],"y":[-8.038,-8.038]},"n":["0p833_1_0p327_-8p038","0p833_1_0p327_-8p038"],"t":16,"s":[401.025,401.025],"e":[401.025,401.025]},{"t":20.0000008146167}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.229886716955,0.739552696078,0.369435897528,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.992,3.49],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]} \ No newline at end of file diff --git a/assets/splash-icon.png b/assets/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/assets/splash-icon.png differ diff --git a/assets/svg/404.svg b/assets/svg/404.svg new file mode 100644 index 0000000..cdb9fbe --- /dev/null +++ b/assets/svg/404.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/Failure.svg b/assets/svg/Failure.svg new file mode 100644 index 0000000..0ab105d --- /dev/null +++ b/assets/svg/Failure.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/svg/NewCard.svg b/assets/svg/NewCard.svg new file mode 100644 index 0000000..3bc0f89 --- /dev/null +++ b/assets/svg/NewCard.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/NewRecipient.svg b/assets/svg/NewRecipient.svg new file mode 100644 index 0000000..933e0bd --- /dev/null +++ b/assets/svg/NewRecipient.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/NoConnection.svg b/assets/svg/NoConnection.svg new file mode 100644 index 0000000..2f633be --- /dev/null +++ b/assets/svg/NoConnection.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/Success.svg b/assets/svg/Success.svg new file mode 100644 index 0000000..5bf9534 --- /dev/null +++ b/assets/svg/Success.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f3c649b --- /dev/null +++ b/babel.config.js @@ -0,0 +1,9 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: [ + ["babel-preset-expo", { jsxImportSource: "nativewind" }], + "nativewind/babel", + ], + }; +}; diff --git a/components.json b/components.json new file mode 100644 index 0000000..704b386 --- /dev/null +++ b/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + } +} \ No newline at end of file diff --git a/components/.DS_Store b/components/.DS_Store new file mode 100644 index 0000000..c59ce89 Binary files /dev/null and b/components/.DS_Store differ diff --git a/components/other/ChatwootFloatingButton.tsx b/components/other/ChatwootFloatingButton.tsx new file mode 100644 index 0000000..dc774f6 --- /dev/null +++ b/components/other/ChatwootFloatingButton.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useMemo, useState, useRef } from "react"; +import { View, TouchableOpacity } from "react-native"; +import { WebView } from "react-native-webview"; +import { MessageCircle } from "lucide-react-native"; +import { useAuthStore } from "~/lib/stores"; + +const CHATWOOT_BASE_URL = "https://chat.app.ambapays.com"; +const CHATWOOT_WEBSITE_TOKEN = "or1QS1gRowqkuBBcQVEb3Wib"; + +export default function ChatwootFloatingButton() { + const [visible, setVisible] = useState(false); + + const webViewRef = useRef(null); + + const user = useAuthStore((state) => state.user); + const profile = useAuthStore((state) => state.profile); + + const identifier = user?.uid || user?.email || ""; + const name = profile?.fullName || user?.displayName || ""; + const email = user?.email || ""; + const phone = profile?.phoneNumber || user?.phoneNumber || ""; + + const html = useMemo( + () => ` + + + + + + + + +`, + [identifier, name, email, phone] + ); + + useEffect(() => { + if (visible && webViewRef.current) { + webViewRef.current.injectJavaScript( + "if (window.$chatwoot) { window.$chatwoot.toggle('open'); } true;" + ); + } + }, [visible]); + + return ( + <> + setVisible(true)} + style={{ + position: "absolute", + right: 20, + bottom: 70, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: "#105D38", + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOpacity: 0.2, + shadowRadius: 6, + shadowOffset: { width: 0, height: 3 }, + elevation: 6, + zIndex: 9999, + }} + > + + + + + { + if (event.nativeEvent.data === "chatwoot:closed") { + setVisible(false); + } + }} + /> + + + ); +} diff --git a/components/other/protectedRoute.tsx b/components/other/protectedRoute.tsx new file mode 100644 index 0000000..42eba85 --- /dev/null +++ b/components/other/protectedRoute.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from "react"; +import { View } from "react-native"; +import { router } from "expo-router"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { Text } from "~/components/ui/text"; +import { ROUTES } from "~/lib/routes"; +import { AuthService } from "~/lib/services/authServices"; + +interface ProtectedRouteProps { + children: React.ReactNode; + fallback?: React.ReactNode; + redirectTo?: string; +} + +export const ProtectedRoute: React.FC = ({ + children, + fallback, + redirectTo = ROUTES.SIGNIN, +}) => { + const { user, loading, profile, profileLoading } = useAuthWithProfile(); + const [isAgent, setIsAgent] = useState(null); + const [checkingAgent, setCheckingAgent] = useState(false); + + useEffect(() => { + // In dev, allow the fake emulator user through without redirecting + if (__DEV__ && user?.uid === "dev-emulator-user") { + return; + } + + if (!loading && !profileLoading && !user && !fallback) { + router.replace(redirectTo); + } + }, [user, loading, profileLoading, redirectTo, fallback]); + + // Check if user is an agent when profile is not available + useEffect(() => { + if (!user || profile || checkingAgent || isAgent !== null) { + return; + } + + const checkAgent = async () => { + setCheckingAgent(true); + try { + const agentExists = await AuthService.checkAgentExists(user.uid); + setIsAgent(agentExists); + } catch (error) { + console.error('ProtectedRoute - error checking agent:', error); + setIsAgent(false); + } finally { + setCheckingAgent(false); + } + }; + + checkAgent(); + }, [user, profile, checkingAgent, isAgent]); + + // Show loading state while checking authentication, profile, or agent status + if (loading || profileLoading || checkingAgent) { + console.log("ProtectedRoute - showing loading state"); + return ( + + Loading... + + ); + } + + // If user is not authenticated, redirect or show fallback + if (!user) { + if (fallback) { + return {fallback}; + } + + return null; + } + + // In dev, allow fake emulator user even without profile + if (__DEV__ && user.uid === "dev-emulator-user") { + return {children}; + } + + // If user has a profile, allow access + if (profile) { + return {children}; + } + + // If user is an agent (even without profile), allow access + if (isAgent === true) { + return {children}; + } + + // If we've checked and user is not an agent and has no profile, redirect + if (isAgent === false) { + console.log("ProtectedRoute - user has no profile and is not an agent, redirecting to signin"); + router.replace(redirectTo); + return null; + } + + // Still checking agent status, show loading + return ( + + Loading... + + ); +}; + +export default ProtectedRoute; diff --git a/components/ui/FourDotLoader.tsx b/components/ui/FourDotLoader.tsx new file mode 100644 index 0000000..1c66955 --- /dev/null +++ b/components/ui/FourDotLoader.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useRef } from "react"; +import { View, Animated, Easing } from "react-native"; + +const FourDotLoader: React.FC = () => { + const dots = [ + useRef(new Animated.Value(0)).current, + useRef(new Animated.Value(0)).current, + useRef(new Animated.Value(0)).current, + useRef(new Animated.Value(0)).current, + ]; + + useEffect(() => { + const animations = dots.map((dot, index) => { + const core = Animated.sequence([ + Animated.timing(dot, { + toValue: -14, + duration: 260, + easing: Easing.inOut(Easing.quad), + useNativeDriver: true, + }), + Animated.timing(dot, { + toValue: 0, + duration: 260, + easing: Easing.inOut(Easing.quad), + useNativeDriver: true, + }), + ]); + + // One-time staggered delay, then infinite loop + const animation = Animated.sequence([ + Animated.delay(index * 120), + Animated.loop(core), + ]); + + animation.start(); + return animation; + }); + + return () => { + animations.forEach((anim) => anim.stop()); + }; + }, []); + + return ( + + {dots.map((dot, index) => ( + + ))} + + ); +}; + +export default FourDotLoader; diff --git a/components/ui/GlobalLoadingOverlay.tsx b/components/ui/GlobalLoadingOverlay.tsx new file mode 100644 index 0000000..71e815d --- /dev/null +++ b/components/ui/GlobalLoadingOverlay.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { StyleSheet, View, Image } from "react-native"; +import { useUiStore } from "~/lib/stores"; + +const GlobalLoadingOverlay: React.FC = () => { + const isLoading = useUiStore((state) => state.isLoading); + const isOpaque = useUiStore((state) => state.isOpaque); + + if (!isLoading) { + return null; + } + + // Opaque mode: full screen white background with primary loader + if (isOpaque) { + return ( + + + + + ); + } + + // Default mode: semi-transparent overlay with loader + return ( + + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + alignItems: "center", + justifyContent: "center", + zIndex: 1000, + }, + backdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(0, 0, 0, 0.25)", + }, + opaqueBackdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "#ffffff", + }, + loaderContainer: { + paddingVertical: 24, + paddingHorizontal: 32, + borderRadius: 16, + }, +}); + +export default GlobalLoadingOverlay; diff --git a/components/ui/PhonePinKeypad.tsx b/components/ui/PhonePinKeypad.tsx new file mode 100644 index 0000000..cb2c184 --- /dev/null +++ b/components/ui/PhonePinKeypad.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { LucideChevronLeft } from "lucide-react-native"; + +interface PhonePinKeypadProps { + onKeyPress: (value: string) => void; + showDecimal?: boolean; +} + +export const PhonePinKeypad: React.FC = ({ + onKeyPress, + showDecimal = false, +}) => { + const keySizeClasses = "w-28 h-10 mx-2 my-2"; + const baseKeyClasses = `${keySizeClasses} bg-[#eee] items-center justify-center`; + + const renderNumberKey = (label: string) => ( + + ); + + return ( + + {/* Row 1: 1, 2, 3 */} + + {renderNumberKey("1")} + {renderNumberKey("2")} + {renderNumberKey("3")} + + + {/* Row 2: 4, 5, 6 */} + + {renderNumberKey("4")} + {renderNumberKey("5")} + {renderNumberKey("6")} + + + {/* Row 3: 7, 8, 9 */} + + {renderNumberKey("7")} + {renderNumberKey("8")} + {renderNumberKey("9")} + + + {/* Row 4: decimal/empty, 0, backspace */} + + {showDecimal ? ( + + ) : ( + + )} + + + + + + + ); +}; diff --git a/components/ui/ScreenWrapper.tsx b/components/ui/ScreenWrapper.tsx new file mode 100644 index 0000000..5f62ccc --- /dev/null +++ b/components/ui/ScreenWrapper.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Platform, View } from "react-native"; +import { SafeAreaView, Edge } from "react-native-safe-area-context"; +import { StatusBar } from "expo-status-bar"; +import { useColorScheme } from "react-native"; +import type { StatusBarStyle } from "expo-status-bar"; + +interface ScreenWrapperProps { + children: React.ReactNode; + /** Safe area edges to apply (e.g. ["top", "bottom"]) */ + edges?: Edge[]; + /** Optional background color for the screen */ + backgroundColor?: string; + /** Optional tailwind / nativewind className for inner content */ + className?: string; + /** Override status bar style; defaults based on device color scheme */ + statusBarStyle?: StatusBarStyle; +} + +export function ScreenWrapper({ + children, + edges = ["top", "bottom"], + backgroundColor = "#ffffff", + className, + statusBarStyle, +}: ScreenWrapperProps) { + const scheme = useColorScheme(); + + const resolvedStatusBarStyle: StatusBarStyle = + statusBarStyle ?? (scheme === "dark" ? "light" : "dark"); + + return ( + + + {children} + + ); +} + +export default ScreenWrapper; diff --git a/components/ui/accCard.tsx b/components/ui/accCard.tsx new file mode 100644 index 0000000..49193e7 --- /dev/null +++ b/components/ui/accCard.tsx @@ -0,0 +1,84 @@ +import { + LucideChevronRightCircle, + LucideSend, + LucideUploadCloud, + LucideCreditCard, +} from "lucide-react-native"; +import React from "react"; +import { View, Text, Image } from "react-native"; +import { Button } from "~/components/ui/button"; +import { useTranslation } from "react-i18next"; + +interface AccountCardProps { + cardType?: string; + cardNumber?: string; + expiryDate?: string; + onPress?: () => void; + selected?: boolean; +} + +export default function AccountCard({ + cardType, + cardNumber, + expiryDate, + onPress, + selected = false, +}: AccountCardProps) { + const { t } = useTranslation(); + + const resolvedCardType = cardType || t("components.acccard.cardTypeFallback"); + const resolvedCardNumber = + cardNumber || t("components.acccard.cardNumberPlaceholder"); + const resolvedExpiryDate = + expiryDate || t("components.acccard.expiryPlaceholder"); + + const getCardColor = (type: string) => { + switch (type?.toLowerCase()) { + case "visa": + return "bg-blue-50"; + case "mastercard": + return "bg-red-50"; + case "american express": + return "bg-green-50"; + case "discover": + return "bg-orange-50"; + default: + return "bg-gray-50"; + } + }; + + return ( + + + + + + + + {resolvedCardType} + + {resolvedCardNumber} + + + {t("components.acccard.expiryLabel", { date: resolvedExpiryDate })} + + + + + + + + + ); +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..fe14f44 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,121 @@ +import * as AccordionPrimitive from '@rn-primitives/accordion'; +import * as React from 'react'; +import { Pressable } from 'react-native'; +import Animated, { + Extrapolation, + FadeIn, + FadeOutUp, + LayoutAnimationConfig, + LinearTransition, + interpolate, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import { ChevronDown } from '~/components/ui/icons'; +import { cn } from '~/lib/utils'; +import { TextClassContext } from '~/components/ui/text'; + +const Accordion = React.forwardRef( + ({ children, ...props }, ref) => { + return ( + + + {children} + + + ); + } +); + +Accordion.displayName = AccordionPrimitive.Root.displayName; + +const AccordionItem = React.forwardRef( + ({ className, value, ...props }, ref) => { + return ( + + + + ); + } +); +AccordionItem.displayName = AccordionPrimitive.Item.displayName; + +const Trigger = Pressable; + +const AccordionTrigger = React.forwardRef< + AccordionPrimitive.TriggerRef, + AccordionPrimitive.TriggerProps +>(({ className, children, ...props }, ref) => { + const { isExpanded } = AccordionPrimitive.useItemContext(); + + const progress = useDerivedValue(() => + isExpanded ? withTiming(1, { duration: 250 }) : withTiming(0, { duration: 200 }) + ); + const chevronStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${progress.value * 180}deg` }], + opacity: interpolate(progress.value, [0, 1], [1, 0.8], Extrapolation.CLAMP), + })); + + return ( + + + + + {typeof children === 'function' ? children({ pressed: false }) : children} + + + + + + + + ); +}); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + AccordionPrimitive.ContentRef, + AccordionPrimitive.ContentProps +>(({ className, children, ...props }, ref) => { + const { isExpanded } = AccordionPrimitive.useItemContext(); + return ( + + + {children} + + + ); +}); + +function InnerContent({ children, className }: { children: React.ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/components/ui/backBar.tsx b/components/ui/backBar.tsx new file mode 100644 index 0000000..6ec7e68 --- /dev/null +++ b/components/ui/backBar.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { ArrowLeftIcon } from "lucide-react-native"; +import { Button } from "~/components/ui/button"; + +interface BackCardProps { + text: string; +} + +export default function BackCard({ text }: BackCardProps) { + return ( + + + + + + {text} + + + + + + + ); +} diff --git a/components/ui/backButton.tsx b/components/ui/backButton.tsx new file mode 100644 index 0000000..8d2b4f5 --- /dev/null +++ b/components/ui/backButton.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { ArrowLeft } from "lucide-react-native"; +import { router, useNavigation } from "expo-router"; +import { useTabStore } from "~/lib/stores"; + +interface BackButtonProps { + onPress?: () => void; + className?: string; + iconSize?: number; + iconColor?: string; +} + +export default function BackButton({ + onPress, + className = "flex flex-row justify-start w-full pr-4", + iconSize = 24, + iconColor = "text-primary", +}: BackButtonProps) { + const { lastVisitedTab } = useTabStore(); + const navigation = useNavigation(); + + const handlePress = () => { + if (onPress) { + onPress(); + } else { + router.back(); + } + }; + + return ( + + + + ); +} diff --git a/components/ui/bottomSheet.tsx b/components/ui/bottomSheet.tsx new file mode 100644 index 0000000..453ac3e --- /dev/null +++ b/components/ui/bottomSheet.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { + Modal, + View, + Pressable, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; + +interface BottomSheetProps { + visible: boolean; + onClose: () => void; + children: React.ReactNode; + dismissOnBackdropPress?: boolean; + maxHeightRatio?: number; +} + +const BottomSheet: React.FC = ({ + visible, + onClose, + children, + dismissOnBackdropPress = true, + maxHeightRatio = 0.9, +}) => { + if (!visible) return null; + + return ( + + + + + + + + + + + + {children} + + + + + + ); +}; + +export default BottomSheet; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..13423a2 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,110 @@ +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; +import { Pressable, Platform } from 'react-native'; +import { cn } from '~/lib/utils'; +import { TextClassContext } from '~/components/ui/text'; + +const buttonVariants = cva( + 'group flex items-center justify-center rounded-md', + { + variants: { + variant: { + default: 'bg-primary active:opacity-90', + destructive: 'bg-destructive active:opacity-90', + outline: + 'border border-input bg-background active:bg-accent', + secondary: 'bg-secondary active:opacity-80', + ghost: 'active:bg-accent', + link: '', + }, + size: { + default: 'h-10 px-4 py-2 native:h-12 native:px-5 native:py-3', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8 native:h-14', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +const buttonTextVariants = cva( + 'text-sm native:text-base font-medium text-foreground', + { + variants: { + variant: { + default: 'text-primary-foreground', + destructive: 'text-destructive-foreground', + outline: 'group-active:text-accent-foreground', + secondary: 'text-secondary-foreground group-active:text-secondary-foreground', + ghost: 'group-active:text-accent-foreground', + link: 'text-primary group-active:underline', + }, + size: { + default: '', + sm: '', + lg: 'native:text-lg', + icon: '', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +type ButtonProps = React.ComponentPropsWithoutRef & + VariantProps; + +const Button = React.forwardRef, ButtonProps>( + ({ className, variant, size, ...props }, ref) => { + // Get ripple color based on variant + const getRippleColor = () => { + switch (variant) { + case 'destructive': + return 'rgba(255, 255, 255, 0.3)'; + case 'outline': + case 'ghost': + return 'rgba(0, 0, 0, 0.1)'; + case 'secondary': + return 'rgba(0, 0, 0, 0.15)'; + default: + return 'rgba(255, 255, 255, 0.3)'; + } + }; + + // Android ripple configuration + const androidRippleConfig = + Platform.OS === 'android' + ? { + color: getRippleColor(), + borderless: false, + } + : undefined; + + return ( + + + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonTextVariants, buttonVariants }; +export type { ButtonProps }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..0ad18d1 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,55 @@ +import type { TextRef, ViewRef } from '@rn-primitives/types'; +import * as React from 'react'; +import { Text, type TextProps, View, type ViewProps } from 'react-native'; +import { cn } from '~/lib/utils'; +import { TextClassContext } from '~/components/ui/text'; + +const Card = React.forwardRef(({ className, ...props }, ref) => ( + +)); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( + +)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef(({ className, ...props }, ref) => ( + + + +)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( + +)); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/components/ui/cardComp.tsx b/components/ui/cardComp.tsx new file mode 100644 index 0000000..ea6f211 --- /dev/null +++ b/components/ui/cardComp.tsx @@ -0,0 +1,37 @@ +import { + LucideChevronRight, + LucideCreditCard, + LucideSend, +} from "lucide-react-native"; +import React from "react"; +import { View, Text, Image } from "react-native"; +import { Button } from "~/components/ui/button"; +import { useTranslation } from "react-i18next"; + +export default function CardComp() { + const { t } = useTranslation(); + + return ( + + + + + + + + {t("components.cardcomp.title")} + + + {t("components.cardcomp.number")} + + + + + + + + + ); +} diff --git a/components/ui/contactModal.tsx b/components/ui/contactModal.tsx new file mode 100644 index 0000000..f5414d9 --- /dev/null +++ b/components/ui/contactModal.tsx @@ -0,0 +1,456 @@ +import React, { useState, useEffect } from "react"; +import { + Modal, + View, + Text, + Image, + TouchableOpacity, + ScrollView, + Linking, +} from "react-native"; +import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; +import { SafeAreaView } from "react-native-safe-area-context"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { X, Phone, Mail, User, MessageCircle } from "lucide-react-native"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { Contact } from "~/lib/stores"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useTranslation } from "react-i18next"; + +interface ContactModalProps { + visible: boolean; + contact: Contact | null; + onClose: () => void; +} + +type LinkedAccount = { + id: string; + bankId: string; + bankName: string; + accountNumber: string; +}; + +const BANK_OPTIONS: { id: string; name: string }[] = [ + { id: "cbe", name: "Commercial Bank of Ethiopia" }, + { id: "dashen", name: "Dashen Bank" }, + { id: "abay", name: "Abay Bank" }, + { id: "awash", name: "Awash Bank" }, + { id: "hibret", name: "Hibret Bank" }, + { id: "telebirr", name: "Ethio Telecom (Telebirr)" }, + { id: "safaricom", name: "Safaricom M-PESA" }, +]; + +export default function ContactModal({ + visible, + contact, + onClose, +}: ContactModalProps) { + const { t } = useTranslation(); + + const [linkedAccounts, setLinkedAccounts] = useState([]); + const [isAddingAccount, setIsAddingAccount] = useState(false); + const [selectedBank, setSelectedBank] = useState(null); + const [accountNumber, setAccountNumber] = useState(""); + + useEffect(() => { + const loadLinkedAccounts = async () => { + if (!visible || !contact?.id) { + setLinkedAccounts([]); + return; + } + + try { + const storageKey = `contact_linked_accounts_${contact.id}`; + const stored = await AsyncStorage.getItem(storageKey); + if (stored) { + const parsed = JSON.parse(stored); + if (Array.isArray(parsed)) { + setLinkedAccounts(parsed as LinkedAccount[]); + } else { + setLinkedAccounts([]); + } + } else { + setLinkedAccounts([]); + } + } catch (error) { + if (__DEV__) { + console.warn( + "[ContactModal] Failed to load linked accounts from storage", + error + ); + } + setLinkedAccounts([]); + } + }; + + loadLinkedAccounts(); + }, [visible, contact]); + + if (!contact) return null; + + const displayName = + contact.name || t("components.contactmodal.unknownContact"); + const initials = displayName + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .substring(0, 2); + + const handleCall = (phoneNumber: string) => { + const cleanNumber = phoneNumber.replace(/[^+\d]/g, ""); + Linking.openURL(`tel:${cleanNumber}`); + }; + + const handleSMS = (phoneNumber: string) => { + const cleanNumber = phoneNumber.replace(/[^+\d]/g, ""); + Linking.openURL(`sms:${cleanNumber}`); + }; + + const handleEmail = (email: string) => { + Linking.openURL(`mailto:${email}`); + }; + + const handleSendMoney = () => { + if (!contact) return; + + // Close modal first + onClose(); + + // Navigate to send money with contact info + router.push({ + pathname: ROUTES.SEND_OR_REQUEST_MONEY, + params: { + selectedContactId: contact.id, + selectedContactName: contact.name, + selectedContactPhone: contact.phoneNumbers?.[0]?.number || "", + }, + }); + }; + + const handleStartAddAccount = () => { + setIsAddingAccount(true); + }; + + const handleSelectBank = (bankId: string) => { + setSelectedBank(bankId); + }; + + const handleSaveAccount = async () => { + if (!selectedBank || !accountNumber.trim()) { + return; + } + + const bank = BANK_OPTIONS.find((b) => b.id === selectedBank); + if (!bank) return; + + if (!contact?.id) { + return; + } + + const newAccount: LinkedAccount = { + id: `${selectedBank}-${Date.now()}`, + bankId: selectedBank, + bankName: bank.name, + accountNumber: accountNumber.trim(), + }; + + const updatedAccounts = [...linkedAccounts, newAccount]; + setLinkedAccounts(updatedAccounts); + + try { + const storageKey = `contact_linked_accounts_${contact.id}`; + await AsyncStorage.setItem(storageKey, JSON.stringify(updatedAccounts)); + } catch (error) { + if (__DEV__) { + console.warn( + "[ContactModal] Failed to persist linked accounts to storage", + error + ); + } + } + setAccountNumber(""); + setSelectedBank(null); + setIsAddingAccount(false); + }; + + const isTelecomWallet = + selectedBank === "telebirr" || selectedBank === "safaricom"; + const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number"; + const accountPlaceholder = isTelecomWallet + ? "Enter phone number" + : "Enter account number"; + + return ( + + + + {/* Header */} + + + {t("components.contactmodal.headerTitle")} + + + + + + + + {/* Contact Avatar & Name */} + + + {contact.imageAvailable && contact.image ? ( + + ) : ( + + {initials} + + )} + + + + {displayName} + + + {contact.firstName && contact.lastName && ( + + {contact.firstName} {contact.lastName} + + )} + + + {/* Contact Information */} + + {/* Phone Numbers */} + {contact.phoneNumbers && contact.phoneNumbers.length > 0 && ( + + + {t("components.contactmodal.phoneNumbersTitle")} + + {contact.phoneNumbers.map((phone, index) => ( + + + + {/* Add horizontal spacer here */} + + + + {phone.number} + + {phone.label && ( + + {phone.label} + + )} + + + {/* + handleCall(phone.number)} + className="p-2 bg-green-100 rounded-full" + > + + + handleSMS(phone.number)} + className="p-2 bg-blue-100 rounded-full" + > + + + */} + + ))} + + )} + + {/* Linked Accounts */} + {linkedAccounts.length > 0 && ( + + + Linked Accounts + + {linkedAccounts.map((account) => ( + + + + {account.bankName} + + + {account.accountNumber} + + + + ))} + + )} + + {/* Email Addresses */} + {contact.emails && contact.emails.length > 0 && ( + + + {t("components.contactmodal.emailAddressesTitle")} + + {contact.emails.map((email, index) => ( + + + + + + + {email.email} + + {email.label && ( + + {email.label} + + )} + + + {/* handleEmail(email.email)} + className="p-2 bg-purple-100 rounded-full" + > + + */} + + ))} + + )} + + {/* No additional info message */} + {(!contact.phoneNumbers || contact.phoneNumbers.length === 0) && + (!contact.emails || contact.emails.length === 0) && ( + + + + {t("components.contactmodal.noAdditionalInfo")} + + + )} + + + + {/* Action Buttons */} + + + + + + + + {/* Add Account Bottom Sheet */} + setIsAddingAccount(false)} + maxHeightRatio={0.9} + > + + + Add Account + + + + + Bank + + {BANK_OPTIONS.map((bank) => { + const isSelected = selectedBank === bank.id; + const initials = bank.name + .split(" ") + .map((part) => part[0]) + .join("") + .toUpperCase() + .slice(0, 2); + + return ( + handleSelectBank(bank.id)} + className={`items-center justify-between px-3 py-4 mb-3 rounded-2xl border ${ + isSelected + ? "border-primary bg-primary/5" + : "border-gray-200 bg-white" + }`} + style={{ width: "30%" }} + > + + + {initials} + + + + {bank.name} + + + ); + })} + + + + + + {accountLabel} + + + setAccountNumber(text.replace(/[^0-9]/g, "")) + } + containerClassName="w-full mb-4" + borderClassName="border-[#E5E7EB] bg-white rounded-[4px]" + placeholderColor="#9CA3AF" + textClassName="text-[#111827] text-sm" + keyboardType="number-pad" + /> + + + + + + ); +} diff --git a/components/ui/dropdown.tsx b/components/ui/dropdown.tsx new file mode 100644 index 0000000..d3c81c4 --- /dev/null +++ b/components/ui/dropdown.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import { View, TouchableOpacity } from "react-native"; +import { Text } from "~/components/ui/text"; +import BottomSheet from "~/components/ui/bottomSheet"; +import { ChevronDown, Check } from "lucide-react-native"; + +export interface DropdownOption { + label: string; + value: string; +} + +interface DropdownProps { + value: string | null; + options: DropdownOption[]; + onSelect: (value: string) => void; + placeholder?: string; +} + +const Dropdown: React.FC = ({ + value, + options, + onSelect, + placeholder = "Select", +}) => { + const [open, setOpen] = useState(false); + + const selectedOption = options.find((opt) => opt.value === value) || null; + + return ( + <> + setOpen(true)} + className="flex-row items-center justify-between w-full border border-[#D9DBE9] bg-white rounded-md px-3 py-4" + > + + {selectedOption ? selectedOption.label : placeholder} + + + + + setOpen(false)} + maxHeightRatio={0.4} + > + + {options.map((opt) => { + const selected = value === opt.value; + return ( + { + onSelect(opt.value); + setOpen(false); + }} + className="py-3 flex-row items-center border-b border-gray-100" + > + {/* Radio indicator on the left */} + + {selected ? ( + + + + ) : ( + + )} + + + {/* Label */} + + {opt.label} + + + ); + })} + + + + ); +}; + +export default Dropdown; diff --git a/components/ui/icons.tsx b/components/ui/icons.tsx new file mode 100644 index 0000000..1384b92 --- /dev/null +++ b/components/ui/icons.tsx @@ -0,0 +1,384 @@ +import React from 'react'; +import { MoonStar, CircleDollarSign, LucideEye, Check, ChevronUp, ChevronDown } from 'lucide-react-native'; +import type { LucideIcon } from 'lucide-react-native'; +import { cssInterop } from 'nativewind'; +import Svg, { ClipPath, Defs, G, Path, Rect } from 'react-native-svg'; + +export function iconWithClassName(icon: LucideIcon) { + cssInterop(icon, { + className: { + target: 'style', + nativeStyleToProp: { + color: true, + opacity: true, + }, + }, + }); +} + +// Apply className support to all icons +iconWithClassName(MoonStar); +iconWithClassName(CircleDollarSign); +iconWithClassName(LucideEye); +iconWithClassName(Check); +iconWithClassName(ChevronUp); +iconWithClassName(ChevronDown); + +const AwashIcon = ({ width = 40, height = 8 }: { width?: number, height?: number }) => { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} + +const TeleBirrIcon = ({ width = 23, height = 24 }: { width?: number, height?: number }) => { + return ( + + + + + + + + + + ); +}; + + +const SuccessIcon = () => { + return ( + + + + + + + + + + ); +}; + +const BottomBarTransferIcon = ({ color, className }: { color?: string, className?: string }) => { + return ( + + + + ); +}; + +const SuccessIconNewRecipient = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const ProfileUserIcon = () => { + return ( + + + + ); +}; + +const CreditDebitCardIcon = ({ width, height }: { width?: number, height?: number }) => { + return ( + + + + + ); +}; + +const SuccessIconNewCard = ({ width, height }: { width?: number, height?: number }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const AmbapayIcon = ({ width = 95, height = 95 }: { width?: number; height?: number }) => { + return ( + + + + + + + + + + + ); +}; + +const GoogleIcon = ({ width = 20, height = 20 }: { width?: number; height?: number }) => { + return ( + + + + + + + ); +}; + +const NotificationIcon = () => { + return ( + + + + + + + + ); +}; + +const ApplePayIcon = () => { + return ( + + + + ); +} + +const GooglePayIcon = () => { + return ( + + + + + + + + + + ); +}; + +const SMSIcon = ({ width = 28, height = 28 }: { width?: number, height?: number }) => { + return ( + + + + ); +} + +const WhatsappIcon = ({ width = 23, height = 23 }: { width?: number, height?: number }) => { + return ( + + + + + ); +} + +// Export all icons +export { + MoonStar, CircleDollarSign, + LucideEye, Check, + ChevronUp, ChevronDown, + AwashIcon, TeleBirrIcon, SuccessIcon, + BottomBarTransferIcon, + ProfileUserIcon, SuccessIconNewRecipient, + CreditDebitCardIcon, + SuccessIconNewCard, AmbapayIcon, + NotificationIcon, ApplePayIcon, GooglePayIcon, + SMSIcon, WhatsappIcon, GoogleIcon +}; + + diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..cac60b9 --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,98 @@ +import * as React from "react"; +import { View, TextInput, type TextInputProps } from "react-native"; +import { cn } from "~/lib/utils"; + +interface InputProps extends TextInputProps { + containerClassName?: string; + borderClassName?: string; + placeholderText?: string; + placeholderClassName?: string; + placeholderColor?: string; + textClassName?: string; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; +} + +const Input = React.forwardRef, InputProps>( + ( + { + containerClassName, + borderClassName, + className, + placeholderText, + placeholderClassName, + placeholderColor, + textClassName, + leftIcon, + rightIcon, + placeholder, + ...props + }, + ref + ) => { + const resolvedPlaceholder = placeholder ?? placeholderText; + + // If no container / icons are provided, fall back to a simple input + if (!leftIcon && !rightIcon && !containerClassName && !borderClassName) { + return ( + + ); + } + + // Enhanced layout: rounded container with optional left/right icons (search-bar style) + return ( + + + {leftIcon ? {leftIcon} : null} + + + + + {rightIcon ? ( + + {rightIcon} + + ) : null} + + ); + } +); + +Input.displayName = "Input"; + +export { Input }; diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..a48a12d --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,26 @@ +import * as LabelPrimitive from '@rn-primitives/label'; +import * as React from 'react'; +import { cn } from '~/lib/utils'; + +const Label = React.forwardRef( + ({ className, onPress, onLongPress, onPressIn, onPressOut, ...props }, ref) => ( + + + + ) +); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/components/ui/moneyCard.tsx b/components/ui/moneyCard.tsx new file mode 100644 index 0000000..21df2d4 --- /dev/null +++ b/components/ui/moneyCard.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { useTranslation } from "react-i18next"; + +export default function MoneyCard() { + const { t } = useTranslation(); + + return ( + + + {t("components.moneycard.brand")} + + + {t("components.moneycard.number")} + + + {t("components.moneycard.expiryLabel", { date: "12/2024" })} + + + ); +} diff --git a/components/ui/numberPad.tsx b/components/ui/numberPad.tsx new file mode 100644 index 0000000..21bc0ae --- /dev/null +++ b/components/ui/numberPad.tsx @@ -0,0 +1,155 @@ +import React from "react"; +import { View } from "react-native"; +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { LucideChevronLeft } from "lucide-react-native"; + +// Number Button Component +interface NumberButtonProps { + value: string; + onPress: (value: string) => void; + variant?: "default" | "special"; + icon?: React.ReactNode; + size?: "default" | "small"; +} + +export const NumberButton = ({ + value, + onPress, + variant = "default", + icon, + size = "default", +}: NumberButtonProps) => { + const sizeClasses = size === "small" ? "w-20 h-14 mx-2" : "w-24 h-18 mx-2"; + const textSize = size === "small" ? "text-xl" : "text-3xl"; + const baseClasses = `rounded-3xl ${sizeClasses} flex items-center justify-center active:scale-95 bg-[#111111]`; + + return ( + + ); +}; + +// Number Pad Component +interface NumberPadProps { + onNumberPress: (value: string) => void; + showDecimal?: boolean; +} + +export const NumberPad: React.FC = ({ + onNumberPress, + showDecimal = true, +}) => { + return ( + + {/* Row 1: 1, 2, 3 */} + + + + + + + {/* Row 2: 4, 5, 6 */} + + + + + + + {/* Row 3: 7, 8, 9 */} + + + + + + + {/* Row 4: decimal/empty, 0, backspace */} + + {showDecimal ? ( + + ) : ( + /* Empty space */ + )} + + } + /> + + + ); +}; + +// Smaller Number Pad for PIN (with smaller buttons) +export const PinNumberPad: React.FC<{ + onNumberPress: (value: string) => void; +}> = ({ onNumberPress }) => { + const baseKeyClasses = + "flex-1 h-14 rounded-3xl bg-[#111111] items-center justify-center"; + + const renderNumberKey = (label: string) => ( + + ); + + return ( + + {/* Row 1: 1, 2, 3 */} + + {renderNumberKey("1")} + {renderNumberKey("2")} + {renderNumberKey("3")} + + + {/* Row 2: 4, 5, 6 */} + + {renderNumberKey("4")} + {renderNumberKey("5")} + {renderNumberKey("6")} + + + {/* Row 3: 7, 8, 9 */} + + {renderNumberKey("7")} + {renderNumberKey("8")} + {renderNumberKey("9")} + + + {/* Row 4: empty, 0, backspace */} + + + + + + + + + ); +}; diff --git a/components/ui/permissionAlertModal.tsx b/components/ui/permissionAlertModal.tsx new file mode 100644 index 0000000..dd3f830 --- /dev/null +++ b/components/ui/permissionAlertModal.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { Modal, View, Text, TouchableOpacity, Platform } from "react-native"; + +interface PermissionAlertModalProps { + visible: boolean; + title: string; + message: string; + primaryText: string; // Right button (e.g. OK / Confirm) + secondaryText: string; // Left button (e.g. Cancel) + onPrimary: () => void; + onSecondary: () => void; + primaryVariant?: "primary" | "danger"; +} + +const PermissionAlertModal: React.FC = ({ + visible, + title, + message, + primaryText, + secondaryText, + onPrimary, + onSecondary, + primaryVariant = "primary", +}) => { + const primaryBgClass = + primaryVariant === "danger" ? "bg-red-500" : "bg-primary"; + return ( + + + + {/* Title & message */} + + {title} + + + {message} + + + {/* Buttons row */} + + {/* Secondary: outlined button */} + + + {secondaryText} + + + + {/* Primary: filled button */} + + + {primaryText} + + + + + + + ); +}; +export default PermissionAlertModal; diff --git a/components/ui/pinConfirmationModal.tsx b/components/ui/pinConfirmationModal.tsx new file mode 100644 index 0000000..5a56e2c --- /dev/null +++ b/components/ui/pinConfirmationModal.tsx @@ -0,0 +1,483 @@ +import React, { useEffect, useState, useRef } from "react"; +import { + View, + Text, + Modal, + Platform, + ActivityIndicator, + TouchableOpacity, +} from "react-native"; +import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; +import { Button } from "~/components/ui/button"; +import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { useUserProfile } from "~/lib/hooks/useUserProfile"; +import { router } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import BackButton from "./backButton"; +import { Fingerprint, Lock } from "lucide-react-native"; +import ModalToast from "~/components/ui/toast"; + +// Only import LocalAuthentication on native platforms +let LocalAuthentication: any = null; +if (Platform.OS !== "web") { + LocalAuthentication = require("expo-local-authentication"); +} +import { useTranslation } from "react-i18next"; + +interface PinConfirmationModalProps { + visible: boolean; + onClose: () => void; + onSuccess: () => void; + title?: string; +} + +export const PinConfirmationModal: React.FC = ({ + visible, + onClose, + onSuccess, + title, +}) => { + const { t } = useTranslation(); + + const effectiveTitle = + title ?? t("components.pinconfirmationmodal.titleDefault"); + const { user } = useAuthWithProfile(); + const { profile } = useUserProfile(user); + + const [pin, setPin] = useState(""); + const [isVerifying, setIsVerifying] = useState(false); + const [isAttemptingBiometric, setIsAttemptingBiometric] = useState(false); + const [biometricAttempted, setBiometricAttempted] = useState(false); + const [authMethod, setAuthMethod] = useState<"none" | "fingerprint" | "pin">( + "none" + ); + const [biometricAvailable, setBiometricAvailable] = useState(false); + const prevVisibleRef = useRef(false); + const [toastVisible, setToastVisible] = useState(false); + const [toastTitle, setToastTitle] = useState(""); + const [toastDescription, setToastDescription] = useState( + undefined + ); + const toastTimeoutRef = useRef | null>(null); + + const showToast = (title: string, description?: string) => { + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + + setToastTitle(title); + setToastDescription(description); + setToastVisible(true); + + toastTimeoutRef.current = setTimeout(() => { + setToastVisible(false); + toastTimeoutRef.current = null; + }, 2500); + }; + + const attemptBiometric = async () => { + // Web: No biometric support + if (Platform.OS === "web" || !LocalAuthentication) { + showToast("Error", "Biometric authentication not available on web"); + setAuthMethod("none"); + return; + } + + if (!user) return; + + try { + const hasHardware = await LocalAuthentication.hasHardwareAsync(); + if (!hasHardware) { + showToast( + t("components.pinconfirmationmodal.toastBiometricErrorTitle"), + t( + "components.pinconfirmationmodal.toastBiometricHardwareNotAvailable" + ) + ); + setAuthMethod("none"); + return; + } + const isEnrolled = await LocalAuthentication.isEnrolledAsync(); + if (!isEnrolled) { + showToast( + t("components.pinconfirmationmodal.toastBiometricErrorTitle"), + t("components.pinconfirmationmodal.toastBiometricNotEnrolled") + ); + setAuthMethod("none"); + return; + } + + setIsAttemptingBiometric(true); + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: effectiveTitle, + cancelLabel: t("components.pinconfirmationmodal.cancelButton"), + disableDeviceFallback: true, + requireConfirmation: false, + }); + setIsAttemptingBiometric(false); + if (result.success) { + setPin(""); + setBiometricAttempted(false); + setAuthMethod("none"); + onSuccess(); + } else { + // User cancelled or failed - reset to choice screen + setAuthMethod("none"); + } + } catch (e) { + setIsAttemptingBiometric(false); + setAuthMethod("none"); + showToast( + t("components.pinconfirmationmodal.toastBiometricErrorTitle"), + t("components.pinconfirmationmodal.toastBiometricFailed") + ); + } + }; + + const handleFingerprintChoice = () => { + setAuthMethod("fingerprint"); + attemptBiometric(); + }; + + const handlePinChoice = () => { + setAuthMethod("pin"); + }; + + // Check biometric availability on mount and when modal opens + useEffect(() => { + const checkBiometricAvailability = async () => { + // Web: Biometrics not available + if (Platform.OS === "web" || !LocalAuthentication) { + setBiometricAvailable(false); + return; + } + + // Native: Check for biometric hardware and enrollment + if (Platform.OS === "android" || Platform.OS === "ios") { + try { + const hasHardware = await LocalAuthentication.hasHardwareAsync(); + const isEnrolled = await LocalAuthentication.isEnrolledAsync(); + setBiometricAvailable(hasHardware && isEnrolled); + } catch (e) { + setBiometricAvailable(false); + } + } else { + setBiometricAvailable(false); + } + }; + + if (visible) { + checkBiometricAvailability(); + } + }, [visible]); + + useEffect(() => { + const prevVisible = prevVisibleRef.current; + prevVisibleRef.current = visible; + + if (visible && !prevVisible) { + // Modal just opened (transition from false to true) + // Reset all states to ensure fresh start + setPin(""); + setIsAttemptingBiometric(false); + setBiometricAttempted(false); + setAuthMethod("none"); + } else if (!visible && prevVisible) { + // Modal just closed (transition from true to false) + // Reset all states for next time + setPin(""); + setIsAttemptingBiometric(false); + setBiometricAttempted(false); + setAuthMethod("none"); + } + }, [visible]); + + // Render nothing when not requested + if (!visible) { + return null; + } + + const handlePinSubmit = async () => { + if (pin.length !== 6) { + showToast( + t("components.pinconfirmationmodal.toastInvalidPinTitle"), + t("components.pinconfirmationmodal.toastInvalidPinDescription") + ); + return; + } + setIsVerifying(true); + + if (!user) { + showToast( + t("components.pinconfirmationmodal.toastAuthErrorTitle"), + t("components.pinconfirmationmodal.toastUserNotFound") + ); + setIsVerifying(false); + return; + } + + const profilePin = profile?.pin; + if (!profilePin) { + showToast( + t("components.pinconfirmationmodal.toastAuthErrorTitle"), + t("components.pinconfirmationmodal.toastPinNotFound") + ); + setIsVerifying(false); + return; + } + + // Incorrect PIN -> show inline toast instead of Alert + if (pin !== profilePin) { + showToast( + t("components.pinconfirmationmodal.toastIncorrectPinTitle"), + t("components.pinconfirmationmodal.toastIncorrectPinDescription") + ); + setPin(""); + setIsVerifying(false); + return; + } + + // PIN is correct, call success callback + setPin(""); + setIsVerifying(false); + setBiometricAttempted(false); + setIsAttemptingBiometric(false); + onSuccess(); + }; + + const handleNumberPress = (input: string) => { + if (input === "clear") { + setPin(""); + return; + } + + if (input === "backspace") { + handleBackspace(); + return; + } + + // Handle digit input (0-9) + if (!/^[0-9]$/.test(input)) return; + + // Limit to 6 digits + if (pin.length >= 6) return; + + setPin(pin + input); + }; + + const handleBackspace = () => { + setPin((prev) => prev.slice(0, -1)); + }; + + const handleClose = () => { + setPin(""); + setIsVerifying(false); + setIsAttemptingBiometric(false); + setBiometricAttempted(false); + setAuthMethod("none"); + onClose(); + }; + + const renderAuthMethodChoice = () => { + // On web, skip biometric option entirely and go straight to PIN + const showBiometricOption = biometricAvailable && Platform.OS !== "web"; + + return ( + + + + {showBiometricOption + ? "Choose Authentication Method" + : "PIN Verification"} + + + {effectiveTitle} + + + + + {showBiometricOption && ( + + + + + + + + {t("components.pinconfirmationmodal.fingerprintTitle")} + + + {t("components.pinconfirmationmodal.fingerprintSubtitle")} + + + + + )} + + {showBiometricOption && } + + + + + + + + + {t("components.pinconfirmationmodal.pinTitle")} + + + {t("components.pinconfirmationmodal.pinSubtitle")} + + + + + + + ); + }; + + const renderPinDots = () => { + return ( + + {[0, 1, 2, 3, 4, 5].map((index) => ( + + ))} + + ); + }; + + return ( + + + { + if (authMethod === "pin" || authMethod === "fingerprint") { + setAuthMethod("none"); + setPin(""); + setIsAttemptingBiometric(false); + } else { + handleClose(); + router.replace(ROUTES.HOME); + } + }} + /> + + {authMethod === "none" ? ( + + {renderAuthMethodChoice()} + + ) : isAttemptingBiometric ? ( + + + + + {t("components.pinconfirmationmodal.biometricWaiting")} + + + { + setAuthMethod("none"); + setIsAttemptingBiometric(false); + }} + className="px-6 py-3" + > + + {t("components.pinconfirmationmodal.cancelButton")} + + + + ) : ( + + + {/* Header + title */} + + + + {t("components.pinconfirmationmodal.pinVerificationTitle")} + + + + + + {effectiveTitle} + + + + + {/* Bottom-aligned PIN area */} + + {/* PIN dots + Keypad - full width above button */} + + {renderPinDots()} + + + + + + {/* Submit Button - just above bottom bar */} + + + + + + + )} + + {/* Toast overlay above everything */} + + + + ); +}; diff --git a/components/ui/profileCard.tsx b/components/ui/profileCard.tsx new file mode 100644 index 0000000..4bbd5a7 --- /dev/null +++ b/components/ui/profileCard.tsx @@ -0,0 +1,67 @@ +import { User } from "lucide-react-native"; +import React from "react"; +import { View, Text, Image, TouchableOpacity } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Contact } from "~/lib/stores"; + +interface ProfileCardProps { + contact?: Contact; + onPress?: () => void; +} + +export default function ProfileCard({ contact, onPress }: ProfileCardProps) { + const { t } = useTranslation(); + + if (!contact) { + return ( + + + + + + {t("components.profilecard.emptyLabelContact")} + + + ); + } + + const displayName = + contact.firstName || + contact.name || + t("components.profilecard.unknownContact"); + const initials = displayName + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .substring(0, 2); + + return ( + + + + {contact.imageAvailable && contact.image ? ( + + ) : ( + + + {initials} + + + )} + + + {displayName} + + + + ); +} diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..984c925 --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,49 @@ +import * as ProgressPrimitive from '@rn-primitives/progress'; +import * as React from 'react'; +import Animated, { + Extrapolation, + interpolate, + useAnimatedStyle, + useDerivedValue, + withSpring, +} from 'react-native-reanimated'; +import { cn } from '~/lib/utils'; + +const Progress = React.forwardRef< + ProgressPrimitive.RootRef, + ProgressPrimitive.RootProps & { + indicatorClassName?: string; + } +>(({ className, value, indicatorClassName, ...props }, ref) => { + return ( + + + + ); +}); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; + +function Indicator({ value, className }: { value: number | undefined | null; className?: string }) { + const progress = useDerivedValue(() => value ?? 0); + + const indicator = useAnimatedStyle(() => { + return { + width: withSpring( + `${interpolate(progress.value, [0, 100], [1, 100], Extrapolation.CLAMP)}%`, + { overshootClamping: true } + ), + }; + }); + + return ( + + + + ); +} diff --git a/components/ui/recipCard.tsx b/components/ui/recipCard.tsx new file mode 100644 index 0000000..05e8a61 --- /dev/null +++ b/components/ui/recipCard.tsx @@ -0,0 +1,39 @@ +import { LucideChevronRight, LucideSend } from "lucide-react-native"; +import React from "react"; +import { View, Text, Image } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Button } from "~/components/ui/button"; + +export default function RecipCard() { + const { t } = useTranslation(); + + return ( + + + + + + + + {t("components.recipcard.name")} + + + {t("components.recipcard.accountNumber")} + + + + + + + + + ); +} diff --git a/components/ui/scanProf.tsx b/components/ui/scanProf.tsx new file mode 100644 index 0000000..48a2d44 --- /dev/null +++ b/components/ui/scanProf.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { Link } from "expo-router"; +import { BellIcon, LucidePersonStanding, QrCode } from "lucide-react-native"; +import { ROUTES } from "~/lib/routes"; + +function ScanProf() { + return ( + + + + ); +} + +export default ScanProf; diff --git a/components/ui/screenWrap.tsx b/components/ui/screenWrap.tsx new file mode 100644 index 0000000..e827fc1 --- /dev/null +++ b/components/ui/screenWrap.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import { View, ScrollView, Platform, KeyboardAvoidingView } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +interface ScreenWrapProps { + children: React.ReactNode; + /** Whether the content should scroll (default: true) */ + scrollable?: boolean; + /** Background color class (default: "bg-background") */ + bgClass?: string; + /** Additional className for the container */ + className?: string; + /** Which edges to apply safe area (default: ['top', 'left', 'right']) */ + edges?: ('top' | 'bottom' | 'left' | 'right')[]; + /** Whether to add keyboard avoiding behavior (default: false) */ + keyboardAvoiding?: boolean; + /** Padding for content area (default: "px-5") */ + paddingClass?: string; + /** Whether to apply max-width constraint for web (default: true) */ + maxWidth?: boolean; +} + +/** + * Consistent screen wrapper that handles: + * - Safe area insets + * - Max-width constraint for web (prevents content from stretching too wide) + * - Consistent padding + * - Optional scrolling + * - Optional keyboard avoiding behavior + */ +export function ScreenWrap({ + children, + scrollable = true, + bgClass = "bg-background", + className = "", + edges = ['top', 'left', 'right'], + keyboardAvoiding = false, + paddingClass = "px-5", + maxWidth = true, +}: ScreenWrapProps) { + const content = ( + + {children} + + ); + + const scrollContent = scrollable ? ( + + {content} + + ) : content; + + const keyboardContent = keyboardAvoiding ? ( + + {scrollContent} + + ) : scrollContent; + + return ( + + {keyboardContent} + + ); +} + +/** + * Simpler wrapper for screens that don't need scrolling + * Just provides safe area and max-width constraint + */ +export function ScreenContainer({ + children, + bgClass = "bg-background", + className = "", + edges = ['top', 'left', 'right'], + paddingClass = "px-5", + maxWidth = true, +}: Omit) { + return ( + + + {children} + + + ); +} + +export default ScreenWrap; + diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..2a0518d --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,138 @@ +import * as SelectPrimitive from '@rn-primitives/select'; +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { Check, ChevronDown, ChevronUp } from '~/components/ui/icons'; +import { cn } from '~/lib/utils'; + +type Option = SelectPrimitive.Option; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef( + ({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + props.disabled && 'opacity-50', + className + )} + {...props} + > + {typeof children === 'function' ? children({ pressed: false }) : children} + + + ) +); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = () => null; + +const SelectScrollDownButton = () => null; + +const SelectContent = React.forwardRef< + SelectPrimitive.ContentRef, + SelectPrimitive.ContentProps & { portalHost?: string } +>(({ className, children, position = 'popper', portalHost, ...props }, ref) => { + const { open } = SelectPrimitive.useRootContext(); + + return ( + + + + + + + {children} + + + + + + + ); +}); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef( + ({ className, children, ...props }, ref) => ( + + + + + + + + + ) +); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + SelectPrimitive.SeparatorRef, + SelectPrimitive.SeparatorProps +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, + type Option, +}; diff --git a/components/ui/sendMoneyBar.tsx b/components/ui/sendMoneyBar.tsx new file mode 100644 index 0000000..30e0e5c --- /dev/null +++ b/components/ui/sendMoneyBar.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { View, Pressable, Image } from "react-native"; +import { Link } from "expo-router"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { Icons } from "~/assets/icons"; + +function SendMoneyBar() { + const { user, profile, profileLoading } = useAuthWithProfile(); + const fullName = profile?.fullName; + const firstName = fullName?.split(" ")[0]; + const avatarSource = profile?.photoUrl + ? { uri: profile.photoUrl } + : Icons.avatar; + + return ( + + + + + + + + + + + ); +} + +export default SendMoneyBar; diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..60233bb --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useRef } from "react"; +import { Animated, StyleProp, ViewStyle } from "react-native"; + +type Dimension = number | `${number}%`; + +export type SkeletonProps = { + width?: Dimension; + height?: Dimension; + radius?: number; + style?: StyleProp; +}; + +const Skeleton: React.FC = ({ + width = "100%", + height = 16, + radius = 8, + style, +}) => { + const opacity = useRef(new Animated.Value(0.6)).current; + + useEffect(() => { + const animation = Animated.loop( + Animated.sequence([ + Animated.timing(opacity, { + toValue: 0.4, + duration: 600, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 1, + duration: 800, + useNativeDriver: true, + }), + ]) + ); + + animation.start(); + + return () => { + animation.stop(); + }; + }, [opacity]); + + return ( + + ); +}; + +export default Skeleton; diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..15c23bb --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,59 @@ +import * as TabsPrimitive from '@rn-primitives/tabs'; +import * as React from 'react'; +import { cn } from '~/lib/utils'; +import { TextClassContext } from '~/components/ui/text'; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef( + ({ className, ...props }, ref) => { + const { value } = TabsPrimitive.useRootContext(); + return ( + + + + ); + } +); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef( + ({ className, ...props }, ref) => ( + + ) +); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/components/ui/text.tsx b/components/ui/text.tsx new file mode 100644 index 0000000..60039ba --- /dev/null +++ b/components/ui/text.tsx @@ -0,0 +1,24 @@ +import * as Slot from '@rn-primitives/slot'; +import type { SlottableTextProps, TextRef } from '@rn-primitives/types'; +import * as React from 'react'; +import { Text as RNText } from 'react-native'; +import { cn } from '~/lib/utils'; + +const TextClassContext = React.createContext(undefined); + +const Text = React.forwardRef( + ({ className, asChild = false, ...props }, ref) => { + const textClass = React.useContext(TextClassContext); + const Component = asChild ? Slot.Text : RNText; + return ( + + ); + } +); +Text.displayName = 'Text'; + +export { Text, TextClassContext }; diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx new file mode 100644 index 0000000..ef7796e --- /dev/null +++ b/components/ui/toast.tsx @@ -0,0 +1,191 @@ +import React, { useEffect, useRef } from "react"; +import { View, Text, StyleSheet, Animated } from "react-native"; +import { CheckCircle2, AlertCircle, Info } from "lucide-react-native"; + +interface ModalToastProps { + visible: boolean; + title: string; + description?: string; + /** + * Visual style of the toast. + * - success: green + * - error: red + * - warning: yellow + * - info: blue + */ + variant?: "success" | "error" | "warning" | "info"; +} + +const ModalToast: React.FC = ({ + visible, + title, + description, + variant = "error", +}) => { + const config = getVariantConfig(variant); + + const Icon = config.icon; + + const translateX = useRef(new Animated.Value(-40)).current; + const opacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + translateX.setValue(-40); + opacity.setValue(0); + + Animated.parallel([ + Animated.spring(translateX, { + toValue: 0, + useNativeDriver: true, + speed: 20, + bounciness: 6, + }), + Animated.timing(opacity, { + toValue: 1, + duration: 180, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible, translateX, opacity]); + + if (!visible) return null; + + return ( + + + + + + + + + + {title} + + {description ? ( + + {description} + + ) : null} + + + + + ); +}; + +function getVariantConfig(variant: "success" | "error" | "warning" | "info") { + switch (variant) { + case "success": + return { + backgroundColor: "#f1f9f5", + iconBackgroundColor: "#e1f0e2", + iconColor: "#16a34a", + titleColor: "#000", + descriptionColor: "#000", + icon: CheckCircle2, + borderColor: "#e1f0e2", + } as const; + case "warning": + return { + backgroundColor: "#fef7eb", + iconBackgroundColor: "#ebe3d5", + iconColor: "#eab308", + titleColor: "#000", + descriptionColor: "#000", + icon: AlertCircle, + borderColor: "#ebe3d5", + } as const; + case "info": + return { + backgroundColor: "#e8eefa", + iconBackgroundColor: "#cdd5e2", + iconColor: "#2563eb", + titleColor: "#000", + descriptionColor: "#000", + icon: Info, + borderColor: "#cdd5e2", + } as const; + case "error": + default: + return { + backgroundColor: "#fbf0f1", + iconBackgroundColor: "#ebd8d4", + iconColor: "#dc2000", + titleColor: "#000000", + descriptionColor: "#000", + icon: AlertCircle, + borderColor: "#ebd8d4", + } as const; + } +} + +const styles = StyleSheet.create({ + absoluteOverlay: { + zIndex: 9999, + elevation: 50, + }, + wrapper: { + alignItems: "center", + paddingHorizontal: 16, + marginTop: 16, + }, + toast: { + width: "100%", + maxWidth: 360, + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 12, + flexDirection: "row", + alignItems: "center", + shadowColor: "#000", + shadowOpacity: 0.18, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + }, + iconContainer: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: "center", + justifyContent: "center", + }, + textContainer: { + flex: 1, + marginLeft: 12, + }, + title: { + fontSize: 14, + fontWeight: "700", + }, + description: { + fontSize: 12, + marginTop: 4, + }, +}); + +export default ModalToast; diff --git a/components/ui/toastProvider.tsx b/components/ui/toastProvider.tsx new file mode 100644 index 0000000..7055d8d --- /dev/null +++ b/components/ui/toastProvider.tsx @@ -0,0 +1,106 @@ +import React, { createContext, useContext, useState, useRef } from "react"; +import { Animated, Pressable, Text, View, StyleSheet } from "react-native"; +import { Portal } from "@gorhom/portal"; + +const ToastContext = createContext(null); + +export const useToast = () => useContext(ToastContext); + +export const ToastProvider = ({ children }) => { + const [toast, setToast] = useState(null); + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + const show = (options) => { + setToast(options); + + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(translateY, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + + setTimeout(() => hide(), options.duration || 3000); + }; + + const hide = () => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(translateY, { + toValue: 20, + duration: 200, + useNativeDriver: true, + }), + ]).start(() => setToast(null)); + }; + + return ( + + {children} + + + {toast && ( + + + {toast.title} + {toast.description && ( + {toast.description} + )} + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + toastContainer: { + position: "absolute", + top: 50, + left: 20, + right: 20, + zIndex: 9999, + elevation: 9999, + shadowOpacity: 0.2, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + }, + toast: { + backgroundColor: "rgba(30, 30, 30, 0.95)", + borderRadius: 14, + padding: 14, + borderLeftWidth: 4, + }, + title: { + color: "white", + fontSize: 16, + fontWeight: "600", + }, + description: { + color: "#d4d4d4", + fontSize: 14, + marginTop: 2, + }, +}); diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx new file mode 100644 index 0000000..8ce91ea --- /dev/null +++ b/components/ui/toggle.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { TouchableOpacity, View, Animated } from "react-native"; + +interface ToggleProps { + value: boolean; + onValueChange: (value: boolean) => void; + disabled?: boolean; +} + +export default function Toggle({ + value, + onValueChange, + disabled = false, +}: ToggleProps) { + const animatedValue = React.useRef(new Animated.Value(value ? 1 : 0)).current; + + React.useEffect(() => { + Animated.spring(animatedValue, { + toValue: value ? 1 : 0, + useNativeDriver: true, + friction: 6, + tension: 40, + }).start(); + }, [value]); + + const translateX = animatedValue.interpolate({ + inputRange: [0, 1], + outputRange: [2, 20], + }); + + const handlePress = () => { + if (!disabled) { + onValueChange(!value); + } + }; + + return ( + + + + ); +} diff --git a/components/ui/topBar.tsx b/components/ui/topBar.tsx new file mode 100644 index 0000000..3fbcacc --- /dev/null +++ b/components/ui/topBar.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { View, Text, Pressable, Image } from "react-native"; +import { Link } from "expo-router"; +import { BellIcon, LucidePersonStanding } from "lucide-react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { ROUTES } from "~/lib/routes"; +import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; +import { Icons } from "~/assets/icons"; +import { useTranslation } from "react-i18next"; + +function TopBar() { + const { t } = useTranslation(); + const { profile, profileLoading } = useAuthWithProfile(); + const insets = useSafeAreaInsets(); + const fullName = profile?.fullName; + const firstName = fullName?.split(" ")[0]; + const avatarSource = profile?.photoUrl + ? { uri: profile.photoUrl } + : Icons.avatar; + + return ( + + + + {t("components.topbar.greeting")} + + + {profileLoading + ? "..." + : firstName && firstName.length > 8 + ? firstName.substring(0, 8) + : firstName} + + + + + + + + + + + + + + + + ); +} + +export default TopBar; diff --git a/components/ui/transactionCard.tsx b/components/ui/transactionCard.tsx new file mode 100644 index 0000000..b20055d --- /dev/null +++ b/components/ui/transactionCard.tsx @@ -0,0 +1,217 @@ +import { + LucideSend, + LucideUploadCloud, + LucideArrowUpRight, + LucideArrowDownLeft, +} from "lucide-react-native"; +import React from "react"; +import { View, Text, TouchableOpacity } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Transaction } from "~/lib/services/transactionService"; + +interface TransactionCardProps { + transaction: Transaction; + onPress?: () => void; +} + +export default function TransactionCard({ + transaction, + onPress, +}: TransactionCardProps) { + const { t } = useTranslation(); + + const formatDate = (date: Date) => { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const formatAmount = (amountInCents: number) => { + return `$${(amountInCents / 100).toFixed(2)}`; + }; + + const getTransactionIcon = (transaction: Transaction) => { + switch (transaction.type) { + case "send": + return ; + case "receive": + return ; + case "add_cash": + return ; + case "cash_out": + return ; + default: + return ; + } + }; + + const getTransactionColor = (transaction: Transaction) => { + switch (transaction.type) { + case "send": + return "bg-red-500"; + case "receive": + return "bg-green-500"; + case "add_cash": + return "bg-blue-500"; + case "cash_out": + return "bg-orange-500"; + default: + return "bg-secondary"; + } + }; + + const getAmountColor = (transaction: Transaction) => { + switch (transaction.type) { + case "send": + return "text-red-600"; + case "receive": + return "text-green-600"; + case "add_cash": + return "text-blue-600"; + case "cash_out": + return "text-orange-600"; + default: + return "text-primary"; + } + }; + + const getTransactionDescription = (transaction: Transaction) => { + switch (transaction.type) { + case "send": + return t("components.transactioncard.descriptionSend", { + recipientName: transaction.recipientName, + }); + case "receive": + return t("components.transactioncard.descriptionReceive", { + senderName: transaction.senderName, + }); + case "add_cash": + return t("components.transactioncard.descriptionAddCash", { + lastFourDigits: transaction.lastFourDigits, + }); + case "cash_out": + return t("components.transactioncard.descriptionCashOut", { + bankProvider: + transaction.bankProvider.charAt(0).toUpperCase() + + transaction.bankProvider.slice(1), + }); + default: + return t("components.transactioncard.descriptionDefault"); + } + }; + + const getTransactionDetails = (transaction: Transaction) => { + const details = []; + + //@ts-ignore + if (transaction.note && transaction.note.trim()) { + //@ts-ignore + details.push(transaction.note); + } + + switch (transaction.type) { + case "send": + if (transaction.recipientName) { + details.push(`Client: ${transaction.recipientName}`); + } + if (transaction.recipientPhoneNumber) { + details.push( + t("components.transactioncard.detailPhone", { + phoneNumber: transaction.recipientPhoneNumber, + }) + ); + } + break; + case "receive": + if (transaction.senderName) { + details.push(`Client: ${transaction.senderName}`); + } + if (transaction.senderPhoneNumber) { + details.push( + t("components.transactioncard.detailPhone", { + phoneNumber: transaction.senderPhoneNumber, + }) + ); + } + break; + case "add_cash": + if (transaction.cardId) { + details.push( + t("components.transactioncard.detailCard", { + lastFourDigits: transaction.lastFourDigits, + }) + ); + } + break; + case "cash_out": + if (transaction.accountNumber) { + details.push( + t("components.transactioncard.detailAccount", { + accountNumber: transaction.accountNumber, + }) + ); + } + details.push( + t("components.transactioncard.detailBankProvider", { + bankProvider: + transaction.bankProvider.charAt(0).toUpperCase() + + transaction.bankProvider.slice(1), + }) + ); + break; + } + + return details; + }; + + return ( + + + + {getTransactionIcon(transaction)} + + + + + {getTransactionDescription(transaction)} + + + {formatDate(transaction.createdAt)} + + {getTransactionDetails(transaction).map((detail, index) => ( + + {detail} + + ))} + + + + + + {transaction.type === "send" || transaction.type === "cash_out" + ? "-" + : "+"} + {formatAmount(transaction.amount)} + + + {transaction.status} + + + + ); +} diff --git a/eas.json b/eas.json new file mode 100644 index 0000000..3060db7 --- /dev/null +++ b/eas.json @@ -0,0 +1,45 @@ +{ + "cli": { + "version": ">= 3.0.0", + "appVersionSource": "local" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "android": { + "buildType": "apk" + }, + "ios": { + "simulator": false + } + }, + "preview": { + "distribution": "internal", + "android": { + "buildType": "apk" + }, + "ios": { + "distribution": "internal" + } + }, + "production": { + "android": { + "buildType": "aab" + }, + "ios": { + "distribution": "store", + "autoIncrement": "buildNumber" + } + } + }, + "submit": { + "production": { + "ios": { + "appleId": "yaltopiatech@gmail.com", + "ascAppId": "com.ambapays.app", + "appleTeamId": "K73ZX6D43Q" + } + } + } +} \ No newline at end of file diff --git a/firebase.config.js b/firebase.config.js new file mode 100644 index 0000000..5082de2 --- /dev/null +++ b/firebase.config.js @@ -0,0 +1,14 @@ +// Firebase Configuration +// Copy these values to your .env file or set them as environment variables + +module.exports = { + EXPO_PUBLIC_FIREBASE_API_KEY: "AIzaSyCVprX0NvjjemRKRpG1ZJHyMwKsJmBuXHc", + EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN: "ambapaydemo.firebaseapp.com", + EXPO_PUBLIC_FIREBASE_DATABASE_URL: "https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app", + EXPO_PUBLIC_FIREBASE_PROJECT_ID: "ambapaydemo", + EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET: "ambapaydemo.firebasestorage.app", + EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: "613864011564", + EXPO_PUBLIC_FIREBASE_APP_ID: "1:613864011564:web:e078c5990d3b2bff249e89", + EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID: "G-F8RVT1BHHC", + EXPO_PUBLIC_GOOGLE_CLIENT_ID: "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com" +}; diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..341d06c --- /dev/null +++ b/firebase.json @@ -0,0 +1,31 @@ +{ + "react-native": { + "android_app_id": "1:613864011564:android:fd35c9ac4fc05b38249e89", + "ios_app_id": "1:613864011564:ios:REPLACE_WITH_ACTUAL_IOS_APP_ID" + }, + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ], + "headers": [ + { + "source": "**/*.@(js|css)", + "headers": [ + { + "key": "Cache-Control", + "value": "max-age=31536000" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..a5ccbf2 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,84 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + function isAuthenticated() { + return request.auth != null; + } + + function isAdmin() { + return isAuthenticated() && request.auth.token.admin == true; + } + + function isSignedInWithProvider(provider) { + return isAuthenticated() && request.auth.token.firebase.sign_in_provider == provider; + } + + function isOwner(resourceData, field) { + return isAuthenticated() && + ((resourceData[field] != null && resourceData[field] == request.auth.uid) || isAdmin()); + } + + function isOwnerOfRequest(resourceData) { + return isAuthenticated() && ( + (resourceData.uid != null && resourceData.uid == request.auth.uid) || + (resourceData.requestorUid != null && resourceData.requestorUid == request.auth.uid) || + (resourceData.requesteeUid != null && resourceData.requesteeUid == request.auth.uid) || + isAdmin() + ); + } + + match /{document=**} { + allow read, write: if isAuthenticated() && + (isAdmin() || isSignedInWithProvider('custom')); + } + + match /users/{userId} { + allow create: if isAuthenticated() && (userId == request.auth.uid || isAdmin()); + allow read, update, delete: if isOwner(resource.data, 'uid') || (request.auth.uid == userId) || isAdmin(); + } + + match /wallets/{walletId} { + allow create: if isOwner(request.resource.data, 'uid'); + allow read, update, delete: if isOwner(resource.data, 'uid'); + } + + match /transactions/{transactionId} { + allow create: if isOwner(request.resource.data, 'uid'); + allow read, update, delete: if isOwner(resource.data, 'uid'); + } + + match /recipients/{recipientId} { + allow create: if isOwner(request.resource.data, 'uid'); + allow read, update, delete: if isOwner(resource.data, 'uid'); + } + + match /campaigns/{campaignId} { + allow create: if isOwner(request.resource.data, 'creatorId'); + allow read, update, delete: if isOwner(resource.data, 'creatorId'); + } + + match /donations/{donationId} { + allow create: if isOwner(request.resource.data, 'donorId'); + allow read, update, delete: if isOwner(resource.data, 'donorId'); + } + + match /notifications/{notificationId} { + allow create: if isOwner(request.resource.data, 'userId'); + allow read, update, delete: if isOwner(resource.data, 'userId'); + } + + match /requests/{requestId} { + allow create: if isOwnerOfRequest(request.resource.data); + allow read, update, delete: if isOwnerOfRequest(resource.data); + } + + match /admin/{document=**} { + allow read, write: if isAdmin(); + } + + match /{document=**} { + allow read, write: if false; + } + } +} diff --git a/google-services.json b/google-services.json new file mode 100644 index 0000000..48d28ab --- /dev/null +++ b/google-services.json @@ -0,0 +1,85 @@ +{ + "project_info": { + "project_number": "613864011564", + "firebase_url": "https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app", + "project_id": "ambapaydemo", + "storage_bucket": "ambapaydemo.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:613864011564:android:fd35c9ac4fc05b38249e89", + "android_client_info": { + "package_name": "com.amba" + } + }, + "oauth_client": [ + { + "client_id": "613864011564-2h1fb41f6conaabli0uq7scacpfmvuiq.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.amba", + "certificate_hash": "e5160567582b67483fec269eadfc9a286a2e14b0" + } + }, + { + "client_id": "613864011564-c8hfpatgmgvse0a1hdhho3ipj3ddrut3.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.amba", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + } + }, + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDTSIxujhye42iB8bDDvUR7jjRLo7Et3CU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:613864011564:android:89060f79143a4d75249e89", + "android_client_info": { + "package_name": "com.anonymous.amba" + } + }, + "oauth_client": [ + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDTSIxujhye42iB8bDDvUR7jjRLo7Et3CU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..ddec839 --- /dev/null +++ b/index.ts @@ -0,0 +1,20 @@ +// Suppress React Native Firebase modular deprecation warnings until migration is complete. +// See https://rnfirebase.io/migrating-to-v22 +//@ts-ignore +globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; + +// Import side effects first and services + +// Initialize services + +// Register app entry through Expo Router +import 'expo-router/entry'; +import "./global.css"; + +// Set up FCM background message handler (must be registered at the top level) +import messaging from '@react-native-firebase/messaging'; + +messaging().setBackgroundMessageHandler(async (remoteMessage) => { + console.log('Background FCM message received:', remoteMessage); + // Background messages are handled automatically by the system +}); \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..8beb344 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/ios/.xcode.env b/ios/.xcode.env new file mode 100644 index 0000000..3d5782c --- /dev/null +++ b/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist new file mode 100644 index 0000000..be81160 --- /dev/null +++ b/ios/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 613864011564-4mof6e14th4jdo4nrqe4qocntpovgof2.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.613864011564-4mof6e14th4jdo4nrqe4qocntpovgof2 + ANDROID_CLIENT_ID + 613864011564-2h1fb41f6conaabli0uq7scacpfmvuiq.apps.googleusercontent.com + API_KEY + AIzaSyCquhCKEsKmvZ5_JzqyWXGoImBF5L2Xlbc + GCM_SENDER_ID + 613864011564 + PLIST_VERSION + 1 + BUNDLE_ID + com.ambapay.ambaagent + PROJECT_ID + ambapaydemo + STORAGE_BUCKET + ambapaydemo.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:613864011564:ios:7de0d13df6fe49e5249e89 + DATABASE_URL + https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app + + \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..b509df8 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,64 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] + +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' +install! 'cocoapods', + :deterministic_uuids => false + +prepare_react_native_project! + +target 'amba' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'npx', + 'expo-modules-autolinking', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', + ) + + # This is necessary for Xcode 14, because it signs resource bundles by default + # when building for devices. + installer.target_installation_results.pod_target_installation_results + .each do |pod_name, target_installation_result| + target_installation_result.resource_bundle_targets.each do |resource_bundle_target| + resource_bundle_target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..5d23c2c --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,4353 @@ +PODS: + - abseil/algorithm (1.20240722.0): + - abseil/algorithm/algorithm (= 1.20240722.0) + - abseil/algorithm/container (= 1.20240722.0) + - abseil/algorithm/algorithm (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/algorithm/container (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base (1.20240722.0): + - abseil/base/atomic_hook (= 1.20240722.0) + - abseil/base/base (= 1.20240722.0) + - abseil/base/base_internal (= 1.20240722.0) + - abseil/base/config (= 1.20240722.0) + - abseil/base/core_headers (= 1.20240722.0) + - abseil/base/cycleclock_internal (= 1.20240722.0) + - abseil/base/dynamic_annotations (= 1.20240722.0) + - abseil/base/endian (= 1.20240722.0) + - abseil/base/errno_saver (= 1.20240722.0) + - abseil/base/fast_type_id (= 1.20240722.0) + - abseil/base/log_severity (= 1.20240722.0) + - abseil/base/malloc_internal (= 1.20240722.0) + - abseil/base/no_destructor (= 1.20240722.0) + - abseil/base/nullability (= 1.20240722.0) + - abseil/base/poison (= 1.20240722.0) + - abseil/base/prefetch (= 1.20240722.0) + - abseil/base/pretty_function (= 1.20240722.0) + - abseil/base/raw_logging_internal (= 1.20240722.0) + - abseil/base/spinlock_wait (= 1.20240722.0) + - abseil/base/strerror (= 1.20240722.0) + - abseil/base/throw_delegate (= 1.20240722.0) + - abseil/base/atomic_hook (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/base (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/cycleclock_internal + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/base_internal (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/config (1.20240722.0): + - abseil/xcprivacy + - abseil/base/core_headers (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/cycleclock_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/xcprivacy + - abseil/base/dynamic_annotations (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/endian (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/errno_saver (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/fast_type_id (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/log_severity (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/malloc_internal (1.20240722.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/base/no_destructor (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/nullability (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/poison (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/xcprivacy + - abseil/base/prefetch (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/pretty_function (1.20240722.0): + - abseil/xcprivacy + - abseil/base/raw_logging_internal (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/xcprivacy + - abseil/base/spinlock_wait (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/strerror (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/throw_delegate (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/common (1.20240722.0): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/xcprivacy + - abseil/container/common_policy_traits (1.20240722.0): + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/compressed_tuple (1.20240722.0): + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/container_memory (1.20240722.0): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/fixed_array (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_map (1.20240722.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_container_defaults + - abseil/container/raw_hash_map + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/flat_hash_set (1.20240722.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_container_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hash_container_defaults (1.20240722.0): + - abseil/base/config + - abseil/container/hash_function_defaults + - abseil/xcprivacy + - abseil/container/hash_function_defaults (1.20240722.0): + - abseil/base/config + - abseil/container/common + - abseil/hash/hash + - abseil/meta/type_traits + - abseil/strings/cord + - abseil/strings/strings + - abseil/xcprivacy + - abseil/container/hash_policy_traits (1.20240722.0): + - abseil/container/common_policy_traits + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hashtable_debug_hooks (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/container/hashtablez_sampler (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/inlined_vector (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/inlined_vector_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/xcprivacy + - abseil/container/layout (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/demangle_internal + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/raw_hash_map (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/xcprivacy + - abseil/container/raw_hash_set (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/hash/hash + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/crc/cpu_detect (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/crc32c (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/crc/cpu_detect + - abseil/crc/crc_internal + - abseil/crc/non_temporal_memcpy + - abseil/strings/str_format + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_cord_state (1.20240722.0): + - abseil/base/config + - abseil/base/no_destructor + - abseil/crc/crc32c + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/crc_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/crc/cpu_detect + - abseil/memory/memory + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/non_temporal_arm_intrinsics (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/non_temporal_memcpy (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/crc/non_temporal_arm_intrinsics + - abseil/xcprivacy + - abseil/debugging/bounded_utf8_length_sequence (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/debugging/debugging_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/debugging/decode_rust_punycode (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/debugging/bounded_utf8_length_sequence + - abseil/debugging/utf8_for_code_point + - abseil/xcprivacy + - abseil/debugging/demangle_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/debugging/demangle_rust + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/debugging/demangle_rust (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/decode_rust_punycode + - abseil/xcprivacy + - abseil/debugging/examine_stack (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/xcprivacy + - abseil/debugging/stacktrace (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/xcprivacy + - abseil/debugging/symbolize (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/debugging/utf8_for_code_point (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/flags/commandlineflag (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/commandlineflag_internal (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/xcprivacy + - abseil/flags/config (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/flags/program_name + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/flag (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/commandlineflag + - abseil/flags/config + - abseil/flags/flag_internal + - abseil/flags/reflection + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/flag_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/marshalling + - abseil/flags/reflection + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/xcprivacy + - abseil/flags/marshalling (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/numeric/int128 + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/path_util (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/private_handle_accessor (1.20240722.0): + - abseil/base/config + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/program_name (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/reflection (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/container/flat_hash_map + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/private_handle_accessor + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/functional/any_invocable (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/bind_front (1.20240722.0): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/function_ref (1.20240722.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/functional/any_invocable + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/hash/city (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/xcprivacy + - abseil/hash/hash (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/hash/low_level_hash (1.20240722.0): + - abseil/base/config + - abseil/base/endian + - abseil/base/prefetch + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/log/absl_check (1.20240722.0): + - abseil/log/internal/check_impl + - abseil/xcprivacy + - abseil/log/absl_log (1.20240722.0): + - abseil/log/internal/log_impl + - abseil/xcprivacy + - abseil/log/absl_vlog_is_on (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/check (1.20240722.0): + - abseil/log/internal/check_impl + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/globals (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/hash/hash + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/append_truncated (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/check_impl (1.20240722.0): + - abseil/base/core_headers + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/check_op (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/nullguard + - abseil/log/internal/nullstream + - abseil/log/internal/strip + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/conditions (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/voidify + - abseil/xcprivacy + - abseil/log/internal/config (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/fnmatch (1.20240722.0): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/format (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/append_truncated + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/strings/str_format + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/globals (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/strings/strings + - abseil/time/time + - abseil/xcprivacy + - abseil/log/internal/log_impl (1.20240722.0): + - abseil/log/absl_vlog_is_on + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/log_message (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/examine_stack + - abseil/log/globals + - abseil/log/internal/append_truncated + - abseil/log/internal/format + - abseil/log/internal/globals + - abseil/log/internal/log_sink_set + - abseil/log/internal/nullguard + - abseil/log/internal/proto + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/log/log_sink_registry + - abseil/memory/memory + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/log_sink_set (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/cleanup/cleanup + - abseil/log/globals + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/nullguard (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/nullstream (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/proto (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/strip (1.20240722.0): + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/log_message + - abseil/log/internal/nullstream + - abseil/xcprivacy + - abseil/log/internal/vlog_config (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/log/internal/fnmatch + - abseil/memory/memory + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/optional + - abseil/xcprivacy + - abseil/log/internal/voidify (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/log/log (1.20240722.0): + - abseil/log/internal/log_impl + - abseil/log/vlog_is_on + - abseil/xcprivacy + - abseil/log/log_entry (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/config + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/log_sink (1.20240722.0): + - abseil/base/config + - abseil/log/log_entry + - abseil/xcprivacy + - abseil/log/log_sink_registry (1.20240722.0): + - abseil/base/config + - abseil/log/internal/log_sink_set + - abseil/log/log_sink + - abseil/xcprivacy + - abseil/log/vlog_is_on (1.20240722.0): + - abseil/log/absl_vlog_is_on + - abseil/xcprivacy + - abseil/memory (1.20240722.0): + - abseil/memory/memory (= 1.20240722.0) + - abseil/memory/memory (1.20240722.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/meta (1.20240722.0): + - abseil/meta/type_traits (= 1.20240722.0) + - abseil/meta/type_traits (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/bits (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/int128 (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/types/compare + - abseil/xcprivacy + - abseil/numeric/representation (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/profiling/exponential_biased (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/profiling/sample_recorder (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/xcprivacy + - abseil/random/bit_gen_ref (1.20240722.0): + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/random + - abseil/xcprivacy + - abseil/random/distributions (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/xcprivacy + - abseil/random/internal/distribution_caller (1.20240722.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/xcprivacy + - abseil/random/internal/fast_uniform_bits (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/fastmath (1.20240722.0): + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/random/internal/generate_real (1.20240722.0): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/iostream_state_saver (1.20240722.0): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/nonsecure_base (1.20240722.0): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/pcg_engine (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/xcprivacy + - abseil/random/internal/platform (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/internal/pool_urbg (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/randen (1.20240722.0): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/xcprivacy + - abseil/random/internal/randen_engine (1.20240722.0): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes (1.20240722.0): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes_impl (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/randen_slow (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/salted_seed_seq (1.20240722.0): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/seed_material (1.20240722.0): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/traits (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/uniform_helper (1.20240722.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/wide_multiply (1.20240722.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/random (1.20240722.0): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/xcprivacy + - abseil/random/seed_gen_exception (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/seed_sequences (1.20240722.0): + - abseil/base/config + - abseil/base/nullability + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/status/status (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/memory/memory + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/status/statusor (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/has_ostream_operator + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/charset (1.20240722.0): + - abseil/base/core_headers + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/strings/cord (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/crc/crc32c + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/strings + - abseil/types/compare + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cord_internal (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_functions (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/xcprivacy + - abseil/strings/cordz_handle (1.20240722.0): + - abseil/base/config + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/strings/cordz_info (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_statistics (1.20240722.0): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_scope (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_tracker (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/has_ostream_operator (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/strings/str_format (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/strings/str_format_internal + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/str_format_internal (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/string_view (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/xcprivacy + - abseil/strings/strings (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/charset + - abseil/strings/internal + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/synchronization/graphcycles_internal (1.20240722.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/synchronization/kernel_timeout_internal (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/synchronization/synchronization (1.20240722.0): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/time (1.20240722.0): + - abseil/time/internal (= 1.20240722.0) + - abseil/time/time (= 1.20240722.0) + - abseil/time/internal (1.20240722.0): + - abseil/time/internal/cctz (= 1.20240722.0) + - abseil/time/internal/cctz (1.20240722.0): + - abseil/time/internal/cctz/civil_time (= 1.20240722.0) + - abseil/time/internal/cctz/time_zone (= 1.20240722.0) + - abseil/time/internal/cctz/civil_time (1.20240722.0): + - abseil/base/config + - abseil/xcprivacy + - abseil/time/internal/cctz/time_zone (1.20240722.0): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/xcprivacy + - abseil/time/time (1.20240722.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types/optional + - abseil/xcprivacy + - abseil/types (1.20240722.0): + - abseil/types/any (= 1.20240722.0) + - abseil/types/bad_any_cast (= 1.20240722.0) + - abseil/types/bad_any_cast_impl (= 1.20240722.0) + - abseil/types/bad_optional_access (= 1.20240722.0) + - abseil/types/bad_variant_access (= 1.20240722.0) + - abseil/types/compare (= 1.20240722.0) + - abseil/types/optional (= 1.20240722.0) + - abseil/types/span (= 1.20240722.0) + - abseil/types/variant (= 1.20240722.0) + - abseil/types/any (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/bad_any_cast (1.20240722.0): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/xcprivacy + - abseil/types/bad_any_cast_impl (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_optional_access (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_variant_access (1.20240722.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/compare (1.20240722.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/optional (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/span (1.20240722.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/variant (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/utility/utility (1.20240722.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/xcprivacy (1.20240722.0) + - AppAuth (2.0.0): + - AppAuth/Core (= 2.0.0) + - AppAuth/ExternalUserAgent (= 2.0.0) + - AppAuth/Core (2.0.0) + - AppAuth/ExternalUserAgent (2.0.0): + - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - boost (1.84.0) + - BoringSSL-GRPC (0.0.37): + - BoringSSL-GRPC/Implementation (= 0.0.37) + - BoringSSL-GRPC/Interface (= 0.0.37) + - BoringSSL-GRPC/Implementation (0.0.37): + - BoringSSL-GRPC/Interface (= 0.0.37) + - BoringSSL-GRPC/Interface (0.0.37) + - DoubleConversion (1.1.6) + - EXApplication (6.1.5): + - ExpoModulesCore + - EXConstants (17.1.7): + - ExpoModulesCore + - EXJSONUtils (0.15.0) + - EXManifests (0.16.6): + - ExpoModulesCore + - Expo (53.0.22): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-client (5.2.4): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Main (= 5.1.16) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Main (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Unsafe (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu (6.1.14): + - DoubleConversion + - expo-dev-menu/Main (= 6.1.14) + - expo-dev-menu/ReactNativeCompatibles (= 6.1.14) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu-interface (1.10.0) + - expo-dev-menu/Main (6.1.14): + - DoubleConversion + - EXManifests + - expo-dev-menu-interface + - expo-dev-menu/Vendored + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/ReactNativeCompatibles (6.1.14): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/SafeAreaView (6.1.14): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/Vendored (6.1.14): + - DoubleConversion + - expo-dev-menu/SafeAreaView + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoAdapterGoogleSignIn (16.0.0): + - ExpoModulesCore + - GoogleSignIn (~> 9.0) + - React-Core + - ExpoAsset (11.1.7): + - ExpoModulesCore + - ExpoContacts (14.2.5): + - ExpoModulesCore + - ExpoCrypto (14.1.5): + - ExpoModulesCore + - ExpoFileSystem (18.1.11): + - ExpoModulesCore + - ExpoFont (13.3.2): + - ExpoModulesCore + - ExpoHaptics (14.1.4): + - ExpoModulesCore + - ExpoHead (5.1.6): + - ExpoModulesCore + - ExpoKeepAwake (14.1.4): + - ExpoModulesCore + - ExpoLinking (7.1.7): + - ExpoModulesCore + - ExpoLocalAuthentication (14.0.1): + - ExpoModulesCore + - ExpoModulesCore (2.5.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoSMS (13.1.4): + - ExpoModulesCore + - ExpoWebBrowser (14.2.0): + - ExpoModulesCore + - EXUpdatesInterface (1.1.0): + - ExpoModulesCore + - fast_float (6.1.4) + - FBLazyVector (0.79.5) + - Firebase/Auth (12.4.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 12.4.0) + - Firebase/CoreOnly (12.4.0): + - FirebaseCore (~> 12.4.0) + - Firebase/Firestore (12.4.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 12.4.0) + - Firebase/Functions (12.4.0): + - Firebase/CoreOnly + - FirebaseFunctions (~> 12.4.0) + - Firebase/Messaging (12.4.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.4.0) + - FirebaseAppCheckInterop (12.4.0) + - FirebaseAuth (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseAuthInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (12.4.0) + - FirebaseCore (12.4.0): + - FirebaseCoreInternal (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseCoreInternal (12.4.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseFirestore (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - FirebaseFirestoreInternal (~> 12.4.0) + - FirebaseSharedSwift (~> 12.4.0) + - FirebaseFirestoreInternal (12.4.0): + - abseil/algorithm (~> 1.20240722.0) + - abseil/base (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/memory (~> 1.20240722.0) + - abseil/meta (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/time (~> 1.20240722.0) + - abseil/types (~> 1.20240722.0) + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - "gRPC-C++ (~> 1.69.0)" + - gRPC-Core (~> 1.69.0) + - leveldb-library (~> 1.22) + - nanopb (~> 3.30910.0) + - FirebaseFunctions (12.4.0): + - FirebaseAppCheckInterop (~> 12.4.0) + - FirebaseAuthInterop (~> 12.4.0) + - FirebaseCore (~> 12.4.0) + - FirebaseCoreExtension (~> 12.4.0) + - FirebaseMessagingInterop (~> 12.4.0) + - FirebaseSharedSwift (~> 12.4.0) + - GTMSessionFetcher/Core (< 6.0, >= 3.4) + - FirebaseInstallations (12.4.0): + - FirebaseCore (~> 12.4.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.4.0): + - FirebaseCore (~> 12.4.0) + - FirebaseInstallations (~> 12.4.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - FirebaseMessagingInterop (12.4.0) + - FirebaseSharedSwift (12.4.0) + - fmt (11.0.2) + - glog (0.3.5) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleSignIn (9.0.0): + - AppAuth (~> 2.0) + - AppCheckCore (~> 11.0) + - GTMAppAuth (~> 5.0) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - "gRPC-C++ (1.69.0)": + - "gRPC-C++/Implementation (= 1.69.0)" + - "gRPC-C++/Interface (= 1.69.0)" + - "gRPC-C++/Implementation (1.69.0)": + - abseil/algorithm/container (~> 1.20240722.0) + - abseil/base/base (~> 1.20240722.0) + - abseil/base/config (~> 1.20240722.0) + - abseil/base/core_headers (~> 1.20240722.0) + - abseil/base/log_severity (~> 1.20240722.0) + - abseil/base/no_destructor (~> 1.20240722.0) + - abseil/cleanup/cleanup (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/container/flat_hash_set (~> 1.20240722.0) + - abseil/container/inlined_vector (~> 1.20240722.0) + - abseil/flags/flag (~> 1.20240722.0) + - abseil/flags/marshalling (~> 1.20240722.0) + - abseil/functional/any_invocable (~> 1.20240722.0) + - abseil/functional/bind_front (~> 1.20240722.0) + - abseil/functional/function_ref (~> 1.20240722.0) + - abseil/hash/hash (~> 1.20240722.0) + - abseil/log/absl_check (~> 1.20240722.0) + - abseil/log/absl_log (~> 1.20240722.0) + - abseil/log/check (~> 1.20240722.0) + - abseil/log/globals (~> 1.20240722.0) + - abseil/log/log (~> 1.20240722.0) + - abseil/memory/memory (~> 1.20240722.0) + - abseil/meta/type_traits (~> 1.20240722.0) + - abseil/numeric/bits (~> 1.20240722.0) + - abseil/random/bit_gen_ref (~> 1.20240722.0) + - abseil/random/distributions (~> 1.20240722.0) + - abseil/random/random (~> 1.20240722.0) + - abseil/status/status (~> 1.20240722.0) + - abseil/status/statusor (~> 1.20240722.0) + - abseil/strings/cord (~> 1.20240722.0) + - abseil/strings/str_format (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/synchronization/synchronization (~> 1.20240722.0) + - abseil/time/time (~> 1.20240722.0) + - abseil/types/optional (~> 1.20240722.0) + - abseil/types/span (~> 1.20240722.0) + - abseil/types/variant (~> 1.20240722.0) + - abseil/utility/utility (~> 1.20240722.0) + - "gRPC-C++/Interface (= 1.69.0)" + - "gRPC-C++/Privacy (= 1.69.0)" + - gRPC-Core (= 1.69.0) + - "gRPC-C++/Interface (1.69.0)" + - "gRPC-C++/Privacy (1.69.0)" + - gRPC-Core (1.69.0): + - gRPC-Core/Implementation (= 1.69.0) + - gRPC-Core/Interface (= 1.69.0) + - gRPC-Core/Implementation (1.69.0): + - abseil/algorithm/container (~> 1.20240722.0) + - abseil/base/base (~> 1.20240722.0) + - abseil/base/config (~> 1.20240722.0) + - abseil/base/core_headers (~> 1.20240722.0) + - abseil/base/log_severity (~> 1.20240722.0) + - abseil/base/no_destructor (~> 1.20240722.0) + - abseil/cleanup/cleanup (~> 1.20240722.0) + - abseil/container/flat_hash_map (~> 1.20240722.0) + - abseil/container/flat_hash_set (~> 1.20240722.0) + - abseil/container/inlined_vector (~> 1.20240722.0) + - abseil/flags/flag (~> 1.20240722.0) + - abseil/flags/marshalling (~> 1.20240722.0) + - abseil/functional/any_invocable (~> 1.20240722.0) + - abseil/functional/bind_front (~> 1.20240722.0) + - abseil/functional/function_ref (~> 1.20240722.0) + - abseil/hash/hash (~> 1.20240722.0) + - abseil/log/check (~> 1.20240722.0) + - abseil/log/globals (~> 1.20240722.0) + - abseil/log/log (~> 1.20240722.0) + - abseil/memory/memory (~> 1.20240722.0) + - abseil/meta/type_traits (~> 1.20240722.0) + - abseil/numeric/bits (~> 1.20240722.0) + - abseil/random/bit_gen_ref (~> 1.20240722.0) + - abseil/random/distributions (~> 1.20240722.0) + - abseil/random/random (~> 1.20240722.0) + - abseil/status/status (~> 1.20240722.0) + - abseil/status/statusor (~> 1.20240722.0) + - abseil/strings/cord (~> 1.20240722.0) + - abseil/strings/str_format (~> 1.20240722.0) + - abseil/strings/strings (~> 1.20240722.0) + - abseil/synchronization/synchronization (~> 1.20240722.0) + - abseil/time/time (~> 1.20240722.0) + - abseil/types/optional (~> 1.20240722.0) + - abseil/types/span (~> 1.20240722.0) + - abseil/types/variant (~> 1.20240722.0) + - abseil/utility/utility (~> 1.20240722.0) + - BoringSSL-GRPC (= 0.0.37) + - gRPC-Core/Interface (= 1.69.0) + - gRPC-Core/Privacy (= 1.69.0) + - gRPC-Core/Interface (1.69.0) + - gRPC-Core/Privacy (1.69.0) + - GTMAppAuth (5.0.0): + - AppAuth/Core (~> 2.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher/Core (3.5.0) + - hermes-engine (0.79.5): + - hermes-engine/Pre-built (= 0.79.5) + - hermes-engine/Pre-built (0.79.5) + - leveldb-library (1.22.6) + - lottie-ios (4.5.0) + - lottie-react-native (7.3.4): + - DoubleConversion + - glog + - hermes-engine + - lottie-ios (= 4.5.0) + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - PromisesObjC (2.4.0) + - RCT-Folly (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCT-Folly/Default (= 2024.11.18.00) + - RCT-Folly/Default (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCT-Folly/Fabric (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCTDeprecation (0.79.5) + - RCTRequired (0.79.5) + - RCTTypeSafety (0.79.5): + - FBLazyVector (= 0.79.5) + - RCTRequired (= 0.79.5) + - React-Core (= 0.79.5) + - React (0.79.5): + - React-Core (= 0.79.5) + - React-Core/DevSupport (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) + - React-RCTActionSheet (= 0.79.5) + - React-RCTAnimation (= 0.79.5) + - React-RCTBlob (= 0.79.5) + - React-RCTImage (= 0.79.5) + - React-RCTLinking (= 0.79.5) + - React-RCTNetwork (= 0.79.5) + - React-RCTSettings (= 0.79.5) + - React-RCTText (= 0.79.5) + - React-RCTVibration (= 0.79.5) + - React-callinvoker (0.79.5) + - React-Core (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default (= 0.79.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/CoreModulesHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/Default (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/DevSupport (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTActionSheetHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTAnimationHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTBlobHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTImageHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTLinkingHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTNetworkHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTSettingsHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTTextHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTVibrationHeaders (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-Core/RCTWebSocket (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTDeprecation + - React-Core/Default (= 0.79.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.1) + - Yoga + - React-CoreModules (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - RCT-Folly (= 2024.11.18.00) + - RCTTypeSafety (= 0.79.5) + - React-Core/CoreModulesHeaders (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-NativeModulesApple + - React-RCTBlob + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.79.5) + - ReactCommon + - SocketRocket (= 0.7.1) + - React-cxxreact (0.79.5): + - boost + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-callinvoker (= 0.79.5) + - React-debug (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-timing (= 0.79.5) + - React-debug (0.79.5) + - React-defaultsnativemodule (0.79.5): + - hermes-engine + - RCT-Folly + - React-domnativemodule + - React-featureflagsnativemodule + - React-hermes + - React-idlecallbacksnativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-RCTFBReactNativeSpec + - React-domnativemodule (0.79.5): + - hermes-engine + - RCT-Folly + - React-Fabric + - React-FabricComponents + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/animations (= 0.79.5) + - React-Fabric/attributedstring (= 0.79.5) + - React-Fabric/componentregistry (= 0.79.5) + - React-Fabric/componentregistrynative (= 0.79.5) + - React-Fabric/components (= 0.79.5) + - React-Fabric/consistency (= 0.79.5) + - React-Fabric/core (= 0.79.5) + - React-Fabric/dom (= 0.79.5) + - React-Fabric/imagemanager (= 0.79.5) + - React-Fabric/leakchecker (= 0.79.5) + - React-Fabric/mounting (= 0.79.5) + - React-Fabric/observers (= 0.79.5) + - React-Fabric/scheduler (= 0.79.5) + - React-Fabric/telemetry (= 0.79.5) + - React-Fabric/templateprocessor (= 0.79.5) + - React-Fabric/uimanager (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/animations (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistry (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistrynative (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.79.5) + - React-Fabric/components/root (= 0.79.5) + - React-Fabric/components/scrollview (= 0.79.5) + - React-Fabric/components/view (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/legacyviewmanagerinterop (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/root (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/scrollview (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/view (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-renderercss + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric/consistency (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/core (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/dom (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/imagemanager (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/leakchecker (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/mounting (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/observers/events (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/scheduler (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/observers/events + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancetimeline + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/telemetry (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/templateprocessor (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager/consistency (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-FabricComponents (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.79.5) + - React-FabricComponents/textlayoutmanager (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.79.5) + - React-FabricComponents/components/iostextinput (= 0.79.5) + - React-FabricComponents/components/modal (= 0.79.5) + - React-FabricComponents/components/rncore (= 0.79.5) + - React-FabricComponents/components/safeareaview (= 0.79.5) + - React-FabricComponents/components/scrollview (= 0.79.5) + - React-FabricComponents/components/text (= 0.79.5) + - React-FabricComponents/components/textinput (= 0.79.5) + - React-FabricComponents/components/unimplementedview (= 0.79.5) + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/inputaccessory (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/iostextinput (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/modal (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/rncore (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/safeareaview (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/scrollview (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/text (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/textinput (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/components/unimplementedview (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricComponents/textlayoutmanager (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-FabricImage (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired (= 0.79.5) + - RCTTypeSafety (= 0.79.5) + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.79.5) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - Yoga + - React-featureflags (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - React-featureflagsnativemodule (0.79.5): + - hermes-engine + - RCT-Folly + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - React-graphics (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-hermes + - React-jsi + - React-jsiexecutor + - React-utils + - React-hermes (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact (= 0.79.5) + - React-jsi + - React-jsiexecutor (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-runtimeexecutor + - React-idlecallbacksnativemodule (0.79.5): + - glog + - hermes-engine + - RCT-Folly + - React-hermes + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimescheduler + - ReactCommon/turbomodule/core + - React-ImageManager (0.79.5): + - glog + - RCT-Folly/Fabric + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - React-jserrorhandler (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - ReactCommon/turbomodule/bridging + - React-jsi (0.79.5): + - boost + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-jsiexecutor (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-jsinspector (0.79.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - React-featureflags + - React-jsi + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-jsinspectortracing (0.79.5): + - RCT-Folly + - React-oscompat + - React-jsitooling (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-jsitracing (0.79.5): + - React-jsi + - React-logger (0.79.5): + - glog + - React-Mapbuffer (0.79.5): + - glog + - React-debug + - React-microtasksnativemodule (0.79.5): + - hermes-engine + - RCT-Folly + - React-hermes + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - react-native-pager-view (6.9.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common (= 5.4.0) + - react-native-safe-area-context/fabric (= 5.4.0) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (5.4.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-webview (13.13.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-NativeModulesApple (0.79.5): + - glog + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-oscompat (0.79.5) + - React-perflogger (0.79.5): + - DoubleConversion + - RCT-Folly (= 2024.11.18.00) + - React-performancetimeline (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact + - React-featureflags + - React-jsinspectortracing + - React-perflogger + - React-timing + - React-RCTActionSheet (0.79.5): + - React-Core/RCTActionSheetHeaders (= 0.79.5) + - React-RCTAnimation (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - RCTTypeSafety + - React-Core/RCTAnimationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - React-RCTAppDelegate (0.79.5): + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimescheduler + - React-utils + - ReactCommon + - React-RCTBlob (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - React-RCTFabric (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-Core + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-jsinspectortracing + - React-performancetimeline + - React-RCTAnimation + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-runtimescheduler + - React-utils + - Yoga + - React-RCTFBReactNativeSpec (0.79.5): + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-hermes + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - ReactCommon + - React-RCTImage (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - RCTTypeSafety + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - React-RCTLinking (0.79.5): + - React-Core/RCTLinkingHeaders (= 0.79.5) + - React-jsi (= 0.79.5) + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactCommon/turbomodule/core (= 0.79.5) + - React-RCTNetwork (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - RCTTypeSafety + - React-Core/RCTNetworkHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - React-RCTRuntime (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-Core + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-RCTSettings (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - RCTTypeSafety + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - React-RCTText (0.79.5): + - React-Core/RCTTextHeaders (= 0.79.5) + - Yoga + - React-RCTVibration (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - React-rendererconsistency (0.79.5) + - React-renderercss (0.79.5): + - React-debug + - React-utils + - React-rendererdebug (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - RCT-Folly (= 2024.11.18.00) + - React-debug + - React-rncore (0.79.5) + - React-RuntimeApple (0.79.5): + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-callinvoker + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - React-RuntimeCore (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-cxxreact + - React-Fabric + - React-featureflags + - React-hermes + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - React-runtimeexecutor (0.79.5): + - React-jsi (= 0.79.5) + - React-RuntimeHermes (0.79.5): + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-utils + - React-runtimescheduler (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-callinvoker + - React-cxxreact + - React-debug + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - React-timing (0.79.5) + - React-utils (0.79.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-debug + - React-hermes + - React-jsi (= 0.79.5) + - ReactAppDependencyProvider (0.79.5): + - ReactCodegen + - ReactCodegen (0.79.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactCommon (0.79.5): + - ReactCommon/turbomodule (= 0.79.5) + - ReactCommon/turbomodule (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/bridging (= 0.79.5) + - ReactCommon/turbomodule/core (= 0.79.5) + - ReactCommon/turbomodule/bridging (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/core (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-debug (= 0.79.5) + - React-featureflags (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-utils (= 0.79.5) + - RecaptchaInterop (101.0.0) + - RNCAsyncStorage (2.1.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNFBApp (23.5.0): + - Firebase/CoreOnly (= 12.4.0) + - React-Core + - RNFBAuth (23.5.0): + - Firebase/Auth (= 12.4.0) + - React-Core + - RNFBApp + - RNFBFirestore (23.5.0): + - Firebase/Firestore (= 12.4.0) + - React-Core + - RNFBApp + - RNFBFunctions (23.5.0): + - Firebase/Functions (= 12.4.0) + - React-Core + - RNFBApp + - RNFBMessaging (23.5.0): + - Firebase/Messaging (= 12.4.0) + - FirebaseCoreExtension + - React-Core + - RNFBApp + - RNGoogleSignin (16.0.0): + - DoubleConversion + - glog + - GoogleSignIn (~> 9.0) + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated (3.17.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated (= 3.17.5) + - RNReanimated/worklets (= 3.17.5) + - Yoga + - RNReanimated/reanimated (3.17.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated/apple (= 3.17.5) + - Yoga + - RNReanimated/reanimated/apple (3.17.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated/worklets (3.17.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/worklets/apple (= 3.17.5) + - Yoga + - RNReanimated/worklets/apple (3.17.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 4.11.1) + - Yoga + - RNScreens/common (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNSVG (15.11.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNSVG/common (= 15.11.2) + - Yoga + - RNSVG/common (15.11.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - SocketRocket (0.7.1) + - Yoga (0.0.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXApplication (from `../node_modules/expo-application/ios`) + - EXConstants (from `../node_modules/expo-constants/ios`) + - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) + - EXManifests (from `../node_modules/expo-manifests/ios`) + - Expo (from `../node_modules/expo`) + - expo-dev-client (from `../node_modules/expo-dev-client/ios`) + - expo-dev-launcher (from `../node_modules/expo-dev-launcher`) + - expo-dev-menu (from `../node_modules/expo-dev-menu`) + - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) + - "ExpoAdapterGoogleSignIn (from `../node_modules/@react-native-google-signin/google-signin/expo/ios`)" + - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoContacts (from `../node_modules/expo-contacts/ios`) + - ExpoCrypto (from `../node_modules/expo-crypto/ios`) + - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) + - ExpoFont (from `../node_modules/expo-font/ios`) + - ExpoHaptics (from `../node_modules/expo-haptics/ios`) + - ExpoHead (from `../node_modules/expo-router/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLinking (from `../node_modules/expo-linking/ios`) + - ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`) + - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoSMS (from `../node_modules/expo-sms/ios`) + - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) + - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`) + - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - lottie-react-native (from `../node_modules/lottie-react-native`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-pager-view (from `../node_modules/react-native-pager-view`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactAppDependencyProvider (from `build/generated/ios`) + - ReactCodegen (from `build/generated/ios`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" + - "RNFBAuth (from `../node_modules/@react-native-firebase/auth`)" + - "RNFBFirestore (from `../node_modules/@react-native-firebase/firestore`)" + - "RNFBFunctions (from `../node_modules/@react-native-firebase/functions`)" + - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" + - "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)" + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - abseil + - AppAuth + - AppCheckCore + - BoringSSL-GRPC + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseFirestore + - FirebaseFirestoreInternal + - FirebaseFunctions + - FirebaseInstallations + - FirebaseMessaging + - FirebaseMessagingInterop + - FirebaseSharedSwift + - GoogleDataTransport + - GoogleSignIn + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMAppAuth + - GTMSessionFetcher + - leveldb-library + - lottie-ios + - nanopb + - PromisesObjC + - RecaptchaInterop + - SocketRocket + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/expo-application/ios" + EXConstants: + :path: "../node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../node_modules/expo-json-utils/ios" + EXManifests: + :path: "../node_modules/expo-manifests/ios" + Expo: + :path: "../node_modules/expo" + expo-dev-client: + :path: "../node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../node_modules/expo-dev-menu-interface/ios" + ExpoAdapterGoogleSignIn: + :path: "../node_modules/@react-native-google-signin/google-signin/expo/ios" + ExpoAsset: + :path: "../node_modules/expo-asset/ios" + ExpoContacts: + :path: "../node_modules/expo-contacts/ios" + ExpoCrypto: + :path: "../node_modules/expo-crypto/ios" + ExpoFileSystem: + :path: "../node_modules/expo-file-system/ios" + ExpoFont: + :path: "../node_modules/expo-font/ios" + ExpoHaptics: + :path: "../node_modules/expo-haptics/ios" + ExpoHead: + :path: "../node_modules/expo-router/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" + ExpoLinking: + :path: "../node_modules/expo-linking/ios" + ExpoLocalAuthentication: + :path: "../node_modules/expo-local-authentication/ios" + ExpoModulesCore: + :path: "../node_modules/expo-modules-core" + ExpoSMS: + :path: "../node_modules/expo-sms/ios" + ExpoWebBrowser: + :path: "../node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../node_modules/expo-updates-interface/ios" + fast_float: + :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + fmt: + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2025-06-04-RNv0.79.3-7f9a871eefeb2c3852365ee80f0b6733ec12ac3b + lottie-react-native: + :path: "../node_modules/lottie-react-native" + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectortracing: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-pager-view: + :path: "../node_modules/react-native-pager-view" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-oscompat: + :path: "../node_modules/react-native/ReactCommon/oscompat" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTFBReactNativeSpec: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../node_modules/react-native/React/Runtime" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactAppDependencyProvider: + :path: build/generated/ios + ReactCodegen: + :path: build/generated/ios + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNFBApp: + :path: "../node_modules/@react-native-firebase/app" + RNFBAuth: + :path: "../node_modules/@react-native-firebase/auth" + RNFBFirestore: + :path: "../node_modules/@react-native-firebase/firestore" + RNFBFunctions: + :path: "../node_modules/@react-native-firebase/functions" + RNFBMessaging: + :path: "../node_modules/@react-native-firebase/messaging" + RNGoogleSignin: + :path: "../node_modules/@react-native-google-signin/google-signin" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + abseil: a05cc83bf02079535e17169a73c5be5ba47f714b + AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + BoringSSL-GRPC: dded2a44897e45f28f08ae87a55ee4bcd19bc508 + DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb + EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718 + EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 + EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd + EXManifests: 691a779b04e4f2c96da46fb9bef4f86174fefcb5 + Expo: 05e259dbef0cb888e4b775813875ac1075288c97 + expo-dev-client: 9b1e78baf0dd87b005f035d180bbb07c05917fad + expo-dev-launcher: 3bdbeb6102a0ebc885ef0cc1e93b8045fb46cf7c + expo-dev-menu: 9a65836a93397539ec700e6ec99de8199ad8c4ca + expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e + ExpoAdapterGoogleSignIn: 5965ec283d2c0f53c483c4e2080ea055a881dfe9 + ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 + ExpoContacts: 43f426162ce3c16c3f993dbac8d120b0ffd3bb87 + ExpoCrypto: a9f1d75baeea6ef8b03c1660621585196c382e85 + ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 + ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e + ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500 + ExpoHead: a35136350ff019825aa642e1e73399b417f4faed + ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7 + ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902 + ExpoLocalAuthentication: b94db59f55df95350223200c746b4ddf0cb7cfc0 + ExpoModulesCore: 471ae18809dc8a5c9a623193a317eef6048a4f8a + ExpoSMS: 770f0b60a777f5d3f65fd7fc369ff62ff97e09a9 + ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 + EXUpdatesInterface: 7ff005b7af94ee63fa452ea7bb95d7a8ff40277a + fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 + FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 + Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e + FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0 + FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889 + FirebaseAuthInterop: 858e6b754966e70740a4370dd1503dfffe6dbb49 + FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 + FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 + FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 + FirebaseFirestore: 2a6183381cf7679b1bb000eb76a8e3178e25dee2 + FirebaseFirestoreInternal: 6577a27cd5dc3722b900042527f86d4ea1626134 + FirebaseFunctions: 9b6439272c887dfc15439b95cf56ddda95722bc7 + FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2 + FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5 + FirebaseMessagingInterop: c1454390729ce3c91c184cffd0c347369d7cc48a + FirebaseSharedSwift: 93426a1de92f19e1199fac5295a4f8df16458daa + fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 + gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 + GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae + leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 + lottie-ios: a881093fab623c467d3bce374367755c272bdd59 + lottie-react-native: 9ea691634bed4b78387252ebb1c84c063aa31534 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5 + RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8 + RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab + React: 6393ae1807614f017a84805bf2417e3497f518a6 + React-callinvoker: c34f666f551f05a325b87e7e3e6df0e082fa3d99 + React-Core: 1ba9acdf7accbd46ccaae99999443ae2722c82b7 + React-CoreModules: 3c3cf4a91257f138e3feb47169a2d7fe341b5495 + React-cxxreact: 444d518a5d3a933e029b5e5ca6d8127c2e43255c + React-debug: c1b10e5982b961738eab5b1d66fa31572ca28b5e + React-defaultsnativemodule: f9906e78406bf7c2fd011ae75a8142b54b6a8718 + React-domnativemodule: 4c2825e5c33a9c40b2249470036a9a1bd3c35f66 + React-Fabric: 094ab3d828a74fdff2c94ae44eff16f7774d8892 + React-FabricComponents: 72088c0f89c9cd08ebc750fed7f2975c4f339e87 + React-FabricImage: 8ff0ec837973420cc5bb6286bb604aad567e98f1 + React-featureflags: 1e3a098a98c63a339a8b5ef4014ba4c4b43fb1f6 + React-featureflagsnativemodule: 5e1f4f9691490e15902f4b79e508cf307e6653a3 + React-graphics: 02cc9270fb3be3e5ca3c36cd5a19492e9ec10a20 + React-hermes: 9ec11ce5f88c0778e027aa06a6e3e6eb19ddae09 + React-idlecallbacksnativemodule: 40ed6ba8b33bcd0813f10196d7c026d0e2cbe671 + React-ImageManager: 5a540f5f1ae6b72c71b1f02877791d6079db5733 + React-jserrorhandler: b3b7f5497c25be2ab38bfcf303fdacfd461dd9d3 + React-jsi: 8f065aa1ae1d35bef3c394cb1663d114c4952fd8 + React-jsiexecutor: fc8e69fb870cb6e69920fd482a76d4ae54a1c40a + React-jsinspector: f62d1c6e39f13b27768d8a1de9940c69d0825b31 + React-jsinspectortracing: 4016df25da9b5c354387daf8deb09ca467679960 + React-jsitooling: 59d6ce3fef0b36008dd5bb18cf4e99ca27586619 + React-jsitracing: 872341bc3663286a14cfd9dcbb53089a6182b163 + React-logger: 85fa3509931497c72ccd2547fcc91e7299d8591e + React-Mapbuffer: 4141a0d94d47529afb85341b9c02baa3cc2a1fd5 + React-microtasksnativemodule: 4b0f5a344ca5ce6692650fae4fb67f7672bbd1b6 + react-native-pager-view: af0a410629d77bcfeec1383d9109a2c5264b8a49 + react-native-safe-area-context: 5928d84c879db2f9eb6969ca70e68f58623dbf25 + react-native-webview: 2489d02c8e729bbd323dcb83ab9ea3b0a150b3f6 + React-NativeModulesApple: 0f1cdafa0517926f47ff74dbd92a1c9bb767cc69 + React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd + React-perflogger: c584fa50e422a46f37404d083fad12eb289d5de4 + React-performancetimeline: e27ae1b65a915937f42237448ff207d8af1ff0cd + React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24 + React-RCTAnimation: 8bb813eb29c6de85be99c62640f3a999df76ba02 + React-RCTAppDelegate: 0200dcd70e996a7061965cfa7f8c443013cc11a1 + React-RCTBlob: a1dd15758420b6a8154019c5c188cf90648bc487 + React-RCTFabric: 5232360b1a8fcfca3955696b085f1e7db4328289 + React-RCTFBReactNativeSpec: b42afeff81dfd0618a4d37c6c6cb99a66b93a363 + React-RCTImage: 8a4f6ce18e73a7e894b886dfb7625e9e9fbc90ef + React-RCTLinking: fa49c624cd63979e7a6295ae9b1351d23ac4395a + React-RCTNetwork: f236fd2897d18522bba24453e2995a4c83e01024 + React-RCTRuntime: 673768dd81fe9f089e2cf97649a50e855a5e9eff + React-RCTSettings: 69e2f25a5a1bf6cb37eef2e5c3bd4bb7e848296b + React-RCTText: 515ce74ed79c31dbf509e6f12770420ebbf23755 + React-RCTVibration: ef30ada606dfed859b2c71577f6f041d47f2cfbb + React-rendererconsistency: c9c28e3b0834d9be2e6aa0ba2d1fd77c76441658 + React-renderercss: e10ecdcc6c860a9b8d272c9b91f3f7e4083025ef + React-rendererdebug: 9645a0aa1c96f682e3db8d5986d0f983973bfff0 + React-rncore: 289894dda4ebcca06104070f1a9c9283f37dd123 + React-RuntimeApple: d35aeb9cce934526e8da3147fd34aa96ff673916 + React-RuntimeCore: 5fe3d6994a1cc00d51ad60e39b37b05824e95a46 + React-runtimeexecutor: ebfd71307b3166c73ac0c441c1ea42e0f17f821d + React-RuntimeHermes: a76cacef9e28fedbf7a49c95d0ccda7a317044b5 + React-runtimescheduler: a27bbf20bd606056f0404afdca7c4242e015cf51 + React-timing: 0f749e1c5ca1147b699b25ec79003950e6366056 + React-utils: aa7c038cfabf6b8f5e63dbf7cd3ed162a4399668 + ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8 + ReactCodegen: 542dbcd3d677ed7c4fe69e28b7c492998f48c9db + ReactCommon: 1aa48867a0fc71a2b4f9e8a1529f5673648354f3 + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + RNCAsyncStorage: 646c1bf2ebfc78d6632a3b0399f471fba8101e73 + RNFBApp: 255fa19a91d6c1309e021ee75d5d209754e5961b + RNFBAuth: 7123a71a1fc8dec4aa4e6cb656562661b3ece9f7 + RNFBFirestore: 82bca32eb782c08c54a21eadce88d5cf676afd9e + RNFBFunctions: 628052960b485bdc0bb2b95cdcc8b739e52840f1 + RNFBMessaging: c304c1d8949f996d3eed1cf4eeba33148d13b4ea + RNGoogleSignin: 9a3687206bd121982d42993da2063f5a271e383c + RNReanimated: a3f55346df73f35c38bf0b446294d3d0b1ac8cf9 + RNScreens: 90b905d545a5ebbe976985702b8a39e3475727b2 + RNSVG: 4470464e129f6bc58b78b961850b054dbbea8ef7 + SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + Yoga: bfcce202dba74007f8974ee9c5f903a9a286c445 + +PODFILE CHECKSUM: f99082fec885db253e1a3f64f7253b17d0a5a451 + +COCOAPODS: 1.16.2 diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json new file mode 100644 index 0000000..044d526 --- /dev/null +++ b/ios/Podfile.properties.json @@ -0,0 +1,5 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "ios.useFrameworks": "dynamic" +} diff --git a/ios/amba.xcodeproj/project.pbxproj b/ios/amba.xcodeproj/project.pbxproj new file mode 100644 index 0000000..10d5e52 --- /dev/null +++ b/ios/amba.xcodeproj/project.pbxproj @@ -0,0 +1,795 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 5CD3D4206A45B74E1BBE351A /* Pods_amba.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83456C5C8942265FA06D4F24 /* Pods_amba.framework */; }; + 634516A4BD31311B590C5C57 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DEF336B80DAC324C2B0E14 /* ExpoModulesProvider.swift */; }; + 73049C62071313E9C99F56F7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 41C5D38A35CE40589CE9DB7A /* PrivacyInfo.xcprivacy */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + CECE0E8A2EDE15EB009637F3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CECE0E892EDE15EB009637F3 /* GoogleService-Info.plist */; }; + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* amba.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = amba.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = amba/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = amba/Info.plist; sourceTree = ""; }; + 269AEFAF2C9B8865892098EB /* Pods-amba.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-amba.release.xcconfig"; path = "Target Support Files/Pods-amba/Pods-amba.release.xcconfig"; sourceTree = ""; }; + 39DEF336B80DAC324C2B0E14 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-amba/ExpoModulesProvider.swift"; sourceTree = ""; }; + 41C5D38A35CE40589CE9DB7A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = amba/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 83456C5C8942265FA06D4F24 /* Pods_amba.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_amba.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 992DF58BDDA9584905BF5CD8 /* Pods-amba.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-amba.debug.xcconfig"; path = "Target Support Files/Pods-amba/Pods-amba.debug.xcconfig"; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = amba/SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + CECE0E892EDE15EB009637F3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = amba/AppDelegate.swift; sourceTree = ""; }; + F11748442D0722820044C1D9 /* amba-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "amba-Bridging-Header.h"; path = "amba/amba-Bridging-Header.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5CD3D4206A45B74E1BBE351A /* Pods_amba.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* amba */ = { + isa = PBXGroup; + children = ( + F11748412D0307B40044C1D9 /* AppDelegate.swift */, + F11748442D0722820044C1D9 /* amba-Bridging-Header.h */, + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 41C5D38A35CE40589CE9DB7A /* PrivacyInfo.xcprivacy */, + ); + name = amba; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 83456C5C8942265FA06D4F24 /* Pods_amba.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 387D75F7B9C9D7CF19B7C54A /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 8DE81FCD0567964A2BAE3FE0 /* amba */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; + 59A2D62CA880E37E75810B47 /* Pods */ = { + isa = PBXGroup; + children = ( + 992DF58BDDA9584905BF5CD8 /* Pods-amba.debug.xcconfig */, + 269AEFAF2C9B8865892098EB /* Pods-amba.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + CECE0E892EDE15EB009637F3 /* GoogleService-Info.plist */, + 13B07FAE1A68108700A75B9A /* amba */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + 59A2D62CA880E37E75810B47 /* Pods */, + 387D75F7B9C9D7CF19B7C54A /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* amba.app */, + ); + name = Products; + sourceTree = ""; + }; + 8DE81FCD0567964A2BAE3FE0 /* amba */ = { + isa = PBXGroup; + children = ( + 39DEF336B80DAC324C2B0E14 /* ExpoModulesProvider.swift */, + ); + name = amba; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = amba/Supporting; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* amba */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "amba" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + 628493227EF1C034B9CAC9A6 /* [Expo] Configure project */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + 7C03B021378BCF79D1F73A5D /* [CP] Embed Pods Frameworks */, + 20ABE89DB5F32FAB721190CA /* [CP-User] [RNFB] Core Configuration */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = amba; + productName = ambaagent; + productReference = 13B07F961A680F5B00A75B9A /* amba.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1620; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "amba" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* amba */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + CECE0E8A2EDE15EB009637F3 /* GoogleService-Info.plist in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + 73049C62071313E9C99F56F7 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-amba-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 20ABE89DB5F32FAB721190CA /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; + }; + 628493227EF1C034B9CAC9A6 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-amba/expo-configure-project.sh\"\n"; + }; + 7C03B021378BCF79D1F73A5D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-amba/Pods-amba-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework", + "${BUILT_PRODUCTS_DIR}/AppCheckCore/AppCheckCore.framework", + "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC/openssl_grpc.framework", + "${BUILT_PRODUCTS_DIR}/DoubleConversion/DoubleConversion.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAppCheckInterop/FirebaseAppCheckInterop.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAuth/FirebaseAuth.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseAuthInterop/FirebaseAuthInterop.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreExtension/FirebaseCoreExtension.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFirestore/FirebaseFirestore.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFirestoreInternal/FirebaseFirestoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseFunctions/FirebaseFunctions.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseMessagingInterop/FirebaseMessagingInterop.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseSharedSwift/FirebaseSharedSwift.framework", + "${BUILT_PRODUCTS_DIR}/GTMAppAuth/GTMAppAuth.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", + "${BUILT_PRODUCTS_DIR}/GoogleSignIn/GoogleSignIn.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/RCT-Folly/folly.framework", + "${BUILT_PRODUCTS_DIR}/RCTDeprecation/RCTDeprecation.framework", + "${BUILT_PRODUCTS_DIR}/RCTTypeSafety/RCTTypeSafety.framework", + "${BUILT_PRODUCTS_DIR}/RNCAsyncStorage/RNCAsyncStorage.framework", + "${BUILT_PRODUCTS_DIR}/RNFBApp/RNFBApp.framework", + "${BUILT_PRODUCTS_DIR}/RNFBAuth/RNFBAuth.framework", + "${BUILT_PRODUCTS_DIR}/RNFBFirestore/RNFBFirestore.framework", + "${BUILT_PRODUCTS_DIR}/RNFBFunctions/RNFBFunctions.framework", + "${BUILT_PRODUCTS_DIR}/RNFBMessaging/RNFBMessaging.framework", + "${BUILT_PRODUCTS_DIR}/RNGoogleSignin/RNGoogleSignin.framework", + "${BUILT_PRODUCTS_DIR}/RNReanimated/RNReanimated.framework", + "${BUILT_PRODUCTS_DIR}/RNSVG/RNSVG.framework", + "${BUILT_PRODUCTS_DIR}/RNScreens/RNScreens.framework", + "${BUILT_PRODUCTS_DIR}/React-Core/React.framework", + "${BUILT_PRODUCTS_DIR}/React-CoreModules/CoreModules.framework", + "${BUILT_PRODUCTS_DIR}/React-Fabric/React_Fabric.framework", + "${BUILT_PRODUCTS_DIR}/React-FabricComponents/React_FabricComponents.framework", + "${BUILT_PRODUCTS_DIR}/React-FabricImage/React_FabricImage.framework", + "${BUILT_PRODUCTS_DIR}/React-ImageManager/React_ImageManager.framework", + "${BUILT_PRODUCTS_DIR}/React-Mapbuffer/React_Mapbuffer.framework", + "${BUILT_PRODUCTS_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTAnimation/RCTAnimation.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTAppDelegate/React_RCTAppDelegate.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTBlob/RCTBlob.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTFBReactNativeSpec/FBReactNativeSpec.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTFabric/RCTFabric.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTImage/RCTImage.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTLinking/RCTLinking.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTNetwork/RCTNetwork.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTRuntime/RCTRuntime.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTSettings/RCTSettings.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTText/RCTText.framework", + "${BUILT_PRODUCTS_DIR}/React-RCTVibration/RCTVibration.framework", + "${BUILT_PRODUCTS_DIR}/React-RuntimeApple/React_RuntimeApple.framework", + "${BUILT_PRODUCTS_DIR}/React-RuntimeCore/React_RuntimeCore.framework", + "${BUILT_PRODUCTS_DIR}/React-RuntimeHermes/React_RuntimeHermes.framework", + "${BUILT_PRODUCTS_DIR}/React-cxxreact/cxxreact.framework", + "${BUILT_PRODUCTS_DIR}/React-debug/React_debug.framework", + "${BUILT_PRODUCTS_DIR}/React-defaultsnativemodule/React_defaultsnativemodule.framework", + "${BUILT_PRODUCTS_DIR}/React-domnativemodule/React_domnativemodule.framework", + "${BUILT_PRODUCTS_DIR}/React-featureflags/React_featureflags.framework", + "${BUILT_PRODUCTS_DIR}/React-featureflagsnativemodule/React_featureflagsnativemodule.framework", + "${BUILT_PRODUCTS_DIR}/React-graphics/React_graphics.framework", + "${BUILT_PRODUCTS_DIR}/React-hermes/reacthermes.framework", + "${BUILT_PRODUCTS_DIR}/React-idlecallbacksnativemodule/idlecallbacksnativemodule.framework", + "${BUILT_PRODUCTS_DIR}/React-jserrorhandler/React_jserrorhandler.framework", + "${BUILT_PRODUCTS_DIR}/React-jsi/jsi.framework", + "${BUILT_PRODUCTS_DIR}/React-jsiexecutor/jsireact.framework", + "${BUILT_PRODUCTS_DIR}/React-jsinspector/jsinspector_modern.framework", + "${BUILT_PRODUCTS_DIR}/React-jsinspectortracing/jsinspector_moderntracing.framework", + "${BUILT_PRODUCTS_DIR}/React-jsitooling/JSITooling.framework", + "${BUILT_PRODUCTS_DIR}/React-logger/logger.framework", + "${BUILT_PRODUCTS_DIR}/React-microtasksnativemodule/React_microtasksnativemodule.framework", + "${BUILT_PRODUCTS_DIR}/React-oscompat/oscompat.framework", + "${BUILT_PRODUCTS_DIR}/React-perflogger/reactperflogger.framework", + "${BUILT_PRODUCTS_DIR}/React-performancetimeline/React_performancetimeline.framework", + "${BUILT_PRODUCTS_DIR}/React-rendererconsistency/React_rendererconsistency.framework", + "${BUILT_PRODUCTS_DIR}/React-renderercss/React_renderercss.framework", + "${BUILT_PRODUCTS_DIR}/React-rendererdebug/React_rendererdebug.framework", + "${BUILT_PRODUCTS_DIR}/React-runtimescheduler/React_runtimescheduler.framework", + "${BUILT_PRODUCTS_DIR}/React-utils/React_utils.framework", + "${BUILT_PRODUCTS_DIR}/ReactAppDependencyProvider/ReactAppDependencyProvider.framework", + "${BUILT_PRODUCTS_DIR}/ReactCodegen/ReactCodegen.framework", + "${BUILT_PRODUCTS_DIR}/ReactCommon/ReactCommon.framework", + "${BUILT_PRODUCTS_DIR}/RecaptchaInterop/RecaptchaInterop.framework", + "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework", + "${BUILT_PRODUCTS_DIR}/Yoga/yoga.framework", + "${BUILT_PRODUCTS_DIR}/abseil/absl.framework", + "${BUILT_PRODUCTS_DIR}/fmt/fmt.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/glog/glog.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/lottie-ios/Lottie.framework", + "${BUILT_PRODUCTS_DIR}/lottie-react-native/lottie_react_native.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/react-native-pager-view/react_native_pager_view.framework", + "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework", + "${BUILT_PRODUCTS_DIR}/react-native-webview/react_native_webview.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppCheckCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DoubleConversion.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAppCheckInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseAuthInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreExtension.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFirestoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseFunctions.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessagingInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseSharedSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMAppAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleSignIn.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTDeprecation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTTypeSafety.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCAsyncStorage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFBApp.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFBAuth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFBFirestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFBFunctions.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFBMessaging.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNGoogleSignin.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNReanimated.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNSVG.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNScreens.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoreModules.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_Fabric.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_FabricComponents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_FabricImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_ImageManager.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_Mapbuffer.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_NativeModulesApple.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTAnimation.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RCTAppDelegate.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTBlob.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBReactNativeSpec.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTFabric.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTLinking.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTNetwork.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTRuntime.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTSettings.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTText.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTVibration.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RuntimeApple.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RuntimeCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_RuntimeHermes.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cxxreact.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_debug.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_defaultsnativemodule.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_domnativemodule.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_featureflags.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_featureflagsnativemodule.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_graphics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/reacthermes.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/idlecallbacksnativemodule.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_jserrorhandler.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsi.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsireact.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsinspector_modern.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsinspector_moderntracing.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JSITooling.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/logger.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_microtasksnativemodule.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/oscompat.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/reactperflogger.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_performancetimeline.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_rendererconsistency.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_renderercss.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_rendererdebug.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_runtimescheduler.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React_utils.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactAppDependencyProvider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactCodegen.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactCommon.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RecaptchaInterop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fmt.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lottie.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lottie_react_native.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_pager_view.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_webview.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-amba/Pods-amba-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-amba/Pods-amba-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-amba/Pods-amba-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + 634516A4BD31311B590C5C57 /* ExpoModulesProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 992DF58BDDA9584905BF5CD8 /* Pods-amba.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1.2; + DEVELOPMENT_TEAM = K73ZX6D43Q; + ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = amba/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1; + NEW_SETTING_2 = ""; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.ambapay.ambaagent; + PRODUCT_NAME = ambaagent; + SWIFT_OBJC_BRIDGING_HEADER = "amba/amba-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 269AEFAF2C9B8865892098EB /* Pods-amba.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1.2; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = K73ZX6D43Q; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = amba/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.1; + NEW_SETTING_2 = ""; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.ambapays.app; + PRODUCT_NAME = amba; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Amba App Store"; + SWIFT_OBJC_BRIDGING_HEADER = "amba/amba-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "amba" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "amba" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/amba.xcodeproj/xcshareddata/xcschemes/amba.xcscheme b/ios/amba.xcodeproj/xcshareddata/xcschemes/amba.xcscheme new file mode 100644 index 0000000..762d355 --- /dev/null +++ b/ios/amba.xcodeproj/xcshareddata/xcschemes/amba.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/amba.xcworkspace/contents.xcworkspacedata b/ios/amba.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..9494357 --- /dev/null +++ b/ios/amba.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/amba/AppDelegate.swift b/ios/amba/AppDelegate.swift new file mode 100644 index 0000000..135cfa9 --- /dev/null +++ b/ios/amba/AppDelegate.swift @@ -0,0 +1,74 @@ +import Expo +import FirebaseCore +import React +import ReactAppDependencyProvider + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + var window: UIWindow? + + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + +#if os(iOS) || os(tvOS) + window = UIWindow(frame: UIScreen.main.bounds) +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a +FirebaseApp.configure() +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + factory.startReactNative( + withModuleName: "main", + in: window, + launchOptions: launchOptions) +#endif + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + // Extension point for config-plugins + + override func sourceURL(for bridge: RCTBridge) -> URL? { + // needed to return the correct URL for expo-dev-client. + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/ios/amba/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/amba/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..2732229 Binary files /dev/null and b/ios/amba/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/amba/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/amba/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/ios/amba/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/amba/Images.xcassets/Contents.json b/ios/amba/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/ios/amba/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/ios/amba/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/amba/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..15f02ab --- /dev/null +++ b/ios/amba/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "components": { + "alpha": "1.000", + "blue": "1.00000000000000", + "green": "1.00000000000000", + "red": "1.00000000000000" + }, + "color-space": "srgb" + }, + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/amba/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/ios/amba/Images.xcassets/SplashScreenLogo.imageset/Contents.json new file mode 100644 index 0000000..f65c008 --- /dev/null +++ b/ios/amba/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "image@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "image@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/amba/Info.plist b/ios/amba/Info.plist new file mode 100644 index 0000000..8d095a8 --- /dev/null +++ b/ios/amba/Info.plist @@ -0,0 +1,68 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 5 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSContactsUsageDescription + AmbaPay needs access to your contacts to help you send money to friends and family quickly and easily. + NSFaceIDUsageDescription + AmbaPay uses Face ID to securely authenticate transactions and protect your account. + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.613864011564-atsg9nau8hicla4td6dedcab15g7qr04 + + + + + diff --git a/ios/amba/PrivacyInfo.xcprivacy b/ios/amba/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..02a8a05 --- /dev/null +++ b/ios/amba/PrivacyInfo.xcprivacy @@ -0,0 +1,50 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + 0A2A.1 + 3B52.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + 1C8F.1 + C56D.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/ios/amba/SplashScreen.storyboard b/ios/amba/SplashScreen.storyboard new file mode 100644 index 0000000..447b418 --- /dev/null +++ b/ios/amba/SplashScreen.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/amba/Supporting/Expo.plist b/ios/amba/Supporting/Expo.plist new file mode 100644 index 0000000..6631ffa --- /dev/null +++ b/ios/amba/Supporting/Expo.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/ios/amba/amba-Bridging-Header.h b/ios/amba/amba-Bridging-Header.h new file mode 100644 index 0000000..8361941 --- /dev/null +++ b/ios/amba/amba-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000..8a656f5 Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/api/baseUrls.ts b/lib/api/baseUrls.ts new file mode 100644 index 0000000..0ed6119 --- /dev/null +++ b/lib/api/baseUrls.ts @@ -0,0 +1,9 @@ +export const BASE_URLS = { + core: "https://api.ambapays.com", + events: "https://events.api.ambapays.com", + referral: "https://referralapi-fclnigvupq-uc.a.run.app", +} as const; + +export type BaseUrlKey = keyof typeof BASE_URLS; + +export const getBaseUrl = (key: BaseUrlKey): string => BASE_URLS[key]; diff --git a/lib/api/client.ts b/lib/api/client.ts new file mode 100644 index 0000000..cb0fe40 --- /dev/null +++ b/lib/api/client.ts @@ -0,0 +1,96 @@ +import { getBaseUrl, type BaseUrlKey } from "./baseUrls"; + +export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + +export interface EndpointConfig { + base: BaseUrlKey; // e.g. "events" + path: string; // e.g. "/api/event/all" + method: HttpMethod; +} + +export const createEndpoint = ( + base: BaseUrlKey, + path: string, + method: HttpMethod = "GET" +): EndpointConfig => ({ base, path, method }); + +interface CallEndpointOptions { + body?: TBody; + query?: Record; + headers?: Record; +} + +export async function callEndpoint( + endpoint: EndpointConfig, + options: CallEndpointOptions = {} +): Promise { + const { body, query, headers } = options; + + const baseUrl = getBaseUrl(endpoint.base); + const url = buildUrl(baseUrl, endpoint.path, query); + + const init: RequestInit = { + method: endpoint.method, + headers: { + "Content-Type": "application/json", + ...(headers || {}), + }, + }; + + if (body && endpoint.method !== "GET") { + // @ts-ignore body is allowed on RequestInit for fetch + init.body = JSON.stringify(body); + } + + // Debug log of what we're calling (without dumping full token) + const authHeader = headers?.Authorization || ""; + const shortToken = authHeader.startsWith("Bearer ") + ? authHeader.slice(7, 27) + "..." + : undefined; + // eslint-disable-next-line no-console + console.log("[api] request", { + url, + method: endpoint.method, + hasAuth: !!authHeader, + tokenPrefix: shortToken, + }); + + const res = await fetch(url, init as any); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + // eslint-disable-next-line no-console + console.log("[api] error response", { + url, + status: res.status, + body: text, + }); + throw new Error(text || `Request failed with status ${res.status}`); + } + + const contentType = res.headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + return (await res.json()) as TResponse; + } + + return (await res.text()) as unknown as TResponse; +} + +function buildUrl( + baseUrl: string, + path: string, + query?: Record +): string { + const trimmedBase = baseUrl.replace(/\/$/, ""); + const trimmedPath = path.startsWith("/") ? path : `/${path}`; + const url = new URL(trimmedBase + trimmedPath); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value === undefined) return; + url.searchParams.set(key, String(value)); + }); + } + + return url.toString(); +} diff --git a/lib/api/eventEndpoints.ts b/lib/api/eventEndpoints.ts new file mode 100644 index 0000000..eb43d3f --- /dev/null +++ b/lib/api/eventEndpoints.ts @@ -0,0 +1,14 @@ +import type { EndpointConfig } from "./client"; + +export const eventEndpoints = { + getEvents: { + base: "events", + path: "/api/event/all", + method: "GET" as const, + } satisfies EndpointConfig, + getEventById: { + base: "events", + path: "/api/event/all", + method: "GET" as const, + } satisfies EndpointConfig, +}; diff --git a/lib/api/referralEndpoints.ts b/lib/api/referralEndpoints.ts new file mode 100644 index 0000000..814d2be --- /dev/null +++ b/lib/api/referralEndpoints.ts @@ -0,0 +1,9 @@ +import type { EndpointConfig } from "./client"; + +export const referralEndpoints = { + apply: { + base: "referral", + path: "/api/referrals/apply", + method: "POST" as const, + } satisfies EndpointConfig, +}; diff --git a/lib/api/ticketEndpoints.ts b/lib/api/ticketEndpoints.ts new file mode 100644 index 0000000..669861c --- /dev/null +++ b/lib/api/ticketEndpoints.ts @@ -0,0 +1,15 @@ +import type { EndpointConfig } from "./client"; + +export const ticketEndpoints = { + getTickets: { + base: "events", + path: "/api/ticket/my-tickets", + method: "GET" as const, + } satisfies EndpointConfig, + + buyTicket: { + base: "events", + path: "/api/ticket/buy", + method: "POST" as const, + } satisfies EndpointConfig, +}; diff --git a/lib/firebase/firebase.native.ts b/lib/firebase/firebase.native.ts new file mode 100644 index 0000000..d97965c --- /dev/null +++ b/lib/firebase/firebase.native.ts @@ -0,0 +1,200 @@ +/** + * Firebase Native Implementation + * Uses @react-native-firebase for Android/iOS + */ +import { Platform } from "react-native"; +import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth"; +import firestore from "@react-native-firebase/firestore"; +import messaging from "@react-native-firebase/messaging"; +import functions from "@react-native-firebase/functions"; +import { + GoogleSignin, + statusCodes, +} from "@react-native-google-signin/google-signin"; + +// Web Client ID from google-services.json +const WEB_CLIENT_ID = + "613864011564-78d915g0hm9sbveskkfcch6mrd8atktb.apps.googleusercontent.com"; + +// iOS Client ID from GoogleService-Info.plist +const IOS_CLIENT_ID = + "613864011564-atsg9nau8hicla4td6dedcab15g7qr04.apps.googleusercontent.com"; + +// Configure Google Sign-In for native +GoogleSignin.configure({ + webClientId: WEB_CLIENT_ID, + iosClientId: Platform.OS === "ios" ? IOS_CLIENT_ID : undefined, + offlineAccess: true, +}); + +// Export types +export type { FirebaseAuthTypes }; +export { statusCodes }; + +// Auth exports +export const firebaseAuth = auth; +export const getAuthInstance = () => auth(); +export const onAuthStateChanged = ( + callback: (user: FirebaseAuthTypes.User | null) => void +) => { + return auth().onAuthStateChanged(callback); +}; + +// Firestore exports +export const firebaseFirestore = firestore; +export const getFirestoreInstance = () => firestore(); +export const FieldValue = firestore.FieldValue; +export const Timestamp = firestore.Timestamp; + +// Collection helpers +export const collection = (path: string) => firestore().collection(path); +export const doc = (collectionPath: string, docId: string) => + firestore().collection(collectionPath).doc(docId); + +// Messaging exports +export const firebaseMessaging = messaging; +export const getMessagingInstance = () => messaging(); +export const AuthorizationStatus = messaging.AuthorizationStatus; + +// Functions exports +export const firebaseFunctions = functions; +export const getFunctionsInstance = () => functions(); +export const httpsCallable = (name: string) => functions().httpsCallable(name); + +// Google Sign-In (Native) +export const signInWithGoogle = async (): Promise<{ + user: FirebaseAuthTypes.User | null; + isNewUser: boolean; + error?: string; +}> => { + try { + // Check if device supports Google Play Services (Android only) + if (Platform.OS === "android") { + await GoogleSignin.hasPlayServices({ + showPlayServicesUpdateDialog: true, + }); + } + + // Sign out first to always show account picker + try { + await GoogleSignin.signOut(); + } catch (e) { + // Ignore sign out errors + } + + // Sign in with Google + const signInResult = await GoogleSignin.signIn(); + const idToken = signInResult.data?.idToken; + + if (!idToken) { + return { + user: null, + isNewUser: false, + error: "No ID token received from Google", + }; + } + + // Create credential and sign in to Firebase + const googleCredential = auth.GoogleAuthProvider.credential(idToken); + const userCredential = await auth().signInWithCredential(googleCredential); + const isNewUser = userCredential.additionalUserInfo?.isNewUser ?? false; + + return { user: userCredential.user, isNewUser }; + } catch (error: any) { + console.error("Google Sign-In error:", error); + + if (error.code === statusCodes.SIGN_IN_CANCELLED) { + return { user: null, isNewUser: false, error: "Sign in was cancelled" }; + } else if (error.code === statusCodes.IN_PROGRESS) { + return { + user: null, + isNewUser: false, + error: "Sign in is already in progress", + }; + } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) { + return { + user: null, + isNewUser: false, + error: "Google Play Services not available", + }; + } + + // Handle iOS-specific errors + if (Platform.OS === "ios" && error.message?.includes("simulator")) { + return { + user: null, + isNewUser: false, + error: + "Google Sign-In may not work on iOS Simulator. Please test on a real device.", + }; + } + + return { + user: null, + isNewUser: false, + error: error.message || "Google Sign-In failed", + }; + } +}; + +export const signOutFromGoogle = async (): Promise => { + try { + await GoogleSignin.signOut(); + } catch (error) { + console.error("Google Sign-Out error:", error); + } +}; + +// Phone Auth (Native) +export const signInWithPhoneNumber = async (phoneNumber: string) => { + return auth().signInWithPhoneNumber(phoneNumber); +}; + +// Email/Password Authentication (Native) +export const createUserWithEmailAndPassword = async ( + email: string, + password: string +): Promise<{ user: FirebaseAuthTypes.User | null; error?: string }> => { + try { + const userCredential = await auth().createUserWithEmailAndPassword( + email, + password + ); + return { user: userCredential.user }; + } catch (error: any) { + console.error('Email/Password signup error:', error); + return { + user: null, + error: error.message || 'Failed to create account', + }; + } +}; + +export const signInWithEmailAndPassword = async ( + email: string, + password: string +): Promise<{ user: FirebaseAuthTypes.User | null; error?: string }> => { + try { + const userCredential = await auth().signInWithEmailAndPassword( + email, + password + ); + return { user: userCredential.user }; + } catch (error: any) { + console.error('Email/Password signin error:', error); + return { + user: null, + error: error.message || 'Failed to sign in', + }; + } +}; + +// Firebase Auth sign out +export const signOut = async (): Promise => { + await signOutFromGoogle(); + await auth().signOut(); +}; + +// Platform identifier +export const isNative = true; +export const isWeb = false; diff --git a/lib/firebase/firebase.web.ts b/lib/firebase/firebase.web.ts new file mode 100644 index 0000000..48b742f --- /dev/null +++ b/lib/firebase/firebase.web.ts @@ -0,0 +1,420 @@ +/** + * Firebase Web Implementation + * Uses Firebase JS SDK for web platform + */ +import { initializeApp, getApps, getApp } from 'firebase/app'; +import { + getAuth, + onAuthStateChanged as webOnAuthStateChanged, + GoogleAuthProvider, + signOut as webSignOut, +} from 'firebase/auth'; +import type { User, ConfirmationResult } from 'firebase/auth'; +import { + getFirestore, + collection as webCollection, + doc as webDoc, + getDoc, + setDoc, + updateDoc, + deleteDoc, + query, + where, + orderBy, + limit, + getDocs, + onSnapshot, + serverTimestamp, + Timestamp as WebTimestamp, + deleteField +} from 'firebase/firestore'; +import { getMessaging, getToken, onMessage } from 'firebase/messaging'; +import { getFunctions, httpsCallable as webHttpsCallable } from 'firebase/functions'; + +// Firebase configuration for web +const firebaseConfig = { + apiKey: "AIzaSyCVprX0NvjjemRKRpG1ZJHyMwKsJmBuXHc", + authDomain: "ambapaydemo.firebaseapp.com", + databaseURL: "https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app", + projectId: "ambapaydemo", + storageBucket: "ambapaydemo.firebasestorage.app", + messagingSenderId: "613864011564", + appId: "1:613864011564:web:e078c5990d3b2bff249e89", + measurementId: "G-F8RVT1BHHC" +}; + +// Initialize Firebase (only once) +const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp(); +const authInstance = getAuth(app); +const firestoreInstance = getFirestore(app); + +// Lazy initialization for messaging (requires browser support) +let messagingInstance: ReturnType | null = null; +const getMessagingInstanceInternal = () => { + if (typeof window !== 'undefined' && 'Notification' in window) { + if (!messagingInstance) { + try { + messagingInstance = getMessaging(app); + } catch (e) { + console.warn('Firebase Messaging not available:', e); + } + } + return messagingInstance; + } + return null; +}; + +const functionsInstance = getFunctions(app); + +// Type compatibility layer +export interface FirebaseAuthTypes { + User: User; + ConfirmationResult: ConfirmationResult; +} + +// Status codes compatibility (for Google Sign-In error handling) +export const statusCodes = { + SIGN_IN_CANCELLED: 'SIGN_IN_CANCELLED', + IN_PROGRESS: 'IN_PROGRESS', + PLAY_SERVICES_NOT_AVAILABLE: 'PLAY_SERVICES_NOT_AVAILABLE', +}; + +// Auth exports +export const firebaseAuth = { GoogleAuthProvider }; +export const getAuthInstance = () => authInstance; +export const onAuthStateChanged = (callback: (user: User | null) => void) => { + return webOnAuthStateChanged(authInstance, callback); +}; + +// Firestore exports +export const firebaseFirestore = { + FieldValue: { + serverTimestamp: () => serverTimestamp(), + delete: () => deleteField(), + } +}; +export const getFirestoreInstance = () => firestoreInstance; +export const FieldValue = { + serverTimestamp: () => serverTimestamp(), + delete: () => deleteField(), +}; +export const Timestamp = WebTimestamp; + +// Collection helpers that return a Firestore-like interface +export const collection = (path: string) => { + const collectionRef = webCollection(firestoreInstance, path); + + return { + doc: (docId: string) => createDocRef(path, docId), + where: (field: string, op: any, value: any) => { + return createQueryBuilder(collectionRef, [where(field, op, value)]); + }, + orderBy: (field: string, direction?: 'asc' | 'desc') => { + return createQueryBuilder(collectionRef, [orderBy(field, direction)]); + }, + get: async () => { + const snapshot = await getDocs(collectionRef); + return { + docs: snapshot.docs.map(docItem => ({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })), + empty: snapshot.empty, + forEach: (callback: (docItem: any) => void) => { + snapshot.forEach(docItem => callback({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })); + }, + }; + }, + }; +}; + +// Query builder for chaining +const createQueryBuilder = (collectionRef: any, constraints: any[] = []) => { + return { + where: (field: string, op: any, value: any) => { + return createQueryBuilder(collectionRef, [...constraints, where(field, op, value)]); + }, + orderBy: (field: string, direction?: 'asc' | 'desc') => { + return createQueryBuilder(collectionRef, [...constraints, orderBy(field, direction)]); + }, + limit: (n: number) => { + return createQueryBuilder(collectionRef, [...constraints, limit(n)]); + }, + get: async () => { + const q = query(collectionRef, ...constraints); + const snapshot = await getDocs(q); + return { + docs: snapshot.docs.map(docItem => ({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })), + empty: snapshot.empty, + forEach: (callback: (docItem: any) => void) => { + snapshot.forEach(docItem => callback({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })); + }, + }; + }, + onSnapshot: (callback: (snapshot: any) => void, errorCallback?: (error: any) => void) => { + const q = query(collectionRef, ...constraints); + return onSnapshot(q, (snapshot) => { + callback({ + docs: snapshot.docs.map(docItem => ({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })), + empty: snapshot.empty, + forEach: (cb: (docItem: any) => void) => { + snapshot.forEach(docItem => cb({ + id: docItem.id, + data: () => docItem.data(), + exists: docItem.exists(), + })); + }, + }); + }, errorCallback); + }, + }; +}; + +// Document reference helper +const createDocRef = (collectionPath: string, docId: string) => { + const docRef = webDoc(firestoreInstance, collectionPath, docId); + + return { + get: async () => { + const snapshot = await getDoc(docRef); + return { + exists: snapshot.exists(), + data: () => snapshot.data(), + id: snapshot.id, + }; + }, + set: async (data: any, options?: { merge?: boolean }) => { + await setDoc(docRef, data, options || {}); + }, + update: async (data: any) => { + await updateDoc(docRef, data); + }, + delete: async () => { + await deleteDoc(docRef); + }, + onSnapshot: (callback: (snapshot: any) => void, errorCallback?: (error: any) => void) => { + return onSnapshot(docRef, (snapshot) => { + callback({ + exists: snapshot.exists(), + data: () => snapshot.data(), + id: snapshot.id, + }); + }, errorCallback); + }, + }; +}; + +export const doc = (collectionPath: string, docId: string) => createDocRef(collectionPath, docId); + +// Messaging exports (web-specific) +export const firebaseMessaging = { + AuthorizationStatus: { + AUTHORIZED: 1, + PROVISIONAL: 2, + DENIED: 0, + NOT_DETERMINED: -1, + }, +}; +export const getMessagingInstance = getMessagingInstanceInternal; +export const AuthorizationStatus = firebaseMessaging.AuthorizationStatus; + +// Functions exports +export const firebaseFunctions = {}; +export const getFunctionsInstance = () => functionsInstance; +export const httpsCallable = (name: string) => { + const callable = webHttpsCallable(functionsInstance, name); + return async (data: any) => { + const result = await callable(data); + return result; + }; +}; + +// Google Sign-In (Web) +export const signInWithGoogle = async (): Promise<{ + user: User | null; + isNewUser: boolean; + error?: string; +}> => { + try { + // Dynamic import for signInWithPopup (tree-shaking friendly) + const authModule = await import('firebase/auth'); + const signInWithPopup = (authModule as any).signInWithPopup; + + if (!signInWithPopup) { + return { user: null, isNewUser: false, error: 'signInWithPopup not available' }; + } + + const provider = new GoogleAuthProvider(); + provider.setCustomParameters({ + prompt: 'select_account' + }); + + const result = await signInWithPopup(authInstance, provider); + // Check if new user - web SDK doesn't directly expose this, check metadata + const isNewUser = result.user.metadata.creationTime === result.user.metadata.lastSignInTime; + + return { user: result.user, isNewUser }; + } catch (error: any) { + console.error('Google Sign-In error:', error); + + if (error.code === 'auth/popup-closed-by-user') { + return { user: null, isNewUser: false, error: 'Sign in was cancelled' }; + } + if (error.code === 'auth/popup-blocked') { + return { user: null, isNewUser: false, error: 'Popup was blocked. Please allow popups for this site.' }; + } + + return { user: null, isNewUser: false, error: error.message || 'Google Sign-In failed' }; + } +}; + +export const signOutFromGoogle = async (): Promise => { + // No separate Google sign out needed on web +}; + +// Phone Auth (Web) - requires reCAPTCHA +let recaptchaVerifier: any = null; + +export const initRecaptcha = async (containerId: string): Promise => { + if (typeof window !== 'undefined' && !recaptchaVerifier) { + // Dynamic import for RecaptchaVerifier + const authModule = await import('firebase/auth'); + const RecaptchaVerifier = (authModule as any).RecaptchaVerifier; + + if (RecaptchaVerifier) { + recaptchaVerifier = new RecaptchaVerifier(authInstance, containerId, { + size: 'invisible', + callback: () => { + console.log('reCAPTCHA verified'); + }, + }); + } + } + return recaptchaVerifier; +}; + +export const signInWithPhoneNumber = async (phoneNumber: string): Promise => { + if (!recaptchaVerifier) { + throw new Error('reCAPTCHA not initialized. Call initRecaptcha first.'); + } + // Dynamic import for signInWithPhoneNumber + const authModule = await import('firebase/auth'); + const webSignInWithPhoneNumber = (authModule as any).signInWithPhoneNumber; + + if (!webSignInWithPhoneNumber) { + throw new Error('signInWithPhoneNumber not available'); + } + + return webSignInWithPhoneNumber(authInstance, phoneNumber, recaptchaVerifier); +}; + +// Email/Password Authentication (Web) +export const createUserWithEmailAndPassword = async ( + email: string, + password: string +): Promise<{ user: User | null; error?: string }> => { + try { + const authModule = await import('firebase/auth'); + const createUser = (authModule as any).createUserWithEmailAndPassword; + + if (!createUser) { + return { user: null, error: 'createUserWithEmailAndPassword not available' }; + } + + const userCredential = await createUser(authInstance, email, password); + return { user: userCredential.user }; + } catch (error: any) { + console.error('Email/Password signup error:', error); + return { + user: null, + error: error.message || 'Failed to create account', + }; + } +}; + +export const signInWithEmailAndPassword = async ( + email: string, + password: string +): Promise<{ user: User | null; error?: string }> => { + try { + const authModule = await import('firebase/auth'); + const signIn = (authModule as any).signInWithEmailAndPassword; + + if (!signIn) { + return { user: null, error: 'signInWithEmailAndPassword not available' }; + } + + const userCredential = await signIn(authInstance, email, password); + return { user: userCredential.user }; + } catch (error: any) { + console.error('Email/Password signin error:', error); + return { + user: null, + error: error.message || 'Failed to sign in', + }; + } +}; + +// Firebase Auth sign out +export const signOut = async (): Promise => { + await webSignOut(authInstance); +}; + +// Web FCM helpers +export const requestNotificationPermission = async (): Promise => { + if (typeof window === 'undefined' || !('Notification' in window)) { + return false; + } + + try { + const permission = await Notification.requestPermission(); + return permission === 'granted'; + } catch (error) { + console.error('Error requesting notification permission:', error); + return false; + } +}; + +export const getWebFCMToken = async (): Promise => { + const messaging = getMessagingInstanceInternal(); + if (!messaging) return null; + + try { + // You'll need to add your VAPID key here for web push + const token = await getToken(messaging, { + vapidKey: 'YOUR_VAPID_KEY_HERE' // TODO: Add your VAPID key + }); + return token; + } catch (error) { + console.error('Error getting FCM token:', error); + return null; + } +}; + +export const onWebMessage = (callback: (payload: any) => void) => { + const messaging = getMessagingInstanceInternal(); + if (!messaging) return () => { }; + + return onMessage(messaging, callback); +}; + +// Platform identifier +export const isNative = false; +export const isWeb = true; diff --git a/lib/firebase/index.ts b/lib/firebase/index.ts new file mode 100644 index 0000000..ce97b68 --- /dev/null +++ b/lib/firebase/index.ts @@ -0,0 +1,16 @@ +/** + * Firebase Native Module (Default) + * + * This is the default/native entry point for Firebase. + * For web builds, bundlers automatically resolve to index.web.ts instead. + */ + +// Re-export everything from the native implementation +export * from './firebase.native'; + +// Re-export types +export type { FirebaseAuthTypes } from './firebase.native'; + +// Default export +import * as nativeFirebase from './firebase.native'; +export default nativeFirebase; diff --git a/lib/firebase/index.web.ts b/lib/firebase/index.web.ts new file mode 100644 index 0000000..199b01e --- /dev/null +++ b/lib/firebase/index.web.ts @@ -0,0 +1,17 @@ +/** + * Firebase Web Module + * + * This is the web-specific entry point for Firebase. + * Bundlers (Webpack/Metro) automatically resolve this for web builds. + */ + +// Re-export everything from the web implementation +export * from './firebase.web'; + +// Re-export types (web compatibility layer) +export type { FirebaseAuthTypes } from './firebase.web'; + +// Default export +import * as webFirebase from './firebase.web'; +export default webFirebase; + diff --git a/lib/hooks/useAuth.ts b/lib/hooks/useAuth.ts new file mode 100644 index 0000000..a21d4e7 --- /dev/null +++ b/lib/hooks/useAuth.ts @@ -0,0 +1,127 @@ +import { AuthService } from "../services/authServices"; +import { useAuthStore } from "../stores/authStore"; + +interface UsePhoneAuthReturn { + sendOTP: (phoneNumber: string) => Promise; + verifyOTP: (code: string) => Promise; + loading: boolean; + error: string | null; + confirmationResult: any | null; +} + +export const usePhoneAuth = (): UsePhoneAuthReturn => { + const { + phoneConfirmationResult: confirmationResult, + phoneLoading: loading, + phoneError: error, + setPhoneConfirmationResult: setConfirmationResult, + setPhoneLoading: setLoading, + setPhoneError: setError, + setUser, + } = useAuthStore(); + + const sendOTP = async (phoneNumber: string) => { + setLoading(true); + setError(null); + + try { + console.log( + "usePhoneAuth: Calling AuthService.sendOTP with:", + phoneNumber + ); + const result = await AuthService.sendOTP(phoneNumber); + console.log("usePhoneAuth: Result from AuthService.sendOTP:", result); + + if (result.error) { + console.log("usePhoneAuth: Setting error:", result.error); + setError(result.error); + } else if (result.confirmationResult) { + console.log("usePhoneAuth: Setting confirmationResult"); + setConfirmationResult(result.confirmationResult); + } else { + console.log("usePhoneAuth: No confirmationResult found in result"); + } + } catch (err) { + console.log("usePhoneAuth: Caught error:", err); + setError(err instanceof Error ? err.message : "Failed to send OTP"); + } finally { + setLoading(false); + } + }; + + const verifyOTP = async (code: string) => { + const isDevBypass = __DEV__; + + if (!confirmationResult && !isDevBypass) { + setError("No verification session found. Please request OTP first."); + return; + } + + setLoading(true); + setError(null); + + try { + let userCredential: any; + + if (isDevBypass) { + console.log("usePhoneAuth: Using DEV OTP bypass (no Firebase call)"); + const devUser = { + uid: "dev-emulator-user", + phoneNumber: "DEV", + }; + userCredential = { user: devUser }; + } else { + console.log("usePhoneAuth: Verifying OTP with native Firebase"); + userCredential = await confirmationResult.confirm(code); + } + + if (userCredential?.user) { + setUser(userCredential.user); + } + + console.log( + "usePhoneAuth: OTP verified successfully, user:", + userCredential?.user?.uid + ); + } catch (err: any) { + console.error("usePhoneAuth: OTP verification failed:", err); + setError(err?.message || "Invalid verification code"); + } finally { + setLoading(false); + } + }; + + return { + sendOTP, + verifyOTP, + loading, + error, + confirmationResult, + }; +}; + +// Re-export the auth store hook for convenience +export const useAuthState = () => { + const { + user, + loading, + profile, + wallet, + profileLoading, + walletLoading, + profileError, + walletError, + formattedBalance, + } = useAuthStore(); + return { + user, + loading, + profile, + wallet, + profileLoading, + walletLoading, + profileError, + walletError, + formattedBalance, + }; +}; diff --git a/lib/hooks/useAuthWithProfile.ts b/lib/hooks/useAuthWithProfile.ts new file mode 100644 index 0000000..08eb835 --- /dev/null +++ b/lib/hooks/useAuthWithProfile.ts @@ -0,0 +1,94 @@ +import { useEffect } from 'react'; +import { useAuthStore } from '../stores/authStore'; +import { useUserProfile } from './useUserProfile'; +import { useUserWallet } from './useUserWallet'; + +/** + * Custom hook that integrates the auth store with profile and wallet hooks + * This maintains the existing functionality while using Zustand for state management + */ +export const useAuthWithProfile = () => { + const { + user, + loading, + phoneSessionInfo, + phoneConfirmationResult, + phoneLoading, + phoneError, + setProfile, + setWallet, + setProfileLoading, + setWalletLoading, + setProfileError, + setWalletError, + setFormattedBalance, + setPhoneSessionInfo, + setPhoneConfirmationResult, + setPhoneLoading, + setPhoneError, + clearPhoneAuth, + signOut, + } = useAuthStore(); + + // Use the existing profile hook + const profileHook = useUserProfile(user); + const walletHook = useUserWallet(user); + + // Sync profile hook data with store - batch updates to reduce re-renders + useEffect(() => { + // Batch state updates to avoid multiple re-renders + setProfile(profileHook.profile); + setProfileLoading(profileHook.loading); + setProfileError(profileHook.error); + }, [profileHook.profile, profileHook.loading, profileHook.error, setProfile, setProfileLoading, setProfileError]); + + // Sync wallet hook data with store - batch updates to reduce re-renders + useEffect(() => { + // Batch state updates to avoid multiple re-renders + setWallet(walletHook.wallet); + setWalletLoading(walletHook.loading); + setWalletError(walletHook.error); + setFormattedBalance(walletHook.formattedBalance); + }, [walletHook.wallet, walletHook.loading, walletHook.error, walletHook.formattedBalance, setWallet, setWalletLoading, setWalletError, setFormattedBalance]); + + + return { + // Auth state + user, + loading, + + // Profile state - use hook values directly + profile: profileHook.profile, + profileLoading: profileHook.loading, + profileError: profileHook.error, + + // Wallet state - use hook values directly + wallet: walletHook.wallet, + walletLoading: walletHook.loading, + walletError: walletHook.error, + formattedBalance: walletHook.formattedBalance, + + // Phone auth state + phoneSessionInfo, + phoneConfirmationResult, + phoneLoading, + phoneError, + + // Actions + signOut, + refreshProfile: profileHook.refreshProfile, + refreshWallet: walletHook.refreshWallet, + + // Phone auth actions + setPhoneSessionInfo, + setPhoneConfirmationResult, + setPhoneLoading, + setPhoneError, + clearPhoneAuth, + + // Wallet actions + addCreditCard: walletHook.addCreditCard, + removeCreditCard: walletHook.removeCreditCard, + balanceInDollars: walletHook.balanceInDollars, + }; +}; diff --git a/lib/hooks/useEvents.ts b/lib/hooks/useEvents.ts new file mode 100644 index 0000000..e214c13 --- /dev/null +++ b/lib/hooks/useEvents.ts @@ -0,0 +1,97 @@ +import { useCallback, useEffect, useState } from "react"; +import { useAuthWithProfile } from "./useAuthWithProfile"; +import { EventService, type EventDto } from "../services/eventService"; + +interface UseEventsOptions { + status?: string; + limit?: number; + + immediate?: boolean; +} + +export function useEvents(options: UseEventsOptions = {}) { + const { status = "ACTIVE", limit = 50, immediate = true } = options; + + const { user } = useAuthWithProfile(); + const [token, setToken] = useState(null); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + // Resolve Firebase ID token + useEffect(() => { + let cancelled = false; + + const loadToken = async () => { + if (!user) return; + try { + const idToken = await user.getIdToken(); + if (!cancelled) { + // Debug: log Firebase auth ID token once when fetched + // NOTE: remove this in production to avoid leaking sensitive tokens in logs + console.log("[useEvents] Firebase ID token:", idToken); + setToken(idToken); + } + } catch (err) { + if (!cancelled) { + setError( + err instanceof Error ? err : new Error("Failed to get auth token") + ); + } + } + }; + + loadToken(); + + return () => { + cancelled = true; + }; + }, [user]); + + const fetchEvents = useCallback( + async (override?: { status?: string; limit?: number }) => { + if (!token) return null; + + const finalStatus = override?.status ?? status; + const finalLimit = override?.limit ?? limit; + + setLoading(true); + setError(null); + + try { + const events = await EventService.getEvents(token, { + status: finalStatus, + limit: finalLimit, + }); + console.log("[useEvents] loaded events", { + status: finalStatus, + limit: finalLimit, + length: events?.length ?? 0, + }); + setData(events); + return events; + } catch (err) { + const error = + err instanceof Error ? err : new Error("Failed to fetch events"); + setError(error); + return null; + } finally { + setLoading(false); + } + }, + [token, status, limit] + ); + + useEffect(() => { + if (token && immediate) { + fetchEvents().catch(() => {}); + } + }, [token, immediate, fetchEvents]); + + return { + data, + error, + loading, + refetch: fetchEvents, + }; +} diff --git a/lib/hooks/useFCM.ts b/lib/hooks/useFCM.ts new file mode 100644 index 0000000..b73401d --- /dev/null +++ b/lib/hooks/useFCM.ts @@ -0,0 +1,146 @@ +/** + * useFCM Hook - Platform-aware FCM token management + * + * Native: Full FCM support with token registration and notification handling + * Web: Gracefully skips FCM (web push requires additional service worker setup) + */ +import { useEffect, useRef } from 'react'; +import { Platform, AppState, AppStateStatus } from 'react-native'; +import { useAuthStore } from '../stores/authStore'; + +// Only import FCMService and messaging on native platforms +let FCMService: any = null; +let messaging: any = null; + +if (Platform.OS !== 'web') { + FCMService = require('../services/fcmService').FCMService; + messaging = require('@react-native-firebase/messaging').default; +} + +/** + * Hook to manage FCM token registration and notification handling + * Call this hook once at the app root level when user is authenticated + */ +export const useFCM = () => { + const { user } = useAuthStore(); + const foregroundUnsubscribeRef = useRef<(() => void) | null>(null); + const notificationOpenedUnsubscribeRef = useRef<(() => void) | null>(null); + const initializedUserIdRef = useRef(null); + + useEffect(() => { + // Skip FCM on web platform + if (Platform.OS === 'web' || !FCMService) { + return; + } + + if (!user) { + // User logged out - clean up + if (initializedUserIdRef.current) { + FCMService.cleanup(); + if (foregroundUnsubscribeRef.current) { + foregroundUnsubscribeRef.current(); + foregroundUnsubscribeRef.current = null; + } + if (notificationOpenedUnsubscribeRef.current) { + notificationOpenedUnsubscribeRef.current(); + notificationOpenedUnsubscribeRef.current = null; + } + initializedUserIdRef.current = null; + } + return; + } + + // User is authenticated - initialize FCM + // Clean up previous user's listeners if user changed + if (initializedUserIdRef.current && initializedUserIdRef.current !== user.uid) { + FCMService.cleanup(); + if (foregroundUnsubscribeRef.current) { + foregroundUnsubscribeRef.current(); + foregroundUnsubscribeRef.current = null; + } + if (notificationOpenedUnsubscribeRef.current) { + notificationOpenedUnsubscribeRef.current(); + notificationOpenedUnsubscribeRef.current = null; + } + } + + initializedUserIdRef.current = user.uid; + + // Update FCM token for the existing user + FCMService.updateTokenForExistingUser(user.uid).catch((error: any) => { + console.error('Failed to update FCM token:', error); + }); + + // Set up handlers only if not already set up for this user + if (!foregroundUnsubscribeRef.current) { + // Set up foreground message handler + foregroundUnsubscribeRef.current = FCMService.setupForegroundMessageHandler(); + + // Set up notification opened handler + notificationOpenedUnsubscribeRef.current = FCMService.setupNotificationOpenedHandler( + (remoteMessage: any) => { + console.log('Notification opened:', remoteMessage); + } + ); + + // Check if app was opened from a notification + FCMService.getInitialNotification().then((remoteMessage: any) => { + if (remoteMessage) { + console.log('App opened from notification:', remoteMessage); + } + }); + } + + // Cleanup on unmount + return () => { + if (foregroundUnsubscribeRef.current) { + foregroundUnsubscribeRef.current(); + foregroundUnsubscribeRef.current = null; + } + if (notificationOpenedUnsubscribeRef.current) { + notificationOpenedUnsubscribeRef.current(); + notificationOpenedUnsubscribeRef.current = null; + } + }; + }, [user]); + + // Save token when app comes to foreground + useEffect(() => { + // Skip on web + if (Platform.OS === 'web' || !FCMService || !user) { + return; + } + + const handleAppStateChange = (nextAppState: AppStateStatus) => { + if (nextAppState === 'active' && user) { + FCMService.updateTokenForExistingUser(user.uid).catch((error: any) => { + console.error('Failed to refresh FCM token on app foreground:', error); + }); + } + }; + + const subscription = AppState.addEventListener('change', handleAppStateChange); + + return () => { + subscription.remove(); + }; + }, [user]); + + // Clean up on unmount + useEffect(() => { + // Skip on web + if (Platform.OS === 'web' || !FCMService) { + return; + } + + return () => { + FCMService.cleanup(); + if (foregroundUnsubscribeRef.current) { + foregroundUnsubscribeRef.current(); + } + if (notificationOpenedUnsubscribeRef.current) { + notificationOpenedUnsubscribeRef.current(); + } + }; + }, []); +}; diff --git a/lib/hooks/useGlobalLoading.ts b/lib/hooks/useGlobalLoading.ts new file mode 100644 index 0000000..1a179e9 --- /dev/null +++ b/lib/hooks/useGlobalLoading.ts @@ -0,0 +1,30 @@ +import { useCallback } from 'react'; +import { useUiStore } from '../stores/uiStore'; + +export const useGlobalLoading = () => { + const showLoader = useUiStore((state) => state.showLoader); + const hideLoader = useUiStore((state) => state.hideLoader); + const setLoading = useUiStore((state) => state.setLoading); + const isGlobalLoading = useUiStore((state) => state.isLoading); + + const withLoading = useCallback((operation: () => Promise | T): Promise => { + showLoader(); + try { + const result = operation(); + return Promise.resolve(result).finally(() => { + hideLoader(); + }); + } catch (error) { + hideLoader(); + return Promise.reject(error); + } + }, [showLoader, hideLoader]); + + return { + showLoader, + hideLoader, + setLoading, + withLoading, + isGlobalLoading + }; +}; diff --git a/lib/hooks/useNotifications.ts b/lib/hooks/useNotifications.ts new file mode 100644 index 0000000..ec123ec --- /dev/null +++ b/lib/hooks/useNotifications.ts @@ -0,0 +1,149 @@ +/** + * useNotifications Hook - Platform-aware notifications management + * Uses Firebase abstraction layer for cross-platform support + */ +import { useState, useEffect, useCallback } from 'react'; +import { collection } from '../firebase'; +import { NotificationService } from '../services/notificationService'; +import { useGlobalLoading } from './useGlobalLoading'; + +export interface UseNotificationsReturn { + notifications: any[]; + loading: boolean; + error: string | null; + unreadCount: number; + markAsRead: (notificationId: string) => Promise; + markAllAsRead: () => Promise; + refreshNotifications: () => void; +} + +export function useNotifications(uid: string | null): UseNotificationsReturn { + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { withLoading } = useGlobalLoading(); + + // Calculate unread count + const unreadCount = notifications.filter(notification => !notification.read).length; + + // Mark single notification as read + const markAsRead = async (notificationId: string): Promise => { + try { + const result = await withLoading(() => NotificationService.markAsRead(notificationId)); + if (result.success) { + // Update local state + setNotifications(prev => + prev.map(notification => + notification.id === notificationId + ? { ...notification, read: true, updatedAt: new Date() } + : notification + ) + ); + } else { + setError(result.error || 'Failed to mark notification as read'); + } + } catch (err) { + console.error('Error marking notification as read:', err); + setError('Failed to mark notification as read'); + } + }; + + // Mark all notifications as read + const markAllAsRead = async (): Promise => { + try { + // Update all notifications in local state + setNotifications(prev => + prev.map(notification => ({ ...notification, read: true, updatedAt: new Date() })) + ); + + // Update in Firestore + const unreadNotifications = notifications.filter(n => !n.read); + const updatePromises = unreadNotifications.map(notification => + notification.id ? withLoading(() => NotificationService.markAsRead(notification.id)) : Promise.resolve({ success: true }) + ); + + await Promise.all(updatePromises); + } catch (err) { + console.error('Error marking all notifications as read:', err); + setError('Failed to mark all notifications as read'); + } + }; + + // Refresh notifications + const [refreshKey, setRefreshKey] = useState(0); + + const refreshNotifications = useCallback((): void => { + setLoading(true); + setError(null); + }, []); + + useEffect(() => { + let isMounted = true; + + const fetchNotifications = async () => { + if (!uid) { + if (isMounted) { + setNotifications([]); + setLoading(false); + setError(null); + } + return; + } + + if (isMounted) { + setLoading(true); + setError(null); + } + + try { + const notificationsCollection = collection('notifications'); + const snapshot = await withLoading(async () => { + return await notificationsCollection + .where('userId', '==', uid) + .orderBy('createdAt', 'desc') + .get(); + }) as any; + + const notificationsData: any[] = []; + + if (snapshot && snapshot.forEach) { + snapshot.forEach((docData: any) => { + const data = docData.data(); + notificationsData.push({ + id: docData.id, + ...data, + }); + }); + } + + if (isMounted) { + setNotifications(notificationsData); + setLoading(false); + setError(null); + } + } catch (err) { + console.error('Error fetching notifications:', err); + if (isMounted) { + setError('Failed to fetch notifications'); + setLoading(false); + } + } + }; + + void fetchNotifications(); + + return () => { + isMounted = false; + }; + }, [uid, refreshKey, withLoading]); + + return { + notifications, + loading, + error, + unreadCount, + markAsRead, + markAllAsRead, + refreshNotifications, + }; +} diff --git a/lib/hooks/useRecipientDetail.ts b/lib/hooks/useRecipientDetail.ts new file mode 100644 index 0000000..801ce86 --- /dev/null +++ b/lib/hooks/useRecipientDetail.ts @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import type { Recipient } from '../services/recipientService'; +import { + useRecipientsStore, + type RecipientDetailCacheEntry, +} from '../stores/recipientsStore'; +import { useGlobalLoading } from './useGlobalLoading'; + +interface UseRecipientDetailReturn { + recipient: Recipient | null; + loading: boolean; + error: string | null; + refreshRecipient: () => Promise; +} + +const defaultEntry: RecipientDetailCacheEntry = { + recipient: null, + loading: false, + error: null, +}; + +export const useRecipientDetail = ( + recipientId: string | null | undefined +): UseRecipientDetailReturn => { + const id = recipientId ?? null; + const lastIdRef = useRef(null); + + const ensureDetail = useRecipientsStore((state) => state.ensureRecipientDetail); + const refreshDetail = useRecipientsStore((state) => state.refreshRecipientDetail); + const removeDetail = useRecipientsStore((state) => state.removeRecipientDetail); + + const entry = useRecipientsStore( + useCallback( + (state) => (id ? state.recipientDetails[id] ?? defaultEntry : defaultEntry), + [id] + ) + ); + const { withLoading } = useGlobalLoading(); + + useEffect(() => { + if (lastIdRef.current && lastIdRef.current !== id) { + removeDetail(lastIdRef.current); + } + lastIdRef.current = id; + }, [id, removeDetail]); + + useEffect(() => { + if (!id) { + return; + } + ensureDetail(id); + }, [ensureDetail, id]); + + const refreshRecipient = useCallback(async () => { + if (!id) { + return; + } + await withLoading(() => refreshDetail(id)); + }, [id, refreshDetail, withLoading]); + + return useMemo( + () => ({ + recipient: id ? entry.recipient : null, + loading: id ? entry.loading : false, + error: id ? entry.error : null, + refreshRecipient, + }), + [entry.error, entry.loading, entry.recipient, id, refreshRecipient] + ); +}; + + diff --git a/lib/hooks/useTickets.ts b/lib/hooks/useTickets.ts new file mode 100644 index 0000000..e438df0 --- /dev/null +++ b/lib/hooks/useTickets.ts @@ -0,0 +1,99 @@ +import { useCallback, useEffect, useState } from "react"; +import { useAuthWithProfile } from "./useAuthWithProfile"; +import { TicketService, type TicketDto } from "../services/ticketService"; + +interface UseTicketsOptions { + status?: string; + limit?: number; + page?: number; + immediate?: boolean; +} + +export function useTickets(options: UseTicketsOptions = {}) { + const { status, limit = 50, page = 1, immediate = true } = options; + + const { user } = useAuthWithProfile(); + const [token, setToken] = useState(null); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let cancelled = false; + + const loadToken = async () => { + if (!user) return; + try { + const idToken = await user.getIdToken(); + if (!cancelled) { + console.log("[useTickets] Firebase ID token:", idToken); + setToken(idToken); + } + } catch (err) { + if (!cancelled) { + setError( + err instanceof Error ? err : new Error("Failed to get auth token") + ); + } + } + }; + + loadToken(); + + return () => { + cancelled = true; + }; + }, [user]); + + const fetchTickets = useCallback( + async (override?: { status?: string; limit?: number; page?: number }) => { + if (!token) return null; + + const finalStatus = override?.status ?? status; + const finalLimit = override?.limit ?? limit; + const finalPage = override?.page ?? page; + + setLoading(true); + setError(null); + + try { + const tickets = await TicketService.getTickets(token, { + status: finalStatus, + limit: finalLimit, + page: finalPage, + }); + + console.log("[useTickets] loaded tickets", { + status: finalStatus, + limit: finalLimit, + page: finalPage, + length: tickets?.length ?? 0, + }); + + setData(tickets); + return tickets; + } catch (err) { + const error = + err instanceof Error ? err : new Error("Failed to fetch tickets"); + setError(error); + return null; + } finally { + setLoading(false); + } + }, + [token, status, limit, page] + ); + + useEffect(() => { + if (token && immediate) { + fetchTickets().catch(() => {}); + } + }, [token, immediate, fetchTickets]); + + return { + data, + error, + loading, + refetch: fetchTickets, + }; +} diff --git a/lib/hooks/useTransactionDetail.ts b/lib/hooks/useTransactionDetail.ts new file mode 100644 index 0000000..1211b87 --- /dev/null +++ b/lib/hooks/useTransactionDetail.ts @@ -0,0 +1,72 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import type { Transaction } from '../services/transactionService'; +import { + useTransactionStore, + type TransactionDetailCacheEntry, +} from '../stores/transactionStore'; +import { useGlobalLoading } from './useGlobalLoading'; + +interface UseTransactionDetailReturn { + transaction: Transaction | null; + loading: boolean; + error: string | null; + refreshTransaction: () => Promise; +} + +const defaultEntry: TransactionDetailCacheEntry = { + transaction: null, + loading: false, + error: null, +}; + +export const useTransactionDetail = ( + transactionId: string | null | undefined +): UseTransactionDetailReturn => { + const id = transactionId ?? null; + const lastIdRef = useRef(null); + + const ensureDetail = useTransactionStore((state) => state.ensureTransactionDetail); + const refreshDetail = useTransactionStore((state) => state.refreshTransactionDetail); + const removeDetail = useTransactionStore((state) => state.removeTransactionDetail); + + const entry = useTransactionStore( + useCallback( + (state) => (id ? state.transactionDetails[id] ?? defaultEntry : defaultEntry), + [id] + ) + ); + const { withLoading } = useGlobalLoading(); + + useEffect(() => { + if (lastIdRef.current && lastIdRef.current !== id) { + removeDetail(lastIdRef.current); + } + lastIdRef.current = id; + }, [id, removeDetail]); + + useEffect(() => { + if (!id) { + return; + } + ensureDetail(id); + }, [ensureDetail, id]); + + const refreshTransaction = useCallback(async () => { + if (!id) { + return; + } + await withLoading(() => refreshDetail(id)); + }, [id, refreshDetail, withLoading]); + + return useMemo( + () => ({ + transaction: id ? entry.transaction : null, + loading: id ? entry.loading : false, + error: id ? entry.error : null, + refreshTransaction, + }), + [entry.error, entry.loading, entry.transaction, id, refreshTransaction] + ); +}; + + diff --git a/lib/hooks/useTransactions.ts b/lib/hooks/useTransactions.ts new file mode 100644 index 0000000..fd7d368 --- /dev/null +++ b/lib/hooks/useTransactions.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { Transaction } from '../services/transactionService'; +import { useTransactionStore } from '../stores/transactionStore'; +import type { TransactionCacheEntry } from '../stores/transactionStore'; +import { useGlobalLoading } from './useGlobalLoading'; + +export interface UseTransactionsReturn { + transactions: Transaction[]; + loading: boolean; + error: string | null; + refreshTransactions: () => Promise; +} + +const defaultEntry: TransactionCacheEntry = { + transactions: [], + loading: false, + error: null, +}; + +export const useTransactions = (uid: string | undefined): UseTransactionsReturn => { + const userId = uid ?? null; + const lastUidRef = useRef(null); + + const ensureSubscription = useTransactionStore((state) => state.ensureSubscription); + const refreshFromStore = useTransactionStore((state) => state.refreshTransactions); + const removeTransactions = useTransactionStore((state) => state.removeTransactions); + const { withLoading } = useGlobalLoading(); + + const entry = useTransactionStore( + useCallback( + (state) => (userId ? state.transactionsByUid[userId] ?? defaultEntry : defaultEntry), + [userId] + ) + ); + + useEffect(() => { + if (lastUidRef.current && lastUidRef.current !== userId) { + removeTransactions(lastUidRef.current); + } + lastUidRef.current = userId; + }, [userId, removeTransactions]); + + useEffect(() => { + if (!userId) { + return; + } + ensureSubscription(userId); + }, [ensureSubscription, userId]); + + const refreshTransactions = useCallback(async () => { + if (!userId) { + return; + } + await withLoading(() => refreshFromStore(userId)); + }, [refreshFromStore, userId, withLoading]); + + return useMemo( + () => ({ + transactions: userId ? entry.transactions : [], + loading: userId ? entry.loading : false, + error: userId ? entry.error : null, + refreshTransactions, + }), + [entry.error, entry.loading, entry.transactions, refreshTransactions, userId] + ); +}; + +export default useTransactions; diff --git a/lib/hooks/useUserProfile.ts b/lib/hooks/useUserProfile.ts new file mode 100644 index 0000000..b517448 --- /dev/null +++ b/lib/hooks/useUserProfile.ts @@ -0,0 +1,91 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { FirebaseAuthTypes } from '../firebase'; +import { UserProfile } from '../services/authServices'; +import { useUserProfileStore } from '../stores/userProfileStore'; +import type { ProfileCacheEntry } from '../stores/userProfileStore'; +import { useGlobalLoading } from './useGlobalLoading'; + +interface UseUserProfileReturn { + profile: UserProfile | null; + loading: boolean; + error: string | null; + refreshProfile: () => Promise; +} + +const defaultEntry: ProfileCacheEntry = { + profile: null, + loading: false, + error: null, +}; + +export const useUserProfile = (user: FirebaseAuthTypes.User | null): UseUserProfileReturn => { + const uid = user?.uid ?? null; + const lastUidRef = useRef(null); + + const ensureSubscription = useUserProfileStore((state) => state.ensureSubscription); + const refreshFromStore = useUserProfileStore((state) => state.refreshProfile); + const removeProfile = useUserProfileStore((state) => state.removeProfile); + + const entry = useUserProfileStore( + useCallback( + (state) => (uid ? state.profiles[uid] ?? defaultEntry : defaultEntry), + [uid] + ) + ); + const { withLoading } = useGlobalLoading(); + + useEffect(() => { + // Clean up subscription from previous user when switching or logging out + if (lastUidRef.current && lastUidRef.current !== uid) { + removeProfile(lastUidRef.current); + } + + lastUidRef.current = uid; + }, [uid, removeProfile]); + + useEffect(() => { + if (!uid) { + return; + } + + ensureSubscription(uid); + }, [uid, ensureSubscription]); + + const refreshProfile = useCallback(async () => { + if (!uid) { + return; + } + await withLoading(() => refreshFromStore(uid)); + }, [refreshFromStore, uid, withLoading]); + + return useMemo( + () => ({ + profile: entry.profile, + loading: uid ? entry.loading : false, + error: uid ? entry.error : null, + refreshProfile, + }), + [entry.error, entry.loading, entry.profile, refreshProfile, uid] + ); +}; + +// Standalone function to fetch any user's profile (useful for admin features) +export const fetchUserProfile = async (uid: string, opts?: { force?: boolean }): Promise => { + if (!uid) { + return null; + } + + const { force = false } = opts ?? {}; + const cached = force ? null : useUserProfileStore.getState().profiles[uid]?.profile; + + if (cached) { + return cached; + } + + try { + return await useUserProfileStore.getState().refreshProfile(uid); + } catch (error) { + console.error('Error fetching user profile:', error); + throw error; + } +}; diff --git a/lib/hooks/useUserWallet.ts b/lib/hooks/useUserWallet.ts new file mode 100644 index 0000000..4e4fb43 --- /dev/null +++ b/lib/hooks/useUserWallet.ts @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { FirebaseAuthTypes } from '../firebase'; +import { UserWallet, WalletService } from '../services/walletService'; +import { useUserWalletStore } from '../stores/userWalletStore'; +import type { WalletCacheEntry } from '../stores/userWalletStore'; +import { useGlobalLoading } from './useGlobalLoading'; + + +interface UseUserWalletReturn { + wallet: UserWallet | null; + loading: boolean; + error: string | null; + refreshWallet: () => Promise; + balanceInDollars: number; // Helper to get balance in dollars + formattedBalance: string; // Helper to get formatted balance + addCreditCard: ( + cardData: { + cardNumber: string; + expiryDate: string; + cvv: string; + } + ) => Promise; + removeCreditCard: (cardId: string) => Promise; +} + +const defaultEntry: WalletCacheEntry = { + wallet: null, + loading: false, + error: null, +}; + +export const useUserWallet = (user: FirebaseAuthTypes.User | null): UseUserWalletReturn => { + const uid = user?.uid ?? null; + const lastUidRef = useRef(null); + + const ensureSubscription = useUserWalletStore((state) => state.ensureSubscription); + const refreshFromStore = useUserWalletStore((state) => state.refreshWallet); + const removeWallet = useUserWalletStore((state) => state.removeWallet); + const setWalletState = useUserWalletStore((state) => state.setWalletState); + + const entry = useUserWalletStore( + useCallback( + (state) => (uid ? state.wallets[uid] ?? defaultEntry : defaultEntry), + [uid] + ) + ); + const { withLoading } = useGlobalLoading(); + + useEffect(() => { + if (lastUidRef.current && lastUidRef.current !== uid) { + removeWallet(lastUidRef.current); + } + lastUidRef.current = uid; + }, [uid, removeWallet]); + + useEffect(() => { + if (!uid) { + return; + } + ensureSubscription(uid); + }, [ensureSubscription, uid]); + + const refreshWallet = useCallback(async () => { + if (!uid) { + return; + } + await withLoading(() => refreshFromStore(uid)); + }, [refreshFromStore, uid, withLoading]); + + const addCreditCard = useCallback(async ( + cardData: { + cardNumber: string; + expiryDate: string; + cvv: string; + } + ) => { + if (!uid) { + return; + } + + setWalletState(uid, { loading: true, error: null }); + + try { + const result = await withLoading(() => WalletService.addCreditCard(uid, cardData)); + if (!result.success) { + setWalletState(uid, { + loading: false, + error: result.error || 'Failed to add credit card', + }); + return; + } + setWalletState(uid, { loading: false, error: null }); + } catch (err) { + setWalletState(uid, { + loading: false, + error: err instanceof Error ? err.message : 'Failed to add credit card', + }); + } + }, [setWalletState, uid, withLoading]); + + const removeCreditCard = useCallback(async (cardId: string) => { + if (!uid) { + return; + } + + setWalletState(uid, { loading: true, error: null }); + + try { + const result = await withLoading(() => WalletService.removeCreditCard(uid, cardId)); + if (!result.success) { + setWalletState(uid, { + loading: false, + error: result.error || 'Failed to remove credit card', + }); + return; + } + setWalletState(uid, { loading: false, error: null }); + } catch (err) { + setWalletState(uid, { + loading: false, + error: err instanceof Error ? err.message : 'Failed to remove credit card', + }); + } + }, [setWalletState, uid, withLoading]); + + return useMemo(() => { + const wallet = uid ? entry.wallet : null; + const balanceInDollars = wallet ? wallet.balance / 100 : 0; + const formattedBalance = wallet + ? `${wallet.currency} ${(wallet.balance / 100).toFixed(2)}` + : `USD 0.00`; + + return { + wallet, + loading: uid ? entry.loading : false, + error: uid ? entry.error : null, + refreshWallet, + balanceInDollars, + formattedBalance, + addCreditCard, + removeCreditCard, + }; + }, [addCreditCard, entry.error, entry.loading, entry.wallet, refreshWallet, removeCreditCard, uid]); +}; + +// Standalone function to fetch any user's wallet (useful for admin features) +export const fetchUserWallet = async (uid: string): Promise => { + if (!uid) { + return null; + } + + const cached = useUserWalletStore.getState().wallets[uid]?.wallet; + if (cached) { + return cached; + } + + try { + return await useUserWalletStore.getState().refreshWallet(uid); + } catch (error) { + console.error('Error fetching user wallet:', error); + const res = await WalletService.getUserWallet(uid); + return res.wallet ?? null; + } +}; diff --git a/lib/i18n.ts b/lib/i18n.ts new file mode 100644 index 0000000..3858132 --- /dev/null +++ b/lib/i18n.ts @@ -0,0 +1,33 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import { useLangStore } from "./stores/langStore"; +import en from "../locales/en/common.json"; +import am from "../locales/am/common.json"; +import fr from "../locales/fr/common.json"; +import ti from "../locales/ti/common.json"; +import om from "../locales/om/common.json"; + +const resources = { + en: { translation: en }, + am: { translation: am }, + fr: { translation: fr }, + ti: { translation: ti }, + om: { translation: om }, +}; + +const initialLanguage = useLangStore.getState().language; + +i18n.use(initReactI18next).init({ + resources, + lng: initialLanguage, + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, +}); + +useLangStore.subscribe((state) => { + i18n.changeLanguage(state.language); +}); + +export default i18n; diff --git a/lib/navigation/routerLoader.ts b/lib/navigation/routerLoader.ts new file mode 100644 index 0000000..75496de --- /dev/null +++ b/lib/navigation/routerLoader.ts @@ -0,0 +1,93 @@ +import { InteractionManager } from 'react-native'; +import { router } from 'expo-router'; +import { showGlobalLoader, hideGlobalLoader } from '../stores/uiStore'; + +let loaderEnhancementEnabled = false; + +// Performance monitoring (only in dev) +const isDev = process.env.NODE_ENV !== 'production'; +const perfLogs: Record = {}; + +const logPerformance = (methodName: string, duration: number) => { + if (!isDev) return; + + if (!perfLogs[methodName]) { + perfLogs[methodName] = []; + } + perfLogs[methodName].push(duration); + + // Log average every 5 calls + if (perfLogs[methodName].length % 5 === 0) { + const avg = perfLogs[methodName].reduce((a, b) => a + b, 0) / perfLogs[methodName].length; + console.log(`[nav-perf] ${methodName}: avg ${avg.toFixed(2)}ms (${perfLogs[methodName].length} calls)`); + } +}; + +const scheduleHide = () => { + // Use requestIdleCallback if available, otherwise InteractionManager + if (typeof requestIdleCallback !== 'undefined') { + requestIdleCallback(() => { + hideGlobalLoader(); + }, { timeout: 100 }); + } else { + InteractionManager.runAfterInteractions(() => { + hideGlobalLoader(); + }); + } +}; + +const wrapNavigationMethod = any>( + methodName: string, + originalMethod: T +): T => { + const wrapped = ((...args: Parameters) => { + const startTime = isDev && typeof performance !== 'undefined' ? performance.now() : 0; + console.log(`[router-loader] ${methodName} invoked`); + + // Show loader after a small delay to avoid flicker on fast navigations + const loaderTimeout = setTimeout(() => { + showGlobalLoader(); + }, 100); + + try { + const result = originalMethod(...args); + + // Clear loader timeout and schedule hide + clearTimeout(loaderTimeout); + scheduleHide(); + + if (isDev && typeof performance !== 'undefined') { + const duration = performance.now() - startTime; + logPerformance(methodName, duration); + if (duration > 300) { + console.warn(`[nav-perf] Slow navigation: ${methodName} took ${duration.toFixed(2)}ms`); + } + } + + return result; + } catch (error) { + clearTimeout(loaderTimeout); + hideGlobalLoader(); + throw error; + } + }) as T; + + return wrapped; +}; + +export const enableRouterLoader = () => { + if (loaderEnhancementEnabled) { + return; + } + loaderEnhancementEnabled = true; + + const originalPush = router.push.bind(router); + const originalReplace = router.replace.bind(router); + const originalNavigate = router.navigate.bind(router); + const originalBack = router.back.bind(router); + + (router as any).push = wrapNavigationMethod('push', originalPush); + (router as any).replace = wrapNavigationMethod('replace', originalReplace); + (router as any).navigate = wrapNavigationMethod('navigate', originalNavigate); + (router as any).back = wrapNavigationMethod('back', originalBack); +}; diff --git a/lib/routes.ts b/lib/routes.ts new file mode 100644 index 0000000..f381991 --- /dev/null +++ b/lib/routes.ts @@ -0,0 +1,139 @@ +/** + * Centralized route configuration for the Amba Pay app + * This file contains all route paths as constants to avoid hardcoded strings + * and make route management easier across the application. + */ + +export const ROUTES = { + // Main app routes + HOME: "/", + SIGNIN: "/auth/signin", + PHONE_SETUP: "/auth/phone-setup", + GOOGLE_SETUP: "/auth/google-setup", + OTP: "/auth/otp", + AGENT_SIGNUP: "/auth/agent-signup", + AGENT_SIGNIN: "/auth/agent-signin", + + // Profile and account routes + PROFILE: "/profile", + EDIT_PROFILE: "/editprofile", + CHANGE_PIN: "/changepin", + HELP_SUPPORT: "/helpsupport", + TERMS: "/terms", + KYC: "/kyc", + FORGOT_PASSWORD: "/auth/forgot", + NOTIFICATION: "/notification", + NOTIFICATION_OPTION: "/notificationOption", + + // Transaction routes + SEND_OR_REQUEST_MONEY: "/sendorrequestmoney", + CASH_OUT: "/cashout", + SEND_BANK: "/sendbank", + ADD_CARD: "/addcard", + ADD_CASH: "/addcash", + CHECKOUT: "/checkout", + DONATION: "/donation", + SELECT_ACCOUNT: "/selectacc", + HISTORY: "/history", + TRANSACTION_DETAIL: "/transdetail", + TRANSACTION_CONFIRM: "/transconfirm", + CARD_ADDED: "/cardaddedcomp", + + // Recipient routes + ADD_RECIPIENT: "/addrecipient", + LIST_RECIPIENTS: "/listrecipient", + SELECT_RECIPIENT: "/selectrecip", + RECIPIENT_DETAIL: "/recipdetail", + SELECT_DONOR: "/selectdonor", + RECIPIENT_ADDED: "/recipaddedcomp", + + // Other features + CROWDFUNDING: "/crowdfunding", + TASK_COMPLETION: "/taskcomp", + CASHOUT_COMPLETION: "/cashoutcomp", + ADDCASH_COMPLETION: "/addcashcomp", + MONEY_REQUESTED: "/moneyrequested", + MONEY_DONATED: "/moneydonated", + REQUEST_PROVIDER: "/requestprovider", + SCHEDULES: "/schedules", + SCHEDULES_ALL: "/schedulesall", + SEND_NOTIFICATION: "/sendnotification", + + EVENTS: "/events", + + EVENT_DETAIL: "/eventdetail", + + MY_TICKETS: "/mytickets", + + EVENT_QR: "/eventqrscreen", + + // QR + QR_SCREEN: "/qrscreen", + SCAN_PROFILE_QR: "/scanprofileqr", + + POINTS: "/points", + POINTS_ACTIVITY: "/pointsactivity", + + LIST_CARD: "/cardmang", +} as const; + +// Type for route values +export type RouteValue = (typeof ROUTES)[keyof typeof ROUTES]; + +// Helper function to check if a route is valid +export const isValidRoute = (route: string): route is RouteValue => { + return Object.values(ROUTES).includes(route as RouteValue); +}; + +// Export individual routes for easier imports +export const { + HOME, + SIGNIN, + PHONE_SETUP, + GOOGLE_SETUP, + OTP, + PROFILE, + EDIT_PROFILE, + CHANGE_PIN, + HELP_SUPPORT, + TERMS, + KYC, + FORGOT_PASSWORD, + NOTIFICATION, + NOTIFICATION_OPTION, + SEND_OR_REQUEST_MONEY, + CASH_OUT, + SEND_BANK, + ADD_CARD, + ADD_CASH, + CHECKOUT, + HISTORY, + TRANSACTION_DETAIL, + TRANSACTION_CONFIRM, + CARD_ADDED, + DONATION, + ADD_RECIPIENT, + LIST_RECIPIENTS, + SELECT_RECIPIENT, + RECIPIENT_DETAIL, + SELECT_DONOR, + RECIPIENT_ADDED, + CROWDFUNDING, + TASK_COMPLETION, + CASHOUT_COMPLETION, + ADDCASH_COMPLETION, + MONEY_REQUESTED, + MONEY_DONATED, + REQUEST_PROVIDER, + SCHEDULES, + SCHEDULES_ALL, + SEND_NOTIFICATION, + EVENTS, + EVENT_DETAIL, + MY_TICKETS, + EVENT_QR, + POINTS, + POINTS_ACTIVITY, + QR_SCREEN, + SCAN_PROFILE_QR, +} = ROUTES; diff --git a/lib/services/authServices.ts b/lib/services/authServices.ts new file mode 100644 index 0000000..f8fba5a --- /dev/null +++ b/lib/services/authServices.ts @@ -0,0 +1,457 @@ +/** + * Auth Services - Platform-aware authentication + * Works on both native (Android/iOS) and web platforms + */ +import { Platform } from "react-native"; +import { + signInWithGoogle as firebaseSignInWithGoogle, + signOutFromGoogle as firebaseSignOutFromGoogle, + signInWithPhoneNumber as firebaseSignInWithPhoneNumber, + createUserWithEmailAndPassword as firebaseCreateUserWithEmailAndPassword, + signInWithEmailAndPassword as firebaseSignInWithEmailAndPassword, + collection, + doc, + FieldValue, + isWeb, +} from "../firebase"; +import { formatPhoneNumber } from "../utils/phoneUtils"; +import { useUserProfileStore } from "../stores/userProfileStore"; +import { withGlobalLoading } from "../stores/uiStore"; + +// Types +interface PhoneAuthResult { + confirmationResult?: any; + error?: string; +} + +interface OTPVerificationResult { + user: any | null; + error?: string; +} + +interface GoogleSignInResult { + user: any | null; + isNewUser: boolean; + error?: string; +} + +export interface UserLinkedAccount { + id: string; + bankId: string; + bankName: string; + accountNumber: string; + isDefault: boolean; +} + +export interface UserProfile { + uid: string; + email: string; + fullName: string; + phoneNumber?: string; + address?: string; + pin?: string; + signupType?: "phone" | "google"; + photoUrl?: string; + fanNumber?: string; + tin?: string; + businessType?: string; + nationalIdUrl?: string; + businessLicenseUrl?: string; + linkedAccounts?: UserLinkedAccount[]; + createdAt: any; + updatedAt: any; +} + +export class AuthService { + // Google Sign-In - Platform aware + static async signInWithGoogle(): Promise { + try { + const result = await firebaseSignInWithGoogle(); + + if (result.error) { + return { user: null, isNewUser: false, error: result.error }; + } + + console.log( + "Google Sign-In successful, user:", + result.user?.uid, + "isNewUser:", + result.isNewUser + ); + + // Log ID token if available (for debugging / backend integration) + try { + if ( + result.user && + typeof (result.user as any).getIdToken === "function" + ) { + const idToken = await (result.user as any).getIdToken(); + console.log("GOOGLE SIGN-IN ID TOKEN:", idToken); + } + } catch (tokenError) { + console.warn("Failed to get Google ID token:", tokenError); + } + return result; + } catch (error: any) { + console.error("Google Sign-In error:", error); + return { + user: null, + isNewUser: false, + error: error.message || "Google Sign-In failed", + }; + } + } + + // Sign out from Google + static async signOutFromGoogle(): Promise { + try { + await firebaseSignOutFromGoogle(); + } catch (error) { + console.error("Google Sign-Out error:", error); + } + } + + // Phone number authentication - send OTP + static async sendOTP(phoneNumber: string): Promise { + try { + // Validate and format phone number + const formattedPhoneNumber = formatPhoneNumber(phoneNumber); + console.log("FORMATTED PHONE NUMBER:", formattedPhoneNumber); + + if (!formattedPhoneNumber) { + return { + error: "Invalid phone number format. Please use format: +1234567890", + }; + } + + console.log("Sending OTP to:", formattedPhoneNumber); + + // Web requires reCAPTCHA initialization first + if (isWeb) { + // Check if reCAPTCHA container exists + const recaptchaContainer = document.getElementById( + "recaptcha-container" + ); + if (!recaptchaContainer) { + console.warn( + "reCAPTCHA container not found. Phone auth may not work." + ); + return { + error: + "Phone authentication not available. Please use Google Sign-In.", + }; + } + + // Initialize reCAPTCHA if needed + const { initRecaptcha } = await import("../firebase/firebase.web"); + await initRecaptcha("recaptcha-container"); + } + + const confirmationResult = await firebaseSignInWithPhoneNumber( + formattedPhoneNumber + ); + return { confirmationResult }; + } catch (error) { + console.error("Phone auth error:", error); + return { + error: error instanceof Error ? error.message : "Failed to send OTP", + }; + } + } + + // Verify OTP code + static async verifyOTP( + confirmationResult: any, + code: string + ): Promise { + try { + const result = await confirmationResult.confirm(code); + const user = result?.user ?? null; + if (user) { + console.log("PHONE AUTH SUCCESS, UID:", user.uid); + try { + if (typeof (user as any).getIdToken === "function") { + const idToken = await (user as any).getIdToken(); + console.log("PHONE AUTH ID TOKEN:", idToken); + } + } catch (tokenError) { + console.warn("Failed to get phone auth ID token:", tokenError); + } + } + return { user }; + } catch (error) { + console.error("OTP verification error:", error); + return { + user: null, + error: + error instanceof Error ? error.message : "Invalid verification code", + }; + } + } + + // Update User Profile + static async updateUserProfile( + uid: string, + profileData: { + fullName?: string; + phoneNumber?: string; + address?: string; + pin?: string; + photoUrl?: string; + fanNumber?: string; + tin?: string; + businessType?: string; + nationalIdUrl?: string; + businessLicenseUrl?: string; + linkedAccounts?: UserLinkedAccount[]; + } + ): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + const userDocRef = doc("users", uid); + + const updateData: any = { + ...profileData, + updatedAt: FieldValue.serverTimestamp(), + }; + + // Remove undefined values + Object.keys(updateData).forEach((key) => { + if (updateData[key] === undefined) { + delete updateData[key]; + } + }); + + console.log("[AuthService.updateUserProfile] Updating user profile", { + uid, + updateData, + }); + + await userDocRef.update(updateData); + console.log( + "[AuthService.updateUserProfile] Profile updated successfully" + ); + + try { + await useUserProfileStore.getState().invalidateProfile(uid); + } catch (cacheError) { + console.warn( + "Failed to refresh user profile cache after update", + cacheError + ); + } + + return { success: true }; + } catch (error) { + console.error( + "[AuthService.updateUserProfile] Profile update error for uid", + uid, + error + ); + return { + success: false, + error: + error instanceof Error ? error.message : "Failed to update profile", + }; + } + }); + } + + // Check if user profile exists in Firestore + static async checkUserProfileExists(uid: string): Promise { + try { + const userDocRef = doc("users", uid); + const userDoc = await userDocRef.get(); + // Handle both boolean and function forms of exists + const existsValue = userDoc.exists; + return typeof existsValue === "function" ? existsValue() : !!existsValue; + } catch (error) { + console.error("Error checking user profile:", error); + return false; + } + } + + // Create user profile in Firestore + static async createUserProfile( + uid: string, + profileData: { + fullName: string; + phoneNumber?: string; + address?: string; + email?: string; + pin?: string; + signupType?: "phone" | "google" | "email"; + createdAt: Date; + updatedAt: Date; + photoUrl?: string; + } + ): Promise { + return withGlobalLoading(async () => { + try { + console.log("Creating user profile in Firestore for UID:", uid); + const userProfile: UserProfile = { + uid: uid, + email: profileData.email || "", + fullName: profileData.fullName, + phoneNumber: profileData.phoneNumber || "", + address: profileData.address || "", + pin: profileData.pin || "", + signupType: profileData.signupType || "phone", + photoUrl: profileData.photoUrl || "", + createdAt: FieldValue.serverTimestamp(), + updatedAt: FieldValue.serverTimestamp(), + }; + + const userRef = doc("users", uid); + await userRef.set(userProfile); + console.log("User profile created successfully in Firestore"); + + try { + await useUserProfileStore.getState().invalidateProfile(uid); + } catch (cacheError) { + console.warn( + "Failed to refresh user profile cache after creation", + cacheError + ); + } + } catch (error) { + console.error("Error creating user profile:", error); + throw error; + } + }); + } + + // Agent Sign Up with Email/Password + static async signUpAgent( + email: string, + password: string + ): Promise<{ user: any | null; error?: string }> { + try { + const result = await firebaseCreateUserWithEmailAndPassword( + email, + password + ); + + if (result.error) { + return { user: null, error: result.error }; + } + + if (!result.user) { + return { user: null, error: "Failed to create user account" }; + } + + // Check if agent exists in agents collection + const agentDocRef = doc("agents", result.user.uid); + const agentDoc = await agentDocRef.get(); + const agentExists = agentDoc.exists; + + if (!agentExists) { + // If agent doesn't exist, create agent document + await agentDocRef.set({ + uid: result.user.uid, + email: email, + createdAt: FieldValue.serverTimestamp(), + updatedAt: FieldValue.serverTimestamp(), + }); + } + + console.log("Agent signup successful, UID:", result.user.uid); + return { user: result.user }; + } catch (error: any) { + console.error("Agent signup error:", error); + return { + user: null, + error: error.message || "Failed to sign up agent", + }; + } + } + + // Agent Sign In with Email/Password + static async signInAgent( + email: string, + password: string + ): Promise<{ user: any | null; error?: string }> { + try { + console.log("Agent signin attempt for email:", email); + const result = await firebaseSignInWithEmailAndPassword(email.trim(), password); + + if (result.error) { + console.error("Firebase signin error:", result.error); + // Map common Firebase errors to user-friendly messages + let errorMessage = result.error; + if (result.error.includes("auth/wrong-password") || result.error.includes("auth/invalid-credential")) { + errorMessage = "Invalid email or password"; + } else if (result.error.includes("auth/user-not-found")) { + errorMessage = "No account found with this email"; + } else if (result.error.includes("auth/invalid-email")) { + errorMessage = "Invalid email address"; + } else if (result.error.includes("auth/too-many-requests")) { + errorMessage = "Too many failed attempts. Please try again later"; + } + return { user: null, error: errorMessage }; + } + + if (!result.user) { + console.error("No user returned from Firebase signin"); + return { user: null, error: "Failed to sign in" }; + } + + console.log("Firebase signin successful, UID:", result.user.uid); + + // Check if agent exists in agents collection + const agentDocRef = doc("agents", result.user.uid); + const agentDoc = await agentDocRef.get(); + const agentExists = typeof agentDoc.exists === "function" ? agentDoc.exists() : !!agentDoc.exists; + + if (!agentExists) { + console.log("Agent document not found, creating it automatically"); + // Create agent document if it doesn't exist + try { + await agentDocRef.set({ + uid: result.user.uid, + email: email.trim(), + createdAt: FieldValue.serverTimestamp(), + updatedAt: FieldValue.serverTimestamp(), + }); + console.log("Agent document created successfully"); + } catch (createError) { + console.error("Error creating agent document:", createError); + // Still allow signin even if document creation fails + } + } else { + console.log("Agent document exists"); + } + + console.log("Agent signin successful, UID:", result.user.uid); + return { user: result.user }; + } catch (error: any) { + console.error("Agent signin error:", error); + let errorMessage = error.message || "Failed to sign in agent"; + // Map common Firebase errors + if (errorMessage.includes("auth/wrong-password") || errorMessage.includes("auth/invalid-credential")) { + errorMessage = "Invalid email or password"; + } else if (errorMessage.includes("auth/user-not-found")) { + errorMessage = "No account found with this email"; + } + return { + user: null, + error: errorMessage, + }; + } + } + + // Check if agent exists in agents collection + static async checkAgentExists(uid: string): Promise { + try { + const agentDocRef = doc("agents", uid); + const agentDoc = await agentDocRef.get(); + const existsValue = agentDoc.exists; + return typeof existsValue === "function" ? existsValue() : !!existsValue; + } catch (error) { + console.error("Error checking agent:", error); + return false; + } + } +} + +export default AuthService; diff --git a/lib/services/eventService.ts b/lib/services/eventService.ts new file mode 100644 index 0000000..4997050 --- /dev/null +++ b/lib/services/eventService.ts @@ -0,0 +1,75 @@ +import { callEndpoint } from "../api/client"; +import { eventEndpoints } from "../api/eventEndpoints"; + +export interface EventDto { + id: string; + name: string; + startDate: string; + endDate: string; + venue: string; + status: string; + ticketCutOff: string; + images: string[]; + [key: string]: any; +} + +interface EventsApiResponse { + totalItems: number; + totalPages: number; + currentPage: number; + data: EventDto[]; + message: string; +} + +interface EventDetailApiResponse { + message: string; + data: EventDto; +} + +export class EventService { + static async getEvents( + token: string, + params?: { status?: string; limit?: number } + ): Promise { + const { status = "ACTIVE", limit = 50 } = params || {}; + + const res = await callEndpoint( + eventEndpoints.getEvents, + { + query: { status, limit }, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + console.log("[EventService] events response", { + totalItems: res.totalItems, + currentPage: res.currentPage, + dataLength: Array.isArray(res.data) ? res.data.length : null, + }); + + return res.data ?? []; + } + + static async getEventById( + token: string, + id: string + ): Promise { + if (!id) return null; + + const endpoint = { + base: eventEndpoints.getEventById.base, + path: `${eventEndpoints.getEventById.path}/${id}`, + method: eventEndpoints.getEventById.method, + } as const; + + const res = await callEndpoint(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return res.data ?? null; + } +} diff --git a/lib/services/fcmService.ts b/lib/services/fcmService.ts new file mode 100644 index 0000000..1530c48 --- /dev/null +++ b/lib/services/fcmService.ts @@ -0,0 +1,348 @@ +/** + * FCM Service - Platform-aware push notification handling + * + * Native (Android/iOS): Uses @react-native-firebase/messaging + * Web: Uses Firebase JS SDK with service workers (limited support) + */ +import { Platform } from 'react-native'; +import { + doc, + FieldValue, + isWeb, +} from '../firebase'; + +// Only import native messaging on native platforms +let messaging: any = null; +if (Platform.OS !== 'web') { + messaging = require('@react-native-firebase/messaging').default; +} + +export class FCMService { + private static tokenRefreshUnsubscribe: (() => void) | null = null; + + /** + * Check if FCM is supported on current platform + */ + static isSupported(): boolean { + // FCM is fully supported on Android + // Limited support on web (requires service worker setup) + // iOS uses APNs through FCM + return Platform.OS === 'android' || Platform.OS === 'ios'; + } + + /** + * Request notification permissions + */ + static async requestPermission(): Promise { + // Web handling + if (Platform.OS === 'web') { + if (typeof window === 'undefined' || !('Notification' in window)) { + console.log('Notifications not supported in this browser'); + return false; + } + + try { + const permission = await Notification.requestPermission(); + return permission === 'granted'; + } catch (error) { + console.error('Error requesting notification permission:', error); + return false; + } + } + + // Native handling (Android/iOS) + if (!messaging) { + return false; + } + + try { + const authStatus = await messaging().requestPermission(); + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + if (!enabled) { + console.warn('User notification permission denied'); + } + + return enabled; + } catch (error) { + console.error('Error requesting notification permission:', error); + return false; + } + } + + /** + * Get the FCM token + */ + static async getToken(): Promise { + // Web handling + if (Platform.OS === 'web') { + // Web FCM requires service worker setup + // For now, return null and log a message + console.log('Web push notifications require additional setup (service worker, VAPID key)'); + return null; + } + + // Native handling + if (!messaging) { + return null; + } + + try { + const token = await messaging().getToken(); + console.log('FCM Token retrieved:', token); + return token; + } catch (error) { + console.error('Error getting FCM token:', error); + return null; + } + } + + /** + * Save FCM token to Firestore user document during user creation + */ + static async saveTokenToFirestoreOnCreate(uid: string, token: string): Promise<{ success: boolean; error?: string }> { + try { + const userDocRef = doc('users', uid); + await userDocRef.update({ + fcmToken: token, + fcmTokenUpdatedAt: FieldValue.serverTimestamp(), + }); + + console.log('FCM token saved to Firestore during user creation for user:', uid); + return { success: true }; + } catch (error) { + console.error('Error saving FCM token to Firestore during creation:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to save FCM token', + }; + } + } + + /** + * Update FCM token in Firestore user document + */ + static async updateTokenInFirestore(uid: string, token: string): Promise<{ success: boolean; error?: string }> { + try { + const userDocRef = doc('users', uid); + const userDoc = await userDocRef.get(); + + if (!userDoc.exists) { + console.log('User document does not exist yet, skipping token update for:', uid); + return { success: true }; + } + + await userDocRef.update({ + fcmToken: token, + fcmTokenUpdatedAt: FieldValue.serverTimestamp(), + }); + + console.log('FCM token updated in Firestore for user:', uid); + return { success: true }; + } catch (error: any) { + console.error('Error updating FCM token in Firestore:', error); + + if (error?.code === 'firestore/not-found') { + console.log('User document not found, skipping token update'); + return { success: true }; + } + + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update FCM token', + }; + } + } + + /** + * Remove FCM token from Firestore (on logout) + */ + static async removeTokenFromFirestore(uid: string): Promise<{ success: boolean; error?: string }> { + try { + const userDocRef = doc('users', uid); + await userDocRef.update({ + fcmToken: FieldValue.delete(), + fcmTokenUpdatedAt: FieldValue.serverTimestamp(), + }); + + console.log('FCM token removed from Firestore for user:', uid); + return { success: true }; + } catch (error) { + console.error('Error removing FCM token from Firestore:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to remove FCM token', + }; + } + } + + /** + * Initialize FCM token for a new user (during user creation) + */ + static async initializeTokenForNewUser(uid: string): Promise<{ success: boolean; error?: string; token?: string }> { + // Skip on web + if (Platform.OS === 'web') { + console.log('Skipping FCM token initialization on web'); + return { success: true }; + } + + try { + const hasPermission = await this.requestPermission(); + if (!hasPermission) { + console.warn('Notification permission not granted, continuing anyway...'); + } + + const token = await this.getToken(); + if (!token) { + return { success: false, error: 'Failed to retrieve FCM token' }; + } + + const saveResult = await this.saveTokenToFirestoreOnCreate(uid, token); + if (!saveResult.success) { + return saveResult; + } + + this.setupTokenRefreshListener(uid); + + return { success: true, token }; + } catch (error) { + console.error('Error initializing FCM token for new user:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to initialize FCM token', + }; + } + } + + /** + * Update FCM token for an existing user + */ + static async updateTokenForExistingUser(uid: string): Promise<{ success: boolean; error?: string }> { + // Skip on web + if (Platform.OS === 'web') { + return { success: true }; + } + + try { + const hasPermission = await this.requestPermission(); + if (!hasPermission) { + console.warn('Notification permission not granted, continuing anyway...'); + } + + const token = await this.getToken(); + if (!token) { + return { success: false, error: 'Failed to retrieve FCM token' }; + } + + const updateResult = await this.updateTokenInFirestore(uid, token); + if (!updateResult.success) { + return updateResult; + } + + this.setupTokenRefreshListener(uid); + + return { success: true }; + } catch (error) { + console.error('Error updating FCM token:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update FCM token', + }; + } + } + + /** + * Set up a listener for FCM token refresh (native only) + */ + static setupTokenRefreshListener(uid: string): void { + if (Platform.OS === 'web' || !messaging) { + return; + } + + if (this.tokenRefreshUnsubscribe) { + this.tokenRefreshUnsubscribe(); + this.tokenRefreshUnsubscribe = null; + } + + this.tokenRefreshUnsubscribe = messaging().onTokenRefresh(async (token: string) => { + console.log('FCM token refreshed:', token); + await this.updateTokenInFirestore(uid, token); + }); + } + + /** + * Clean up token refresh listener + */ + static cleanup(): void { + if (this.tokenRefreshUnsubscribe) { + this.tokenRefreshUnsubscribe(); + this.tokenRefreshUnsubscribe = null; + } + } + + /** + * Set up foreground message handler (native only) + */ + static setupForegroundMessageHandler(): () => void { + if (Platform.OS === 'web' || !messaging) { + return () => { }; + } + + return messaging().onMessage(async (remoteMessage: any) => { + console.log('Foreground FCM message received:', remoteMessage); + + if (remoteMessage.notification) { + console.log('Notification:', remoteMessage.notification); + } + }); + } + + /** + * Set up background message handler (native only) + */ + static setupBackgroundMessageHandler(): void { + if (Platform.OS === 'web' || !messaging) { + return; + } + + messaging().setBackgroundMessageHandler(async (remoteMessage: any) => { + console.log('Background FCM message received:', remoteMessage); + }); + } + + /** + * Get the initial notification when app is opened from a notification + */ + static async getInitialNotification(): Promise { + if (Platform.OS === 'web' || !messaging) { + return null; + } + + try { + const remoteMessage = await messaging().getInitialNotification(); + if (remoteMessage) { + console.log('App opened from notification:', remoteMessage); + } + return remoteMessage; + } catch (error) { + console.error('Error getting initial notification:', error); + return null; + } + } + + /** + * Set up notification opened handler + */ + static setupNotificationOpenedHandler(callback: (remoteMessage: any) => void): () => void { + if (Platform.OS === 'web' || !messaging) { + return () => { }; + } + + return messaging().onNotificationOpenedApp((remoteMessage: any) => { + console.log('Notification opened app:', remoteMessage); + callback(remoteMessage); + }); + } +} diff --git a/lib/services/kycDocumentService.ts b/lib/services/kycDocumentService.ts new file mode 100644 index 0000000..4aa5634 --- /dev/null +++ b/lib/services/kycDocumentService.ts @@ -0,0 +1,88 @@ +import { isNative } from "../firebase"; + +export type KycDocumentType = "personal-id" | "business-license"; + +export async function uploadKycDocument( + uid: string, + localUri: string, + docType: KycDocumentType +): Promise { + if (!uid || !localUri) { + throw new Error("Missing uid or image uri"); + } + + const fileName = `${docType}_${uid}_${Date.now()}.jpg`; + + if (isNative) { + let storageModule: any; + try { + console.log("[kycDocumentService] Using native Firebase Storage upload", { + uid, + localUri, + docType, + }); + storageModule = require("@react-native-firebase/storage").default; + } catch (e) { + console.error( + "[kycDocumentService] @react-native-firebase/storage not available", + e + ); + throw new Error("Storage module not installed"); + } + + const path = `kyc-documents/${uid}/${fileName}`; + console.log("[kycDocumentService] Upload path (native):", path); + const ref = storageModule().ref(path); + + try { + console.log( + "[kycDocumentService] Calling putFile with localUri", + localUri + ); + await ref.putFile(localUri); + } catch (error) { + console.error("[kycDocumentService] Error during putFile", error); + throw error; + } + + try { + console.log("[kycDocumentService] Fetching download URL for path", path); + const downloadUrl = await ref.getDownloadURL(); + console.log("[kycDocumentService] getDownloadURL success", downloadUrl); + return downloadUrl; + } catch (error) { + console.error("[kycDocumentService] Error during getDownloadURL", error); + throw error; + } + } + + console.log("[kycDocumentService] Using web Firebase Storage upload", { + uid, + localUri, + docType, + }); + const { getStorage, ref, uploadBytes, getDownloadURL } = await import( + "firebase/storage" + ); + + const storage = getStorage(); + const response = await fetch(localUri); + const blob = await response.blob(); + const storageRefPath = `kyc-documents/${uid}/${fileName}`; + console.log("[kycDocumentService] Upload path (web):", storageRefPath); + const storageRef = ref(storage, storageRefPath); + try { + await uploadBytes(storageRef, blob); + } catch (error) { + console.error("[kycDocumentService] Error during uploadBytes", error); + throw error; + } + try { + const downloadUrl = await getDownloadURL(storageRef); + console.log("[kycDocumentService] Web getDownloadURL success", downloadUrl); + return downloadUrl; + } catch (error) { + console.error("[kycDocumentService] Error during getDownloadURL", error); + throw error; + } +} diff --git a/lib/services/notificationService.ts b/lib/services/notificationService.ts new file mode 100644 index 0000000..2965811 --- /dev/null +++ b/lib/services/notificationService.ts @@ -0,0 +1,151 @@ +/** + * Notification Service - Platform-aware notification management + * Uses Firebase abstraction layer for cross-platform support + */ +import { doc, FieldValue } from '../firebase'; +import { WalletService } from './walletService'; +import { TransactionService } from './transactionService'; +import { RequestService } from './requestService'; + +export class NotificationService { + + /** + * Mark notification as read + */ + static async markAsRead(notificationId: string): Promise<{ success: boolean; error?: string }> { + try { + const notificationRef = doc('notifications', notificationId); + await notificationRef.update({ + read: true, + updatedAt: FieldValue.serverTimestamp(), + }); + + return { success: true }; + } catch (error) { + console.error('Error marking notification as read:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to mark notification as read', + }; + } + } + + /** + * Mark all notifications as read for a user + */ + static async markAllAsRead(uid: string): Promise<{ success: boolean; error?: string }> { + try { + // This would require a batch update in a real implementation + return { success: true }; + } catch (error) { + console.error('Error marking all notifications as read:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to mark all notifications as read', + }; + } + } + + /** + * Delete a notification + */ + static async deleteNotification(notificationId: string): Promise<{ success: boolean; error?: string }> { + try { + const notificationRef = doc('notifications', notificationId); + await notificationRef.delete(); + return { success: true }; + } catch (error) { + console.error('Error deleting notification:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete notification', + }; + } + } + + /** + * Accept or decline a money request and create appropriate notifications + */ + static async acceptDeclineMoneyRequest( + requestId: string, + requesteeUid: string, + requestorUid: string, + requestorName: string, + requesteeName: string, + amount: number, + action: 'accept' | 'decline' + ): Promise<{ success: boolean; error?: string }> { + try { + // If accepting, handle wallet transactions + if (action === 'accept') { + // Check if requestee has sufficient balance + const requesteeWalletResult = await WalletService.getUserWallet(requesteeUid); + if (!requesteeWalletResult.success || !requesteeWalletResult.wallet) { + return { success: false, error: 'Requestee wallet not found' }; + } + + if (requesteeWalletResult.wallet.balance < amount) { + return { success: false, error: 'Insufficient balance to fulfill request' }; + } + + // Deduct amount from requestee's wallet + const newRequesteeBalance = requesteeWalletResult.wallet.balance - amount; + const deductResult = await WalletService.updateWalletBalance(requesteeUid, newRequesteeBalance); + if (!deductResult.success) { + return { success: false, error: 'Failed to deduct amount from requestee wallet' }; + } + + // Add amount to requestor's wallet + const requestorWalletResult = await WalletService.getUserWallet(requestorUid); + if (!requestorWalletResult.success || !requestorWalletResult.wallet) { + await WalletService.createUserWallet(requestorUid); + const newRequestorBalance = amount; + await WalletService.updateWalletBalance(requestorUid, newRequestorBalance); + } else { + const newRequestorBalance = requestorWalletResult.wallet.balance + amount; + const addResult = await WalletService.updateWalletBalance(requestorUid, newRequestorBalance); + if (!addResult.success) { + return { success: false, error: 'Failed to add amount to requestor wallet' }; + } + } + + // Create transactions + await TransactionService.sendMoney(requesteeUid, { + amount, + recipientName: requestorName, + recipientPhoneNumber: '', + recipientType: 'contact', + recipientId: requestorUid, + note: `Money request fulfilled for ${requestorName}`, + }); + + await TransactionService.sendMoney(requestorUid, { + amount, + recipientName: requesteeName, + recipientPhoneNumber: '', + recipientType: 'contact', + recipientId: requesteeUid, + note: `Money request received from ${requesteeName}`, + }); + } + + // Update the request status + const statusUpdateResult = await RequestService.updateRequestStatus( + requestId, + action === 'accept' ? 'accepted' : 'declined' + ); + + if (!statusUpdateResult.success) { + return { success: false, error: 'Failed to update request status' }; + } + + return { success: true }; + } catch (error) { + console.error('Error handling money request response:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to handle request response', + }; + } + } +} diff --git a/lib/services/pointsService.ts b/lib/services/pointsService.ts new file mode 100644 index 0000000..81d8d5c --- /dev/null +++ b/lib/services/pointsService.ts @@ -0,0 +1,173 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export type PointsActivityType = + | "contact_sync" + | "send_money" + | "login" + | "purchase_ticket" + | "add_recipient" + | "share_event" + | "make_request" + | "referral_link"; + +export interface PointsActivityEntry { + id: string; + type: PointsActivityType; + points: number; + timestamp: string; + metadata?: Record; +} + +export interface PointsState { + total: number; + activities: PointsActivityEntry[]; +} + +const STORAGE_KEY = "user_points"; + +const DEFAULT_STATE: PointsState = { + total: 0, + activities: [], +}; + +export const POINT_VALUES: Record = { + contact_sync: 10, + send_money: 10, + login: 5, + purchase_ticket: 15, + add_recipient: 10, + share_event: 5, + make_request: 8, + referral_link: 20, +}; + +async function loadState(): Promise { + try { + const stored = await AsyncStorage.getItem(STORAGE_KEY); + if (!stored) return DEFAULT_STATE; + const parsed = JSON.parse(stored); + if ( + typeof parsed === "object" && + parsed !== null && + typeof parsed.total === "number" && + Array.isArray(parsed.activities) + ) { + return parsed as PointsState; + } + return DEFAULT_STATE; + } catch (error) { + if (__DEV__) { + console.warn("[pointsService] Failed to load state", error); + } + return DEFAULT_STATE; + } +} + +async function saveState(state: PointsState): Promise { + try { + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch (error) { + if (__DEV__) { + console.warn("[pointsService] Failed to save state", error); + } + } +} + +export async function getPointsState(): Promise { + return loadState(); +} + +export async function awardPoints( + type: PointsActivityType, + metadata?: Record +): Promise { + const current = await loadState(); + const delta = POINT_VALUES[type] ?? 0; + + const entry: PointsActivityEntry = { + id: `${type}-${Date.now()}`, + type, + points: delta, + timestamp: new Date().toISOString(), + metadata, + }; + + const next: PointsState = { + total: current.total + delta, + activities: [entry, ...current.activities], + }; + + await saveState(next); + return next; +} + +const LOGIN_LAST_AWARD_KEY = "user_points_login_last_award"; +const CONTACT_SYNC_AWARDED_KEY = "user_points_contact_sync_awarded"; + +export async function awardDailyLoginPoints( + metadata?: Record +): Promise { + const today = new Date().toISOString().slice(0, 10); + + try { + const lastAwardDate = await AsyncStorage.getItem(LOGIN_LAST_AWARD_KEY); + if (lastAwardDate === today) { + return loadState(); + } + } catch (error) { + if (__DEV__) { + console.warn( + "[pointsService] Failed to read last login award date", + error + ); + } + } + + const updatedState = await awardPoints("login", metadata); + + try { + await AsyncStorage.setItem(LOGIN_LAST_AWARD_KEY, today); + } catch (error) { + if (__DEV__) { + console.warn( + "[pointsService] Failed to save last login award date", + error + ); + } + } + + return updatedState; +} + +export async function awardFirstContactSyncPoints( + metadata?: Record +): Promise { + try { + const alreadyAwarded = await AsyncStorage.getItem(CONTACT_SYNC_AWARDED_KEY); + if (alreadyAwarded === "true") { + return loadState(); + } + } catch (error) { + if (__DEV__) { + console.warn( + "[pointsService] Failed to read contact sync awarded flag", + error + ); + } + } + + const updatedState = await awardPoints("contact_sync", metadata); + + try { + await AsyncStorage.setItem(CONTACT_SYNC_AWARDED_KEY, "true"); + } catch (error) { + if (__DEV__) { + console.warn( + "[pointsService] Failed to save contact sync awarded flag", + error + ); + } + } + + return updatedState; +} diff --git a/lib/services/profileImageService.ts b/lib/services/profileImageService.ts new file mode 100644 index 0000000..6ca1555 --- /dev/null +++ b/lib/services/profileImageService.ts @@ -0,0 +1,95 @@ +import { Platform } from "react-native"; +import { isNative } from "../firebase"; + +export async function uploadProfileImage( + uid: string, + localUri: string +): Promise { + if (!uid || !localUri) { + throw new Error("Missing uid or image uri"); + } + + const fileName = `profile_${uid}_${Date.now()}.jpg`; + + if (isNative) { + let storageModule: any; + try { + console.log( + "[profileImageService] Using native Firebase Storage upload", + { + uid, + localUri, + } + ); + storageModule = require("@react-native-firebase/storage").default; + } catch (e) { + console.error( + "[profileImageService] @react-native-firebase/storage not available", + e + ); + throw new Error("Storage module not installed"); + } + + const path = `profile-images/${uid}/${fileName}`; + console.log("[profileImageService] Upload path (native):", path); + const ref = storageModule().ref(path); + + try { + console.log( + "[profileImageService] Calling putFile with localUri", + localUri + ); + const result = await ref.putFile(localUri); + console.log( + "[profileImageService] putFile result:", + result?.state || result + ); + } catch (error) { + console.error("[profileImageService] Error during putFile", error); + throw error; + } + + try { + console.log("[profileImageService] Fetching download URL for path", path); + const downloadUrl = await ref.getDownloadURL(); + console.log("[profileImageService] getDownloadURL success", downloadUrl); + return downloadUrl; + } catch (error) { + console.error("[profileImageService] Error during getDownloadURL", error); + throw error; + } + } + + // Web / other platforms using Firebase JS SDK storage + console.log("[profileImageService] Using web Firebase Storage upload", { + uid, + localUri, + }); + const { getStorage, ref, uploadBytes, getDownloadURL } = await import( + "firebase/storage" + ); + + const storage = getStorage(); + const response = await fetch(localUri); + const blob = await response.blob(); + const storageRefPath = `profile-images/${uid}/${fileName}`; + console.log("[profileImageService] Upload path (web):", storageRefPath); + const storageRef = ref(storage, storageRefPath); + try { + await uploadBytes(storageRef, blob); + } catch (error) { + console.error("[profileImageService] Error during uploadBytes", error); + throw error; + } + try { + const downloadUrl = await getDownloadURL(storageRef); + console.log( + "[profileImageService] Web getDownloadURL success", + downloadUrl + ); + return downloadUrl; + } catch (error) { + console.error("[profileImageService] Error during getDownloadURL", error); + throw error; + } +} diff --git a/lib/services/recipientService.ts b/lib/services/recipientService.ts new file mode 100644 index 0000000..b9f885b --- /dev/null +++ b/lib/services/recipientService.ts @@ -0,0 +1,248 @@ +/** + * Recipient Service - Platform-aware recipient management + * Uses Firebase abstraction layer for cross-platform support + */ +import { doc, collection } from '../firebase'; + +export interface Recipient { + id: string; + uid: string; + fullName: string; + phoneNumber: string; + createdAt: Date; + updatedAt: Date; + isActive: boolean; +} + +export class RecipientService { + private static snapshotExists(docSnap: any): boolean { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; + } + + // Add a new recipient + static async addRecipient( + uid: string, + recipientData: { + fullName: string; + phoneNumber: string; + } + ): Promise<{ success: boolean; error?: string; recipientId?: string }> { + try { + // Validate input + if (!recipientData.fullName.trim()) { + return { success: false, error: 'Full name is required' }; + } + + if (!recipientData.phoneNumber.trim()) { + return { success: false, error: 'Phone number is required' }; + } + + // Basic phone number validation + const cleanPhone = recipientData.phoneNumber.replace(/[^+\d]/g, ''); + if (cleanPhone.length < 7) { + return { success: false, error: 'Please enter a valid phone number' }; + } + + // Check if recipient already exists + const existingRecipient = await RecipientService.getRecipientByPhone(uid, cleanPhone); + if (existingRecipient) { + return { success: false, error: 'This recipient already exists' }; + } + + // Generate recipient ID + const recipientId = `recipient_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const newRecipient: Recipient = { + id: recipientId, + uid, + fullName: recipientData.fullName.trim(), + phoneNumber: cleanPhone, + createdAt: new Date(), + updatedAt: new Date(), + isActive: true, + }; + + // Save to Firestore + const recipientRef = doc('recipients', recipientId); + await recipientRef.set(newRecipient); + + return { success: true, recipientId }; + } catch (error) { + console.error('Add recipient error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to add recipient' + }; + } + } + + // Get all recipients for a user + static async getUserRecipients(uid: string): Promise { + try { + const recipientsCollection = collection('recipients'); + const querySnapshot = await recipientsCollection + .where('uid', '==', uid) + .where('isActive', '==', true) + .get(); + + const recipients: Recipient[] = []; + + querySnapshot.forEach((docData: any) => { + recipients.push(docData.data() as Recipient); + }); + + // Sort by creation date (newest first) + recipients.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + return recipients; + } catch (error) { + console.error('Error fetching user recipients:', error); + throw error; + } + } + + // Get recipient by phone number for a specific user + static async getRecipientByPhone(uid: string, phoneNumber: string): Promise { + try { + const recipientsCollection = collection('recipients'); + const querySnapshot = await recipientsCollection + .where('uid', '==', uid) + .where('phoneNumber', '==', phoneNumber) + .where('isActive', '==', true) + .get(); + + if (querySnapshot.empty) { + return null; + } + + return querySnapshot.docs[0].data() as Recipient; + } catch (error) { + console.error('Error fetching recipient by phone:', error); + throw error; + } + } + + // Update recipient + static async updateRecipient( + recipientId: string, + updates: { + fullName?: string; + phoneNumber?: string; + } + ): Promise<{ success: boolean; error?: string }> { + try { + const recipientRef = doc('recipients', recipientId); + const recipientDoc = await recipientRef.get(); + + if (!RecipientService.snapshotExists(recipientDoc)) { + return { success: false, error: 'Recipient not found' }; + } + + const updateData: any = { + updatedAt: new Date(), + }; + + if (updates.fullName) { + updateData.fullName = updates.fullName.trim(); + } + + if (updates.phoneNumber) { + const cleanPhone = updates.phoneNumber.replace(/[^+\d]/g, ''); + if (cleanPhone.length < 7) { + return { success: false, error: 'Please enter a valid phone number' }; + } + updateData.phoneNumber = cleanPhone; + } + + await recipientRef.update(updateData); + + console.log('Recipient updated successfully'); + return { success: true }; + } catch (error) { + console.error('Update recipient error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update recipient' + }; + } + } + + // Delete recipient (soft delete) + static async deleteRecipient(recipientId: string): Promise<{ success: boolean; error?: string }> { + try { + const recipientRef = doc('recipients', recipientId); + + await recipientRef.update({ + isActive: false, + updatedAt: new Date(), + }); + + console.log('Recipient deleted successfully'); + return { success: true }; + } catch (error) { + console.error('Delete recipient error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete recipient' + }; + } + } + + // Hard delete recipient (permanent) + static async permanentDeleteRecipient(recipientId: string): Promise<{ success: boolean; error?: string }> { + try { + const recipientRef = doc('recipients', recipientId); + await recipientRef.delete(); + + console.log('Recipient permanently deleted'); + return { success: true }; + } catch (error) { + console.error('Permanent delete recipient error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to permanently delete recipient' + }; + } + } + + static async getRecipientById(recipientId: string): Promise<{ success: boolean; recipient?: Recipient; error?: string }> { + try { + const recipientRef = doc('recipients', recipientId); + const docSnap = await recipientRef.get(); + + if (!RecipientService.snapshotExists(docSnap)) { + return { success: false, error: 'Recipient not found' }; + } + + const data = docSnap.data() as Recipient | undefined; + if (!data) { + return { success: false, error: 'Recipient data unavailable' }; + } + + const recipient: Recipient = { + ...data, + id: data.id ?? recipientId, + createdAt: (data.createdAt as any)?.toDate?.() ?? data.createdAt ?? new Date(), + updatedAt: (data.updatedAt as any)?.toDate?.() ?? data.updatedAt ?? new Date(), + }; + + return { success: true, recipient }; + } catch (error) { + console.error('Error fetching recipient by id:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch recipient', + }; + } + } +} + +export default RecipientService; diff --git a/lib/services/referralService.ts b/lib/services/referralService.ts new file mode 100644 index 0000000..6fc6a49 --- /dev/null +++ b/lib/services/referralService.ts @@ -0,0 +1,73 @@ +import { callEndpoint } from "../api/client"; +import { referralEndpoints } from "../api/referralEndpoints"; + +export type ReferralReason = "signup" | "transaction" | "event"; + +export interface ApplyReferralPayload { + referralCode: string; + uid: string; + referralReason: ReferralReason; + contextId: string; +} + +export interface ApplyReferralResult { + success: boolean; + message?: string; + error?: string; +} + +export async function applyReferral( + payload: ApplyReferralPayload +): Promise { + const trimmedCode = payload.referralCode.trim(); + + if (!trimmedCode) { + return { success: false, error: "Referral code is required" }; + } + + const body = { + referralCode: trimmedCode, + uid: payload.uid, + referralReason: payload.referralReason, + contextId: payload.contextId, + }; + + try { + console.log("[ReferralService] Applying referral via API client", body); + + const res = await callEndpoint(referralEndpoints.apply, { + body, + headers: { + "source-app": "amba-pay", + }, + }); + + const message = + (res && (res.message || res.successMessage)) || + "Referral applied successfully"; + + console.log("[ReferralService] Referral applied successfully", res); + + return { success: true, message }; + } catch (error) { + console.error("[ReferralService] Error calling referral API", error); + + let parsedError: string | undefined; + if (error instanceof Error) { + try { + const parsed = JSON.parse(error.message); + if (parsed && typeof parsed.message === "string") { + parsedError = parsed.message; + } + } catch { + // ignore JSON parse failure, fall back to message + parsedError = error.message; + } + } + + return { + success: false, + error: parsedError || "Failed to apply referral code", + }; + } +} diff --git a/lib/services/requestService.ts b/lib/services/requestService.ts new file mode 100644 index 0000000..6380fb3 --- /dev/null +++ b/lib/services/requestService.ts @@ -0,0 +1,195 @@ +/** + * Request Service - Platform-aware money request management + * Uses Firebase abstraction layer for cross-platform support + */ +import { doc, collection, FieldValue } from '../firebase'; +import { withGlobalLoading } from '../stores/uiStore'; + +export interface MoneyRequest { + id?: string; + requestorName: string; + requesteeName: string; + requestorPhoneNumber: string; + requesteePhoneNumber: string; + requestorType: 'saved' | 'contact'; + amount: number; + note?: string; + notificationMethod: 'SMS' | 'WhatsApp'; + status: 'pending' | 'accepted' | 'declined' | 'expired'; + createdAt: Date; + updatedAt: Date; +} + +export class RequestService { + private static snapshotExists(docSnap: any): boolean { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; + } + + /** + * Find user UID by phone number + */ + static async findUserByPhoneNumber(phoneNumber: string): Promise { + try { + const usersCollection = collection('users'); + const querySnapshot = await usersCollection + .where('phoneNumber', '==', phoneNumber) + .get(); + + if (querySnapshot.empty) { + return null; + } + + return querySnapshot.docs[0].id; + } catch (error) { + console.error('Error finding user by phone number:', error); + return null; + } + } + + /** + * Get user details by phone number + */ + static async getUserByPhoneNumber(phoneNumber: string): Promise<{ success: boolean; user?: { uid: string; displayName: string; phoneNumber: string }; error?: string }> { + try { + const usersCollection = collection('users'); + const querySnapshot = await usersCollection + .where('phoneNumber', '==', phoneNumber) + .get(); + + if (querySnapshot.empty) { + return { success: false, error: 'User not found' }; + } + + const userDoc = querySnapshot.docs[0]; + const userData = userDoc.data(); + + return { + success: true, + user: { + uid: userDoc.id, + displayName: userData.displayName || userData.name || 'Unknown', + phoneNumber: userData.phoneNumber + } + }; + } catch (error) { + console.error('Error getting user by phone number:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get user' + }; + } + } + + /** + * Get request by ID + */ + static async getRequestById(requestId: string): Promise<{ success: boolean; request?: MoneyRequest; error?: string }> { + try { + const requestRef = doc('requests', requestId); + const requestSnapshot = await requestRef.get(); + + if (!RequestService.snapshotExists(requestSnapshot)) { + return { success: false, error: 'Request not found' }; + } + + const requestData = requestSnapshot.data() as any; + const request: MoneyRequest = { + id: requestSnapshot.id, + requestorName: requestData.requestorName, + requesteeName: requestData.requesteeName, + requestorPhoneNumber: requestData.requestorPhoneNumber, + requesteePhoneNumber: requestData.requesteePhoneNumber, + requestorType: requestData.requestorType, + amount: requestData.amount, + note: requestData.note, + notificationMethod: requestData.notificationMethod || 'SMS', + status: requestData.status, + createdAt: requestData.createdAt?.toDate?.() || new Date(), + updatedAt: requestData.updatedAt?.toDate?.() || new Date(), + }; + + return { + success: true, + request + }; + } catch (error) { + console.error('Error getting request by ID:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get request' + }; + } + } + + /** + * Update request status + */ + static async updateRequestStatus( + requestId: string, + status: 'pending' | 'accepted' | 'declined' | 'expired' + ): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + const requestRef = doc('requests', requestId); + await requestRef.update({ + status, + updatedAt: FieldValue.serverTimestamp(), + }); + + return { + success: true, + }; + } catch (error) { + console.error('Error updating request status:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update request status', + }; + } + }); + } + + /** + * Create a new money request + */ + static async createRequest( + uid: string, + requestData: Omit + ): Promise<{ success: boolean; requestId?: string; error?: string }> { + return withGlobalLoading(async () => { + try { + // Generate a unique request ID + const requestId = `request_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + const request = { + ...requestData, + uid, + createdAt: FieldValue.serverTimestamp(), + updatedAt: FieldValue.serverTimestamp(), + }; + + const requestRef = doc('requests', requestId); + await requestRef.set(request); + + return { + success: true, + requestId, + }; + } catch (error) { + console.error('Error creating money request:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create request', + }; + } + }); + } +} diff --git a/lib/services/ticketService.ts b/lib/services/ticketService.ts new file mode 100644 index 0000000..7791ae4 --- /dev/null +++ b/lib/services/ticketService.ts @@ -0,0 +1,67 @@ +import { callEndpoint } from "../api/client"; +import { ticketEndpoints } from "../api/ticketEndpoints"; + +export interface TicketDto { + id: string; + eventId?: string; + status?: string; + [key: string]: any; +} + +interface TicketsApiResponse { + totalItems: number; + totalPages: number; + currentPage: number; + data: TicketDto[]; + message: string; +} + +export class TicketService { + static async getTickets( + token: string, + params?: { status?: string; limit?: number; page?: number } + ): Promise { + const { status, limit = 50, page = 1 } = params || {}; + + const res = await callEndpoint( + ticketEndpoints.getTickets, + { + query: { + ...(status ? { status } : {}), + limit, + page, + }, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + console.log("[TicketService] tickets response", { + totalItems: res.totalItems, + currentPage: res.currentPage, + dataLength: Array.isArray(res.data) ? res.data.length : null, + }); + + return res.data ?? []; + } + + static async buyTicket( + token: string, + payload: { ticketTierId: string; eventId: string; ticketCount: number } + ): Promise { + const res = await callEndpoint(ticketEndpoints.buyTicket, { + body: { + ticketTierId: payload.ticketTierId, + eventId: payload.eventId, + ticketCount: payload.ticketCount, + }, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + console.log("[TicketService] buy ticket response", res); + return res; + } +} diff --git a/lib/services/transactionService.ts b/lib/services/transactionService.ts new file mode 100644 index 0000000..29f7666 --- /dev/null +++ b/lib/services/transactionService.ts @@ -0,0 +1,323 @@ +/** + * Transaction Service - Platform-aware transaction management + * Uses Firebase abstraction layer for cross-platform support + */ +import { doc, collection } from '../firebase'; +import { WalletService } from './walletService'; +import { calculateTotalAmountForSending, calculateProcessingFee } from '../utils/feeUtils'; +import { useTransactionStore } from '../stores/transactionStore'; +import { withGlobalLoading } from '../stores/uiStore'; + +const snapshotExists = (docSnap: any): boolean => { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; +}; + +// Base transaction interface +interface BaseTransaction { + id: string; + uid: string; + amount: number; + status: 'pending' | 'completed' | 'failed'; + createdAt: Date; + updatedAt: Date; +} + +// Send money transaction +interface SendTransaction extends BaseTransaction { + type: 'send'; + recipientName: string; + recipientPhoneNumber: string; + recipientType: 'saved' | 'contact'; + recipientId: string; + note: string; + fulfilled: boolean; +} + +// Receive money transaction +interface ReceiveTransaction extends BaseTransaction { + type: 'receive'; + senderName: string; + senderPhoneNumber: string; + senderType: 'saved' | 'contact'; + senderId: string; + note: string; +} + +// Add cash transaction +interface AddCashTransaction extends BaseTransaction { + type: 'add_cash'; + cardId: string; + cardType: string; + lastFourDigits: string; +} + +// Cash out transaction +interface CashOutTransaction extends BaseTransaction { + type: 'cash_out'; + bankProvider: 'awash' | 'telebirr'; + accountNumber?: string; + note: string; +} + +// Discriminated union for all transaction types +export type Transaction = SendTransaction | ReceiveTransaction | AddCashTransaction | CashOutTransaction; + +export class TransactionService { + /** + * Create a new transaction when sending money + */ + static async sendMoney( + uid: string, + transactionData: { + amount: number; + recipientName: string; + recipientPhoneNumber: string; + recipientType: 'saved' | 'contact'; + recipientId: string; + note: string; + } + ): Promise<{ success: boolean; error?: string; transactionId?: string }> { + return withGlobalLoading(async () => { + try { + // Validate input + if (!transactionData.amount || transactionData.amount <= 0) { + return { success: false, error: 'Invalid amount' }; + } + + if (!transactionData.recipientName || !transactionData.recipientPhoneNumber) { + return { success: false, error: 'Recipient information is required' }; + } + + // Check if user has sufficient balance + const walletResult = await WalletService.getUserWallet(uid); + if (!walletResult.success) { + return { success: false, error: 'Failed to get wallet information' }; + } + + const wallet = walletResult.wallet; + if (!wallet) { + return { success: false, error: 'Wallet not found' }; + } + + // Calculate total amount including processing fee + const totalAmountRequired = calculateTotalAmountForSending(transactionData.amount); + + if (wallet.balance < totalAmountRequired) { + const processingFee = calculateProcessingFee(transactionData.amount); + return { + success: false, + error: `Insufficient balance. Required: $${(totalAmountRequired / 100).toFixed(2)} (including $${(processingFee / 100).toFixed(2)} processing fee). Available: $${(wallet.balance / 100).toFixed(2)}` + }; + } + + // Generate transaction ID + const transactionId = `transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Create transaction record + const newTransaction: SendTransaction = { + id: transactionId, + uid, + amount: transactionData.amount, + recipientName: transactionData.recipientName, + recipientPhoneNumber: transactionData.recipientPhoneNumber, + recipientType: transactionData.recipientType, + recipientId: transactionData.recipientId, + note: transactionData.note || '', + fulfilled: false, + status: 'pending', + type: 'send', + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Save transaction to Firestore + const transactionRef = doc('transactions', transactionId); + await transactionRef.set(newTransaction); + + // Update wallet balance + const totalAmountToDeduct = calculateTotalAmountForSending(transactionData.amount); + const newBalance = wallet.balance - totalAmountToDeduct; + const updateResult = await WalletService.updateWalletBalance(uid, newBalance); + + if (!updateResult.success) { + return { success: false, error: 'Failed to update wallet balance' }; + } + + // Update transaction status to completed + await transactionRef.update({ + status: 'completed', + updatedAt: new Date(), + }); + + console.log('Transaction completed successfully:', transactionId); + + try { + const transactionStore = useTransactionStore.getState(); + await transactionStore.invalidateTransactions(uid); + await transactionStore.invalidateTransactionDetail(transactionId); + } catch (cacheError) { + console.warn('Failed to refresh transaction cache after sendMoney', cacheError); + } + + return { success: true, transactionId }; + } catch (error) { + console.error('Error creating transaction:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create transaction' + }; + } + }); + } + + /** + * Create a cash out transaction + */ + static async cashOut( + uid: string, + transactionData: { + amount: number; + bankProvider: 'awash' | 'telebirr'; + accountNumber?: string; + note: string; + } + ): Promise<{ success: boolean; error?: string; transactionId?: string }> { + return withGlobalLoading(async () => { + try { + // Validate input + if (!transactionData.amount || transactionData.amount <= 0) { + return { success: false, error: 'Invalid amount' }; + } + + if (!transactionData.bankProvider) { + return { success: false, error: 'Bank provider is required' }; + } + + // Check if user has sufficient balance + const walletResult = await WalletService.getUserWallet(uid); + if (!walletResult.success) { + return { success: false, error: 'Failed to get wallet information' }; + } + + const wallet = walletResult.wallet; + if (!wallet) { + return { success: false, error: 'Wallet not found' }; + } + + if (wallet.balance < transactionData.amount) { + return { + success: false, + error: `Insufficient balance. Required: $${(transactionData.amount / 100).toFixed(2)}. Available: $${(wallet.balance / 100).toFixed(2)}` + }; + } + + // Generate transaction ID + const transactionId = `transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Create transaction record + const newTransaction: CashOutTransaction = { + id: transactionId, + uid, + amount: transactionData.amount, + bankProvider: transactionData.bankProvider, + note: transactionData.note || '', + status: 'pending', + type: 'cash_out', + createdAt: new Date(), + updatedAt: new Date(), + ...(transactionData.accountNumber ? { accountNumber: transactionData.accountNumber } : {}), + } as CashOutTransaction; + + // Save transaction to Firestore + const transactionRef = doc('transactions', transactionId); + await transactionRef.set(newTransaction); + + // Update wallet balance + const newBalance = wallet.balance - transactionData.amount; + const updateResult = await WalletService.updateWalletBalance(uid, newBalance); + + if (!updateResult.success) { + return { success: false, error: 'Failed to update wallet balance' }; + } + + // Update transaction status to completed + await transactionRef.update({ + status: 'completed', + updatedAt: new Date(), + }); + + console.log('Cash out transaction completed successfully:', transactionId); + + try { + const transactionStore = useTransactionStore.getState(); + await transactionStore.invalidateTransactions(uid); + await transactionStore.invalidateTransactionDetail(transactionId); + } catch (cacheError) { + console.warn('Failed to refresh transaction cache after cashOut', cacheError); + } + + return { success: true, transactionId }; + } catch (error) { + console.error('Error creating cash out transaction:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create cash out transaction' + }; + } + }); + } + + /** + * Get all transactions for a user + */ + static async getUserTransactions(uid: string): Promise<{ success: boolean; transactions?: Transaction[]; error?: string }> { + try { + return { success: true, transactions: [] }; + } catch (error) { + console.error('Error getting user transactions:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get transactions' + }; + } + } + + static async getTransactionById(transactionId: string): Promise<{ success: boolean; transaction?: Transaction; error?: string }> { + try { + const docRef = doc('transactions', transactionId); + const docSnap = await docRef.get(); + + if (!snapshotExists(docSnap)) { + return { success: false, error: 'Transaction not found' }; + } + + const data = docSnap.data(); + if (!data) { + return { success: false, error: 'Transaction data unavailable' }; + } + + const transaction = { + ...data, + id: data.id ?? transactionId, + createdAt: data.createdAt?.toDate ? data.createdAt.toDate() : data.createdAt ?? new Date(), + updatedAt: data.updatedAt?.toDate ? data.updatedAt.toDate() : data.updatedAt ?? new Date(), + } as Transaction; + + return { success: true, transaction }; + } catch (error) { + console.error('Error fetching transaction by id:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch transaction', + }; + } + } +} diff --git a/lib/services/twilioService.ts b/lib/services/twilioService.ts new file mode 100644 index 0000000..a58afc6 --- /dev/null +++ b/lib/services/twilioService.ts @@ -0,0 +1,65 @@ +/** + * Twilio Service for Client-side Usage + * Provides easy-to-use functions for SMS and WhatsApp messaging + * Uses Firebase abstraction layer for cross-platform support + */ +import { httpsCallable } from '../firebase'; + +// SMS Functions +export const sendSMS = async (to: string, message: string, from?: string) => { + const sendSMSFunction = httpsCallable('sendSMS'); + return await sendSMSFunction({ to, message, from }); +}; + +export const sendBulkSMS = async (recipients: string[], message: string, from?: string) => { + const sendBulkSMSFunction = httpsCallable('sendBulkSMS'); + return await sendBulkSMSFunction({ recipients, message, from }); +}; + +// WhatsApp Functions +export const sendWhatsApp = async (to: string, message: string, from?: string) => { + const sendWhatsAppFunction = httpsCallable('sendWhatsApp'); + return await sendWhatsAppFunction({ to, message, from }); +}; + +export const sendBulkWhatsApp = async (recipients: string[], message: string, from?: string) => { + const sendBulkWhatsAppFunction = httpsCallable('sendBulkWhatsApp'); + return await sendBulkWhatsAppFunction({ recipients, message, from }); +}; + +// Utility Functions +export const getMessageStatus = async (messageSid: string) => { + const getMessageStatusFunction = httpsCallable('getMessageStatus'); + return await getMessageStatusFunction({ messageSid }); +}; + +export const getMessageHistory = async (from?: string, to?: string, limit?: number) => { + const getMessageHistoryFunction = httpsCallable('getMessageHistory'); + return await getMessageHistoryFunction({ from, to, limit }); +}; + +export const validatePhoneNumber = async (phoneNumber: string, countryCode?: string) => { + const validatePhoneNumberFunction = httpsCallable('validatePhoneNumber'); + return await validatePhoneNumberFunction({ phoneNumber, countryCode }); +}; + +// Helper function to format phone numbers for WhatsApp +export const formatWhatsAppNumber = (phoneNumber: string): string => { + if (phoneNumber.startsWith("whatsapp:")) { + return phoneNumber; + } + return `whatsapp:${phoneNumber}`; +}; + +// Helper function to format phone numbers for SMS +export const formatSMSNumber = (phoneNumber: string): string => { + const cleaned = phoneNumber.replace(/\D/g, ""); + if (cleaned.length === 10) { + return `+1${cleaned}`; + } else if (cleaned.length === 11 && cleaned.startsWith("1")) { + return `+${cleaned}`; + } else if (cleaned.length > 10) { + return `+${cleaned}`; + } + return phoneNumber; +}; diff --git a/lib/services/userQrService.ts b/lib/services/userQrService.ts new file mode 100644 index 0000000..e880491 --- /dev/null +++ b/lib/services/userQrService.ts @@ -0,0 +1,64 @@ +import { doc } from "../firebase"; + +export interface ProfileQrPayload { + type: "AMBA_PROFILE"; + version: 1; + accountId: string; + name: string; + phoneNumber: string; +} + +export class UserQrService { + static async getOrCreateProfileQr(params: { + uid: string; + accountId: string; + name: string; + phoneNumber: string; + }): Promise { + const { uid, accountId, name, phoneNumber } = params; + + try { + const userRef = doc("users", uid); + const snap = await userRef.get(); + const data = snap?.data ? snap.data() : undefined; + const existingQr: unknown = data?.qrCode; + + if (typeof existingQr === "string" && existingQr.length > 0) { + return existingQr; + } + + const payload: ProfileQrPayload = { + type: "AMBA_PROFILE", + version: 1, + accountId, + name, + phoneNumber, + }; + const payloadString = JSON.stringify(payload); + + try { + await userRef.update({ qrCode: payloadString }); + } catch (error) { + console.warn( + "[UserQrService] Failed to persist qrCode field for user", + uid, + error + ); + } + + return payloadString; + } catch (error) { + console.error("[UserQrService] getOrCreateProfileQr error", error); + const fallback: ProfileQrPayload = { + type: "AMBA_PROFILE", + version: 1, + accountId: params.accountId, + name: params.name, + phoneNumber: params.phoneNumber, + }; + return JSON.stringify(fallback); + } + } +} + +export default UserQrService; diff --git a/lib/services/userSearchService.ts b/lib/services/userSearchService.ts new file mode 100644 index 0000000..347bcfd --- /dev/null +++ b/lib/services/userSearchService.ts @@ -0,0 +1,29 @@ +import { collection } from "../firebase"; +import type { UserProfile } from "./authServices"; + +export class UserSearchService { + static async findUserByEmail(email: string): Promise { + const trimmed = email.trim().toLowerCase(); + if (!trimmed) return null; + + try { + const usersRef = collection("users"); + const querySnapshot = await usersRef + .where("email", "==", trimmed) + .limit(1) + .get(); + + if (querySnapshot.empty) { + return null; + } + + const docSnap = querySnapshot.docs[0]; + return (docSnap.data ? docSnap.data() : docSnap.data()) as UserProfile; + } catch (error) { + console.error("[UserSearchService] Error searching user by email", error); + return null; + } + } +} + +export default UserSearchService; diff --git a/lib/services/walletService.ts b/lib/services/walletService.ts new file mode 100644 index 0000000..5616ae6 --- /dev/null +++ b/lib/services/walletService.ts @@ -0,0 +1,272 @@ +/** + * Wallet Service - Platform-aware wallet management + * Uses Firebase abstraction layer for cross-platform support + */ +import { doc, collection, FieldValue } from '../firebase'; +import { useUserWalletStore } from '../stores/userWalletStore'; +import { withGlobalLoading } from '../stores/uiStore'; + +export interface CreditCard { + id: string; + cardNumber: string; + expiryDate: string; + cardType?: string; + lastFourDigits: string; + createdAt: Date; + isActive: boolean; +} + +export interface UserWallet { + uid: string; + balance: number; + currency: string; + isActive: boolean; + cards: CreditCard[]; + createdAt: Date; + updatedAt: Date; +} + +export class WalletService { + private static snapshotExists(docSnap: any): boolean { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; + } + + // Create User Wallet + static async createUserWallet(uid: string): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + const walletData: UserWallet = { + uid, + balance: 0, + currency: 'USD', + isActive: true, + cards: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + const walletRef = doc('wallets', uid); + await walletRef.set(walletData); + console.log('Wallet created successfully for user:', uid); + + try { + await useUserWalletStore.getState().invalidateWallet(uid); + } catch (cacheError) { + console.warn('Failed to refresh wallet cache after creation', cacheError); + } + + return { success: true }; + } catch (error) { + console.error('Wallet creation error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create wallet' + }; + } + }); + } + + // Update Wallet Balance + static async updateWalletBalance( + uid: string, + newBalanceInCents: number + ): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + const walletDocRef = doc('wallets', uid); + await walletDocRef.update({ + balance: newBalanceInCents, + updatedAt: new Date(), + }); + + console.log('Wallet balance updated successfully'); + + try { + await useUserWalletStore.getState().invalidateWallet(uid); + } catch (cacheError) { + console.warn('Failed to refresh wallet cache after balance update', cacheError); + } + + return { success: true }; + } catch (error) { + console.error('Wallet balance update error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update wallet balance' + }; + } + }); + } + + // Add Credit Card to Wallet + static async addCreditCard( + uid: string, + cardData: { + cardNumber: string; + expiryDate: string; + cvv: string; + } + ): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + // Basic validation + if (!cardData.cardNumber || !cardData.expiryDate || !cardData.cvv) { + return { success: false, error: 'All card fields are required' }; + } + + // Validate card number + const cleanCardNumber = cardData.cardNumber.replace(/\s|-/g, ''); + if (!/^\d{16}$/.test(cleanCardNumber)) { + return { success: false, error: 'Card number must be 16 digits' }; + } + + // Validate expiry date + if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardData.expiryDate)) { + return { success: false, error: 'Expiry date must be in MM/YY format' }; + } + + // Validate CVV + if (!/^\d{3,4}$/.test(cardData.cvv)) { + return { success: false, error: 'CVV must be 3 or 4 digits' }; + } + + // Detect card type + const cardType = WalletService.detectCardType(cleanCardNumber); + + // Create card object + const newCard: CreditCard = { + id: `card_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + cardNumber: WalletService.maskCardNumber(cleanCardNumber), + expiryDate: cardData.expiryDate, + cardType, + lastFourDigits: cleanCardNumber.slice(-4), + createdAt: new Date(), + isActive: true, + }; + + // Get current wallet + const walletDocRef = doc('wallets', uid); + const walletDoc = await walletDocRef.get(); + + if (!WalletService.snapshotExists(walletDoc)) { + return { success: false, error: 'Wallet not found' }; + } + + const currentWallet = walletDoc.data() as UserWallet; + const updatedCards = [...(currentWallet.cards || []), newCard]; + + // Update wallet with new card + await walletDocRef.update({ + cards: updatedCards, + updatedAt: new Date(), + }); + + console.log('Credit card added successfully'); + + try { + await useUserWalletStore.getState().invalidateWallet(uid); + } catch (cacheError) { + console.warn('Failed to refresh wallet cache after adding card', cacheError); + } + + return { success: true }; + } catch (error) { + console.error('Add credit card error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to add credit card' + }; + } + }); + } + + // Remove Credit Card from Wallet + static async removeCreditCard( + uid: string, + cardId: string + ): Promise<{ success: boolean; error?: string }> { + return withGlobalLoading(async () => { + try { + const walletDocRef = doc('wallets', uid); + const walletDoc = await walletDocRef.get(); + + if (!WalletService.snapshotExists(walletDoc)) { + return { success: false, error: 'Wallet not found' }; + } + + const currentWallet = walletDoc.data() as UserWallet; + const updatedCards = (currentWallet.cards || []).filter(card => card.id !== cardId); + + await walletDocRef.update({ + cards: updatedCards, + updatedAt: new Date(), + }); + + console.log('Credit card removed successfully'); + + try { + await useUserWalletStore.getState().invalidateWallet(uid); + } catch (cacheError) { + console.warn('Failed to refresh wallet cache after removing card', cacheError); + } + + return { success: true }; + } catch (error) { + console.error('Remove credit card error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to remove credit card' + }; + } + }); + } + + // Helper: Detect card type + private static detectCardType(cardNumber: string): string { + if (cardNumber.startsWith('4')) { + return 'Visa'; + } else if (cardNumber.startsWith('5') || cardNumber.startsWith('2')) { + return 'MasterCard'; + } else if (cardNumber.startsWith('3')) { + return 'American Express'; + } else if (cardNumber.startsWith('6')) { + return 'Discover'; + } + return 'Unknown'; + } + + // Helper: Mask card number + private static maskCardNumber(cardNumber: string): string { + return '**** **** **** ' + cardNumber.slice(-4); + } + + // Get User Wallet + static async getUserWallet(uid: string): Promise<{ success: boolean; wallet?: UserWallet; error?: string }> { + try { + const walletDocRef = doc('wallets', uid); + const walletDoc = await walletDocRef.get(); + + if (WalletService.snapshotExists(walletDoc)) { + const wallet = walletDoc.data() as UserWallet; + return { success: true, wallet }; + } + return { success: false, error: 'Wallet not found' }; + } catch (error) { + console.error('Error fetching user wallet:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch wallet' + }; + } + } +} + +export default WalletService; diff --git a/lib/stores/authStore.ts b/lib/stores/authStore.ts new file mode 100644 index 0000000..9c45415 --- /dev/null +++ b/lib/stores/authStore.ts @@ -0,0 +1,223 @@ +import { create } from "zustand"; +import { Platform } from "react-native"; +import { + onAuthStateChanged, + signOut as firebaseSignOut, + isWeb, + getAuthInstance, +} from "../firebase"; +import { UserProfile, AuthService } from "../services/authServices"; +import { UserWallet } from "../services/walletService"; +import { useUserProfileStore } from "./userProfileStore"; +import { useTransactionStore } from "./transactionStore"; +import { useUserWalletStore } from "./userWalletStore"; +import { withGlobalLoading } from "./uiStore"; +import { awardDailyLoginPoints } from "../services/pointsService"; + +// Conditionally import FCMService only for native +let FCMService: any = null; +if (Platform.OS !== "web") { + FCMService = require("../services/fcmService").FCMService; +} + +interface AuthState { + // User state + user: any | null; + profile: UserProfile | null; + wallet: UserWallet | null; + + // Loading states + loading: boolean; + profileLoading: boolean; + walletLoading: boolean; + + // Error states + profileError: string | null; + walletError: string | null; + + // Phone authentication state + phoneSessionInfo: string | null; + phoneConfirmationResult: any | null; + phoneLoading: boolean; + phoneError: string | null; + + // Computed values + formattedBalance: string; + + // Actions + setUser: (user: any | null) => void; + setProfile: (profile: UserProfile | null) => void; + setWallet: (wallet: UserWallet | null) => void; + setLoading: (loading: boolean) => void; + setProfileLoading: (loading: boolean) => void; + setWalletLoading: (loading: boolean) => void; + setProfileError: (error: string | null) => void; + setWalletError: (error: string | null) => void; + setFormattedBalance: (balance: string) => void; + + // Phone auth setters + setPhoneSessionInfo: (sessionInfo: string | null) => void; + setPhoneConfirmationResult: (confirmationResult: any | null) => void; + setPhoneLoading: (loading: boolean) => void; + setPhoneError: (error: string | null) => void; + clearPhoneAuth: () => void; + + // Auth actions + signOut: () => Promise; + refreshProfile: () => Promise; + refreshWallet: () => Promise; + + // Initialize auth listener + initializeAuth: () => () => void; +} + +export const useAuthStore = create((set, get) => ({ + // Initial state + user: null, + profile: null, + wallet: null, + loading: true, + profileLoading: false, + walletLoading: false, + profileError: null, + walletError: null, + phoneSessionInfo: null, + phoneConfirmationResult: null, + phoneLoading: false, + phoneError: null, + formattedBalance: "0.00", + + // Setters + setUser: (user) => set({ user }), + setProfile: (profile) => set({ profile }), + setWallet: (wallet) => set({ wallet }), + setLoading: (loading) => set({ loading }), + setProfileLoading: (profileLoading) => set({ profileLoading }), + setWalletLoading: (walletLoading) => set({ walletLoading }), + setProfileError: (error) => set({ profileError: error }), + setWalletError: (error) => set({ walletError: error }), + setFormattedBalance: (formattedBalance) => set({ formattedBalance }), + + // Phone auth setters + setPhoneSessionInfo: (phoneSessionInfo) => set({ phoneSessionInfo }), + setPhoneConfirmationResult: (phoneConfirmationResult) => + set({ phoneConfirmationResult }), + setPhoneLoading: (phoneLoading) => set({ phoneLoading }), + setPhoneError: (phoneError) => set({ phoneError }), + + // Phone auth actions + clearPhoneAuth: () => + set({ + phoneSessionInfo: null, + phoneConfirmationResult: null, + phoneLoading: false, + phoneError: null, + }), + + // Auth actions + signOut: async () => { + // Get current user from our abstracted firebase module + const currentUser = getAuthInstance().currentUser; + + // If there is no current Firebase user (common in dev with mocked flows), + // just clear local state and exit without throwing. + if (!currentUser) { + console.warn( + "AuthStore.signOut called with no current Firebase user; clearing local state only" + ); + get().setUser(null); + get().clearPhoneAuth(); + useUserProfileStore.getState().clearAll(); + useTransactionStore.getState().clearAll(); + useUserWalletStore.getState().clearAll(); + return; + } + + try { + const uid = get().user?.uid; + + // Remove FCM token from Firestore before signing out (native only) + if (uid && FCMService && Platform.OS !== "web") { + try { + await FCMService.removeTokenFromFirestore(uid); + FCMService.cleanup(); + } catch (fcmError) { + console.error("Error removing FCM token on sign out:", fcmError); + } + } + + await withGlobalLoading(async () => { + // Sign out from Google if signed in with Google + await AuthService.signOutFromGoogle(); + // Sign out from Firebase + await firebaseSignOut(); + }); + + get().clearPhoneAuth(); + useUserProfileStore.getState().clearAll(); + useTransactionStore.getState().clearAll(); + useUserWalletStore.getState().clearAll(); + } catch (error) { + console.error("Error signing out:", error); + throw error; + } + }, + + refreshProfile: async () => { + const uid = get().user?.uid; + if (!uid) { + console.warn( + "AuthStore.refreshProfile called without an authenticated user" + ); + return; + } + + const profileStore = useUserProfileStore.getState(); + profileStore.ensureSubscription(uid); + await withGlobalLoading(() => profileStore.refreshProfile(uid)); + }, + + refreshWallet: async () => { + const uid = get().user?.uid; + if (!uid) { + console.warn( + "AuthStore.refreshWallet called without an authenticated user" + ); + return; + } + + const walletStore = useUserWalletStore.getState(); + walletStore.ensureSubscription(uid); + await withGlobalLoading(() => walletStore.refreshWallet(uid)); + }, + + // Initialize auth listener + initializeAuth: () => { + console.log("AuthStore: Initializing auth listener"); + const unsubscribe = onAuthStateChanged((fbUser: any) => { + // In dev, if we've explicitly set a fake emulator user, don't let + // Firebase auth (which will report null) wipe it out. + if (__DEV__) { + const current = get().user; + if (current?.uid === "dev-emulator-user") { + if (get().loading) { + set({ loading: false }); + } + return; + } + } + + console.log("AuthStore: Auth state changed, user:", fbUser?.uid); + set({ user: fbUser || null, loading: false }); + if (fbUser) { + awardDailyLoginPoints().catch((error) => { + console.warn( + "[AuthStore] Failed to maybe award daily login points", + error + ); + }); + } + }); + return unsubscribe; + }, +})); diff --git a/lib/stores/contactsStore.ts b/lib/stores/contactsStore.ts new file mode 100644 index 0000000..8def010 --- /dev/null +++ b/lib/stores/contactsStore.ts @@ -0,0 +1,254 @@ +/** + * Contacts Store - Platform-aware contact management + * + * Native (Android/iOS): Uses expo-contacts to access device contacts + * Web: Gracefully degrades - contacts feature not available + */ +import { create } from "zustand"; +import { Platform, Alert } from "react-native"; +import { formatPhoneNumber, isValidPhoneNumber } from "../utils/phoneUtils"; +import { awardFirstContactSyncPoints } from "../services/pointsService"; + +// Only import expo-contacts on native platforms +let Contacts: any = null; +if (Platform.OS !== "web") { + Contacts = require("expo-contacts"); +} + +export interface Contact { + id: string; + name: string; + firstName?: string; + lastName?: string; + phoneNumbers?: Array<{ + number: string; + formattedNumber: string; + isPrimary?: boolean; + label?: string; + }>; + emails?: Array<{ + email: string; + isPrimary?: boolean; + label?: string; + }>; + imageAvailable?: boolean; + image?: any; +} + +interface ContactsState { + // State + contacts: Contact[]; + loading: boolean; + error: string | null; + hasPermission: boolean; + isInitialized: boolean; + isSupported: boolean; // New: indicates if contacts are supported on this platform + + // Actions + setContacts: (contacts: Contact[]) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + setHasPermission: (hasPermission: boolean) => void; + setIsInitialized: (isInitialized: boolean) => void; + + // Permission actions + checkPermission: () => Promise; + requestPermission: () => Promise; + + // Data actions + loadContacts: () => Promise; + refreshContacts: () => Promise; + findContactByPhoneNumber: (phoneNumber: string) => Contact | null; + + // Initialize + initialize: () => Promise; +} + +export const useContactsStore = create((set, get) => ({ + // Initial state + contacts: [], + loading: false, + error: null, + hasPermission: false, + isInitialized: false, + isSupported: Platform.OS !== "web", // Contacts not supported on web + + // Setters + setContacts: (contacts) => set({ contacts }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + setHasPermission: (hasPermission) => set({ hasPermission }), + setIsInitialized: (isInitialized) => set({ isInitialized }), + + // Check permission status + checkPermission: async (): Promise => { + // Web: No permission needed (feature not available) + if (Platform.OS === "web" || !Contacts) { + set({ hasPermission: false, isSupported: false }); + return false; + } + + try { + const { status } = await Contacts.getPermissionsAsync(); + const granted = status === "granted"; + set({ hasPermission: granted }); + return granted; + } catch (err) { + console.error("Error checking contacts permission:", err); + set({ error: "Failed to check contacts permission" }); + return false; + } + }, + + // Request permission + requestPermission: async (): Promise => { + // Web: Show message that contacts aren't supported + if (Platform.OS === "web" || !Contacts) { + set({ hasPermission: false, isSupported: false }); + console.log("Contacts not supported on web platform"); + return; + } + + try { + set({ error: null }); + + const { status } = await Contacts.requestPermissionsAsync(); + + if (status === "granted") { + set({ hasPermission: true }); + await get().loadContacts(); + } else { + set({ hasPermission: false, error: "Contacts permission denied" }); + + Alert.alert( + "Permission Required", + "To show your contacts as potential recipients, please grant contacts permission in your device settings.", + [{ text: "OK", style: "default" }] + ); + } + } catch (err) { + console.error("Error requesting contacts permission:", err); + set({ error: "Failed to request contacts permission" }); + } + }, + + // Load contacts from device + loadContacts: async (): Promise => { + // Web: Skip loading + if (Platform.OS === "web" || !Contacts) { + set({ loading: false, contacts: [], isSupported: false }); + return; + } + + try { + set({ loading: true, error: null }); + + const { data } = await Contacts.getContactsAsync({ + fields: [ + Contacts.Fields.ID, + Contacts.Fields.Name, + Contacts.Fields.FirstName, + Contacts.Fields.LastName, + Contacts.Fields.PhoneNumbers, + Contacts.Fields.Emails, + Contacts.Fields.ImageAvailable, + Contacts.Fields.Image, + ], + sort: Contacts.SortTypes.FirstName, + }); + + // Filter and format contacts + const formattedContacts: Contact[] = data + .filter((contact: any) => { + return ( + contact.name && + contact.phoneNumbers && + contact.phoneNumbers.length > 0 && + contact.phoneNumbers.some( + (phone: any) => phone.number && isValidPhoneNumber(phone.number) + ) + ); + }) + .map((contact: any) => ({ + id: contact.id || "", + name: contact.name || "", + firstName: contact.firstName, + lastName: contact.lastName, + phoneNumbers: contact.phoneNumbers + ?.filter( + (phone: any) => phone.number && isValidPhoneNumber(phone.number) + ) + ?.map((phone: any) => ({ + number: formatPhoneNumber(phone.number || ""), + formattedNumber: formatPhoneNumber(phone.number || ""), + isPrimary: phone.isPrimary, + label: phone.label, + })), + emails: contact.emails?.map((email: any) => ({ + email: email.email || "", + isPrimary: email.isPrimary, + label: email.label, + })), + imageAvailable: contact.imageAvailable, + image: contact.image, + })); + + set({ contacts: formattedContacts }); + console.log(`Loaded ${formattedContacts.length} contacts`); + + try { + await awardFirstContactSyncPoints(); + } catch (error) { + console.warn( + "[ContactsStore] Failed to award contact sync points", + error + ); + } + } catch (err) { + console.error("Error loading contacts:", err); + set({ error: "Failed to load contacts", contacts: [] }); + } finally { + set({ loading: false }); + } + }, + + // Refresh contacts + refreshContacts: async (): Promise => { + if (Platform.OS === "web") { + return; + } + + const permitted = await get().checkPermission(); + if (permitted) { + await get().loadContacts(); + } + }, + + // Find contact by phone number + findContactByPhoneNumber: (phoneNumber: string): Contact | null => { + const { contacts } = get(); + const formattedNumber = formatPhoneNumber(phoneNumber); + return ( + contacts.find((contact) => + contact.phoneNumbers?.some( + (phone) => phone.formattedNumber === formattedNumber + ) + ) || null + ); + }, + + // Initialize on mount + initialize: async () => { + // Web: Mark as initialized but skip permission/loading + if (Platform.OS === "web") { + set({ isInitialized: true, isSupported: false }); + return; + } + + const permitted = await get().checkPermission(); + if (permitted) { + await get().loadContacts(); + } + set({ isInitialized: true }); + }, +})); diff --git a/lib/stores/index.ts b/lib/stores/index.ts new file mode 100644 index 0000000..9c92431 --- /dev/null +++ b/lib/stores/index.ts @@ -0,0 +1,11 @@ +// Export all stores from a single location +export { useAuthStore } from "./authStore"; +export { useContactsStore } from "./contactsStore"; +export { useRecipientsStore } from "./recipientsStore"; +export { useTabStore } from "./tabStore"; +export { usePinStore } from "./pinStore"; +export { useUiStore } from "./uiStore"; +export { useLangStore } from "./langStore"; + +// Re-export types +export type { Contact } from "./contactsStore"; diff --git a/lib/stores/langStore.ts b/lib/stores/langStore.ts new file mode 100644 index 0000000..36cf24a --- /dev/null +++ b/lib/stores/langStore.ts @@ -0,0 +1,45 @@ +import { create } from "zustand"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export type Language = "en" | "am" | "fr" | "ti" | "om"; + +interface LangState { + language: Language; + setLanguage: (language: Language) => void; +} + +const LANGUAGE_STORAGE_KEY = "app_language"; + +export const useLangStore = create((set) => { + // Hydrate language from AsyncStorage (non-blocking) + (async () => { + try { + const stored = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY); + if ( + stored === "en" || + stored === "am" || + stored === "fr" || + stored === "ti" || + stored === "om" + ) { + set({ language: stored as Language }); + } + } catch (error) { + if (__DEV__) { + console.warn("Failed to load language from storage", error); + } + } + })(); + + return { + language: "en", + setLanguage: (language: Language) => { + set({ language }); + AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language).catch((error) => { + if (__DEV__) { + console.warn("Failed to persist language", error); + } + }); + }, + }; +}); diff --git a/lib/stores/pinStore.ts b/lib/stores/pinStore.ts new file mode 100644 index 0000000..57291a0 --- /dev/null +++ b/lib/stores/pinStore.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; + +interface PinStore { + pinConfirmed: boolean; + setPinConfirmed: (confirmed: boolean) => void; + resetPinConfirmed: () => void; +} + +export const usePinStore = create((set) => ({ + pinConfirmed: false, + setPinConfirmed: (confirmed: boolean) => set({ pinConfirmed: confirmed }), + resetPinConfirmed: () => set({ pinConfirmed: false }), +})); + +export default usePinStore; diff --git a/lib/stores/recipientsStore.ts b/lib/stores/recipientsStore.ts new file mode 100644 index 0000000..e09fcbd --- /dev/null +++ b/lib/stores/recipientsStore.ts @@ -0,0 +1,386 @@ +/** + * Recipients Store - Platform-aware recipient management + * Uses Firebase abstraction layer for cross-platform support + */ +import { create } from 'zustand'; +import { doc } from '../firebase'; +import { RecipientService, Recipient } from '../services/recipientService'; +import { withGlobalLoading } from './uiStore'; + +export interface RecipientDetailCacheEntry { + recipient: Recipient | null; + loading: boolean; + error: string | null; + lastFetched?: number; +} + +const buildDetailEntry = ( + existing: RecipientDetailCacheEntry | undefined, + overrides: Partial +): RecipientDetailCacheEntry => ({ + recipient: existing?.recipient ?? null, + loading: existing?.loading ?? false, + error: existing?.error ?? null, + lastFetched: existing?.lastFetched, + ...overrides, +}); + +const snapshotExists = (docSnap: any): boolean => { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; +}; + +const normalizeRecipient = (data: Recipient | (Recipient & { createdAt?: any; updatedAt?: any }) | null | undefined): Recipient | null => { + if (!data) { + return null; + } + + const createdAtValue = + (data.createdAt as any)?.toDate?.() ?? data.createdAt ?? new Date(); + const updatedAtValue = + (data.updatedAt as any)?.toDate?.() ?? data.updatedAt ?? new Date(); + + return { + ...data, + createdAt: createdAtValue, + updatedAt: updatedAtValue, + }; +}; + +interface RecipientsState { + // State + recipients: Recipient[]; + recipientDetails: Record; + loading: boolean; + error: string | null; + addError: string | null; + updateError: string | null; + deleteError: string | null; + isInitialized: boolean; + currentUser: any | null; + + // Actions + setRecipients: (recipients: Recipient[]) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + setAddError: (error: string | null) => void; + setUpdateError: (error: string | null) => void; + setDeleteError: (error: string | null) => void; + setIsInitialized: (isInitialized: boolean) => void; + setCurrentUser: (user: any | null) => void; + + // Data actions + fetchRecipients: () => Promise; + addRecipient: (recipientData: { fullName: string; phoneNumber: string }) => Promise; + updateRecipient: (recipientId: string, updates: { fullName?: string; phoneNumber?: string }) => Promise; + deleteRecipient: (recipientId: string) => Promise; + refreshRecipients: () => Promise; + ensureRecipientDetail: (recipientId: string) => void; + refreshRecipientDetail: (recipientId: string) => Promise; + invalidateRecipientDetail: (recipientId: string) => Promise; + removeRecipientDetail: (recipientId: string) => void; + clearRecipientDetails: () => void; + + // Error clearing + clearErrors: () => void; + clearAddError: () => void; + clearUpdateError: () => void; + clearDeleteError: () => void; + + // Initialize + initialize: (user: any | null) => Promise; +} + +export const useRecipientsStore = create((set, get) => ({ + // Initial state + recipients: [], + recipientDetails: {}, + loading: false, + error: null, + addError: null, + updateError: null, + deleteError: null, + isInitialized: false, + currentUser: null, + + // Setters + setRecipients: (recipients) => set({ recipients }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + setAddError: (addError) => set({ addError }), + setUpdateError: (updateError) => set({ updateError }), + setDeleteError: (deleteError) => set({ deleteError }), + setIsInitialized: (isInitialized) => set({ isInitialized }), + setCurrentUser: (currentUser) => set({ currentUser }), + + // Fetch recipients + fetchRecipients: async () => { + const { currentUser } = get(); + + if (!currentUser?.uid) { + set({ recipients: [], recipientDetails: {}, error: null, isInitialized: true }); + return; + } + + set({ loading: true, error: null }); + + try { + const userRecipients = await RecipientService.getUserRecipients(currentUser.uid); + const normalizedRecipients = userRecipients + .map((recipient) => normalizeRecipient(recipient)) + .filter((recipient): recipient is Recipient => recipient !== null); + + set((state) => { + const nextDetails = { ...state.recipientDetails }; + normalizedRecipients.forEach((recipient) => { + const detailEntry = nextDetails[recipient.id]; + if (detailEntry) { + nextDetails[recipient.id] = buildDetailEntry(detailEntry, { + recipient, + loading: false, + error: null, + lastFetched: Date.now(), + }); + } + }); + + return { + recipients: normalizedRecipients, + recipientDetails: nextDetails, + }; + }); + } catch (err) { + console.error('Error fetching recipients:', err); + set({ + error: err instanceof Error ? err.message : 'Failed to fetch recipients', + recipients: [], + recipientDetails: {}, + }); + } finally { + set({ loading: false, isInitialized: true }); + } + }, + + // Add recipient + addRecipient: async (recipientData: { fullName: string; phoneNumber: string }) => { + const { currentUser } = get(); + + if (!currentUser?.uid) { + set({ addError: 'User not found' }); + return; + } + + set({ loading: true, addError: null }); + + try { + const result = await withGlobalLoading(() => RecipientService.addRecipient(currentUser.uid, recipientData)); + if (!result.success) { + set({ addError: result.error || 'Failed to add recipient' }); + return; + } + + await get().fetchRecipients(); + if (result.recipientId) { + await get().invalidateRecipientDetail(result.recipientId); + } + } catch (err) { + set({ addError: err instanceof Error ? err.message : 'Failed to add recipient' }); + } finally { + set({ loading: false }); + } + }, + + // Update recipient + updateRecipient: async (recipientId: string, updates: { fullName?: string; phoneNumber?: string }) => { + set({ loading: true, updateError: null }); + + try { + const result = await withGlobalLoading(() => RecipientService.updateRecipient(recipientId, updates)); + if (!result.success) { + set({ updateError: result.error || 'Failed to update recipient' }); + return; + } + + await get().fetchRecipients(); + await get().invalidateRecipientDetail(recipientId); + } catch (err) { + set({ updateError: err instanceof Error ? err.message : 'Failed to update recipient' }); + } finally { + set({ loading: false }); + } + }, + + // Delete recipient + deleteRecipient: async (recipientId: string) => { + set({ loading: true, deleteError: null }); + + try { + const result = await withGlobalLoading(() => RecipientService.deleteRecipient(recipientId)); + if (!result.success) { + set({ deleteError: result.error || 'Failed to delete recipient' }); + return; + } + + await get().fetchRecipients(); + get().removeRecipientDetail(recipientId); + } catch (err) { + set({ deleteError: err instanceof Error ? err.message : 'Failed to delete recipient' }); + } finally { + set({ loading: false }); + } + }, + + // Refresh recipients + refreshRecipients: async () => { + await get().fetchRecipients(); + }, + + ensureRecipientDetail: (recipientId: string) => { + if (!recipientId) { + return; + } + + const existing = get().recipientDetails[recipientId]; + if (existing?.loading) { + return; + } + + if (existing?.lastFetched) { + return; + } + + void get().refreshRecipientDetail(recipientId).catch((error) => { + set((state) => ({ + recipientDetails: { + ...state.recipientDetails, + [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch recipient', + }), + }, + })); + }); + }, + + refreshRecipientDetail: async (recipientId: string) => { + if (!recipientId) { + return null; + } + + set((state) => ({ + recipientDetails: { + ...state.recipientDetails, + [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { + loading: true, + error: null, + }), + }, + })); + + try { + const docRef = doc('recipients', recipientId); + const docSnap = await docRef.get(); + const recipient = normalizeRecipient(snapshotExists(docSnap) ? (docSnap.data() as Recipient) : null); + + set((state) => ({ + recipientDetails: { + ...state.recipientDetails, + [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { + recipient, + loading: false, + error: recipient ? null : 'Recipient not found', + lastFetched: Date.now(), + }), + }, + })); + + return recipient; + } catch (error) { + set((state) => ({ + recipientDetails: { + ...state.recipientDetails, + [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch recipient', + }), + }, + })); + throw error; + } + }, + + invalidateRecipientDetail: async (recipientId: string) => { + if (!recipientId) { + return null; + } + + set((state) => ({ + recipientDetails: { + ...state.recipientDetails, + [recipientId]: buildDetailEntry(state.recipientDetails[recipientId], { + loading: true, + error: null, + lastFetched: undefined, + }), + }, + })); + + return get().refreshRecipientDetail(recipientId); + }, + + removeRecipientDetail: (recipientId: string) => { + set((state) => { + const { [recipientId]: _removed, ...rest } = state.recipientDetails; + return { recipientDetails: rest }; + }); + }, + + clearRecipientDetails: () => { + set({ recipientDetails: {} }); + }, + + // Error clearing + clearErrors: () => set({ + error: null, + addError: null, + updateError: null, + deleteError: null, + }), + + clearAddError: () => set({ addError: null }), + clearUpdateError: () => set({ updateError: null }), + clearDeleteError: () => set({ deleteError: null }), + + // Initialize + initialize: async (user: any | null) => { + const previousUser = get().currentUser; + + if (previousUser?.uid && previousUser.uid !== user?.uid) { + get().clearRecipientDetails(); + set({ recipients: [] }); + } + + set({ currentUser: user }); + + if (user?.uid) { + await get().fetchRecipients(); + } else { + get().clearRecipientDetails(); + set({ recipients: [], error: null, isInitialized: true }); + } + }, +})); + +export const getRecipientDetailFromCache = (recipientId: string): Recipient | null => { + if (!recipientId) { + return null; + } + return useRecipientsStore.getState().recipientDetails[recipientId]?.recipient ?? null; +}; diff --git a/lib/stores/tabStore.ts b/lib/stores/tabStore.ts new file mode 100644 index 0000000..41df933 --- /dev/null +++ b/lib/stores/tabStore.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand'; + +interface TabState { + lastVisitedTab: string | null; + setLastVisitedTab: (tab: string | null) => void; +} + +export const useTabStore = create((set) => ({ + lastVisitedTab: null, + setLastVisitedTab: (tab) => set({ lastVisitedTab: tab }), +})); diff --git a/lib/stores/transactionStore.ts b/lib/stores/transactionStore.ts new file mode 100644 index 0000000..ac19ec0 --- /dev/null +++ b/lib/stores/transactionStore.ts @@ -0,0 +1,349 @@ +/** + * Transaction Store - Platform-aware transaction management + * Uses Firebase abstraction layer for cross-platform support + */ +import { create } from 'zustand'; +import { doc, collection } from '../firebase'; +import type { Transaction } from '../services/transactionService'; + +export interface TransactionCacheEntry { + transactions: Transaction[]; + loading: boolean; + error: string | null; + lastFetched?: number; +} + +export interface TransactionDetailCacheEntry { + transaction: Transaction | null; + loading: boolean; + error: string | null; + lastFetched?: number; +} + +interface TransactionStoreState { + transactionsByUid: Record; + transactionDetails: Record; + ensureSubscription: (uid: string) => void; + refreshTransactions: (uid: string) => Promise; + invalidateTransactions: (uid: string) => Promise; + removeTransactions: (uid: string) => void; + ensureTransactionDetail: (transactionId: string) => void; + refreshTransactionDetail: (transactionId: string) => Promise; + invalidateTransactionDetail: (transactionId: string) => Promise; + removeTransactionDetail: (transactionId: string) => void; + clearAll: () => void; +} + +const buildEntry = ( + existing: TransactionCacheEntry | undefined, + overrides: Partial +): TransactionCacheEntry => ({ + transactions: existing?.transactions ?? [], + loading: existing?.loading ?? false, + error: existing?.error ?? null, + lastFetched: existing?.lastFetched, + ...overrides, +}); + +const buildDetailEntry = ( + existing: TransactionDetailCacheEntry | undefined, + overrides: Partial +): TransactionDetailCacheEntry => ({ + transaction: existing?.transaction ?? null, + loading: existing?.loading ?? false, + error: existing?.error ?? null, + lastFetched: existing?.lastFetched, + ...overrides, +}); + +const snapshotExists = (docSnap: any): boolean => { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; +}; + +const mapSnapshotToTransactions = (snapshot: any): Transaction[] => { + if (!snapshot || !snapshot.docs) { + return []; + } + + return snapshot.docs.map((docItem: any) => { + const data = docItem.data(); + return { + ...data, + id: data.id ?? docItem.id, + createdAt: data.createdAt?.toDate ? data.createdAt.toDate() : data.createdAt ?? new Date(), + updatedAt: data.updatedAt?.toDate ? data.updatedAt.toDate() : data.updatedAt ?? new Date(), + } as Transaction; + }); +}; + +const mapDocumentSnapshotToTransaction = ( + transactionId: string, + docSnap: any +): Transaction | null => { + if (!snapshotExists(docSnap)) { + return null; + } + + const data = docSnap.data(); + if (!data) { + return null; + } + + return { + ...data, + id: data.id ?? transactionId, + createdAt: data.createdAt?.toDate ? data.createdAt.toDate() : data.createdAt ?? new Date(), + updatedAt: data.updatedAt?.toDate ? data.updatedAt.toDate() : data.updatedAt ?? new Date(), + } as Transaction; +}; + +export const useTransactionStore = create((set, get) => ({ + transactionsByUid: {}, + transactionDetails: {}, + + ensureSubscription: (uid: string) => { + if (!uid) { + return; + } + + const existing = get().transactionsByUid[uid]; + if (existing?.loading) { + return; + } + + if (existing?.lastFetched) { + return; + } + + void get().refreshTransactions(uid).catch((error) => { + set((state) => ({ + transactionsByUid: { + ...state.transactionsByUid, + [uid]: buildEntry(state.transactionsByUid[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch transactions', + }), + }, + })); + }); + }, + + refreshTransactions: async (uid: string) => { + if (!uid) { + return []; + } + + set((state) => ({ + transactionsByUid: { + ...state.transactionsByUid, + [uid]: buildEntry(state.transactionsByUid[uid], { + loading: true, + error: null, + }), + }, + })); + + try { + const transactionsCollection = collection('transactions'); + const snapshot = await transactionsCollection + .where('uid', '==', uid) + .orderBy('createdAt', 'desc') + .get(); + + const transactions = mapSnapshotToTransactions(snapshot); + + set((state) => { + const nextDetails = { ...state.transactionDetails }; + transactions.forEach((transaction) => { + const detailEntry = nextDetails[transaction.id]; + if (detailEntry) { + nextDetails[transaction.id] = buildDetailEntry(detailEntry, { + transaction, + loading: false, + error: null, + lastFetched: Date.now(), + }); + } + }); + + return { + transactionsByUid: { + ...state.transactionsByUid, + [uid]: buildEntry(state.transactionsByUid[uid], { + transactions, + loading: false, + error: null, + lastFetched: Date.now(), + }), + }, + transactionDetails: nextDetails, + }; + }); + + return transactions; + } catch (error) { + set((state) => ({ + transactionsByUid: { + ...state.transactionsByUid, + [uid]: buildEntry(state.transactionsByUid[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch transactions', + }), + }, + })); + throw error; + } + }, + + invalidateTransactions: async (uid: string) => { + if (!uid) { + return []; + } + + set((state) => ({ + transactionsByUid: { + ...state.transactionsByUid, + [uid]: buildEntry(state.transactionsByUid[uid], { + loading: true, + error: null, + lastFetched: undefined, + }), + }, + })); + + return get().refreshTransactions(uid); + }, + + removeTransactions: (uid: string) => { + set((state) => { + const { [uid]: _removed, ...rest } = state.transactionsByUid; + return { transactionsByUid: rest }; + }); + }, + + ensureTransactionDetail: (transactionId: string) => { + if (!transactionId) { + return; + } + + const existing = get().transactionDetails[transactionId]; + if (existing?.loading) { + return; + } + + if (existing?.lastFetched) { + return; + } + + void get().refreshTransactionDetail(transactionId).catch((error) => { + set((state) => ({ + transactionDetails: { + ...state.transactionDetails, + [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch transaction', + }), + }, + })); + }); + }, + + refreshTransactionDetail: async (transactionId: string) => { + if (!transactionId) { + return null; + } + + set((state) => ({ + transactionDetails: { + ...state.transactionDetails, + [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { + loading: true, + error: null, + }), + }, + })); + + try { + const docRef = doc('transactions', transactionId); + const docSnap = await docRef.get(); + const transaction = mapDocumentSnapshotToTransaction(transactionId, docSnap); + + set((state) => ({ + transactionDetails: { + ...state.transactionDetails, + [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { + transaction, + loading: false, + error: transaction ? null : 'Transaction not found', + lastFetched: Date.now(), + }), + }, + })); + + return transaction; + } catch (error) { + set((state) => ({ + transactionDetails: { + ...state.transactionDetails, + [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch transaction', + }), + }, + })); + throw error; + } + }, + + invalidateTransactionDetail: async (transactionId: string) => { + if (!transactionId) { + return null; + } + + set((state) => ({ + transactionDetails: { + ...state.transactionDetails, + [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { + loading: true, + error: null, + lastFetched: undefined, + }), + }, + })); + + return get().refreshTransactionDetail(transactionId); + }, + + removeTransactionDetail: (transactionId: string) => { + set((state) => { + const { [transactionId]: _removed, ...rest } = state.transactionDetails; + return { transactionDetails: rest }; + }); + }, + + clearAll: () => { + set({ transactionsByUid: {}, transactionDetails: {} }); + }, +})); + +export const getTransactionsFromCache = (uid: string): Transaction[] => { + if (!uid) { + return []; + } + return useTransactionStore.getState().transactionsByUid[uid]?.transactions ?? []; +}; + +export const getTransactionDetailFromCache = (transactionId: string): Transaction | null => { + if (!transactionId) { + return null; + } + return useTransactionStore.getState().transactionDetails[transactionId]?.transaction ?? null; +}; diff --git a/lib/stores/uiStore.ts b/lib/stores/uiStore.ts new file mode 100644 index 0000000..4b18d25 --- /dev/null +++ b/lib/stores/uiStore.ts @@ -0,0 +1,85 @@ +import { create } from "zustand"; + +type ShowLoaderFn = (options?: { opaque?: boolean }) => void; + +type HideLoaderFn = () => void; + +type SetLoadingFn = (loading: boolean, options?: { opaque?: boolean }) => void; + +interface UiState { + loadingCount: number; + isLoading: boolean; + isOpaque: boolean; + showLoader: ShowLoaderFn; + hideLoader: HideLoaderFn; + setLoading: SetLoadingFn; + setOpaque: (opaque: boolean) => void; + resetLoading: () => void; +} + +export const useUiStore = create((set) => ({ + loadingCount: 0, + isLoading: false, + isOpaque: false, + showLoader: (options) => + set((state) => { + const nextCount = state.loadingCount + 1; + return { + loadingCount: nextCount, + isLoading: true, + // Only set opaque if explicitly passed, otherwise keep current state + isOpaque: options?.opaque ?? state.isOpaque, + }; + }), + hideLoader: () => + set((state) => { + const nextCount = Math.max(state.loadingCount - 1, 0); + return { + loadingCount: nextCount, + isLoading: nextCount > 0, + // Reset opaque when no more loaders + isOpaque: nextCount > 0 ? state.isOpaque : false, + }; + }), + setLoading: (loading: boolean, options) => + set(() => + loading + ? { + loadingCount: 1, + isLoading: true, + isOpaque: options?.opaque ?? false, + } + : { + loadingCount: 0, + isLoading: false, + isOpaque: false, + } + ), + setOpaque: (opaque: boolean) => set({ isOpaque: opaque }), + resetLoading: () => + set({ loadingCount: 0, isLoading: false, isOpaque: false }), +})); + +export const showGlobalLoader: ShowLoaderFn = (options) => { + useUiStore.getState().showLoader(options); +}; + +export const hideGlobalLoader: HideLoaderFn = () => { + useUiStore.getState().hideLoader(); +}; + +export const setGlobalLoading: SetLoadingFn = (loading: boolean, options) => { + useUiStore.getState().setLoading(loading, options); +}; + +export const withGlobalLoading = async ( + operation: () => Promise | T +): Promise => { + showGlobalLoader(); + try { + const result = operation(); + return await Promise.resolve(result); + } finally { + hideGlobalLoader(); + } +}; diff --git a/lib/stores/userProfileStore.ts b/lib/stores/userProfileStore.ts new file mode 100644 index 0000000..a27dd01 --- /dev/null +++ b/lib/stores/userProfileStore.ts @@ -0,0 +1,258 @@ +/** + * User Profile Store - Platform-aware user profile management + * Uses Firebase abstraction layer for cross-platform support + */ +import { create } from 'zustand'; +import { doc } from '../firebase'; +import type { UserProfile } from '../services/authServices'; + +export interface ProfileCacheEntry { + profile: UserProfile | null; + loading: boolean; + error: string | null; + lastFetched?: number; +} + +interface UserProfileStoreState { + profiles: Record; + subscriptions: Record void>; + ensureSubscription: (uid: string) => void; + refreshProfile: (uid: string) => Promise; + invalidateProfile: (uid: string) => Promise; + removeProfile: (uid: string) => void; + clearAll: () => void; +} + +const buildEntry = ( + existing: ProfileCacheEntry | undefined, + overrides: Partial +): ProfileCacheEntry => ({ + profile: existing?.profile ?? null, + loading: existing?.loading ?? false, + error: existing?.error ?? null, + lastFetched: existing?.lastFetched, + ...overrides, +}); + +const snapshotExists = (docSnap: any): boolean => { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; +}; + +export const useUserProfileStore = create((set, get) => ({ + profiles: {}, + subscriptions: {}, + + ensureSubscription: (uid: string) => { + if (!uid) { + return; + } + + // If already subscribed, don't create another subscription + if (get().subscriptions[uid]) { + return; + } + + const existing = get().profiles[uid]; + if (existing?.loading) { + return; + } + + // If we have cached data, set up a real-time listener + if (existing?.lastFetched) { + const userDocRef = doc('users', uid); + const unsubscribe = userDocRef.onSnapshot( + (docSnap: any) => { + const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + profile, + loading: false, + error: profile ? null : 'Profile not found', + lastFetched: Date.now(), + }), + }, + })); + }, + (error: any) => { + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: false, + error: error.message || 'Failed to fetch profile', + }), + }, + })); + } + ); + + set((state) => ({ + subscriptions: { + ...state.subscriptions, + [uid]: unsubscribe, + }, + })); + return; + } + + // Initial fetch + void get().refreshProfile(uid).catch((error) => { + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch profile', + }), + }, + })); + }); + }, + + refreshProfile: async (uid: string) => { + if (!uid) { + return null; + } + + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: true, + error: null, + }), + }, + })); + + try { + const userDocRef = doc('users', uid); + const docSnap = await userDocRef.get(); + const profile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; + + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + profile, + loading: false, + error: profile ? null : 'Profile not found', + lastFetched: Date.now(), + }), + }, + })); + + // Set up real-time listener after initial fetch if not already subscribed + if (!get().subscriptions[uid] && profile) { + const unsubscribe = userDocRef.onSnapshot( + (docSnap: any) => { + const updatedProfile = snapshotExists(docSnap) ? (docSnap.data() as UserProfile) : null; + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + profile: updatedProfile, + loading: false, + error: updatedProfile ? null : 'Profile not found', + lastFetched: Date.now(), + }), + }, + })); + }, + (error: any) => { + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: false, + error: error.message || 'Failed to fetch profile', + }), + }, + })); + } + ); + + set((state) => ({ + subscriptions: { + ...state.subscriptions, + [uid]: unsubscribe, + }, + })); + } + + return profile; + } catch (error) { + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch profile', + }), + }, + })); + throw error; + } + }, + + invalidateProfile: async (uid: string) => { + if (!uid) { + return null; + } + + set((state) => ({ + profiles: { + ...state.profiles, + [uid]: buildEntry(state.profiles[uid], { + loading: true, + error: null, + lastFetched: undefined, + }), + }, + })); + + return get().refreshProfile(uid); + }, + + removeProfile: (uid: string) => { + // Unsubscribe from listener if exists + const unsubscribe = get().subscriptions[uid]; + if (unsubscribe) { + unsubscribe(); + } + + set((state) => { + const { [uid]: _removedProfile, ...restProfiles } = state.profiles; + const { [uid]: _removedSub, ...restSubs } = state.subscriptions; + return { + profiles: restProfiles, + subscriptions: restSubs, + }; + }); + }, + + clearAll: () => { + // Unsubscribe from all listeners + const subscriptions = get().subscriptions; + Object.values(subscriptions).forEach((unsubscribe) => { + unsubscribe(); + }); + set({ profiles: {}, subscriptions: {} }); + }, +})); + +export const getUserProfileFromCache = (uid: string): UserProfile | null => { + if (!uid) { + return null; + } + return useUserProfileStore.getState().profiles[uid]?.profile ?? null; +}; diff --git a/lib/stores/userWalletStore.ts b/lib/stores/userWalletStore.ts new file mode 100644 index 0000000..7302df5 --- /dev/null +++ b/lib/stores/userWalletStore.ts @@ -0,0 +1,271 @@ +/** + * User Wallet Store - Platform-aware wallet management + * Uses Firebase abstraction layer for cross-platform support + */ +import { create } from 'zustand'; +import { doc } from '../firebase'; +import type { UserWallet } from '../services/walletService'; + +export interface WalletCacheEntry { + wallet: UserWallet | null; + loading: boolean; + error: string | null; + lastFetched?: number; +} + +interface UserWalletStoreState { + wallets: Record; + subscriptions: Record void>; + ensureSubscription: (uid: string) => void; + refreshWallet: (uid: string) => Promise; + invalidateWallet: (uid: string) => Promise; + removeWallet: (uid: string) => void; + clearAll: () => void; + setWalletState: (uid: string, updates: Partial) => void; +} + +const buildEntry = ( + existing: WalletCacheEntry | undefined, + overrides: Partial +): WalletCacheEntry => ({ + wallet: existing?.wallet ?? null, + loading: existing?.loading ?? false, + error: existing?.error ?? null, + lastFetched: existing?.lastFetched, + ...overrides, +}); + +const snapshotExists = (docSnap: any): boolean => { + const existsValue = docSnap?.exists; + if (typeof existsValue === 'function') { + try { + return !!existsValue.call(docSnap); + } catch { + return false; + } + } + return !!existsValue; +}; + +export const useUserWalletStore = create((set, get) => ({ + wallets: {}, + subscriptions: {}, + + ensureSubscription: (uid: string) => { + if (!uid) { + return; + } + + // If already subscribed, don't create another subscription + if (get().subscriptions[uid]) { + return; + } + + const existing = get().wallets[uid]; + if (existing?.loading) { + return; + } + + // If we have cached data, set up a real-time listener + if (existing?.lastFetched) { + const walletDocRef = doc('wallets', uid); + const unsubscribe = walletDocRef.onSnapshot( + (docSnap: any) => { + const wallet = snapshotExists(docSnap) ? (docSnap.data() as UserWallet) : null; + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + wallet, + loading: false, + error: wallet ? null : 'Wallet not found', + lastFetched: Date.now(), + }), + }, + })); + }, + (error: any) => { + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: false, + error: error.message || 'Failed to fetch wallet', + }), + }, + })); + } + ); + + set((state) => ({ + subscriptions: { + ...state.subscriptions, + [uid]: unsubscribe, + }, + })); + return; + } + + // Initial fetch + void get().refreshWallet(uid).catch((error) => { + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch wallet', + }), + }, + })); + }); + }, + + refreshWallet: async (uid: string) => { + if (!uid) { + return null; + } + + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: true, + error: null, + }), + }, + })); + + try { + const walletDocRef = doc('wallets', uid); + const docSnap = await walletDocRef.get(); + const wallet = snapshotExists(docSnap) ? (docSnap.data() as UserWallet) : null; + + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + wallet, + loading: false, + error: wallet ? null : 'Wallet not found', + lastFetched: Date.now(), + }), + }, + })); + + // Set up real-time listener after initial fetch if not already subscribed + if (!get().subscriptions[uid] && wallet) { + const unsubscribe = walletDocRef.onSnapshot( + (docSnap: any) => { + const updatedWallet = snapshotExists(docSnap) ? (docSnap.data() as UserWallet) : null; + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + wallet: updatedWallet, + loading: false, + error: updatedWallet ? null : 'Wallet not found', + lastFetched: Date.now(), + }), + }, + })); + }, + (error: any) => { + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: false, + error: error.message || 'Failed to fetch wallet', + }), + }, + })); + } + ); + + set((state) => ({ + subscriptions: { + ...state.subscriptions, + [uid]: unsubscribe, + }, + })); + } + + return wallet; + } catch (error) { + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: false, + error: error instanceof Error ? error.message : 'Failed to fetch wallet', + }), + }, + })); + throw error; + } + }, + + invalidateWallet: async (uid: string) => { + if (!uid) { + return null; + } + + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], { + loading: true, + error: null, + lastFetched: undefined, + }), + }, + })); + + return get().refreshWallet(uid); + }, + + removeWallet: (uid: string) => { + // Unsubscribe from listener if exists + const unsubscribe = get().subscriptions[uid]; + if (unsubscribe) { + unsubscribe(); + } + + set((state) => { + const { [uid]: _removedWallet, ...restWallets } = state.wallets; + const { [uid]: _removedSub, ...restSubs } = state.subscriptions; + return { + wallets: restWallets, + subscriptions: restSubs, + }; + }); + }, + + clearAll: () => { + // Unsubscribe from all listeners + const subscriptions = get().subscriptions; + Object.values(subscriptions).forEach((unsubscribe) => { + unsubscribe(); + }); + set({ wallets: {}, subscriptions: {} }); + }, + + setWalletState: (uid: string, updates: Partial) => { + if (!uid) { + return; + } + set((state) => ({ + wallets: { + ...state.wallets, + [uid]: buildEntry(state.wallets[uid], updates), + }, + })); + }, +})); + +export const getWalletFromCache = (uid: string): UserWallet | null => { + if (!uid) { + return null; + } + return useUserWalletStore.getState().wallets[uid]?.wallet ?? null; +}; diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..df3613e --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} \ No newline at end of file diff --git a/lib/utils/alertUtils.ts b/lib/utils/alertUtils.ts new file mode 100644 index 0000000..5a17bd0 --- /dev/null +++ b/lib/utils/alertUtils.ts @@ -0,0 +1,202 @@ +import { Alert, Platform } from 'react-native'; + +// Track active alerts to prevent stacking +const activeAlerts = new Set(); + +export interface AlertButton { + text?: string; + onPress?: () => void; + style?: 'default' | 'cancel' | 'destructive'; +} + +export interface ShowAlertOptions { + title: string; + message?: string; + buttons?: AlertButton[]; + cancelable?: boolean; + onDismiss?: () => void; + alertId?: string; +} + +/** + * Cross-platform alert that works on web, iOS, and Android + * On web, uses window.confirm() for confirmation dialogs and window.alert() for simple alerts + * On native platforms, uses React Native's Alert.alert() + */ +export const showAlert = (options: ShowAlertOptions) => { + const { + title, + message, + buttons, + cancelable, + onDismiss, + alertId, + } = options; + + // Generate unique ID for this alert + const id = alertId || `${title}-${message || ''}`; + + // Prevent stacking + if (activeAlerts.has(id)) { + return; + } + + activeAlerts.add(id); + + if (Platform.OS === 'web') { + // Web implementation + handleWebAlert(id, title, message, buttons, onDismiss); + } else { + // Native implementation + handleNativeAlert(id, title, message, buttons, cancelable, onDismiss); + } +}; + +/** + * Handle alerts on web using window.confirm/alert + */ +const handleWebAlert = ( + id: string, + title: string, + message?: string, + buttons?: AlertButton[], + onDismiss?: () => void +) => { + const fullMessage = message ? `${title}\n\n${message}` : title; + + // Use setTimeout to ensure the alert runs after current execution + setTimeout(() => { + try { + if (!buttons || buttons.length === 0) { + // Simple alert with OK button + window.alert(fullMessage); + activeAlerts.delete(id); + onDismiss?.(); + } else if (buttons.length === 1) { + // Single button - use alert + window.alert(fullMessage); + activeAlerts.delete(id); + buttons[0].onPress?.(); + onDismiss?.(); + } else if (buttons.length === 2) { + // Two buttons - use confirm + // Find cancel and confirm buttons + const cancelButton = buttons.find(b => b.style === 'cancel'); + const confirmButton = buttons.find(b => b.style !== 'cancel') || buttons[1]; + + const result = window.confirm(fullMessage); + activeAlerts.delete(id); + + if (result) { + // User clicked OK - trigger the non-cancel button + confirmButton?.onPress?.(); + } else { + // User clicked Cancel + cancelButton?.onPress?.(); + } + onDismiss?.(); + } else { + // More than 2 buttons - use confirm for first two, warn about limitation + console.warn('Web alerts only support up to 2 buttons. Additional buttons will be ignored.'); + const cancelButton = buttons.find(b => b.style === 'cancel'); + const confirmButton = buttons.find(b => b.style === 'destructive') || + buttons.find(b => b.style !== 'cancel') || + buttons[0]; + + const result = window.confirm(fullMessage); + activeAlerts.delete(id); + + if (result) { + confirmButton?.onPress?.(); + } else { + cancelButton?.onPress?.(); + } + onDismiss?.(); + } + } catch (error) { + activeAlerts.delete(id); + console.error('Error showing web alert:', error); + } + }, 0); +}; + +/** + * Handle alerts on native platforms using React Native Alert + */ +const handleNativeAlert = ( + id: string, + title: string, + message?: string, + buttons?: AlertButton[], + cancelable?: boolean, + onDismiss?: () => void +) => { + // Wrap button callbacks to clear the alert ID + const wrappedButtons = buttons?.map(button => ({ + ...button, + onPress: () => { + activeAlerts.delete(id); + button.onPress?.(); + }, + })); + + // Wrap onDismiss to clear the alert ID + const wrappedOptions = { + cancelable, + onDismiss: () => { + activeAlerts.delete(id); + onDismiss?.(); + }, + }; + + Alert.alert(title, message, wrappedButtons, wrappedOptions); +}; + +/** + * Simple alert with just an OK button + * Works on all platforms + */ +export const alertOk = (title: string, message?: string, onOk?: () => void) => { + showAlert({ + title, + message, + buttons: [{ text: 'OK', onPress: onOk }], + }); +}; + +/** + * Confirmation dialog with Cancel and Confirm buttons + * Works on all platforms + */ +export const alertConfirm = ( + title: string, + message: string, + onConfirm: () => void, + onCancel?: () => void, + confirmText: string = 'Confirm', + cancelText: string = 'Cancel', + destructive: boolean = false +) => { + showAlert({ + title, + message, + buttons: [ + { text: cancelText, style: 'cancel', onPress: onCancel }, + { text: confirmText, style: destructive ? 'destructive' : 'default', onPress: onConfirm }, + ], + }); +}; + +/** + * Clear a specific alert from the active set (useful for manual cleanup) + */ +export const clearAlert = (alertId: string) => { + activeAlerts.delete(alertId); +}; + +/** + * Clear all active alerts (useful for cleanup on unmount) + */ +export const clearAllAlerts = () => { + activeAlerts.clear(); +}; diff --git a/lib/utils/feeUtils.ts b/lib/utils/feeUtils.ts new file mode 100644 index 0000000..5961e5f --- /dev/null +++ b/lib/utils/feeUtils.ts @@ -0,0 +1,75 @@ +/** + * Fee calculation utilities for money transactions + */ + +export const PROCESSING_FEE_RATE = 0.0125; // 1.25% + +/** + * Calculate the processing fee for a given amount + * @param amount - The transaction amount in cents + * @returns The processing fee in cents + */ +export const calculateProcessingFee = (amount: number): number => { + return Math.ceil(amount * PROCESSING_FEE_RATE); +}; + +/** + * Calculate the total amount required (including processing fee) for sending money + * @param amount - The amount to send in cents + * @returns The total amount required in cents (amount + processing fee) + */ +export const calculateTotalAmountForSending = (amount: number): number => { + const fee = calculateProcessingFee(amount); + return amount + fee; +}; + +/** + * Calculate the total amount required (including processing fee) for accepting a money request + * @param amount - The amount to send in cents + * @returns The total amount required in cents (amount + processing fee) + */ +export const calculateTotalAmountForRequesting = (amount: number): number => { + const fee = calculateProcessingFee(amount); + return amount + fee; +}; + +/** + * Format fee information for display + * @param amount - The base amount in cents + * @returns Object with formatted fee information + */ +export const getFeeInformation = (amount: number) => { + const fee = calculateProcessingFee(amount); + const total = amount + fee; + + return { + baseAmount: amount, + fee, + total, + feePercentage: PROCESSING_FEE_RATE * 100, + formatted: { + baseAmount: (amount / 100).toFixed(2), + fee: (fee / 100).toFixed(2), + total: (total / 100).toFixed(2), + } + }; +}; + +/** + * Validate if user has sufficient balance for a transaction including processing fee + * @param userBalance - User's current balance in cents + * @param transactionAmount - The transaction amount in cents + * @returns Object with validation result and fee information + */ +export const validateBalanceWithFee = (userBalance: number, transactionAmount: number) => { + const totalRequired = calculateTotalAmountForSending(transactionAmount); + const feeInfo = getFeeInformation(transactionAmount); + + return { + hasSufficientBalance: userBalance >= totalRequired, + requiredBalance: totalRequired, + currentBalance: userBalance, + shortfall: Math.max(0, totalRequired - userBalance), + feeInfo + }; +}; diff --git a/lib/utils/monetaryUtils.ts b/lib/utils/monetaryUtils.ts new file mode 100644 index 0000000..4b276ca --- /dev/null +++ b/lib/utils/monetaryUtils.ts @@ -0,0 +1,39 @@ +import { Big } from 'big.js'; + +export const parseDisplayToCents = (value: string): number => { + if (!value || value === "") return 0; + + // Handle edge cases + if (value === "." || value === "0.") return 0; + + try { + const bigValue = new Big(value); + // Multiply by 100 and round to get cents + return bigValue.times(100).round(0).toNumber(); + } catch (error) { + return 0; + } +}; + +export const formatDisplayAmount = (value: string): string => { + if (!value || value === "") return "0.00"; + + // Handle edge cases + if (value === ".") return "0.00"; + if (value === "0.") return "0.00"; + + if (value.endsWith(".")) { + return value + "00"; + } + + if (value.includes(".") && value.split(".")[1].length === 1) { + return value + "0"; + } + + try { + const bigValue = new Big(value); + return bigValue.toFixed(2); + } catch (error) { + return "0.00"; + } +}; diff --git a/lib/utils/phoneUtils.ts b/lib/utils/phoneUtils.ts new file mode 100644 index 0000000..aa3d516 --- /dev/null +++ b/lib/utils/phoneUtils.ts @@ -0,0 +1,57 @@ +import { parsePhoneNumberWithError, isValidPhoneNumber as libIsValidPhoneNumber, formatIncompletePhoneNumber, CountryCode } from 'libphonenumber-js'; + +export const formatPhoneNumber = (phoneNumber: string, defaultCountry: CountryCode = 'ET'): string => { + if (!phoneNumber) return ''; + + try { + const parsedNumber = parsePhoneNumberWithError(phoneNumber, defaultCountry); + if (parsedNumber && parsedNumber.isValid()) { + const e164Formatted = parsedNumber.format('E.164'); + return parsedNumber.format('E.164'); // Returns format like +251911234567 + } + } catch (error) { + try { + const formatted = formatIncompletePhoneNumber(phoneNumber, defaultCountry); + if (formatted) { + return formatted; + } + } catch (formatError) { + console.warn('Failed to format phone number:', phoneNumber, formatError); + } + } + return phoneNumber; +}; + +export const isValidPhoneNumber = (phoneNumber: string, defaultCountry: CountryCode = 'ET'): boolean => { + if (!phoneNumber) return false; + + try { + return libIsValidPhoneNumber(phoneNumber, defaultCountry); + } catch (error) { + return false; + } +}; + +export const parsePhoneNumberDetails = (phoneNumber: string, defaultCountry: CountryCode = 'ET') => { + if (!phoneNumber) return null; + + try { + const parsedNumber = parsePhoneNumberWithError(phoneNumber, defaultCountry); + if (parsedNumber && parsedNumber.isValid()) { + return { + country: parsedNumber.country, + countryCallingCode: parsedNumber.countryCallingCode, + nationalNumber: parsedNumber.nationalNumber, + international: parsedNumber.format('INTERNATIONAL'), + e164: parsedNumber.format('E.164'), + national: parsedNumber.format('NATIONAL'), + rfc3966: parsedNumber.format('RFC3966'), + isValid: true + }; + } + } catch (error) { + console.warn('Failed to parse phone number:', phoneNumber, error); + } + + return null; +}; diff --git a/lib/utils/responsive.ts b/lib/utils/responsive.ts new file mode 100644 index 0000000..5711bc8 --- /dev/null +++ b/lib/utils/responsive.ts @@ -0,0 +1,103 @@ +import { Dimensions, PixelRatio, Platform } from 'react-native'; + +// Base dimensions (design reference - typically iPhone 14/15 or similar) +const BASE_WIDTH = 390; +const BASE_HEIGHT = 844; + +const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); + +/** + * Scale a value based on screen width (horizontal scaling) + * Use for: horizontal padding, margins, widths, font sizes + */ +export function scale(size: number): number { + const scaleFactor = SCREEN_WIDTH / BASE_WIDTH; + const newSize = size * scaleFactor; + + if (Platform.OS === 'web') { + // On web, respect user zoom settings by using rem-like scaling + return size; + } + + return Math.round(PixelRatio.roundToNearestPixel(newSize)); +} + +/** + * Scale a value based on screen height (vertical scaling) + * Use for: vertical padding, margins, heights + */ +export function verticalScale(size: number): number { + const scaleFactor = SCREEN_HEIGHT / BASE_HEIGHT; + const newSize = size * scaleFactor; + + if (Platform.OS === 'web') { + return size; + } + + return Math.round(PixelRatio.roundToNearestPixel(newSize)); +} + +/** + * Moderate scaling - scales less aggressively (recommended for most use cases) + * Use for: font sizes, icon sizes, border radius + * @param factor - how much to scale (0 = no scaling, 1 = full scaling, default 0.5) + */ +export function moderateScale(size: number, factor: number = 0.5): number { + const scaleFactor = SCREEN_WIDTH / BASE_WIDTH; + const newSize = size + (scaleFactor - 1) * size * factor; + + if (Platform.OS === 'web') { + return size; + } + + return Math.round(PixelRatio.roundToNearestPixel(newSize)); +} + +/** + * Get responsive value based on screen size breakpoints + * Use for: completely different layouts at different sizes + */ +export function responsive(options: { + small?: T; // < 375px (iPhone SE, small phones) + medium?: T; // 375-428px (most phones) + large?: T; // > 428px (tablets, large phones, web) + default: T; +}): T { + if (SCREEN_WIDTH < 375) { + return options.small ?? options.default; + } + if (SCREEN_WIDTH <= 428) { + return options.medium ?? options.default; + } + return options.large ?? options.default; +} + +/** + * Check if device is a tablet or large screen + */ +export function isTablet(): boolean { + const aspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH; + return ( + (Platform.OS !== 'web' && SCREEN_WIDTH >= 768) || + (Platform.OS === 'web' && SCREEN_WIDTH >= 1024) || + aspectRatio < 1.6 + ); +} + +/** + * Get screen dimensions (updates on rotation/resize) + */ +export function getScreenDimensions() { + const { width, height } = Dimensions.get('window'); + return { width, height }; +} + +// Font scaling with accessibility support +export function scaledFontSize(size: number): number { + const scaled = moderateScale(size, 0.3); + // Respect system font scaling but cap it to prevent extreme sizes + const maxScale = 1.3; + const fontScale = Math.min(PixelRatio.getFontScale(), maxScale); + return Math.round(scaled * fontScale); +} + diff --git a/lib/utils/validationSchemas.ts b/lib/utils/validationSchemas.ts new file mode 100644 index 0000000..bc4cf20 --- /dev/null +++ b/lib/utils/validationSchemas.ts @@ -0,0 +1,187 @@ +import * as v from 'valibot'; +import { isValidPhoneNumber } from './phoneUtils'; + +// Common validation schemas +export const fullNameSchema = v.pipe( + v.string(), + v.minLength(1, 'Please enter your full name'), + v.trim(), + v.minLength(1, 'Full name cannot be empty') +); + +export const emailSchema = v.pipe( + v.string(), + v.minLength(1, 'Please enter your email address'), + v.email('Please enter a valid email address'), + v.trim() +); + +export const phoneNumberSchema = v.pipe( + v.string(), + v.minLength(1, 'Please enter a phone number'), + v.trim(), + v.custom((value) => { + if (typeof value !== 'string') return false; + const cleanPhone = value.replace(/[^+\d]/g, ''); + if (cleanPhone.length < 7) { + return false; + } + // Use the existing phone validation utility + return isValidPhoneNumber(value); + }, 'Please enter a valid phone number') +); + +export const pinSchema = v.pipe( + v.string(), + v.minLength(1, 'Please enter a PIN'), + v.length(6, 'PIN must be exactly 6 digits'), + v.regex(/^\d{6}$/, 'PIN must contain only digits') +); + +export const confirmPinSchema = (pin: string) => v.pipe( + v.string(), + v.minLength(1, 'Please confirm your PIN'), + v.custom((value) => { + return typeof value === 'string' && value === pin; + }, 'PIN and confirm PIN do not match') +); + +export const addressSchema = v.optional( + v.pipe( + v.string(), + v.trim() + ) +); + +export const cardNumberSchema = v.pipe( + v.string(), + v.minLength(1, 'Card number is required'), + v.trim(), + v.custom((value) => { + if (typeof value !== 'string') return false; + const cleanNumber = value.replace(/\s/g, ''); + // Basic card number validation (13-19 digits) + return /^\d{13,19}$/.test(cleanNumber); + }, 'Please enter a valid card number') +); + +export const expiryDateSchema = v.pipe( + v.string(), + v.minLength(1, 'Expiry date is required'), + v.trim(), + v.custom((value) => { + if (typeof value !== 'string') return false; + // Validate MM/YY format + if (!/^\d{2}\/\d{2}$/.test(value)) { + return false; + } + const [month, year] = value.split('/'); + const monthNum = parseInt(month, 10); + const yearNum = parseInt(year, 10); + + if (monthNum < 1 || monthNum > 12) { + return false; + } + + // Check if card is expired (basic check - assumes 20XX) + const currentYear = new Date().getFullYear() % 100; + const currentMonth = new Date().getMonth() + 1; + + if (yearNum < currentYear || (yearNum === currentYear && monthNum < currentMonth)) { + return false; + } + + return true; + }, 'Please enter a valid expiry date') +); + +export const cvvSchema = v.pipe( + v.string(), + v.minLength(1, 'CVV is required'), + v.trim(), + v.regex(/^\d{3,4}$/, 'CVV must be 3 or 4 digits') +); + +// Amount validation schemas +export const amountSchema = (options?: { + min?: number; // in cents + max?: number; // in cents + minDisplay?: string; // for error messages + maxDisplay?: string; // for error messages +}) => { + const min = options?.min ?? 1; // Default 1 cent + const max = options?.max ?? 99999; // Default $999.99 + const minDisplay = options?.minDisplay ?? '$0.01'; + const maxDisplay = options?.maxDisplay ?? '$999.99'; + + return v.pipe( + v.string(), + v.custom((value) => { + if (typeof value !== 'string') return false; + return !(!value || value === '' || value === '0' || value === '0.' || value === '0.00'); + }, 'Please enter an amount'), + v.custom((value) => { + if (typeof value !== 'string') return false; + // Parse amount to cents + const numValue = parseFloat(value); + if (isNaN(numValue)) { + return false; + } + const amountInCents = Math.round(numValue * 100); + + return amountInCents >= min && amountInCents <= max; + }, `Amount must be between ${minDisplay} and ${maxDisplay}`) + ); +}; + +// Form schemas +export const phoneSetupSchema = v.object({ + fullName: fullNameSchema, + email: emailSchema, + address: addressSchema, + pin: pinSchema, + confirmPin: v.string(), // Will be validated separately with confirmPinSchema +}); + +export const addRecipientSchema = v.object({ + fullName: fullNameSchema, + phoneNumber: phoneNumberSchema, +}); + +export const addCardSchema = v.object({ + cardNumber: cardNumberSchema, + expiryDate: expiryDateSchema, + cvv: cvvSchema, +}); + +export const profileUpdateSchema = v.object({ + fullName: fullNameSchema, + phoneNumber: v.optional(phoneNumberSchema), + email: v.optional(emailSchema), + address: addressSchema, +}); + +// Helper function to get validation errors +export const getValidationError = (error: v.ValiError): string => { + if (error.issues && error.issues.length > 0) { + return error.issues[0].message || 'Validation error'; + } + return 'Validation error'; +}; + +// Helper function to validate and get errors +export const validate = >( + schema: T, + input: unknown +): { success: true; data: v.InferInput } | { success: false; error: string } => { + try { + const data = v.parse(schema, input); + return { success: true, data }; + } catch (error) { + if (error instanceof v.ValiError) { + return { success: false, error: getValidationError(error) }; + } + return { success: false, error: 'Validation error' }; + } +}; + diff --git a/locales/.DS_Store b/locales/.DS_Store new file mode 100644 index 0000000..8622265 Binary files /dev/null and b/locales/.DS_Store differ diff --git a/locales/am/common.json b/locales/am/common.json new file mode 100644 index 0000000..27be810 --- /dev/null +++ b/locales/am/common.json @@ -0,0 +1,674 @@ +{ + "common": { + "back": "ተመለስ" + }, + "signin": { + "phoneLabel": "ስልክ ቁጥር", + "phonePlaceholder": "9XXXXXXXX", + "button": "ግባ", + "buttonLoading": "የኮድ መላክ በመካሄድ ላይ...", + "validationErrorTitle": "ስህተት", + "validationInvalidPhone": "እባክዎን ትክክለኛ የስልክ ቁጥር ያስገቡ", + "toastErrorTitle": "ስህተት", + "toastOtpFailed": "OTP መላክ አልተሳካም። እባክዎን የስልክ ቁጥርዎን እንደገና ያረጋግጡ።", + "toastAuthErrorTitle": "የስልክ ማረጋገጫ ስህተት" + }, + "otp": { + "title": "ማረጋገጫ", + "description": "ወደ ስልክዎ የማረጋገጫ ኮድ ልከናል", + "codeLabel": "የOTP ኮድ", + "codePlaceholder": "123456", + "verifyButton": "አረጋግጥ", + "verifyButtonLoading": "በመረጋገጥ ላይ...", + "resendButton": "ኮዱን እንደገና ላክ", + "resendButtonCountdown": "ኮዱን እንደገና ለመላክ {{countdown}} ሰከንድ ይጠብቁ", + "validationErrorTitle": "ስህተት", + "validationInvalidCode": "እባክዎን የሚሰራ 6-አሃድ OTP ኮድ ያስገቡ", + "toastErrorTitle": "ስህተት", + "toastInvalidCode": "የOTP ኮድ የተሳሳተ ነው። እባክዎን እንደገና ይሞክሩ።", + "toastInfoTitle": "መረጃ", + "toastBackInfo": "ወደ መግቢያ ገፅ በመመለስ ላይ", + "toastResendInfo": "ኮዱን እንደገና ለመላክ ወደ መግቢያ ገፅ ይመለሱ", + "toastSuccessTitle": "ተሳክቷል", + "toastDevSuccess": "OTP በዲቨልፕር ሁኔታ ተረጋግጧል", + "toastVerificationErrorTitle": "የOTP ማረጋገጫ ስህተት" + }, + "phoneSetup": { + "title": "የመጨረሻ ማቀናበር", + "subtitle": "ገንዘብ ከመላክዎ በፊት ጥቂት ጥያቄዎች አሉን።", + "fullNameLabel": "ሙሉ ስም *", + "fullNamePlaceholder": "አበበ ከበደ", + "addressLabel": "አድራሻ", + "addressPlaceholder": "አዲስ አበባ፣ ኢትዮጵያ", + "emailLabel": "የኢሜይል አድራሻ *", + "emailPlaceholder": "abebe@example.com", + "pinLabel": "PIN *", + "pinPlaceholder": "6 አሃድ PIN ያስገቡ", + "confirmPinLabel": "PIN ያረጋግጡ *", + "confirmPinPlaceholder": "PINዎን ያረጋግጡ", + "button": "ተከናውኗል", + "buttonLoading": "በማቀናበር ላይ...", + "validationErrorTitle": "ስህተት", + "validationFullNameRequired": "እባክዎን ሙሉ ስምዎን ያስገቡ", + "validationEmailRequired": "እባክዎን የኢሜይል አድራሻዎን ያስገቡ", + "validationEmailInvalid": "እባክዎን የሚሰራ የኢሜይል አድራሻ ያስገቡ", + "validationPinRequired": "እባክዎን PIN ያስገቡ", + "validationPinLength": "PIN ትክክለኛ 6 አሃድ መሆን አለበት", + "validationConfirmPinRequired": "እባክዎን PINዎን ያረጋግጡ", + "validationPinsMismatch": "PIN እና የማረጋገጫ PIN አይጣጣሙም", + "toastSuccessTitle": "ተሳክቷል", + "toastDevSuccess": "የመገለጫ ማቀናበር በዲቨልፕር ሁኔታ ተጠናቋል", + "toastNoUserTitle": "ስህተት", + "toastNoUser": "ምንም የተጠቃሚ መለያ አልተገኘም", + "toastSetupErrorTitle": "የማቀናበር ስህተት", + "toastSetupError": "የመገለጫ ማቀናበር አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "forgot": { + "headerTitle": "የይለፍ ቃል መለወጥ", + "title": "የይለፍ ቃል ዳግም አዘምን", + "description": "የይለፍ ቃልዎን ለመቀየር ወደ መሣሪያዎ ኮድ ልከናል።", + "tabsPhone": "ስልክ", + "tabsEmail": "ኢሜይል", + "phoneSelectorLabel": "የስልክ ምርጫ", + "phonePlaceholder": "+25112515232", + "otpLabel": "የOTP ኮድ", + "otpPlaceholder": "123123", + "passwordLabel": "የይለፍ ቃል", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "የይለፍ ቃል ያረጋግጡ", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "ዳግም አስጀምር", + "resendButton": "ኮዱን እንደገና ላክ", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "test@gmail.com", + "toastSuccessTitle": "ተሳክቷል", + "toastResetSuccess": "የይለፍ ቃል መቀየር ጥያቄዎ ተቀብሏል።", + "toastInfoTitle": "መረጃ", + "toastResendInfo": "የማረጋገጫ ኮዱን እንደገና ልከናል።" + }, + "home": { + "balanceLabel": "ቀሪ ብር", + "accountDetails": "የአካውንት ዝርዝሮች", + "addButton": "ጨምር", + "cashOutButton": "ገንዘብ አውጣ", + "recipientsTitle": "ተቀባዮች", + "contactsAllowAccess": "ፍቃድ ስጥ", + "contactsGrantAccess": "ፍቃድ ስጥ", + "contactsLoading": "በመጫን ላይ...", + "contactsError": "እውቂያዎችን ማጫን አልተሳካም", + "contactsNoContacts": "እውቂያ የለም", + "transactionsTitle": "ግብይቶች", + "transactionsLoading": "ግብይቶችን በመጫን ላይ...", + "transactionsError": "ግብይቶችን ማጫን አልተሳካም", + "transactionsNoTransactions": "ግብይት አልተፈጠረም", + "transactionsEmptySubtitle": "የግብይት ታሪክዎ እዚህ ይታያል", + "transactionsMore": "+{{count}} ተጨማሪ ግብይቶች", + "cashoutErrorTitle": "ስህተት", + "cashoutNoBalance": "ቀሪ ብር አልተገኘም", + "cashoutMinError": "ቀሪ ብርዎ ከአስፈላጊው መጠን ያነሰ ነው ($10)", + "addCashNoPaymentMethodTitle": "የክፍያ መንገድ የለም", + "addCashNoPaymentMethodMessage": "ወደ ዋሌትዎ ገንዘብ ከመጨመርዎ በፊት ካርድ መጨመር አለብዎት።", + "addCashCancel": "ሰርዝ", + "addCashAddCard": "ካርድ ጨምር" + }, + "history": { + "title": "ታሪክ", + "subtitle": "የግብይቶች ዝርዝር።", + "loading": "ግብይቶችን በመጫን ላይ...", + "errorTitle": "ግብይቶችን ማጫን አልተሳካም", + "emptyTitle": "ግብይት አልተፈጠረም", + "emptySubtitle": "የግብይት ታሪክዎ እዚህ ይታያል", + "filterTitle": "ግብይቶችን ፍለጋ", + "filterSubtitle": "ታሪክዎን ለማጣራት የቀን መጠን እና አይነት ይምረጡ።", + "dateRangeLabel": "የቀን ክልል", + "fromLabel": "ከ", + "toLabel": "እስከ", + "selectStart": "የመጀመሪያ ቀን ምረጥ", + "selectEnd": "የመጨረሻ ቀን ምረጥ", + "clearDates": "ቀኖችን ሰርዝ", + "typeLabel": "አይነት", + "typeAll": "ሁሉም", + "typeIncoming": "ገቢ", + "typeOutgoing": "ወጪ", + "applyFilters": "ማጣሪያውን ተግብር", + "searchPlaceholder": "ታሪክ ፈልግ", + "toastErrorTitle": "ስህተት", + "toastTransactionsError": "ግብይቶችን ማጫን አልተሳካም።" + }, + "cardmang": { + "title": "ካርዶች", + "subtitle": "የክፍያ መረጃዎችን ሁሉ እዚህ ያቆጥቡ።", + "searchPlaceholder": "የአካውንት ስም፣ የካርድ ቁጥር ፈልግ", + "addCardButton": "ካርድ ጨምር", + "paymentOptionsTitle": "የክፍያ መንገዶች", + "cardTypeFallback": "ካርድ", + "cardExpires": "የሚያበቃው {{date}}", + "loading": "ካርዶችን በመጫን ላይ...", + "errorTitle": "ካርዶችን ማጫን አልተሳካም", + "emptyTitle": "ካርድ አልታከለም", + "emptySubtitle": "የመጀመሪያ ካርድዎን በመጨመር ክፍያዎትን ይቆጥቡ", + "toastRemoveSuccessTitle": "ተሳክቷል", + "toastRemoveSuccess": "ካርዱ ተሰርዟል።", + "toastRemoveErrorTitle": "ስህተት", + "toastRemoveError": "ካርዱን ማስወገድ አልተሳካም።" + }, + "listrecipient": { + "title": "ተቀባይ", + "searchPlaceholder": "ስም፣ የስልክ ቁጥር፣ አካውንት ፈልግ", + "addButton": "አዲስ ተቀባይ ጨምር", + "savedRecipientsTitle": "የተቀመጡ ተቀባዮች ({{count}})", + "savedRecipientsLoading": "ተቀባዮችን በመጫን ላይ...", + "savedRecipientsError": "ተቀባዮችን ማጫን አልተሳካም", + "savedRecipientsEmpty": "የተቀመጠ ተቀባይ የለም", + "contactsTitle": "የእርስዎ እውቂያዎች ({{count}})", + "contactsLoading": "እውቂያዎችን በመጫን ላይ...", + "contactsErrorTitle": "እውቂያዎችን ማጫን አልተሳካም", + "contactsErrorSubtitle": "እውቂያዎችዎን ማጫን እየተፈጠረ ችግኝ ነበር።", + "contactsPermissionTitle": "የእውቂያ ፍቃድ ያስፈልጋል", + "contactsPermissionSubtitle": "ምናልባት የሚሆኑ ተቀባዮችን ለማየት የእውቂያዎችዎን ፍቃድ ይስጡ።", + "contactsPermissionButton": "ፍቃድ ስጥ", + "contactsEmptyTitle": "እውቂያ አልተገኘም", + "contactsEmptySubtitle": "በመሣሪያዎ ላይ የስልክ ቁጥር ያላቸው እውቂያዎች አልተገኙም", + "contactNoPhone": "የስልክ ቁጥር የለም", + "toastErrorTitle": "ስህተት", + "toastContactsError": "እውቂያዎችን ማጫን አልተሳካም።", + "toastRecipientsError": "ተቀባዮችን ማጫን አልተሳካም።" + }, + "sendorrequestmoney": { + "verifyingSecurity": "የደህንነት ማረጋገጫ በመካሄድ ላይ...", + "availableBalanceLabel": "የሚገኝ ቀሪ ብር", + "processingFee": "የክፍያ ክፍያ: ${{fee}} ({{percent}}%)", + "totalLabel": "ጠቅላላ: ${{total}}", + "validationErrorTitle": "የተሳሳተ መጠን", + "validationEnterAmount": "እባክዎን መጠን ያስገቡ", + "validationMinAmount": "አነስተኛው መጠን $0.01 ነው", + "validationMaxAmount": "ከፍተኛው መጠን $999.99 ነው", + "validationInsufficientBalance": "ቀሪ ብር ያልተሟላ። የሚፈለገው: ${{required}} (ከዚህ ${{fee}} የክፍያ ክፍያ ነው)፣ ያለዎት: ${{available}}", + "requestButton": "ጥያቄ ጥያቄ", + "requestButtonWithAmount": "${{amount}} ጠይቅ", + "payButton": "ክፈል", + "payButtonWithAmount": "${{amount}} ክፈል", + "pinModalTitle": "የካሽ PINዎን ያረጋግጡ" + }, + "addcard": { + "title": "ካርድ ጨምር", + "sectionCardTitle": "ካርድ", + "sectionCardSubtitle": "ካርዶችዎን እዚህ መቆጠብ ትችላለህ።", + "cardNumberLabel": "የካርድ ቁጥር", + "cardNumberPlaceholder": "1234 5678 9012 3456", + "expiryDateLabel": "የመታቀቂያ ቀን", + "expiryDatePlaceholder": "MM/YY", + "cvvLabel": "CVV", + "cvvPlaceholder": "123", + "addButton": "ካርድ ጨምር", + "addButtonLoading": "በመጨመር ላይ...", + "validationErrorTitle": "ስህተት", + "validationCardNumberRequired": "እባክዎን የካርድ ቁጥር ያስገቡ", + "validationExpiryRequired": "እባክዎን የመታቀቂያ ቀን ያስገቡ", + "validationCvvRequired": "እባክዎን CVV ያስገቡ", + "validationInvalidCard": "እባክዎ ትክክለኛ የካርድ መረጃ ያስገቡ", + "toastErrorTitle": "የካርድ ስህተት", + "toastAddFailed": "የክሬዲት ካርድ መጨመር አልተሳካም።" + }, + "addcash": { + "verifyingSecurity": "የደህንነት ማረጋገጫ በመካሄድ ላይ...", + "title": "ጨምር", + "validationErrorTitle": "የተሳሳተ መጠን", + "validationEnterAmount": "እባክዎን ከ $0.00 በላይ መጠን ያስገቡ", + "validationMinAmount": "አነስተኛው መጠን $10.00 ነው", + "validationMaxAmount": "ከፍተኛው መጠን $999.99 ነው", + "addButton": "ጨምር", + "addButtonWithAmount": "${{amount}} ጨምር", + "pinModalTitle": "ገንዘብ ለመጨመር የካሽ PINዎን ያረጋግጡ" + }, + "addrecipient": { + "title": "ተቀባይ ጨምር", + "sectionTitle": "ተቀባይ", + "sectionSubtitle": "ገንዘብ ለመላክ የምትፈልጉትን ማንኛውንም እዚህ ያክሉ።", + "fullNameLabel": "ሙሉ ስም", + "fullNamePlaceholder": "ኪሩቤል ኪብሩ", + "phoneLabel": "የስልክ ቁጥር", + "phonePlaceholder": "+251983032475", + "addButton": "ጨምር", + "addButtonLoading": "በመጨመር ላይ...", + "validationErrorTitle": "የማረጋገጫ ስህተት", + "validationFullNameRequired": "እባክዎን የተቀባዩን ሙሉ ስም ያስገቡ", + "validationPhoneRequired": "እባክዎን የስልክ ቁጥር ያስገቡ", + "validationPhoneInvalid": "እባክዎን ትክክለኛ የስልክ ቁጥር ያስገቡ", + "toastErrorTitle": "ስህተት", + "toastAddError": "ተቀባይን መጨመር አልተሳካም።" + }, + "addcashcomp": { + "successNote": "በተሳካ ሁኔታ ገንዘብ ወደ ዋሌትዎ ጨምረዋል።", + "addAgainButton": "ገንዘብ እንደገና ጨምር", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ" + }, + "cashout": { + "verifyingSecurity": "የደህንነት ማረጋገጫ በመካሄድ ላይ...", + "availableBalanceLabel": "የሚገኝ ቀሪ ብር", + "validationErrorTitle": "የተሳሳተ መጠን", + "validationEnterAmount": "እባክዎን መጠን ያስገቡ", + "validationMinAmount": "አነስተኛው መጠን $0.01 ነው", + "validationMaxAmount": "ከፍተኛው መጠን $999.99 ነው", + "validationInsufficientBalance": "ቀሪ ብር ያልተሟላ። የሚፈለገው: ${{required}}፣ ያለዎት: ${{available}}", + "button": "ገንዘብ አውጣ", + "buttonWithAmount": "${{amount}} ገንዘብ አውጣ", + "pinModalTitle": "ገንዘብ ለማውጣት የካሽ PINዎን ያረጋግጡ" + }, + "cardaddedcomp": { + "title": "ተከናውኗል", + "description": "ካርድዎን በተሳካ ሁኔታ ጨምረዋል።", + "addButton": "ካርድ ጨምር", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ", + "shareMessageWithParam": "{{message}} በአምባ መተግበሪያ", + "shareMessageDefault": "አዲስ ካርድ ወደ አምባ መተግበሪያዬ ጨምርኩ!", + "shareTitle": "ካርድ በተሳካ ሁኔታ ታከለ", + "toastErrorTitle": "ስህተት", + "toastShareError": "ማጋራት አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "cashoutcomp": { + "successNote": "በተሳካ ሁኔታ ገንዘብ ወደ መለያዎ አወጣችሁ።", + "cashOutAgainButton": "ገንዘብ እንደገና አውጣ", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ", + "toastErrorTitle": "ስህተት", + "toastNoBalance": "ቀሪ ብር አልተገኘም", + "toastMinError": "ቀሪ ብርዎ ከአስፈላጊው መጠን ($10) ያነሰ ነው" + }, + "crowdfunding": { + "tabsOverview": "አጠቃላይ", + "tabsCampaign": "ዘመቻ", + "tabsFaq": "FAQ", + "title": "የዘመቻው ርእስ", + "description": "Lorem ipsum dolor sit amet consectetur. በግልጽ ጽሑፍ የተገለፀ የዘመቻ መግለጫ እዚህ ይመጣል።", + "pledgedAmount": "$123,000", + "pledgedOf": "pledged of {{target}}", + "backersCountLabel": "backers.", + "daysToGoLabel": "days to go.", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "test@gmail.com", + "otpLabel": "የOTP ኮድ", + "otpPlaceholder": "123123", + "passwordLabel": "የይለፍ ቃል", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "የይለፍ ቃል ያረጋግጡ", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "ዳግም አስጀምር", + "resendButton": "ኮዱን እንደገና ላክ" + }, + "eventdetail": { + "shareMessage": "ከኔ ጋር በዚህ ክስተት ተሳትፉ!", + "title": "የክስተቱ ስም", + "description": "Lorem ipsum dolor sit amet consectetur. ለምሳሌ የክስተቱ መግለጫ ጽሑፍ እዚህ ይገባል።", + "location": "ሚሌኒየም ሃል፣ አዲስ አበባ", + "dateTime": "ሰኞ፣ ጥር 10፣ 2026 ከ6:00 ማታ - 1:00 እኩለ ሌሊት (EAT)", + "peopleComing": "የምታውቋቸው ሰዎች በመምጣት ላይ ናቸው!", + "ticketLabel": "ቲኬት #{{index}}", + "ticketPrice": "$55.44", + "buyButton": "አሁን ግዛ", + "shareButton": "አጋራ", + "guestListButton": "የእንግዶች ዝርዝር", + "toastErrorTitle": "ስህተት", + "toastShareError": "ክስተቱን መካፈል አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "eventqrscreen": { + "printButton": "አትም", + "goBackButton": "ተመለስ" + }, + "events": { + "title": "ክስተቶች", + "subtitle": "ከማህበረሰብዎ ጋር ይተባበሩ", + "searchPlaceholder": "የክስተት ስም፣ ቦታ", + "filterButton": "ማጣሪያ", + "myTicketsButton": "የኔ ቲኬቶች", + "featuredTitle": "የተለዩ ክስተቶች", + "cardTitle": "የክስተት ስም", + "cardDescription": "Lorem ipsum dolor sit amet consectetur. ለምሳሌ የክስተቱ መግለጫ ጽሑፍ እዚህ ይገባል።", + "ticketCountPrefix": "100 - ", + "ticketLocation": "ቲኬቶች ሎስ አንጀለስ", + "ticketDate": "23 Jan26" + }, + "moneydonated": { + "title": "ተከናውኗል", + "description": "የገንዘብ ጥያቄውን በተሳካ ሁኔታ አሟሉ።", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ" + }, + "moneyrequested": { + "title": "ተከናውኗል", + "description": "የክሬዲት ጥያቄዎን በተሳካ ሁኔታ አስገብተዋል።", + "descriptionWithName": "የክሬዲት ጥያቄዎን ከ{{fullName}} ጋር በተያያዘ በተሳካ ሁኔታ አስገብተዋል።", + "requestAgainButton": "እንደገና ጠይቅ", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ" + }, + "mytickets": { + "title": "ክስተቶች", + "subtitle": "የቲኬቶችዎን መረጃ ሁሉ እዚህ ያቆጥቡ።", + "searchPlaceholder": "የክስተት ስም", + "filterButton": "ማጣሪያ", + "ticketsTitle": "ቲኬቶች" + }, + "notification": { + "title": "ማስታወቂያ", + "sectionToday": "ዛሬ", + "loading": "ማስታወቂያዎችን በመጫን ላይ...", + "errorWithMessage": "ስህተት: {{error}}", + "emptyTitle": "ማስታወቂያ የለም", + "emptySubtitle": "ስለ የገንዘብ ጥያቄዎችና ግብይቶች ማስታወቂያዎች እዚህ ይታያሉ።", + "toastErrorTitle": "ስህተት", + "toastRequestNotPending": "ጥያቄውን በዚህ ጊዜ መክበር አይቻልም።", + "toastRequestActionFailed": "ጥያቄውን {{action}} መስራት አልተሳካም።" + }, + "notificationOption": { + "title": "ተጠርጣሪዎቹን አስታውቃቸው", + "sectionTitle": "የማስታወቂያ አማራጭ", + "sectionSubtitle": "የሚመርጡትን የማስታወቂያ መንገድ ይምረጡ።", + "smsLabel": "የSMS ማስታወቂያ", + "whatsappLabel": "WhatsApp", + "continueButton": "ቀጥል", + "toastErrorTitle": "ስህተት", + "toastAuthRequired": "ጥያቄ ለመላክ መመዝገብ ያስፈልጋል።", + "toastMissingInfo": "የጥያቄ መረጃ ይጎድላል።", + "toastInvalidAmount": "የተሳሳተ መጠን።", + "toastCreateFailed": "የገንዘብ ጥያቄ መፍጠር አልተሳካም።", + "toastRequestFailed": "ገንዘብ መጠየቅ አልተሳካም። እባክዎን እንደገና ይሞክሩ።", + "toastSelectMethod": "እባክዎን የማስታወቂያ መንገድ ይምረጡ።" + }, + "points": { + "title": "ነጥቦች", + "referTitle": "ጓደኞችዎን ያመልክቱ", + "earnSubtitle": "እና ነጥቦች ይስበሩ", + "copyButton": "ቅዳ", + "shareButton": "አጋራ", + "activityButton": "እንቅስቃሴ", + "howToEarnButton": "ነጥቦችን እንዴት ማግኘት እችላለሁ", + "rewardsTitle": "ሽልማቶች", + "reward1Title": "ግብይት", + "reward1Description": "የግብይት ክፍያዎ ተወግዷል" + }, + "pointsactivity": { + "title": "እንቅስቃሴ", + "activity1Title": "ተቀባይ ጨምር", + "activity1Date": "13 ጃን 24 • 8:00 ጠዋት", + "activity2Title": "የግብይት ቅናሽ", + "activity2Date": "13 ጃን 24 • 8:00 ጠዋት", + "pointsPill": "{{sign}} {{points}} ነጥቦች" + }, + "profile": { + "title": "መገለጫ", + "pointsBadge": "1200 ነጥቦች", + "loadingProfile": "መገለጫን በመጫን ላይ...", + "errorWithMessage": "ስህተት: {{error}}", + "fullNameLabel": "ሙሉ ስም", + "fullNamePlaceholder": "ሙሉ ስም", + "addressLabel": "አድራሻ", + "addressPlaceholder": "አድራሻ", + "phoneLabel": "የስልክ ቁጥር", + "phonePlaceholder": "የስልክ ቁጥር", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "ኢሜይል", + "languageLabel": "ቋንቋ", + "languagePlaceholder": "ቋንቋ ይምረጡ", + "languageOptionEnglish": "እንግሊዝኛ", + "languageOptionAmharic": "አማርኛ", + "languageOptionFrench": "ፈረንሳይኛ", + "languageOptionTigrinya": "ትግርኛ", + "languageOptionOromo": "ኦሮምኛ", + "accountNumberLabel": "የመለያ ቁጥር", + "accountNumberPlaceholder": "አልተገኘም", + "usernameLabel": "የተጠቃሚ ስም", + "usernamePlaceholder": "@username", + "editButton": "መገለጫውን አርትዕ", + "saveButton": "ለውጦችን አስቀምጥ", + "savingButton": "በመከታተል ላይ...", + "cancelButton": "ሰርዝ", + "pointsButton": "ነጥቦች", + "logoutButton": "ውጣ", + "toastLoggedOutTitle": "ወጥተዋል", + "toastLoggedOutDescription": "ከመተግበሪያው ተወጥተዋል።", + "toastErrorTitle": "ስህተት", + "toastLogoutFailed": "መውጣት አልተሳካም። እባክዎን እንደገና ይሞክሩ።", + "toastUserNotFound": "ተጠቃሚ አልተገኘም።", + "toastFullNameRequiredTitle": "ሙሉ ስም ያስፈልጋል", + "toastFullNameRequiredDescription": "እባክዎን ሙሉ ስምዎን ያስገቡ።", + "toastProfileUpdatedTitle": "መገለጫው ተዘምኗል", + "toastProfileUpdatedDescription": "መገለጫዎ በተሳካ ሁኔታ ተዘምኗል።", + "toastUpdateErrorTitle": "የማዘመን ስህተት", + "toastUpdateErrorDescription": "መገለጫውን ማዘመን አልተሳካም።" + }, + "qrscreen": { + "shareMessage": "የአምባ ኪውኤር ኮዴን በመስካሄ ገንዘብ ይላኩ ወይም ይቀበሉ።", + "shareButton": "አጋራ", + "goBackButton": "ተመለስ", + "toastErrorTitle": "ስህተት", + "toastShareError": "ኪውኤር መካፈል አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "selectacc": { + "title": "መለያ ይምረጡ", + "addingAmount": "በመጨመር ላይ: ${{amount}}", + "accountsTitle": "መለያዎች", + "accountsDescriptionSelected": "ካርድ ተመርጧል! ለመቀጠል የ\"Continue\" አዝራሩን ይጫኑ።", + "accountsDescriptionUnselected": "ገንዘብ ለመጨመር የሚፈልጉትን ካርድ ይምረጡ።", + "loadingCards": "ካርዶችን በመጫን ላይ...", + "errorTitle": "ካርዶችን ማጫን አልተሳካም", + "errorWithMessage": "ስህተት: {{error}}", + "emptyTitle": "ካርድ አልተገኘም", + "emptySubtitle": "ገንዘብ ለመጨመር ካርድ ያክሉ።", + "buttonProcessing": "በመስራት ላይ...", + "buttonProceed": "ቀጥል", + "toastErrorTitle": "ስህተት", + "toastMissingInfo": "አስፈላጊ መረጃ ይጎድላል።", + "toastAddCashFailed": "ገንዘብ ወደ ዋሌት መጨመር አልተሳካም።", + "toastAddCashFailedWithRetry": "ገንዘብ ወደ ዋሌት መጨመር አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "recipaddedcomp": { + "title": "አዲስ ተቀባይ", + "description": "አዲስ ተቀባይ በተሳካ ሁኔታ ጨምረዋል።", + "addButton": "ጨምር", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ", + "shareMessageWithParam": "{{message}} በአምባ መተግበሪያ", + "shareMessageDefault": "አዲስ ተቀባይ ወደ አምባ መተግበሪያዬ ጨምርኩ!", + "shareTitle": "ተቀባይ በተሳካ ሁኔታ ታከለ", + "toastErrorTitle": "ስህተት", + "toastShareError": "መካፈል አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "selectdonor": { + "title": "አበል ሰጪን ምረጥ", + "requestButton": "ጥያቄ ላክ", + "requestButtonLoading": "ጥያቄ በመላክ ላይ...", + "toLabel": "ወደ", + "forLabel": "ምክንያት", + "searchPlaceholder": "በስም ወይም በስልክ ቁጥር ፈልግ", + "notePlaceholder": "ማስታወሻ ጨምር (አማራጭ)", + "donorsTitle": "አበል ሰጪዎች ({{count}})", + "loadingDonors": "አበል ሰጪዎችን በመጫን ላይ...", + "errorTitle": "አበል ሰጪዎችን ማጫን አልተሳካም", + "errorWithMessage": "ስህተት: {{error}}", + "contactsPermissionTitle": "የእውቂያ ፍቃድ ያስፈልጋል", + "contactsPermissionSubtitle": "የሚችሉ አበል ሰጪዎችን ለማየት የእውቂያዎን ፍቃድ ይስጡ", + "contactsAllowAccess": "ፍቃድ ስጥ", + "emptyTitleSearch": "ከፍለጋዎ ጋር የሚዛመዱ አበል ሰጪዎች አልተገኙም", + "emptyTitleDefault": "አበል ሰጪ አልተገኘም", + "emptySubtitleSearch": "ሌላ የፍለጋ ቃል ይሞክሩ", + "emptySubtitleDefault": "የተቀመጡ ተቀባዮችን ያክሉ ወይም የእውቂያ ፍቃድ ይስጡ", + "toastErrorTitle": "ስህተት", + "toastMissingInfo": "እባክዎን አበል ሰጪን ይምረጡ እና መጠን እንዲገለጽ ያረጋግጡ።", + "toastInvalidAmount": "የተሳሳተ መጠን።", + "toastDonorNotFound": "የተመረጠው አበል ሰጪ አልተገኘም።" + }, + "selectrecip": { + "title": "ተቀባይን ምረጥ", + "sendButton": "ላክ", + "sendButtonLoading": "በመላክ ላይ...", + "toLabel": "ወደ", + "forLabel": "ምክንያት", + "searchPlaceholder": "በስም ወይም በስልክ ቁጥር ፈልግ", + "notePlaceholder": "ማስታወሻ ጨምር (አማራጭ)", + "recipientsTitle": "ተቀባዮች ({{count}})", + "loadingRecipients": "ተቀባዮችን በመጫን ላይ...", + "errorTitle": "ተቀባዮችን ማጫን አልተሳካም", + "errorWithMessage": "ስህተት: {{error}}", + "contactsPermissionTitle": "የእውቂያ ፍቃድ ያስፈልጋል", + "contactsPermissionSubtitle": "ምናልባት የሚሆኑ ተቀባዮችን ለማየት የእውቂያዎን ፍቃድ ይስጡ", + "contactsAllowAccess": "ፍቃድ ስጥ", + "emptyTitleSearch": "ከፍለጋዎ ጋር የሚዛመዱ ተቀባዮች አልተገኙም", + "emptyTitleDefault": "ተቀባይ አልተገኘም", + "emptySubtitleSearch": "ሌላ የፍለጋ ቃል ይሞክሩ", + "emptySubtitleDefault": "የተቀመጡ ተቀባዮችን ያክሉ ወይም የእውቂያ ፍቃድ ይስጡ", + "toastErrorTitle": "ስህተት", + "toastMissingInfo": "እባክዎን ተቀባይን ይምረጡ እና መጠን እንዲገለጽ ያረጋግጡ።", + "toastInvalidAmount": "የተሳሳተ መጠን።", + "toastWalletNotFound": "ዋሌት አልተገኘም።", + "toastInsufficientBalanceTitle": "ቀሪ ብር ያልተሟላ", + "toastInsufficientBalanceDescription": "የሚፈለገው: ${{required}} (ከዚህ ${{fee}} የክፍያ ክፍያ ነው)፣ ያለዎት: ${{available}}", + "toastRecipientNotFound": "የተመረጠው ተቀባይ አልተገኘም።" + }, + "sendbank": { + "amountTitleCashOut": "ገንዘብ አውጣ", + "amountTitleToRecipient": "ወደ {{recipientName}}", + "noteWithText": "ማስታወሻ: {{note}}", + "paymentOptionsTitle": "የክፍያ መንገዶች", + "paymentOptionsSelected": "የተመረጠው: {{providerName}}", + "paymentOptionsUnselected": "የሚመርጡትን የክፍያ መንገድ ይምረጡ።", + "awashName": "አዋሽ ባንክ", + "awashSubtitle": "የባንክ ሽግግር", + "telebirrName": "ቴለቢርር", + "telebirrSubtitle": "ሞባይል ሞኒ", + "sendButtonCashOut": "${{amount}} ገንዘብ በ{{providerName}} አውጣ", + "sendButtonSend": "${{amount}} በ{{providerName}} ላክ", + "processingTitle": "ግብይቱ በመስራት ላይ...", + "processingSubtitle": "እባክዎን ክፍያውን እያሰራን መጠባበቅ ይኖርቦታል", + "poweredBy": "በሚያስተናግዱት", + "toastErrorTitle": "ስህተት", + "toastNoMethod": "እባክዎን መጀመሪያ የክፍያ መንገድ ይምረጡ", + "toastMissingInfo": "የግብይት መረጃ ይጎድላል", + "toastInvalidAmount": "የተሳሳተ መጠን።", + "toastInsufficientBalanceTitle": "ቀሪ ብር ያልተሟላ", + "toastInsufficientBalanceCashoutDescription": "የሚፈለገው: ${{required}}፣ ያለዎት: ${{available}}", + "toastInsufficientBalanceSendDescription": "የሚፈለገው: ${{required}} (ከዚህ ${{fee}} የክፍያ ክፍያ ነው)፣ ያለዎት: ${{available}}", + "toastMissingRecipient": "የተቀባይ መረጃ ይጎድላል", + "toastProcessFailed": "ግብይቱን መስራት አልተሳካም።", + "toastProcessFailedWithRetry": "ግብይቱን መስራት አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "taskcomp": { + "successDescription": "ገንዘብ በተሳካ ሁኔታ ላክዎታል።", + "sendAgainButton": "እንደገና ላክ", + "shareButton": "አጋራ", + "goHomeButton": "ወደ መነሻ ገፅ ተመለስ", + "shareMessageWithParam": "{{message}} በአምባ መተግበሪያ", + "shareMessageDefault": "ገንዘብ በአምባ መተግበሪያ በተሳካ ሁኔታ ላክሁ!", + "shareTitle": "የገንዘብ ሽግግር ተሳክቷል", + "toastErrorTitle": "ስህተት", + "toastNoBalance": "ቀሪ ብር አልተገኘም።", + "toastMinError": "ቀሪ ብርዎ ከአስፈላጊው መጠን ($10) ያነሰ ነው", + "toastShareError": "መካፈል አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "transconfirm": { + "title": "የግብይት ዝርዝር", + "planningDescription": "ገንዘብ ለ{{recipientName}} ለመላክ እያዘጋጁ ናቸው", + "sectionTitle": "የግብይት ዝርዝር", + "noteLabel": "ማስታወሻ", + "processingFeeLabel": "የክፍያ ክፍያ (1.25%)", + "subtotalLabel": "ንዑስ ድምር", + "totalLabel": "ጠቅላላ", + "buttonProcessing": "በመስራት ላይ...", + "buttonConfirm": "አረጋግጥ", + "toastErrorTitle": "ስህተት", + "toastMissingDetails": "የግብይት ዝርዝሮች ይጎድላሉ", + "toastInvalidAmount": "የተሳሳተ መጠን።", + "toastWalletNotFound": "ዋሌት አልተገኘም።", + "toastInsufficientBalanceTitle": "ቀሪ ብር ያልተሟላ", + "toastInsufficientBalanceDescription": "የሚፈለገው: ${{required}} (ከዚህ ${{fee}} የክፍያ ክፍያ ነው)፣ ያለዎት: ${{available}}", + "toastRecipientMissing": "የተቀባይ ዝርዝሮች ይጎድላሉ", + "toastSendFailed": "ገንዘብ መላክ አልተሳካም።", + "toastSendFailedWithRetry": "ገንዘብ መላክ አልተሳካም። እባክዎን እንደገና ይሞክሩ።" + }, + "transdetail": { + "title": "የግብይት ዝርዝር", + "descriptionSend": "ገንዘብ ለ{{recipientName}} ላክዎታል", + "descriptionReceive": "ገንዘብ ከ{{recipientName}} ተቀብለዋል", + "descriptionAddCash": "ገንዘብ ወደ ዋሌትዎ ጨምረዋል", + "descriptionCashOut": "ገንዘብ ወደ ባንክዎ አውጥተዋል", + "descriptionDefault": "ግብይት", + "sectionTitle": "የግብይት ዝርዝር", + "dateLabel": "ቀን", + "statusLabel": "ሁኔታ", + "statusUnknown": "ያልታወቀ", + "dateUnknown": "ያልታወቀ", + "noteLabel": "ማስታወሻ", + "processingFeeLabel": "የክፍያ ክፍያ (1.25%)", + "subtotalLabel": "ንዑስ ድምር", + "totalLabel": "ጠቅላላ", + "sendAgainButton": "እንደገና ላክ" + }, + "components": { + "acccard": { + "cardTypeFallback": "ካርድ", + "cardNumberPlaceholder": "**** **** **** ****", + "expiryPlaceholder": "MM/YY", + "expiryLabel": "የሚያበቃው {{date}}" + }, + "accordion": {}, + "backbar": {}, + "bottomsheet": {}, + "button": {}, + "card": {}, + "cardcomp": { + "title": "Master Card", + "number": "13131-1313-3131-1313-1312" + }, + "profilecard": { + "emptyLabelContact": "እውቂያ", + "unknownContact": "ያልታወቀ" + }, + "recipcard": { + "name": "አበበ ከበደ", + "accountNumber": "1030230213021" + }, + "topbar": { + "greeting": "ሰላም" + }, + "contactmodal": { + "headerTitle": "የእውቂያ ዝርዝር", + "phoneNumbersTitle": "የስልክ ቁጥሮች", + "emailAddressesTitle": "የኢሜይል አድራሻዎች", + "noAdditionalInfo": "ለዚህ እውቂያ ተጨማሪ የስልክ ወይም የኢሜይል መረጃ የለም።", + "sendMoneyButton": "ገንዘብ ላክ", + "closeButton": "ዝጋ", + "unknownContact": "ያልታወቀ እውቂያ" + }, + "pinconfirmationmodal": { + "titleDefault": "የካሽ PINዎን ያረጋግጡ", + "toastBiometricErrorTitle": "የባዮሜትሪክ ስህተት", + "toastBiometricHardwareNotAvailable": "የባዮሜትሪክ ማሳያ አልተገኘም", + "toastBiometricNotEnrolled": "ባዮሜትሪክ በመሣሪያው ላይ አልተመዘገበም", + "toastBiometricFailed": "የባዮሜትሪክ ማረጋገጥ አልተሳካም", + "toastInvalidPinTitle": "የተሳሳተ PIN", + "toastInvalidPinDescription": "እባክዎን 6-አሃድ PIN ያስገቡ", + "toastAuthErrorTitle": "የማረጋገጫ ስህተት", + "toastUserNotFound": "ተጠቃሚ አልተገኘም", + "toastPinNotFound": "PIN አልተገኘም", + "toastIncorrectPinTitle": "የተሳሳተ PIN", + "toastIncorrectPinDescription": "እባክዎን እንደገና ይሞክሩ።", + "authChoiceTitle": "የማረጋገጫ መንገድ ይምረጡ", + "fingerprintTitle": "የእጅ አሻራ", + "fingerprintSubtitle": "ማረጋገጥ ለመፈጸም የእጅ አሻራዎን ይጠቀሙ", + "pinTitle": "የAmbaPay PIN", + "pinSubtitle": "6-አሃድ PINዎን ያስገቡ", + "biometricWaiting": "የባዮሜትሪክ ማረጋገጥን በመጠባበቅ ላይ...", + "cancelButton": "ሰርዝ", + "pinVerificationTitle": "የPIN ማረጋገጫ", + "submitButtonVerifying": "በመረጋገጥ ላይ...", + "submitButtonConfirm": "አረጋግጥ" + }, + "transactioncard": { + "descriptionSend": "ገንዘብ ለ{{recipientName}} ላክዎታል", + "descriptionReceive": "ገንዘብ ከ{{senderName}} ተቀብለዋል", + "descriptionAddCash": "ገንዘብ ወደ ዋሌትዎ ጨምረዋል (****{{lastFourDigits}})", + "descriptionCashOut": "ገንዘብ ወደ {{bankProvider}} አውጥተዋል", + "descriptionDefault": "ግብይት", + "detailPhone": "ስልክ: {{phoneNumber}}", + "detailCard": "ካርድ: ****{{lastFourDigits}}", + "detailAccount": "መለያ: {{accountNumber}}", + "detailBankProvider": "በ {{bankProvider}}" + } + } +} \ No newline at end of file diff --git a/locales/en/HermelaHagos.pdf b/locales/en/HermelaHagos.pdf new file mode 100644 index 0000000..30a52e2 Binary files /dev/null and b/locales/en/HermelaHagos.pdf differ diff --git a/locales/en/common.json b/locales/en/common.json new file mode 100644 index 0000000..08b3130 --- /dev/null +++ b/locales/en/common.json @@ -0,0 +1,724 @@ +{ + "common": { + "back": "Back" + }, + "signin": { + "phoneLabel": "Phone Number", + "phonePlaceholder": "9XXXXXXXX", + "button": "Signin", + "buttonLoading": "Sending OTP...", + "validationErrorTitle": "Error", + "validationInvalidPhone": "Please enter a valid phone number", + "toastErrorTitle": "Error", + "toastOtpFailed": "Failed to send OTP. Please check your phone number format.", + "toastAuthErrorTitle": "Phone Authentication Error" + }, + "otp": { + "title": "Verification", + "description": "We have sent a code to your phone number", + "codeLabel": "OTP Code", + "codePlaceholder": "123456", + "verifyButton": "Verify", + "verifyButtonLoading": "Verifying...", + "resendButton": "Resend Code", + "resendButtonCountdown": "Resend Code in {{countdown}}s", + "validationErrorTitle": "Error", + "validationInvalidCode": "Please enter a valid 6-digit OTP code", + "toastErrorTitle": "Error", + "toastInvalidCode": "Invalid OTP code. Please try again.", + "toastInfoTitle": "Info", + "toastBackInfo": "Going back to sign in", + "toastResendInfo": "Please go back to the sign-in screen to resend the code", + "toastSuccessTitle": "Success", + "toastDevSuccess": "OTP verified (dev mode)", + "toastVerificationErrorTitle": "OTP Verification Error" + }, + "phoneSetup": { + "title": "Final Setup", + "subtitle": "Some questions before you send money.", + "fullNameLabel": "Full Name *", + "fullNamePlaceholder": "Abebe Kebede", + "addressLabel": "Address", + "addressPlaceholder": "Addis Ababa, Ethiopia", + "emailLabel": "Email Address *", + "emailPlaceholder": "abebe@example.com", + "pinLabel": "PIN *", + "pinPlaceholder": "Enter 6-digit PIN", + "confirmPinLabel": "Confirm PIN *", + "confirmPinPlaceholder": "Confirm your PIN", + "button": "Done", + "buttonLoading": "Setting up...", + "validationErrorTitle": "Error", + "validationFullNameRequired": "Please enter your full name", + "validationEmailRequired": "Please enter your email address", + "validationEmailInvalid": "Please enter a valid email address", + "validationPinRequired": "Please enter a PIN", + "validationPinLength": "PIN must be exactly 6 digits", + "validationConfirmPinRequired": "Please confirm your PIN", + "validationPinsMismatch": "PIN and confirm PIN do not match", + "toastSuccessTitle": "Success", + "toastDevSuccess": "Profile setup complete (dev mode)", + "toastNoUserTitle": "Error", + "toastNoUser": "No authenticated user found", + "toastSetupErrorTitle": "Setup Error", + "toastSetupError": "Failed to complete profile setup. Please try again." + }, + "forgot": { + "headerTitle": "Reset Pass", + "title": "Reset Password", + "description": "We have sent a code to your device to reset your password.", + "tabsPhone": "Phone", + "tabsEmail": "Email", + "phoneSelectorLabel": "Phone Selector", + "phonePlaceholder": "+25112515232", + "otpLabel": "OTP Code", + "otpPlaceholder": "123123", + "passwordLabel": "Password", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Reset", + "resendButton": "Resend Code", + "emailLabel": "Email", + "emailPlaceholder": "test@gmail.com", + "toastSuccessTitle": "Success", + "toastResetSuccess": "Password reset request sent.", + "toastInfoTitle": "Info", + "toastResendInfo": "We have resent the reset code." + }, + "home": { + "balanceLabel": "Balance", + "accountDetails": "Account Details", + "addButton": "Add", + "cashOutButton": "Cash Out", + "recipientsTitle": "Recipients", + "contactsAllowAccess": "Allow Access", + "contactsGrantAccess": "Grant Access", + "contactsLoading": "Loading...", + "contactsError": "Error loading contacts", + "contactsNoContacts": "No Contacts", + "transactionsTitle": "Transactions", + "transactionsLoading": "Loading transactions...", + "transactionsError": "Error loading transactions", + "transactionsNoTransactions": "No transactions yet", + "transactionsEmptySubtitle": "Your transaction history will appear here", + "transactionsMore": "+{{count}} more transactions", + "cashoutErrorTitle": "Error", + "cashoutNoBalance": "No balance found", + "cashoutMinError": "Balance is less than the minimum amount ($10)", + "addCashNoPaymentMethodTitle": "No Payment Method", + "addCashNoPaymentMethodMessage": "You need to add a card before you can add cash to your wallet.", + "addCashCancel": "Cancel", + "addCashAddCard": "Add Card" + }, + "history": { + "title": "History", + "subtitle": "List of transactions.", + "loading": "Loading transactions...", + "errorTitle": "Error loading transactions", + "emptyTitle": "No transactions yet", + "emptySubtitle": "Your transaction history will appear here", + "filterTitle": "Filter transactions", + "filterSubtitle": "Choose a date range and type to narrow your history.", + "dateRangeLabel": "Date range", + "fromLabel": "From", + "toLabel": "To", + "selectStart": "Select start", + "selectEnd": "Select end", + "clearDates": "Clear dates", + "typeLabel": "Type", + "typeAll": "All", + "typeIncoming": "Incoming", + "typeOutgoing": "Outgoing", + "applyFilters": "Apply filters", + "searchPlaceholder": "Search History", + "toastErrorTitle": "Error", + "toastTransactionsError": "Error loading transactions." + }, + "cardmang": { + "title": "Cards", + "subtitle": "Manage all your payment information here.", + "searchPlaceholder": "Search account name, card number", + "addCardButton": "Add Card", + "paymentOptionsTitle": "Payment Options", + "cardTypeFallback": "Card", + "cardExpires": "Expires {{date}}", + "loading": "Loading cards...", + "errorTitle": "Error loading cards", + "emptyTitle": "No cards added yet", + "emptySubtitle": "Add your first card to start managing your payments", + "toastRemoveSuccessTitle": "Success", + "toastRemoveSuccess": "Card removed successfully!", + "toastRemoveErrorTitle": "Error", + "toastRemoveError": "Failed to remove card" + }, + "listrecipient": { + "title": "Recipient", + "searchPlaceholder": "Search name, phone number, account", + "addButton": "Add New Recipient", + "savedRecipientsTitle": "Saved Recipients ({{count}})", + "savedRecipientsLoading": "Loading recipients...", + "savedRecipientsError": "Error loading recipients", + "savedRecipientsEmpty": "No saved recipients yet", + "contactsTitle": "Your Contacts ({{count}})", + "contactsLoading": "Loading contacts...", + "contactsErrorTitle": "Error loading contacts", + "contactsErrorSubtitle": "There was a problem loading your contacts.", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential recipients", + "contactsPermissionButton": "Allow Access", + "contactsEmptyTitle": "No contacts found", + "contactsEmptySubtitle": "No contacts with phone numbers were found on your device", + "contactNoPhone": "No phone number", + "toastErrorTitle": "Error", + "toastContactsError": "Error loading contacts.", + "toastRecipientsError": "Error loading recipients." + }, + "sendorrequestmoney": { + "verifyingSecurity": "Verifying security...", + "availableBalanceLabel": "Available Balance", + "processingFee": "Processing fee: ${{fee}} ({{percent}}%)", + "totalLabel": "Total: ${{total}}", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount", + "validationMinAmount": "Minimum amount is $0.01", + "validationMaxAmount": "Maximum amount is $999.99", + "validationInsufficientBalance": "Insufficient balance. Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "requestButton": "Request", + "requestButtonWithAmount": "Request ${{amount}}", + "payButton": "Pay", + "payButtonWithAmount": "Pay ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN" + }, + "addcard": { + "title": "Add Card", + "sectionCardTitle": "Card", + "sectionCardSubtitle": "You can save your cards here.", + "cardNumberLabel": "Card Number", + "cardNumberPlaceholder": "1234 5678 9012 3456", + "expiryDateLabel": "Expiry Date", + "expiryDatePlaceholder": "MM/YY", + "cvvLabel": "CVV", + "cvvPlaceholder": "123", + "addButton": "Add Card", + "addButtonLoading": "Adding...", + "validationErrorTitle": "Error", + "validationCardNumberRequired": "Card number is required", + "validationExpiryRequired": "Expiry date is required", + "validationCvvRequired": "CVV is required", + "validationInvalidCard": "Please enter valid card details", + "toastErrorTitle": "Card Error", + "toastAddFailed": "Failed to add credit card" + }, + "addcash": { + "verifyingSecurity": "Verifying security...", + "title": "Add", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount greater than $0.00", + "validationMinAmount": "Minimum amount is $10.00", + "validationMaxAmount": "Maximum amount is $999.99", + "addButton": "Add", + "addButtonWithAmount": "Add ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN to add money" + }, + "addrecipient": { + "title": "Add Recipient", + "sectionTitle": "Recipient", + "sectionSubtitle": "Add anyone you want to send money to.", + "fullNameLabel": "Full Name", + "fullNamePlaceholder": "Kirubel Kibru", + "phoneLabel": "Phone Number", + "phonePlaceholder": "+251983032475", + "addButton": "Add", + "addButtonLoading": "Adding...", + "validationErrorTitle": "Validation Error", + "validationFullNameRequired": "Please enter the recipient's full name", + "validationPhoneRequired": "Please enter a phone number", + "validationPhoneInvalid": "Please enter a valid phone number", + "toastErrorTitle": "Error", + "toastAddError": "Failed to add recipient" + }, + "addcashcomp": { + "successNote": "You have successfully added credits to your wallet.", + "addAgainButton": "Add Cash Again", + "goHomeButton": "Go Back Home" + }, + "cashout": { + "verifyingSecurity": "Verifying security...", + "availableBalanceLabel": "Available Balance", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount", + "validationMinAmount": "Minimum amount is $0.01", + "validationMaxAmount": "Maximum amount is $999.99", + "validationInsufficientBalance": "Insufficient balance. Required: ${{required}}. Available: ${{available}}", + "button": "Cash Out", + "buttonWithAmount": "Cash Out ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN to cash out" + }, + "cardaddedcomp": { + "title": "Done", + "description": "You have successfully added your card.", + "addButton": "Add", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just added a new card to my Amba App!", + "shareTitle": "Card Added Successfully", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share. Please try again." + }, + "cashoutcomp": { + "successNote": "You have successfully cashed out to your account.", + "cashOutAgainButton": "Cash Out Again", + "goHomeButton": "Go Back Home", + "toastErrorTitle": "Error", + "toastNoBalance": "No balance found", + "toastMinError": "Balance is less than the minimum amount ($10)" + }, + "crowdfunding": { + "tabsOverview": "Overview", + "tabsCampaign": "Campaign", + "tabsFaq": "FAQ", + "title": "Title of the Campaigns", + "description": "Lorem ipsum dolor sit amet consectetur. Duis auctor sed eget pellentesque fringilla. Massa sed porta tortor suspendisse.", + "pledgedAmount": "$123,000", + "pledgedOf": "pledged of {{target}}", + "backersCountLabel": "backers.", + "daysToGoLabel": "days to go.", + "emailLabel": "Email", + "emailPlaceholder": "test@gmail.com", + "otpLabel": "OTP Code", + "otpPlaceholder": "123123", + "passwordLabel": "Password", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Reset", + "resendButton": "Resend Code" + }, + "eventdetail": { + "shareMessage": "Join me at this event!", + "title": "Event Name", + "description": "No description available", + "location": "Millenium Hall Addis Ababa", + "dateTime": "Sat, Jan 10, 2026 at 6:00 PM - 1:00 AM (EAT)", + "peopleComing": "People you know are coming!", + "ticketLabel": "Ticket #{{index}}", + "ticketPrice": "$55.44", + "buyButton": "Buy Now", + "shareButton": "Share", + "guestListButton": "Guest List", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share event. Please try again." + }, + "eventqrscreen": { + "printButton": "Print", + "goBackButton": "Go Back" + }, + "events": { + "title": "Events", + "subtitle": "Engage with your community", + "searchPlaceholder": "Event Name , Location", + "filterButton": "Filter", + "myTicketsButton": "My Tickets", + "featuredTitle": "Featured Events", + "cardTitle": "Event Name", + "cardDescription": "Lorem ipsum dolor sit amet consectetur. Convallis mauris porttitor leo pellentesque amet pulvinar nullam.", + "ticketCountPrefix": "100 - ", + "ticketLocation": "Tickets Los Angeles", + "ticketDate": "23 Jan26" + }, + "moneydonated": { + "title": "Done", + "description": "You have successfully honored the money request.", + "goHomeButton": "Go Back Home" + }, + "moneyrequested": { + "title": "Done", + "description": "You have successfully requested credit.", + "descriptionWithName": "You have successfully requested credit from {{fullName}}.", + "requestAgainButton": "Request Again", + "goHomeButton": "Go Back Home" + }, + "mytickets": { + "title": "Events", + "subtitle": "Manage all your tickets information here.", + "searchPlaceholder": "Event Name", + "filterButton": "Filter", + "ticketsTitle": "Tickets" + }, + "notification": { + "title": "Notification", + "sectionToday": "TODAY", + "loading": "Loading notifications...", + "errorWithMessage": "Error: {{error}}", + "emptyTitle": "No notifications", + "emptySubtitle": "You'll see notifications about money requests and transactions here.", + "toastErrorTitle": "Error", + "toastRequestNotPending": "Request cannot be honored at this time.", + "toastRequestActionFailed": "Failed to {{action}} request." + }, + "notificationOption": { + "title": "Ping Them", + "sectionTitle": "Notification Option", + "sectionSubtitle": "Select your preferred notification method.", + "smsLabel": "SMS Notification", + "whatsappLabel": "WhatsApp", + "continueButton": "Continue", + "toastErrorTitle": "Error", + "toastAuthRequired": "You must be signed in to send a request.", + "toastMissingInfo": "Missing request information.", + "toastInvalidAmount": "Invalid amount.", + "toastCreateFailed": "Failed to create money request.", + "toastRequestFailed": "Failed to request money. Please try again.", + "toastSelectMethod": "Please select a notification method." + }, + "points": { + "title": "Points", + "referTitle": "Refer your friends", + "earnSubtitle": "& Earn Points", + "copyButton": "Copy", + "shareButton": "Share", + "activityButton": "Activity", + "howToEarnButton": "How to Earn", + "rewardsTitle": "Rewards", + "reward1Title": "Transaction", + "reward1Description": "Waived your transaction fee" + }, + "pointsactivity": { + "title": "Activity", + "activity1Title": "Add Recipient", + "activity1Date": "13 Jan 24 • 8:00 AM", + "activity2Title": "Transaction Discount", + "activity2Date": "13 Jan 24 • 8:00 AM", + "pointsPill": "{{sign}} {{points}} Points" + }, + "profile": { + "title": "Profile", + "pointsBadge": "1200 points", + "loadingProfile": "Loading profile...", + "errorWithMessage": "Error: {{error}}", + "fullNameLabel": "Full Name", + "fullNamePlaceholder": "Full Name", + "addressLabel": "Address", + "addressPlaceholder": "Address", + "phoneLabel": "Phone Number", + "phonePlaceholder": "Phone Number", + "emailLabel": "Email", + "emailPlaceholder": "Email", + "languageLabel": "Language", + "languagePlaceholder": "Select language", + "languageOptionEnglish": "English", + "languageOptionAmharic": "Amharic", + "languageOptionFrench": "French", + "languageOptionTigrinya": "Tigrinya", + "languageOptionOromo": "Oromo", + "accountNumberLabel": "Account Number", + "accountNumberPlaceholder": "Not available", + "usernameLabel": "Username", + "usernamePlaceholder": "@username", + "editButton": "Edit Profile", + "saveButton": "Save Changes", + "savingButton": "Saving...", + "cancelButton": "Cancel", + "pointsButton": "Points", + "logoutButton": "Log Out", + "toastLoggedOutTitle": "Logged out", + "toastLoggedOutDescription": "You have been signed out.", + "toastErrorTitle": "Error", + "toastLogoutFailed": "Failed to log out. Please try again.", + "toastUserNotFound": "User not found.", + "toastFullNameRequiredTitle": "Full name required", + "toastFullNameRequiredDescription": "Please enter your full name.", + "toastProfileUpdatedTitle": "Profile updated", + "toastProfileUpdatedDescription": "Your profile was updated.", + "toastUpdateErrorTitle": "Update Error", + "toastUpdateErrorDescription": "Failed to update profile." + }, + "qrscreen": { + "shareMessage": "Scan my Amba QR to send or receive money.", + "shareButton": "Share", + "goBackButton": "Go Back", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share QR. Please try again." + }, + "selectacc": { + "title": "Select Account", + "addingAmount": "Adding: ${{amount}}", + "accountsTitle": "Accounts", + "accountsDescriptionSelected": "Card selected! Tap Continue to proceed.", + "accountsDescriptionUnselected": "Select the card you want to add cash to.", + "loadingCards": "Loading cards...", + "errorTitle": "Error loading cards", + "errorWithMessage": "Error: {{error}}", + "emptyTitle": "No cards available", + "emptySubtitle": "Add a card to continue with adding cash.", + "buttonProcessing": "Processing...", + "buttonProceed": "Proceed", + "toastErrorTitle": "Error", + "toastMissingInfo": "Missing required information.", + "toastAddCashFailed": "Failed to add cash to wallet.", + "toastAddCashFailedWithRetry": "Failed to add cash to wallet. Please try again." + }, + "recipaddedcomp": { + "title": "New Recipient", + "description": "You have successfully added a new recipient.", + "addButton": "Add", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just added a new recipient to my Amba App!", + "shareTitle": "Recipient Added Successfully", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share. Please try again." + }, + "selectdonor": { + "title": "Select Donor", + "requestButton": "Request", + "requestButtonLoading": "Requesting...", + "toLabel": "To", + "forLabel": "For", + "searchPlaceholder": "Search by name or phone number", + "notePlaceholder": "Add a note (optional)", + "donorsTitle": "Contacts ({{count}})", + "loadingDonors": "Loading donors...", + "errorTitle": "Error loading donors", + "errorWithMessage": "Error: {{error}}", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential donors", + "contactsAllowAccess": "Allow Access", + "emptyTitleSearch": "No donors found matching your search", + "emptyTitleDefault": "No donors found", + "emptySubtitleSearch": "Try a different search term", + "emptySubtitleDefault": "Add saved recipients or grant contacts permission", + "toastErrorTitle": "Error", + "toastMissingInfo": "Please select a donor and ensure amount is specified", + "toastInvalidAmount": "Invalid amount", + "toastDonorNotFound": "Selected donor not found" + }, + "selectrecip": { + "title": "Select Recipient", + "sendButton": "Send", + "sendButtonLoading": "Sending...", + "toLabel": "To", + "forLabel": "For", + "searchPlaceholder": "Search by name or phone number", + "notePlaceholder": "Add a note (optional)", + "recipientsTitle": "Recipients ({{count}})", + "loadingRecipients": "Loading recipients...", + "errorTitle": "Error loading recipients", + "errorWithMessage": "Error: {{error}}", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential recipients", + "contactsAllowAccess": "Allow Access", + "emptyTitleSearch": "No recipients found matching your search", + "emptyTitleDefault": "No recipients found", + "emptySubtitleSearch": "Try a different search term", + "emptySubtitleDefault": "Add saved recipients or grant contacts permission", + "toastErrorTitle": "Error", + "toastMissingInfo": "Please select a recipient and ensure amount is specified", + "toastInvalidAmount": "Invalid amount", + "toastWalletNotFound": "Wallet not found", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastRecipientNotFound": "Selected recipient not found" + }, + "sendbank": { + "amountTitleCashOut": "Cash Out", + "amountTitleToRecipient": "to {{recipientName}}", + "noteWithText": "Note: {{note}}", + "paymentOptionsTitle": "Payment Options", + "paymentOptionsSelected": "Selected: {{providerName}}", + "paymentOptionsUnselected": "Select your preferred payment method.", + "awashName": "Awash Bank", + "awashSubtitle": "Bank Transfer", + "telebirrName": "Telebirr", + "telebirrSubtitle": "Mobile Money", + "sendButtonCashOut": "Cash Out ${{amount}} via {{providerName}}", + "sendButtonSend": "Send ${{amount}} via {{providerName}}", + "processingTitle": "Processing Transaction...", + "processingSubtitle": "Please wait while we process your payment", + "poweredBy": "Powered By", + "toastErrorTitle": "Error", + "toastNoMethod": "Please select a payment method first", + "toastNoAccount": "Please select an account first", + "chooseAccountTitle": "Choose Account", + "accountLabel": "Account Number", + "noAccounts": "No linked accounts found", + "continueButton": "Continue", + "toastMissingInfo": "Missing transaction information", + "toastInvalidAmount": "Invalid amount", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceCashoutDescription": "Required: ${{required}}. Available: ${{available}}", + "toastInsufficientBalanceSendDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastMissingRecipient": "Missing recipient information", + "toastProcessFailed": "Failed to process transaction", + "toastProcessFailedWithRetry": "Failed to process transaction. Please try again." + }, + "checkout": { + "title": "Checkout", + "subtitle": "Choose how you want to complete this payment", + "recipientLabel": "Recipient", + "totalLabel": "Total", + "paymentOptionsTitle": "Payment options", + "cardInfoTitle": "Card information", + "paymentEmailTitle": "Payment email", + "appleIdTitle": "Apple ID", + "contactInfoTitle": "Contact information", + "billingAddressTitle": "Billing address", + "cardNumberPlaceholder": "Card number", + "expiryPlaceholder": "MM/YY", + "cvvPlaceholder": "CVV", + "paymentEmailPlaceholder": "Payment email", + "appleIdPlaceholder": "Apple ID", + "payButton": "Pay", + "payButtonProcessing": "Processing..." + }, + "donation": { + "title": "Donations", + "subtitle": "Choose a cause to support with this transfer", + "chooseAmountTitle": "Choose amount", + "donationTypeOneTime": "One-Time", + "donationTypeMonthly": "Monthly", + "donationRaisedLabel": "Donation Raised", + "donateAnonymouslyLabel": "Donate Anonymously", + "anonymousLabel": "Anonymous", + "displayNameFallback": "Your name", + "skipButton": "Skip", + "donateButton": "Donate" + }, + "taskcomp": { + "successDescription": "You have successfully transferred money.", + "sendAgainButton": "Send Again", + "shareButton": "Share", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just successfully transferred money via Amba App!", + "shareTitle": "Money Transfer Success", + "toastErrorTitle": "Error", + "toastNoBalance": "No balance found", + "toastMinError": "Balance is less than the minimum amount ($10)", + "toastShareError": "Failed to share. Please try again.", + "ratingTitle": "Rate this transfer", + "ratingSubtitle": "Tell us how this transaction went so we can make AmbaPay better.", + "ratingOverallLabel": "Overall experience", + "ratingPurposeLabel": "What was this transaction for?", + "ratingPurposePlaceholder": "Select a category", + "ratingPurposeFamily": "Family", + "ratingPurposeMedical": "Medical", + "ratingPurposeLoan": "Loan", + "ratingPurposePurchase": "Purchase", + "ratingPurposeOther": "Other", + "ratingOtherLabel": "Add your own reason", + "ratingOtherPlaceholder": "Write your reason", + "ratingSubmitButton": "Submit & go home" + }, + "transconfirm": { + "title": "Transaction Details", + "planningDescription": "You are planning to send money to {{recipientName}}", + "sectionTitle": "Transaction Detail", + "noteLabel": "Note", + "processingFeeLabel": "Processing Fee (1.25%)", + "subtotalLabel": "Subtotal", + "totalLabel": "Total", + "buttonProcessing": "Processing...", + "buttonConfirm": "Confirm", + "toastErrorTitle": "Error", + "toastMissingDetails": "Transaction details missing", + "toastInvalidAmount": "Invalid amount", + "toastWalletNotFound": "Wallet not found", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastRecipientMissing": "Recipient details missing", + "toastSendFailed": "Failed to send money", + "toastSendFailedWithRetry": "Failed to send money. Please try again." + }, + "transdetail": { + "title": "Transaction Details", + "descriptionSend": "You sent money to {{recipientName}}", + "descriptionReceive": "You received money from {{recipientName}}", + "descriptionAddCash": "You added cash to your wallet", + "descriptionCashOut": "You cashed out to your bank account", + "descriptionDefault": "Transaction", + "sectionTitle": "Transaction Detail", + "dateLabel": "Date", + "statusLabel": "Status", + "statusUnknown": "Unknown", + "dateUnknown": "Unknown", + "noteLabel": "Note", + "processingFeeLabel": "Processing Fee (1.25%)", + "subtotalLabel": "Subtotal", + "totalLabel": "Total", + "sendAgainButton": "Send Again" + }, + "components": { + "acccard": { + "cardTypeFallback": "Card", + "cardNumberPlaceholder": "**** **** **** ****", + "expiryPlaceholder": "MM/YY", + "expiryLabel": "Expires {{date}}" + }, + "accordion": {}, + "backbar": {}, + "bottomsheet": {}, + "button": {}, + "card": {}, + "cardcomp": { + "title": "Master Card", + "number": "13131-1313-3131-1313-1312" + }, + "profilecard": { + "emptyLabelContact": "Contact", + "unknownContact": "Unknown" + }, + "recipcard": { + "name": "Abebe Kebde", + "accountNumber": "1030230213021" + }, + "topbar": { + "greeting": "Hello" + }, + "contactmodal": { + "headerTitle": "Contact Details", + "phoneNumbersTitle": "Phone Numbers", + "emailAddressesTitle": "Email Addresses", + "noAdditionalInfo": "No phone numbers or email addresses available for this contact.", + "sendMoneyButton": "Send Money", + "closeButton": "Close", + "unknownContact": "Unknown contact" + }, + "pinconfirmationmodal": { + "titleDefault": "Confirm your Cash PIN", + "toastBiometricErrorTitle": "Biometric error", + "toastBiometricHardwareNotAvailable": "Biometric hardware not available", + "toastBiometricNotEnrolled": "No biometric enrolled on device", + "toastBiometricFailed": "Biometric authentication failed", + "toastInvalidPinTitle": "Invalid PIN", + "toastInvalidPinDescription": "Please enter a 6-digit PIN", + "toastAuthErrorTitle": "Authentication error", + "toastUserNotFound": "User not found", + "toastPinNotFound": "PIN not found", + "toastIncorrectPinTitle": "Incorrect PIN", + "toastIncorrectPinDescription": "Please try again.", + "authChoiceTitle": "Choose Authentication Method", + "fingerprintTitle": "Fingerprint", + "fingerprintSubtitle": "Use your fingerprint to authenticate", + "pinTitle": "AmbaPay PIN", + "pinSubtitle": "Enter your 6-digit PIN", + "biometricWaiting": "Waiting for biometric authentication…", + "cancelButton": "Cancel", + "pinVerificationTitle": "PIN Verification", + "submitButtonVerifying": "Verifying...", + "submitButtonConfirm": "Confirm" + }, + "transactioncard": { + "descriptionSend": "Sent to {{recipientName}}", + "descriptionReceive": "Received from {{senderName}}", + "descriptionAddCash": "Added Cash (****{{lastFourDigits}})", + "descriptionCashOut": "Cash Out to {{bankProvider}}", + "descriptionDefault": "Transaction", + "detailPhone": "Phone: {{phoneNumber}}", + "detailCard": "Card: ****{{lastFourDigits}}", + "detailAccount": "Account: {{accountNumber}}", + "detailBankProvider": "via {{bankProvider}}" + } + } +} diff --git a/locales/fr/common.json b/locales/fr/common.json new file mode 100644 index 0000000..0a99b1d --- /dev/null +++ b/locales/fr/common.json @@ -0,0 +1,665 @@ +{ + "common": { + "back": "Retour" + }, + "signin": { + "phoneLabel": "Phone Number", + "phonePlaceholder": "9XXXXXXXX", + "button": "Signin", + "buttonLoading": "Sending OTP...", + "validationErrorTitle": "Error", + "validationInvalidPhone": "Please enter a valid phone number", + "toastErrorTitle": "Error", + "toastOtpFailed": "Failed to send OTP. Please check your phone number format.", + "toastAuthErrorTitle": "Phone Authentication Error" + }, + "otp": { + "title": "Verification", + "description": "We have sent a code to your phone number", + "codeLabel": "OTP Code", + "codePlaceholder": "123456", + "verifyButton": "Verify", + "verifyButtonLoading": "Verifying...", + "resendButton": "Resend Code", + "resendButtonCountdown": "Resend Code in {{countdown}}s", + "validationErrorTitle": "Error", + "validationInvalidCode": "Please enter a valid 6-digit OTP code", + "toastErrorTitle": "Error", + "toastInvalidCode": "Invalid OTP code. Please try again.", + "toastInfoTitle": "Info", + "toastBackInfo": "Going back to sign in", + "toastResendInfo": "Please go back to the sign-in screen to resend the code", + "toastSuccessTitle": "Success", + "toastDevSuccess": "OTP verified (dev mode)", + "toastVerificationErrorTitle": "OTP Verification Error" + }, + "phoneSetup": { + "title": "Final Setup", + "subtitle": "Some questions before you send money.", + "fullNameLabel": "Full Name *", + "fullNamePlaceholder": "Abebe Kebede", + "addressLabel": "Address", + "addressPlaceholder": "Addis Ababa, Ethiopia", + "emailLabel": "Email Address *", + "emailPlaceholder": "abebe@example.com", + "pinLabel": "PIN *", + "pinPlaceholder": "Enter 6-digit PIN", + "confirmPinLabel": "Confirm PIN *", + "confirmPinPlaceholder": "Confirm your PIN", + "button": "Done", + "buttonLoading": "Setting up...", + "validationErrorTitle": "Error", + "validationFullNameRequired": "Please enter your full name", + "validationEmailRequired": "Please enter your email address", + "validationEmailInvalid": "Please enter a valid email address", + "validationPinRequired": "Please enter a PIN", + "validationPinLength": "PIN must be exactly 6 digits", + "validationConfirmPinRequired": "Please confirm your PIN", + "validationPinsMismatch": "PIN and confirm PIN do not match", + "toastSuccessTitle": "Success", + "toastDevSuccess": "Profile setup complete (dev mode)", + "toastNoUserTitle": "Error", + "toastNoUser": "No authenticated user found", + "toastSetupErrorTitle": "Setup Error", + "toastSetupError": "Failed to complete profile setup. Please try again." + }, + "forgot": { + "headerTitle": "Reset Pass", + "title": "Reset Password", + "description": "We have sent a code to your device to reset your password.", + "tabsPhone": "Phone", + "tabsEmail": "Email", + "phoneSelectorLabel": "Phone Selector", + "phonePlaceholder": "+25112515232", + "otpLabel": "OTP Code", + "otpPlaceholder": "123123", + "passwordLabel": "Password", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Reset", + "resendButton": "Resend Code", + "emailLabel": "Email", + "emailPlaceholder": "test@gmail.com", + "toastSuccessTitle": "Success", + "toastResetSuccess": "Password reset request sent.", + "toastInfoTitle": "Info", + "toastResendInfo": "We have resent the reset code." + }, + "home": { + "balanceLabel": "Balance", + "accountDetails": "Account Details", + "addButton": "Add", + "cashOutButton": "Cash Out", + "recipientsTitle": "Recipients", + "contactsAllowAccess": "Allow Access", + "contactsGrantAccess": "Grant Access", + "contactsLoading": "Loading...", + "contactsError": "Error loading contacts", + "contactsNoContacts": "No Contacts", + "transactionsTitle": "Transactions", + "transactionsLoading": "Loading transactions...", + "transactionsError": "Error loading transactions", + "transactionsNoTransactions": "No transactions yet", + "transactionsEmptySubtitle": "Your transaction history will appear here", + "transactionsMore": "+{{count}} more transactions", + "cashoutErrorTitle": "Error", + "cashoutNoBalance": "No balance found", + "cashoutMinError": "Balance is less than the minimum amount ($10)", + "addCashNoPaymentMethodTitle": "No Payment Method", + "addCashNoPaymentMethodMessage": "You need to add a card before you can add cash to your wallet.", + "addCashCancel": "Cancel", + "addCashAddCard": "Add Card" + }, + "history": { + "title": "History", + "subtitle": "List of transactions.", + "loading": "Loading transactions...", + "errorTitle": "Error loading transactions", + "emptyTitle": "No transactions yet", + "emptySubtitle": "Your transaction history will appear here", + "filterTitle": "Filter transactions", + "filterSubtitle": "Choose a date range and type to narrow your history.", + "dateRangeLabel": "Date range", + "fromLabel": "From", + "toLabel": "To", + "selectStart": "Select start", + "selectEnd": "Select end", + "clearDates": "Clear dates", + "typeLabel": "Type", + "typeAll": "All", + "typeIncoming": "Incoming", + "typeOutgoing": "Outgoing", + "applyFilters": "Apply filters", + "searchPlaceholder": "Search History", + "toastErrorTitle": "Error", + "toastTransactionsError": "Error loading transactions." + }, + "cardmang": { + "title": "Cards", + "subtitle": "Manage all your payment information here.", + "searchPlaceholder": "Search account name, card number", + "addCardButton": "Add Card", + "paymentOptionsTitle": "Payment Options", + "cardTypeFallback": "Card", + "cardExpires": "Expires {{date}}", + "loading": "Loading cards...", + "errorTitle": "Error loading cards", + "emptyTitle": "No cards added yet", + "emptySubtitle": "Add your first card to start managing your payments", + "toastRemoveSuccessTitle": "Success", + "toastRemoveSuccess": "Card removed successfully!", + "toastRemoveErrorTitle": "Error", + "toastRemoveError": "Failed to remove card" + }, + "listrecipient": { + "title": "Recipient", + "searchPlaceholder": "Search name, phone number, account", + "addButton": "Add New Recipient", + "savedRecipientsTitle": "Saved Recipients ({{count}})", + "savedRecipientsLoading": "Loading recipients...", + "savedRecipientsError": "Error loading recipients", + "savedRecipientsEmpty": "No saved recipients yet", + "contactsTitle": "Your Contacts ({{count}})", + "contactsLoading": "Loading contacts...", + "contactsErrorTitle": "Error loading contacts", + "contactsErrorSubtitle": "There was a problem loading your contacts.", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential recipients", + "contactsPermissionButton": "Allow Access", + "contactsEmptyTitle": "No contacts found", + "contactsEmptySubtitle": "No contacts with phone numbers were found on your device", + "contactNoPhone": "No phone number", + "toastErrorTitle": "Error", + "toastContactsError": "Error loading contacts.", + "toastRecipientsError": "Error loading recipients." + }, + "sendorrequestmoney": { + "verifyingSecurity": "Verifying security...", + "availableBalanceLabel": "Available Balance", + "processingFee": "Processing fee: ${{fee}} ({{percent}}%)", + "totalLabel": "Total: ${{total}}", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount", + "validationMinAmount": "Minimum amount is $0.01", + "validationMaxAmount": "Maximum amount is $999.99", + "validationInsufficientBalance": "Insufficient balance. Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "requestButton": "Request", + "requestButtonWithAmount": "Request ${{amount}}", + "payButton": "Pay", + "payButtonWithAmount": "Pay ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN" + }, + "addcard": { + "title": "Add Card", + "sectionCardTitle": "Card", + "sectionCardSubtitle": "You can save your cards here.", + "cardNumberLabel": "Card Number", + "cardNumberPlaceholder": "1234 5678 9012 3456", + "expiryDateLabel": "Expiry Date", + "expiryDatePlaceholder": "MM/YY", + "cvvLabel": "CVV", + "cvvPlaceholder": "123", + "addButton": "Add Card", + "addButtonLoading": "Adding...", + "validationErrorTitle": "Error", + "validationCardNumberRequired": "Card number is required", + "validationExpiryRequired": "Expiry date is required", + "validationCvvRequired": "CVV is required", + "validationInvalidCard": "Veuillez entrer des informations de carte valides", + "toastErrorTitle": "Card Error", + "toastAddFailed": "Failed to add credit card" + }, + "addcash": { + "verifyingSecurity": "Verifying security...", + "title": "Add", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount greater than $0.00", + "validationMinAmount": "Minimum amount is $10.00", + "validationMaxAmount": "Maximum amount is $999.99", + "addButton": "Add", + "addButtonWithAmount": "Add ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN to add money" + }, + "addrecipient": { + "title": "Add Recipient", + "sectionTitle": "Recipient", + "sectionSubtitle": "Add anyone you want to send money to.", + "fullNameLabel": "Full Name", + "fullNamePlaceholder": "Kirubel Kibru", + "phoneLabel": "Phone Number", + "phonePlaceholder": "+251983032475", + "addButton": "Add", + "addButtonLoading": "Adding...", + "validationErrorTitle": "Validation Error", + "validationFullNameRequired": "Please enter the recipient's full name", + "validationPhoneRequired": "Please enter a phone number", + "validationPhoneInvalid": "Please enter a valid phone number", + "toastErrorTitle": "Error", + "toastAddError": "Failed to add recipient" + }, + "addcashcomp": { + "successNote": "You have successfully added credits to your wallet.", + "addAgainButton": "Add Cash Again", + "goHomeButton": "Go Back Home" + }, + "cashout": { + "verifyingSecurity": "Verifying security...", + "availableBalanceLabel": "Available Balance", + "validationErrorTitle": "Invalid Amount", + "validationEnterAmount": "Please enter an amount", + "validationMinAmount": "Minimum amount is $0.01", + "validationMaxAmount": "Maximum amount is $999.99", + "validationInsufficientBalance": "Insufficient balance. Required: ${{required}}. Available: ${{available}}", + "button": "Cash Out", + "buttonWithAmount": "Cash Out ${{amount}}", + "pinModalTitle": "Confirm your Cash PIN to cash out" + }, + "cardaddedcomp": { + "title": "Done", + "description": "You have successfully added your card.", + "addButton": "Add", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just added a new card to my Amba App!", + "shareTitle": "Card Added Successfully", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share. Please try again." + }, + "cashoutcomp": { + "successNote": "You have successfully cashed out to your account.", + "cashOutAgainButton": "Cash Out Again", + "goHomeButton": "Go Back Home", + "toastErrorTitle": "Error", + "toastNoBalance": "No balance found", + "toastMinError": "Balance is less than the minimum amount ($10)" + }, + "crowdfunding": { + "tabsOverview": "Overview", + "tabsCampaign": "Campaign", + "tabsFaq": "FAQ", + "title": "Title of the Campaigns", + "description": "Lorem ipsum dolor sit amet consectetur. Duis auctor sed eget pellentesque fringilla. Massa sed porta tortor suspendisse.", + "pledgedAmount": "$123,000", + "pledgedOf": "pledged of {{target}}", + "backersCountLabel": "backers.", + "daysToGoLabel": "days to go.", + "emailLabel": "Email", + "emailPlaceholder": "test@gmail.com", + "otpLabel": "OTP Code", + "otpPlaceholder": "123123", + "passwordLabel": "Password", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Confirm Password", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Reset", + "resendButton": "Resend Code" + }, + "eventdetail": { + "shareMessage": "Join me at this event!", + "title": "Event Name", + "description": "Lorem ipsum dolor sit amet consectetur. Velit id ullamcorper sed nibh lacus elementum varius augue consequat. Orci mi ac eleifend lectus est orci.", + "location": "Millenium Hall Addis Ababa", + "dateTime": "Sat, Jan 10, 2026 at 6:00 PM - 1:00 AM (EAT)", + "peopleComing": "People you know are coming!", + "ticketLabel": "Ticket #{{index}}", + "ticketPrice": "$55.44", + "buyButton": "Buy Now", + "shareButton": "Share", + "guestListButton": "Guest List", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share event. Please try again." + }, + "eventqrscreen": { + "printButton": "Print", + "goBackButton": "Go Back" + }, + "events": { + "title": "Events", + "subtitle": "Engage with your community", + "searchPlaceholder": "Event Name , Location", + "filterButton": "Filter", + "myTicketsButton": "My Tickets", + "featuredTitle": "Featured Events", + "cardTitle": "Event Name", + "cardDescription": "Lorem ipsum dolor sit amet consectetur. Convallis mauris porttitor leo pellentesque amet pulvinar nullam.", + "ticketCountPrefix": "100 - ", + "ticketLocation": "Tickets Los Angeles", + "ticketDate": "23 Jan26" + }, + "moneydonated": { + "title": "Done", + "description": "You have successfully honored the money request.", + "goHomeButton": "Go Back Home" + }, + "moneyrequested": { + "title": "Done", + "description": "You have successfully requested credit.", + "descriptionWithName": "You have successfully requested credit from {{fullName}}.", + "requestAgainButton": "Request Again", + "goHomeButton": "Go Back Home" + }, + "mytickets": { + "title": "Events", + "subtitle": "Manage all your tickets information here.", + "searchPlaceholder": "Event Name", + "filterButton": "Filter", + "ticketsTitle": "Tickets" + }, + "notification": { + "title": "Notification", + "sectionToday": "TODAY", + "loading": "Loading notifications...", + "errorWithMessage": "Error: {{error}}", + "emptyTitle": "No notifications", + "emptySubtitle": "You'll see notifications about money requests and transactions here.", + "toastErrorTitle": "Error", + "toastRequestNotPending": "Request cannot be honored at this time.", + "toastRequestActionFailed": "Failed to {{action}} request." + }, + "notificationOption": { + "title": "Ping Them", + "sectionTitle": "Notification Option", + "sectionSubtitle": "Select your preferred notification method.", + "smsLabel": "SMS Notification", + "whatsappLabel": "WhatsApp", + "continueButton": "Continue", + "toastErrorTitle": "Error", + "toastAuthRequired": "You must be signed in to send a request.", + "toastMissingInfo": "Missing request information.", + "toastInvalidAmount": "Invalid amount.", + "toastCreateFailed": "Failed to create money request.", + "toastRequestFailed": "Failed to request money. Please try again.", + "toastSelectMethod": "Please select a notification method." + }, + "points": { + "title": "Points", + "referTitle": "Refer your friends", + "earnSubtitle": "& Earn Points", + "copyButton": "Copy", + "shareButton": "Share", + "activityButton": "Activity", + "howToEarnButton": "How to Earn", + "rewardsTitle": "Rewards", + "reward1Title": "Transaction", + "reward1Description": "Waived your transaction fee" + }, + "pointsactivity": { + "title": "Activity", + "activity1Title": "Add Recipient", + "activity1Date": "13 Jan 24 • 8:00 AM", + "activity2Title": "Transaction Discount", + "activity2Date": "13 Jan 24 • 8:00 AM", + "pointsPill": "{{sign}} {{points}} Points" + }, + "profile": { + "title": "Profile", + "pointsBadge": "1200 points", + "loadingProfile": "Loading profile...", + "errorWithMessage": "Error: {{error}}", + "fullNameLabel": "Full Name", + "fullNamePlaceholder": "Full Name", + "addressLabel": "Address", + "addressPlaceholder": "Address", + "phoneLabel": "Phone Number", + "phonePlaceholder": "Phone Number", + "emailLabel": "Email", + "emailPlaceholder": "Email", + "languageLabel": "Language", + "languagePlaceholder": "Select language", + "languageOptionEnglish": "English", + "languageOptionAmharic": "Amharic", + "languageOptionFrench": "French", + "languageOptionTigrinya": "Tigrinya", + "languageOptionOromo": "Oromo", + "accountNumberLabel": "Account Number", + "accountNumberPlaceholder": "Not available", + "usernameLabel": "Username", + "usernamePlaceholder": "@username", + "editButton": "Edit Profile", + "saveButton": "Save Changes", + "savingButton": "Saving...", + "cancelButton": "Cancel", + "pointsButton": "Points", + "logoutButton": "Log Out", + "toastLoggedOutTitle": "Logged out", + "toastLoggedOutDescription": "You have been signed out.", + "toastErrorTitle": "Error", + "toastLogoutFailed": "Failed to log out. Please try again.", + "toastUserNotFound": "User not found.", + "toastFullNameRequiredTitle": "Full name required", + "toastFullNameRequiredDescription": "Please enter your full name.", + "toastProfileUpdatedTitle": "Profile updated", + "toastProfileUpdatedDescription": "Your profile was updated.", + "toastUpdateErrorTitle": "Update Error", + "toastUpdateErrorDescription": "Failed to update profile." + }, + "qrscreen": { + "shareMessage": "Scan my Amba QR to send or receive money.", + "shareButton": "Share", + "goBackButton": "Go Back", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share QR. Please try again." + }, + "selectacc": { + "title": "Select Account", + "addingAmount": "Adding: ${{amount}}", + "accountsTitle": "Accounts", + "accountsDescriptionSelected": "Card selected! Tap Continue to proceed.", + "accountsDescriptionUnselected": "Select the card you want to add cash to.", + "loadingCards": "Loading cards...", + "errorTitle": "Error loading cards", + "errorWithMessage": "Error: {{error}}", + "emptyTitle": "No cards available", + "emptySubtitle": "Add a card to continue with adding cash.", + "buttonProcessing": "Processing...", + "buttonProceed": "Proceed", + "toastErrorTitle": "Error", + "toastMissingInfo": "Missing required information.", + "toastAddCashFailed": "Failed to add cash to wallet.", + "toastAddCashFailedWithRetry": "Failed to add cash to wallet. Please try again." + }, + "recipaddedcomp": { + "title": "New Recipient", + "description": "You have successfully added a new recipient.", + "addButton": "Add", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just added a new recipient to my Amba App!", + "shareTitle": "Recipient Added Successfully", + "toastErrorTitle": "Error", + "toastShareError": "Failed to share. Please try again." + }, + "selectdonor": { + "title": "Select Donor", + "requestButton": "Request", + "requestButtonLoading": "Requesting...", + "toLabel": "To", + "forLabel": "For", + "searchPlaceholder": "Search by name or phone number", + "notePlaceholder": "Add a note (optional)", + "donorsTitle": "Contacts ({{count}})", + "loadingDonors": "Loading donors...", + "errorTitle": "Error loading donors", + "errorWithMessage": "Error: {{error}}", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential donors", + "contactsAllowAccess": "Allow Access", + "emptyTitleSearch": "No donors found matching your search", + "emptyTitleDefault": "No donors found", + "emptySubtitleSearch": "Try a different search term", + "emptySubtitleDefault": "Add saved recipients or grant contacts permission", + "toastErrorTitle": "Error", + "toastMissingInfo": "Please select a donor and ensure amount is specified", + "toastInvalidAmount": "Invalid amount", + "toastDonorNotFound": "Selected donor not found" + }, + "selectrecip": { + "title": "Select Recipient", + "sendButton": "Send", + "sendButtonLoading": "Sending...", + "toLabel": "To", + "forLabel": "For", + "searchPlaceholder": "Search by name or phone number", + "notePlaceholder": "Add a note (optional)", + "recipientsTitle": "Recipients ({{count}})", + "loadingRecipients": "Loading recipients...", + "errorTitle": "Error loading recipients", + "errorWithMessage": "Error: {{error}}", + "contactsPermissionTitle": "Contacts Permission Required", + "contactsPermissionSubtitle": "Allow access to your contacts to see potential recipients", + "contactsAllowAccess": "Allow Access", + "emptyTitleSearch": "No recipients found matching your search", + "emptyTitleDefault": "No recipients found", + "emptySubtitleSearch": "Try a different search term", + "emptySubtitleDefault": "Add saved recipients or grant contacts permission", + "toastErrorTitle": "Error", + "toastMissingInfo": "Please select a recipient and ensure amount is specified", + "toastInvalidAmount": "Invalid amount", + "toastWalletNotFound": "Wallet not found", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastRecipientNotFound": "Selected recipient not found" + }, + "sendbank": { + "amountTitleCashOut": "Cash Out", + "amountTitleToRecipient": "to {{recipientName}}", + "noteWithText": "Note: {{note}}", + "paymentOptionsTitle": "Payment Options", + "paymentOptionsSelected": "Selected: {{providerName}}", + "paymentOptionsUnselected": "Select your preferred payment method.", + "awashName": "Awash Bank", + "awashSubtitle": "Bank Transfer", + "telebirrName": "Telebirr", + "telebirrSubtitle": "Mobile Money", + "sendButtonCashOut": "Cash Out ${{amount}} via {{providerName}}", + "sendButtonSend": "Send ${{amount}} via {{providerName}}", + "processingTitle": "Processing Transaction...", + "processingSubtitle": "Please wait while we process your payment", + "poweredBy": "Powered By", + "toastErrorTitle": "Error", + "toastNoMethod": "Please select a payment method first", + "toastMissingInfo": "Missing transaction information", + "toastInvalidAmount": "Invalid amount", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceCashoutDescription": "Required: ${{required}}. Available: ${{available}}", + "toastInsufficientBalanceSendDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastMissingRecipient": "Missing recipient information", + "toastProcessFailed": "Failed to process transaction", + "toastProcessFailedWithRetry": "Failed to process transaction. Please try again." + }, + "taskcomp": { + "successDescription": "You have successfully transferred money.", + "sendAgainButton": "Send Again", + "shareButton": "Share", + "goHomeButton": "Go Back Home", + "shareMessageWithParam": "{{message}} via Amba App", + "shareMessageDefault": "I just successfully transferred money via Amba App!", + "shareTitle": "Money Transfer Success", + "toastErrorTitle": "Error", + "toastNoBalance": "No balance found", + "toastMinError": "Balance is less than the minimum amount ($10)", + "toastShareError": "Failed to share. Please try again." + }, + "transconfirm": { + "title": "Transaction Details", + "planningDescription": "You are planning to send money to {{recipientName}}", + "sectionTitle": "Transaction Detail", + "noteLabel": "Note", + "processingFeeLabel": "Processing Fee (1.25%)", + "subtotalLabel": "Subtotal", + "totalLabel": "Total", + "buttonProcessing": "Processing...", + "buttonConfirm": "Confirm", + "toastErrorTitle": "Error", + "toastMissingDetails": "Transaction details missing", + "toastInvalidAmount": "Invalid amount", + "toastWalletNotFound": "Wallet not found", + "toastInsufficientBalanceTitle": "Insufficient Balance", + "toastInsufficientBalanceDescription": "Required: ${{required}} (including ${{fee}} processing fee). Available: ${{available}}", + "toastRecipientMissing": "Recipient details missing", + "toastSendFailed": "Failed to send money", + "toastSendFailedWithRetry": "Failed to send money. Please try again." + }, + "transdetail": { + "title": "Transaction Details", + "descriptionSend": "You sent money to {{recipientName}}", + "descriptionReceive": "You received money from {{recipientName}}", + "descriptionAddCash": "You added cash to your wallet", + "descriptionCashOut": "You cashed out to your bank account", + "descriptionDefault": "Transaction", + "sectionTitle": "Transaction Detail", + "dateLabel": "Date", + "statusLabel": "Status", + "statusUnknown": "Unknown", + "dateUnknown": "Unknown", + "noteLabel": "Note", + "processingFeeLabel": "Processing Fee (1.25%)", + "subtotalLabel": "Subtotal", + "totalLabel": "Total", + "sendAgainButton": "Send Again" + }, + "components": { + "acccard": { + "cardTypeFallback": "Card", + "cardNumberPlaceholder": "**** **** **** ****", + "expiryPlaceholder": "MM/YY", + "expiryLabel": "Expires {{date}}" + }, + "accordion": {}, + "backbar": {}, + "bottomsheet": {}, + "button": {}, + "card": {}, + "cardcomp": { + "title": "Master Card", + "number": "13131-1313-3131-1313-1312" + }, + "profilecard": { + "emptyLabelContact": "Contact", + "unknownContact": "Unknown" + }, + "recipcard": { + "name": "Abebe Kebde", + "accountNumber": "1030230213021" + }, + "topbar": { + "greeting": "Hello" + }, + "pinconfirmationmodal": { + "titleDefault": "Confirm your Cash PIN", + "toastBiometricErrorTitle": "Biometric error", + "toastBiometricHardwareNotAvailable": "Biometric hardware not available", + "toastBiometricNotEnrolled": "No biometric enrolled on device", + "toastBiometricFailed": "Biometric authentication failed", + "toastInvalidPinTitle": "Invalid PIN", + "toastInvalidPinDescription": "Please enter a 6-digit PIN", + "toastAuthErrorTitle": "Authentication error", + "toastUserNotFound": "User not found", + "toastPinNotFound": "PIN not found", + "toastIncorrectPinTitle": "Incorrect PIN", + "toastIncorrectPinDescription": "Please try again.", + "authChoiceTitle": "Choose Authentication Method", + "fingerprintTitle": "Fingerprint", + "fingerprintSubtitle": "Use your fingerprint to authenticate", + "pinTitle": "AmbaPay PIN", + "pinSubtitle": "Enter your 6-digit PIN", + "biometricWaiting": "Waiting for biometric authentication…", + "cancelButton": "Cancel", + "pinVerificationTitle": "PIN Verification", + "submitButtonVerifying": "Verifying...", + "submitButtonConfirm": "Confirm" + }, + "transactioncard": { + "descriptionSend": "Sent to {{recipientName}}", + "descriptionReceive": "Received from {{senderName}}", + "descriptionAddCash": "Added Cash (****{{lastFourDigits}})", + "descriptionCashOut": "Cash Out to {{bankProvider}}", + "descriptionDefault": "Transaction", + "detailPhone": "Phone: {{phoneNumber}}", + "detailCard": "Card: ****{{lastFourDigits}}", + "detailAccount": "Account: {{accountNumber}}", + "detailBankProvider": "via {{bankProvider}}" + } + } +} \ No newline at end of file diff --git a/locales/om/common.json b/locales/om/common.json new file mode 100644 index 0000000..b7c8de7 --- /dev/null +++ b/locales/om/common.json @@ -0,0 +1,715 @@ +{ + "signin": { + "phoneLabel": "Lakkoofsa Bilbila", + "phonePlaceholder": "9XXXXXXXX", + "button": "Seeni", + "buttonLoading": "OTP ergaa jira...", + "validationErrorTitle": "Dogoggora", + "validationInvalidPhone": "Mee lakkoofsa bilbila sirrii galchi", + "toastErrorTitle": "Dogoggora", + "toastOtpFailed": "OTP erguu hin milkoofne. Mee sirna lakkoofsa bilbila keeti ilaali.", + "toastAuthErrorTitle": "Dogoggora Mirkaneessa Bilbila" + }, + "otp": { + "title": "Mirkaneessa", + "description": "Lakkoofsa bilbila kee irratti koodni ergameera", + "codeLabel": "Koodii OTP", + "codePlaceholder": "123456", + "verifyButton": "Mirkaneessi", + "verifyButtonLoading": "Mirkaneessaa jira...", + "resendButton": "Koodii Irra Deebi'i", + "resendButtonCountdown": "Koodii irra deebi'ee erguuf {{countdown}}s eegaa", + "validationErrorTitle": "Dogoggora", + "validationInvalidCode": "Mee koodii OTP lakkofsa 6 sirrii galchi", + "toastErrorTitle": "Dogoggora", + "toastInvalidCode": "Koodiin OTP sirrii miti. Irra deebi'ii yaali.", + "toastInfoTitle": "Odeeffannoo", + "toastBackInfo": "Gara fuula seensaa deebi'aa jira", + "toastResendInfo": "Koodii irra deebi'ee argachuuf gara fuula seensaa deebi'i", + "toastSuccessTitle": "Milkaa'e", + "toastDevSuccess": "OTP (modus deeveelooperaa) mirkanaa'eera", + "toastVerificationErrorTitle": "Dogoggora Mirkaneessa OTP" + }, + "phoneSetup": { + "title": "Sajoo Xumuraa", + "subtitle": "Maallaqa erguu dura gaaffii muraasa si gaafanna.", + "fullNameLabel": "Maqaa Guutuu *", + "fullNamePlaceholder": "Abebe Kebede", + "addressLabel": "Teessoo", + "addressPlaceholder": "Finfinnee, Itoophiyaa", + "emailLabel": "Teessoo Imeelii *", + "emailPlaceholder": "abebe@example.com", + "pinLabel": "PIN *", + "pinPlaceholder": "PIN lakkofsa 6 galchi", + "confirmPinLabel": "PIN Mirkaneessi *", + "confirmPinPlaceholder": "PIN kee mirkaneessi", + "button": "Fixi", + "buttonLoading": "Sajoo taasisaa jira...", + "validationErrorTitle": "Dogoggora", + "validationFullNameRequired": "Mee maqaa guutuu kee galchi", + "validationEmailRequired": "Mee teessoo imeelii kee galchi", + "validationEmailInvalid": "Mee imeelii sirrii galchi", + "validationPinRequired": "Mee PIN galchi", + "validationPinLength": "PIN lakkofsa 6 qofaa qabaachuu qaba", + "validationConfirmPinRequired": "Mee PIN kee mirkaneessi", + "validationPinsMismatch": "PIN fi PIN mirkaneessaa wal hin fakkaatan", + "toastSuccessTitle": "Milkaa'e", + "toastDevSuccess": "Sajoon piroofaayilii (modus deeveelooperaa) xumurameera", + "toastNoUserTitle": "Dogoggora", + "toastNoUser": "Fayyadamtoonni mirkanaa'an hin argamne", + "toastSetupErrorTitle": "Dogoggora Sajoo", + "toastSetupError": "Sajoo piroofaayilii xumuruun hin milkoofne. Irra deebi'ii yaali." + }, + "forgot": { + "headerTitle": "Jecha Darbii Fooyyessi", + "title": "Jecha Darbii Haaromsii", + "description": "Jecha darbii kee haaromsuuf gara meeshaa kee koodni ergameera.", + "tabsPhone": "Bilbila", + "tabsEmail": "Imeelii", + "phoneSelectorLabel": "Filannoo Bilbila", + "phonePlaceholder": "+25112515232", + "otpLabel": "Koodii OTP", + "otpPlaceholder": "123123", + "passwordLabel": "Jecha Darbii", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Jecha darbii Mirkaneessi", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Haaromsii", + "resendButton": "Koodii Irra Deebi'i", + "emailLabel": "Imeelii", + "emailPlaceholder": "test@gmail.com", + "toastSuccessTitle": "Milkaa'e", + "toastResetSuccess": "Gaaffiin jecha darbii haaromsuu ergameera.", + "toastInfoTitle": "Odeeffannoo", + "toastResendInfo": "Koodii haaromsaa irra deebi'ee ergineera." + }, + "home": { + "balanceLabel": "Baalaansii", + "accountDetails": "Faayila Herrega", + "addButton": "Dabaluu", + "cashOutButton": "Maallaqa Baasi", + "recipientsTitle": "Fudhattoota", + "contactsAllowAccess": "Eeyyama Kenni", + "contactsGrantAccess": "Eeyyama Kenni", + "contactsLoading": "Fayyadamtoota fe'aa jira...", + "contactsError": "Fayyadamtoota fe'uu hin milkoofne", + "contactsNoContacts": "Fayyadamtoonni hin jiran", + "transactionsTitle": "Giddugala Hojii Maallaqaa", + "transactionsLoading": "Hojii maallaqaa fe'aa jira...", + "transactionsError": "Hojii maallaqaa fe'uu hin milkoofne", + "transactionsNoTransactions": "Hojii maallaqaa hin qabdu", + "transactionsEmptySubtitle": "Seenaa hojimaallaqaa kee as keessatti ni argita", + "transactionsMore": "+{{count}} hojii maallaqaa dabalataa", + "cashoutErrorTitle": "Dogoggora", + "cashoutNoBalance": "Baalaansiin hin argamne", + "cashoutMinError": "Baalaansiin qarshii $10 gadi dha", + "addCashNoPaymentMethodTitle": "Mala Kaffaltii Hin Jiruu", + "addCashNoPaymentMethodMessage": "Maallaqa gara walleetii kee dabaluu dura kaardii dabaluun si barbaachisa.", + "addCashCancel": "Haquu", + "addCashAddCard": "Kaardii Dabaluu" + }, + "history": { + "title": "Seenaa", + "subtitle": "Tarree hojii maallaqaa.", + "loading": "Hojii maallaqaa fe'aa jira...", + "errorTitle": "Hojii maallaqaa fe'uu hin milkoofne", + "emptyTitle": "Hojii maallaqaa hin qabdu", + "emptySubtitle": "Seenaa hojimaallaqaa kee as keessatti ni argita", + "filterTitle": "Hojii maallaqaa sivi", + "filterSubtitle": "Seenaa kee xiqqeessuuf bal'ina guyyaa fi akaakuu filadhu.", + "dateRangeLabel": "Bal'ina Guyyaa", + "fromLabel": "Irra", + "toLabel": "Gara", + "selectStart": "Guyyaa jalqabaa filadhu", + "selectEnd": "Guyyaa xumuraa filadhu", + "clearDates": "Guyyaa haqi", + "typeLabel": "Gosa", + "typeAll": "Hundaa", + "typeIncoming": "Seenaa seenuu", + "typeOutgoing": "Seenaa ba'u", + "applyFilters": "Sivii Fayyadami", + "searchPlaceholder": "Seenaa barbaadi", + "toastErrorTitle": "Dogoggora", + "toastTransactionsError": "Seenaa hojimaallaqaa fe'uu hin milkoofne." + }, + "cardmang": { + "title": "Kaardii", + "subtitle": "Odeeffannoo kaffaltii kee hunda asii bulchi.", + "searchPlaceholder": "Maqaa herregaa, lakkoofsa kaardii barbaadi", + "addCardButton": "Kaardii Dabaluu", + "paymentOptionsTitle": "Filannoowwan Kaffaltii", + "cardTypeFallback": "Kaardii", + "cardExpires": "Yeroo itti xumuramu {{date}}", + "loading": "Kaardii fe'aa jira...", + "errorTitle": "Kaardii fe'uu hin milkoofne", + "emptyTitle": "Kaardiin tokko illee hin dabalamin", + "emptySubtitle": "Kaffaltii bulchuu jalqabuuf kaardii tokko dabaluu", + "toastRemoveSuccessTitle": "Milkaa'e", + "toastRemoveSuccess": "Kaardiin haquu milkaa'eera!", + "toastRemoveErrorTitle": "Dogoggora", + "toastRemoveError": "Kaardii haquu hin milkoofne" + }, + "listrecipient": { + "title": "Fudhattaa", + "searchPlaceholder": "Maqaa, lakkoofsa bilbila, herrega barbaadi", + "addButton": "Namicha Fudhattaa Haaraa Dabaluu", + "savedRecipientsTitle": "Fudhattoota Kuufaman ({{count}})", + "savedRecipientsLoading": "Fudhattoota fe'aa jira...", + "savedRecipientsError": "Fudhattoota fe'uu hin milkoofne", + "savedRecipientsEmpty": "Fudhattoon kuufame hin jiru", + "contactsTitle": "Fayyadamtoota kee ({{count}})", + "contactsLoading": "Fayyadamtoota fe'aa jira...", + "contactsErrorTitle": "Fayyadamtoota fe'uu hin milkoofne", + "contactsErrorSubtitle": "Fayyadamtoota kee fe'uu keessatti rakkoon mudateera.", + "contactsPermissionTitle": "Eeyyama Fayyadamtootaa Barbaachisa", + "contactsPermissionSubtitle": "Fudhattoota malaa agaruuf eeyyama qunnamtii kenni", + "contactsPermissionButton": "Eeyyama Kenni", + "contactsEmptyTitle": "Fayyadamtoonni hin argamne", + "contactsEmptySubtitle": "Meeshaa kee irratti lakkoofsa bilbila qaban hin argamne", + "contactNoPhone": "Lakkoofsi bilbila hin jiru", + "toastErrorTitle": "Dogoggora", + "toastContactsError": "Fayyadamtoota fe'uu hin milkoofne.", + "toastRecipientsError": "Fudhattoota fe'uu hin milkoofne." + }, + "sendorrequestmoney": { + "verifyingSecurity": "Nageenya mirkaneessaa jira...", + "availableBalanceLabel": "Baalaansii Jiru", + "processingFee": "Mindaa hojimaalaa: ${{fee}} ({{percent}}%)", + "totalLabel": "Waliigala: ${{total}}", + "validationErrorTitle": "Hanga dogoggora", + "validationEnterAmount": "Mee hanga maallaqaa galchi", + "validationMinAmount": "Hanga xiqqaan maallaqaa $0.01 dha", + "validationMaxAmount": "Hanga guddaan maallaqaa $999.99 dha", + "validationInsufficientBalance": "Baalaansiin hin gahuu. Barbaadame: ${{required}} (akka ${{fee}} mindaa dabalatee). Jiru: ${{available}}", + "requestButton": "Gaafadhu", + "requestButtonWithAmount": "Gaafadhu ${{amount}}", + "payButton": "Kaffali", + "payButtonWithAmount": "Kaffali ${{amount}}", + "pinModalTitle": "PIN Cash kee mirkaneessi" + }, + "addcard": { + "title": "Kaardii Dabaluu", + "sectionCardTitle": "Kaardii", + "sectionCardSubtitle": "Kaardii kee asitti kuusuu dandeessa.", + "cardNumberLabel": "Lakk. Kaardii", + "cardNumberPlaceholder": "1234 5678 9012 3456", + "expiryDateLabel": "Guyyaa Xumuramaa", + "expiryDatePlaceholder": "MM/YY", + "cvvLabel": "CVV", + "cvvPlaceholder": "123", + "addButton": "Kaardii Dabaluu", + "addButtonLoading": "Dabaluu...", + "validationErrorTitle": "Dogoggora", + "validationCardNumberRequired": "Lakkoofsi kaardii barbaachisaadha", + "validationExpiryRequired": "Guyyaan xumuramaa barbaachisaadha", + "validationCvvRequired": "CVV barbaachisaadha", + "toastErrorTitle": "Dogoggora Kaardii", + "toastAddFailed": "Kaardii kireeditii dabaluu hin milkoofne" + }, + "addcash": { + "verifyingSecurity": "Nageenya mirkaneessaa jira...", + "title": "Dabaluu", + "validationErrorTitle": "Hanga dogoggora", + "validationEnterAmount": "Mee hanga $0.00 oli galchi", + "validationMinAmount": "Hanga xiqqaan $10.00 dha", + "validationMaxAmount": "Hanga guddaan $999.99 dha", + "addButton": "Dabaluu", + "addButtonWithAmount": "Dabaluu ${{amount}}", + "pinModalTitle": "Maallaqa dabaluu dura PIN Cash kee mirkaneessi" + }, + "addrecipient": { + "title": "Fudhattaa Dabaluu", + "sectionTitle": "Fudhattaa", + "sectionSubtitle": "Nama barbaadde kamuu asitti dabaluu dandeessa.", + "fullNameLabel": "Maqaa Guutuu", + "fullNamePlaceholder": "Kirubel Kibru", + "phoneLabel": "Lakkoofsa Bilbila", + "phonePlaceholder": "+251983032475", + "addButton": "Dabaluu", + "addButtonLoading": "Dabaluu...", + "validationErrorTitle": "Dogoggora Mirkaneessaa", + "validationFullNameRequired": "Mee maqaa guutuu fudhattaa galchi", + "validationPhoneRequired": "Mee lakkoofsa bilbila galchi", + "validationPhoneInvalid": "Mee lakkoofsa bilbila sirrii galchi", + "toastErrorTitle": "Dogoggora", + "toastAddError": "Fudhattaa dabaluu hin milkoofne" + }, + "addcashcomp": { + "successNote": "Maallaqa gara walleetii kee milkaa'inaan daballatte.", + "addAgainButton": "Irra deebi'ii maallaqa dabaluu", + "goHomeButton": "Gara fuula jalqabaa deebi'i" + }, + "cashout": { + "verifyingSecurity": "Nageenya mirkaneessaa jira...", + "availableBalanceLabel": "Baalaansii Jiru", + "validationErrorTitle": "Hanga dogoggora", + "validationEnterAmount": "Mee hanga maallaqaa galchi", + "validationMinAmount": "Hanga xiqqaan $0.01 dha", + "validationMaxAmount": "Hanga guddaan $999.99 dha", + "validationInsufficientBalance": "Baalaansiin hin gahuu. Barbaadame: ${{required}}. Jiru: ${{available}}", + "button": "Maallaqa Baasi", + "buttonWithAmount": "Baasi ${{amount}}", + "pinModalTitle": "Maallaqa baasuu dura PIN Cash kee mirkaneessi" + }, + "cardaddedcomp": { + "title": "Xumurame", + "description": "Kaardii kee milkaa'inaan dabalte.", + "addButton": "Dabaluu", + "goHomeButton": "Gara fuula jalqabaa deebi'i", + "shareMessageWithParam": "{{message}} ttiin Amba App fayyadamee", + "shareMessageDefault": "Kaardii haaraa gara Amba App koo dabaladhe!", + "shareTitle": "Kaardiin milkaa'inaan dabalame", + "toastErrorTitle": "Dogoggora", + "toastShareError": "Qooduun hin milkoofne. Irra deebi'ii yaali." + }, + "cashoutcomp": { + "successNote": "Maallaqa gara herrega kee milkaa'inaan baaste.", + "cashOutAgainButton": "Irra deebi'ii maallaqa baasi", + "goHomeButton": "Gara fuula jalqabaa deebi'i", + "toastErrorTitle": "Dogoggora", + "toastNoBalance": "Baalaansiin hin argamne", + "toastMinError": "Baalaansiin qarshii $10 gadi dha" + }, + "crowdfunding": { + "tabsOverview": "Kutaa Waliigalaa", + "tabsCampaign": "Kampaanii", + "tabsFaq": "FAQ", + "title": "Mata-duree kampaanii", + "description": "Dubbii gabaabaa kampaanii asitti ibsa. Hirmaannaa fi kaayyoo deeggarsa ibsa.", + "pledgedAmount": "$123,000", + "pledgedOf": "Pledged of {{target}}", + "backersCountLabel": "deeggartoota.", + "daysToGoLabel": "guyyaa hafe.", + "emailLabel": "Imeelii", + "emailPlaceholder": "test@gmail.com", + "otpLabel": "Koodii OTP", + "otpPlaceholder": "123123", + "passwordLabel": "Jecha Darbii", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "Jecha Darbii Mirkaneessi", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "Haaromsii", + "resendButton": "Koodii Irra Deebi'i" + }, + "eventdetail": { + "shareMessage": "Koree kana waliin nuu hirmaadhu!", + "title": "Maqaa Eventii", + "description": "Ibsa bal'aa eventii kanaa asitti argita. Bakka, yeroo fi gochaalee adda addaa ni ibsa.", + "location": "Millenium Hall Finfinnee", + "dateTime": "Sanbata, Ama 10, 2026 sa'aa 6:00 galgalaa - 1:00 halkan (EAT)", + "peopleComing": "Namoonni si beekan hirmaachuuf jiru!", + "ticketLabel": "Tikeetii #{{index}}", + "ticketPrice": "$55.44", + "buyButton": "Amma Bitu", + "shareButton": "Qoodi", + "guestListButton": "Tarree Hirmaattootaa", + "toastErrorTitle": "Dogoggora", + "toastShareError": "Event qooduu hin milkoofne. Irra deebi'ii yaali." + }, + "eventqrscreen": { + "printButton": "Maxxansi", + "goBackButton": "Deebi'i" + }, + "events": { + "title": "Eventoota", + "subtitle": "Ummata kee waliin hirmaadhu", + "searchPlaceholder": "Maqaa eventii, bakka", + "filterButton": "Sivi", + "myTicketsButton": "Tikeetota Koo", + "featuredTitle": "Eventoota Ijoo", + "cardTitle": "Maqaa Eventii", + "cardDescription": "Ibsa gabaabaa eventii kanaa asitti argita.", + "ticketCountPrefix": "100 - ", + "ticketLocation": "Tikeetota Los Angeles", + "ticketDate": "23 Jan26" + }, + "moneydonated": { + "title": "Xumurame", + "description": "Gaaffii maallaqaa milkaa'inaan deebiste.", + "goHomeButton": "Gara fuula jalqabaa deebi'i" + }, + "moneyrequested": { + "title": "Xumurame", + "description": "Kireeditii milkaa'inaan gaafatte.", + "descriptionWithName": "Kireeditii {{fullName}} irraa milkaa'inaan gaafatte.", + "requestAgainButton": "Irra deebi'ii Gaafadhu", + "goHomeButton": "Gara fuula jalqabaa deebi'i" + }, + "mytickets": { + "title": "Eventoota", + "subtitle": "Odeeffannoo tikeetota kee hunda asii bulchi.", + "searchPlaceholder": "Maqaa Eventii", + "filterButton": "Sivi", + "ticketsTitle": "Tikeetota" + }, + "notification": { + "title": "Beeksisa", + "sectionToday": "HAR'A", + "loading": "Beeksisota fe'aa jira...", + "errorWithMessage": "Dogoggora: {{error}}", + "emptyTitle": "Beeksisni hin jiru", + "emptySubtitle": "Beeksisoota gaaffii maallaqaa fi hojimaallaqaa asitti ni argita.", + "toastErrorTitle": "Dogoggora", + "toastRequestNotPending": "Gaaffiin yeroo kana irratti hin raawwatamu.", + "toastRequestActionFailed": "Gaaffii {{action}} gochuu hin milkoofne." + }, + "notificationOption": { + "title": "Isaanitti himi", + "sectionTitle": "Filannoo Beeksisaa", + "sectionSubtitle": "Mala beeksisaa filannoo kee filadhu.", + "smsLabel": "Beeksisa SMS", + "whatsappLabel": "WhatsApp", + "continueButton": "Itti fufi", + "toastErrorTitle": "Dogoggora", + "toastAuthRequired": "Gaaffii erguu dura galmaa'uun si barbaachisa.", + "toastMissingInfo": "Odeeffannoon gaaffii hin guutamne.", + "toastInvalidAmount": "Hanga dogoggora.", + "toastCreateFailed": "Gaaffii maallaqaa uumuu hin milkoofne.", + "toastRequestFailed": "Maallaqa gaafachuu hin milkoofne. Irra deebi'ii yaali.", + "toastSelectMethod": "Mee mala beeksisaa tokkotu fili." + }, + "points": { + "title": "Pointota", + "referTitle": "Hiriyaa kee afferi", + "earnSubtitle": "fi Point argadhu", + "copyButton": "Cuquri", + "shareButton": "Qoodi", + "activityButton": "Hojii", + "howToEarnButton": "Point akkamitti argadha?", + "rewardsTitle": "Badhaasa", + "reward1Title": "Hojimaallaqaa", + "reward1Description": "Mindaa hojimaallaqaa siif haqameera" + }, + "pointsactivity": { + "title": "Hojii", + "activity1Title": "Fudhattaa Dabaluu", + "activity1Date": "13 Jan 24 • 8:00 AM", + "activity2Title": "Gatii Hojimaallaqaa Hir'ifame", + "activity2Date": "13 Jan 24 • 8:00 AM", + "pointsPill": "{{sign}} {{points}} Point" + }, + "profile": { + "title": "Piroofaayilii", + "pointsBadge": "1200 point", + "loadingProfile": "Piroofaayilii fe'aa jira...", + "errorWithMessage": "Dogoggora: {{error}}", + "fullNameLabel": "Maqaa Guutuu", + "fullNamePlaceholder": "Maqaa Guutuu", + "addressLabel": "Teessoo", + "addressPlaceholder": "Teessoo", + "phoneLabel": "Lakkoofsa Bilbila", + "phonePlaceholder": "Lakkoofsa Bilbila", + "emailLabel": "Imeelii", + "emailPlaceholder": "Imeelii", + "languageLabel": "Afaan", + "languagePlaceholder": "Afaan filadhu", + "languageOptionEnglish": "Afaan Ingiliffaa", + "languageOptionAmharic": "Afaan Amaaraa", + "languageOptionFrench": "Afaan Faransaayii", + "languageOptionTigrinya": "Afaan Tigree", + "languageOptionOromo": "Afaan Oromoo", + "accountNumberLabel": "Lakkoofsa Herregaa", + "accountNumberPlaceholder": "Hin argamne", + "usernameLabel": "Maqaa Fayyadamaa", + "usernamePlaceholder": "@username", + "editButton": "Piroofaayilii Gulaali", + "saveButton": "Jijjiirama Kuusi", + "savingButton": "Kuusaa jira...", + "cancelButton": "Haquu", + "pointsButton": "Pointota", + "logoutButton": "Ba'i", + "toastLoggedOutTitle": "Baate", + "toastLoggedOutDescription": "App irraa baatee jirta.", + "toastErrorTitle": "Dogoggora", + "toastLogoutFailed": "Ba'uun hin milkoofne. Irra deebi'ii yaali.", + "toastUserNotFound": "Fayyadamaan hin argamne.", + "toastFullNameRequiredTitle": "Maqaa guutuu barbaachisa", + "toastFullNameRequiredDescription": "Mee maqaa guutuu kee galchi.", + "toastProfileUpdatedTitle": "Piroofaayiliin ni haaromfame", + "toastProfileUpdatedDescription": "Piroofaayiliin kee milkaa'inaan ni haaromfame.", + "toastUpdateErrorTitle": "Dogoggora Haaromsaa", + "toastUpdateErrorDescription": "Piroofaayiliin haaromsuu hin milkoofne." + }, + "qrscreen": { + "shareMessage": "QR Amba koo sikantee maallaqa ergi yookaan fudhadhu.", + "shareButton": "Qoodi", + "goBackButton": "Deebi'i", + "toastErrorTitle": "Dogoggora", + "toastShareError": "QR qooduu hin milkoofne. Irra deebi'ii yaali." + }, + "selectacc": { + "title": "Herrega Filadhu", + "addingAmount": "Dabaluu: ${{amount}}", + "accountsTitle": "Herregota", + "accountsDescriptionSelected": "Kaardiin filatameera! Itti fufuuf Continue tuqi.", + "accountsDescriptionUnselected": "Maallaqa itti dabaltu kaardii filadhu.", + "loadingCards": "Kaardii fe'aa jira...", + "errorTitle": "Kaardii fe'uu hin milkoofne", + "errorWithMessage": "Dogoggora: {{error}}", + "emptyTitle": "Kaardiin hin jiru", + "emptySubtitle": "Maallaqa dabaluu itti fufuuf kaardii dabaluu.", + "buttonProcessing": "Hojjechaa jira...", + "buttonProceed": "Itti fufi", + "toastErrorTitle": "Dogoggora", + "toastMissingInfo": "Odeeffannoon barbaachisaan hin guutamne.", + "toastAddCashFailed": "Maallaqa gara walleetii dabaluu hin milkoofne.", + "toastAddCashFailedWithRetry": "Maallaqa dabaluu hin milkoofne. Irra deebi'ii yaali." + }, + "recipaddedcomp": { + "title": "Fudhattaa Haaraa", + "description": "Fudhattaa haaraa milkaa'inaan dabalte.", + "addButton": "Dabaluu", + "goHomeButton": "Gara fuula jalqabaa deebi'i", + "shareMessageWithParam": "{{message}} ttiin Amba App fayyadamee", + "shareMessageDefault": "Fudhattaa haaraa gara Amba App koo dabaladhe!", + "shareTitle": "Fudhattaan milkaa'inaan dabalame", + "toastErrorTitle": "Dogoggora", + "toastShareError": "Qooduun hin milkoofne. Irra deebi'ii yaali." + }, + "selectdonor": { + "title": "Deeggaraa Filadhu", + "requestButton": "Gaafadhu", + "requestButtonLoading": "Gaafachaa jira...", + "toLabel": "Gara", + "forLabel": "Sababaan", + "searchPlaceholder": "Maqaa yookaan lakkoofsa bilbilaan barbaadi", + "notePlaceholder": "Yaada dabalataa (filannoo)", + "donorsTitle": "Deeggartoota ({{count}})", + "loadingDonors": "Deeggartoota fe'aa jira...", + "errorTitle": "Deeggartoota fe'uu hin milkoofne", + "errorWithMessage": "Dogoggora: {{error}}", + "contactsPermissionTitle": "Eeyyama Fayyadamtootaa Barbaachisa", + "contactsPermissionSubtitle": "Deeggartoota malaa arguuuf eeyyama qunnamtii kenni", + "contactsAllowAccess": "Eeyyama Kenni", + "emptyTitleSearch": "Deeggartoonni sirrii barbaadame hin argamne", + "emptyTitleDefault": "Deeggaraan hin argamne", + "emptySubtitleSearch": "Jechoota barbaadduu biroo fayyadami", + "emptySubtitleDefault": "Fudhattoota kuufaman dabaluu yookaan eeyyama qunnamtii kenni", + "toastErrorTitle": "Dogoggora", + "toastMissingInfo": "Mee deeggaraa fi hanga maallaqaa mirkaneessi.", + "toastInvalidAmount": "Hanga dogoggora.", + "toastDonorNotFound": "Deeggaraan filatame hin argamne" + }, + "selectrecip": { + "title": "Fudhattaa Filadhu", + "sendButton": "Ergi", + "sendButtonLoading": "Ergaa jira...", + "toLabel": "Gara", + "forLabel": "Sababaan", + "searchPlaceholder": "Maqaa yookaan lakkoofsa bilbila barbaadi", + "notePlaceholder": "Yaada dabalataa (filannoo)", + "recipientsTitle": "Fudhattoota ({{count}})", + "loadingRecipients": "Fudhattoota fe'aa jira...", + "errorTitle": "Fudhattoota fe'uu hin milkoofne", + "errorWithMessage": "Dogoggora: {{error}}", + "contactsPermissionTitle": "Eeyyama Fayyadamtootaa Barbaachisa", + "contactsPermissionSubtitle": "Fudhattoota malaa arguuuf eeyyama qunnamtii kenni", + "contactsAllowAccess": "Eeyyama Kenni", + "emptyTitleSearch": "Fudhattoonni barbaadduu wajjin wal-simuu hin argamne", + "emptyTitleDefault": "Fudhattaa hin argamne", + "emptySubtitleSearch": "Jechoota barbaadduu biroo fayyadami", + "emptySubtitleDefault": "Fudhattoota kuufaman dabaluu yookaan eeyyama qunnamtii kenni", + "toastErrorTitle": "Dogoggora", + "toastMissingInfo": "Mee fudhattaa fili fi hanga maallaqaa mirkaneessi.", + "toastInvalidAmount": "Hanga dogoggora.", + "toastWalletNotFound": "Walleetiin hin argamne", + "toastInsufficientBalanceTitle": "Baalaansiin hin gahuu", + "toastInsufficientBalanceDescription": "Barbaadame: ${{required}} (akka ${{fee}} mindaa dabalatee). Jiru: ${{available}}", + "toastRecipientNotFound": "Fudhattaan filatame hin argamne" + }, + "sendbank": { + "amountTitleCashOut": "Maallaqa Baasi", + "amountTitleToRecipient": "gara {{recipientName}}", + "noteWithText": "Yaada: {{note}}", + "paymentOptionsTitle": "Filannoowwan Kaffaltii", + "paymentOptionsSelected": "Filatame: {{providerName}}", + "paymentOptionsUnselected": "Mala kaffaltii filannoo kee fili.", + "awashName": "Awash Bank", + "awashSubtitle": "Jijjiirraa Bankii", + "telebirrName": "Telebirr", + "telebirrSubtitle": "Maallaqa Bilbila", + "sendButtonCashOut": "Maallaqa ${{amount}} {{providerName}} fayyadamuun baasi", + "sendButtonSend": "Maallaqa ${{amount}} {{providerName}} fayyadamuun ergi", + "processingTitle": "Hojimaata Gidduutti...", + "processingSubtitle": "Kaffaltiin akka hojjetu yeroo xiqqoo eeggadhu", + "poweredBy": "Kan hojjechisu", + "toastErrorTitle": "Dogoggora", + "toastNoMethod": "Jalqaba mala kaffaltii filadhu", + "toastMissingInfo": "Odeeffannoon hojimaallaqaa dhabuun jira", + "toastInvalidAmount": "Hanga dogoggora.", + "toastInsufficientBalanceTitle": "Baalaansiin hin gahuu", + "toastInsufficientBalanceCashoutDescription": "Barbaadame: ${{required}}. Jiru: ${{available}}", + "toastInsufficientBalanceSendDescription": "Barbaadame: ${{required}} (akka ${{fee}} mindaa dabalatee). Jiru: ${{available}}", + "toastMissingRecipient": "Odeeffannoon fudhattaa dhabuun jira", + "toastProcessFailed": "Hojimaata raawwachuu hin milkoofne", + "toastProcessFailedWithRetry": "Hojimaata raawwachuu hin milkoofne. Irra deebi'ii yaali." + }, + "checkout": { + "title": "Checkout", + "subtitle": "Kaffaltii kana akkamitti xumuruuf barbaadda?", + "recipientLabel": "Nama fudhataa", + "totalLabel": "Waliigala", + "paymentOptionsTitle": "Filannoowwan kaffaltii", + "cardInfoTitle": "Odeeffannoo kaardii", + "paymentEmailTitle": "Imeelii kaffaltii", + "appleIdTitle": "Apple ID", + "contactInfoTitle": "Odeeffannoo quunnamtii", + "billingAddressTitle": "Teessoo biilii", + "cardNumberPlaceholder": "Lakk. kaardii", + "expiryPlaceholder": "MM/YY", + "cvvPlaceholder": "CVV", + "paymentEmailPlaceholder": "Imeelii kaffaltii", + "appleIdPlaceholder": "Apple ID", + "payButton": "Kaffali", + "payButtonProcessing": "Hojjechaa jira..." + }, + "donation": { + "title": "Deeggarsa", + "subtitle": "Gargaarsa kaffaltii kanaan deeggaruu barbaaddu filadhu", + "chooseAmountTitle": "Hanga deeggarsa filadhu", + "donationTypeOneTime": "Yeroo tokko", + "donationTypeMonthly": "Ji'a ji'aan", + "donationRaisedLabel": "Deeggarsi walitti qabame", + "donateAnonymouslyLabel": "Maqaa malee deeggari", + "anonymousLabel": "Maqaa hin taane", + "displayNameFallback": "Maqaa kee", + "skipButton": "Hafu", + "donateButton": "Deeggari" + }, + "taskcomp": { + "successDescription": "Maallaqa milkaa'inaan ergeera.", + "sendAgainButton": "Irra deebi'ii ergi", + "shareButton": "Qoodi", + "goHomeButton": "Gara fuula jalqabaa deebi'i", + "shareMessageWithParam": "{{message}} ttiin Amba App fayyadamee", + "shareMessageDefault": "Maallaqa Amba App fayyadamuun milkaa'inaan ergeera!", + "shareTitle": "Ergi Maallaqaa Milkaa'e", + "toastErrorTitle": "Dogoggora", + "toastNoBalance": "Baalaansiin hin argamne", + "toastMinError": "Baalaansiin qarshii $10 gadi dha", + "toastShareError": "Qooduun hin milkoofne. Irra deebi'ii yaali.", + "ratingTitle": "Ergii kana madaali", + "ratingSubtitle": "Maallaqni kun akkamitti siif ergame nuu himi, tajaajila AmbaPay ittiin fooyyessina.", + "ratingOverallLabel": "Muuxannoo waliigalaa", + "ratingPurposeLabel": "Maallaqni kun maalif ergame?", + "ratingPurposePlaceholder": "Kutaa filadhu", + "ratingPurposeFamily": "Maatii", + "ratingPurposeMedical": "Fayyaa", + "ratingPurposeLoan": "Liqa", + "ratingPurposePurchase": "Bittaa", + "ratingPurposeOther": "Kan biraa", + "ratingOtherLabel": "Sababa kee dabali", + "ratingOtherPlaceholder": "Sababa kee barreessi", + "ratingSubmitButton": "Galchi fi gara mana deebi'i" + }, + "transconfirm": { + "title": "Faayila Hojimaallaqaa", + "planningDescription": "Maallaqa gara {{recipientName}} erguuf qophaa'aa jirta", + "sectionTitle": "Faayila Hojimaallaqaa", + "noteLabel": "Yaada", + "processingFeeLabel": "Mindaa hojimaalaa (1.25%)", + "subtotalLabel": "Hanga Waliigalaa Xiqqaa", + "totalLabel": "Waliigala", + "buttonProcessing": "Hojjechaa jira...", + "buttonConfirm": "Mirkaneessi", + "toastErrorTitle": "Dogoggora", + "toastMissingDetails": "Faayilli hojimaallaqaa dhabuun jiru", + "toastInvalidAmount": "Hanga dogoggora.", + "toastWalletNotFound": "Walleetiin hin argamne", + "toastInsufficientBalanceTitle": "Baalaansiin hin gahuu", + "toastInsufficientBalanceDescription": "Barbaadame: ${{required}} (akka ${{fee}} mindaa dabalatee). Jiru: ${{available}}", + "toastRecipientMissing": "Odeeffannoon fudhattaa dhabuun jira", + "toastSendFailed": "Maallaqa erguu hin milkoofne", + "toastSendFailedWithRetry": "Maallaqa erguu hin milkoofne. Irra deebi'ii yaali." + }, + "transdetail": { + "title": "Faayila Hojimaallaqaa", + "descriptionSend": "Maallaqa gara {{recipientName}} ergeera", + "descriptionReceive": "Maallaqa {{recipientName}} irraa fudhatteerta", + "descriptionAddCash": "Maallaqa gara walleetii kee dabalte", + "descriptionCashOut": "Maallaqa gara baankii keeti baaste", + "descriptionDefault": "Hojimaallaqaa", + "sectionTitle": "Faayila Hojimaallaqaa", + "dateLabel": "Guyyaa", + "statusLabel": "Haala", + "statusUnknown": "Hin beekamu", + "dateUnknown": "Hin beekamu", + "noteLabel": "Yaada", + "processingFeeLabel": "Mindaa hojimaalaa (1.25%)", + "subtotalLabel": "Hanga Waliigalaa Xiqqaa", + "totalLabel": "Waliigala", + "sendAgainButton": "Irra deebi'ii ergi" + }, + "components": { + "acccard": { + "cardTypeFallback": "Kaardii", + "cardNumberPlaceholder": "**** **** **** ****", + "expiryPlaceholder": "MM/YY", + "expiryLabel": "Yeroo itti xumuramu {{date}}" + }, + "accordion": {}, + "backbar": {}, + "bottomsheet": {}, + "button": {}, + "card": {}, + "cardcomp": { + "title": "Master Card", + "number": "13131-1313-3131-1313-1312" + }, + "profilecard": { + "emptyLabelContact": "Quunnamtii", + "unknownContact": "Hin beekamu" + }, + "recipcard": { + "name": "Abebe Kebde", + "accountNumber": "1030230213021" + }, + "topbar": { + "greeting": "Akkam" + }, + "contactmodal": { + "headerTitle": "Faayila Quunnamtii", + "phoneNumbersTitle": "Lakkoofsa bilbila", + "emailAddressesTitle": "Teessoo imeelii", + "noAdditionalInfo": "Quunnamtiin kun lakkoofsa bilbila yookaan imeelii dabalataa hin qabu.", + "sendMoneyButton": "Maallaqa Ergi", + "closeButton": "Cufi", + "unknownContact": "Quunnamtiin hin beekamne" + }, + "pinconfirmationmodal": { + "titleDefault": "PIN Cash kee mirkaneessi", + "toastBiometricErrorTitle": "Dogoggora biometric", + "toastBiometricHardwareNotAvailable": "Meeshaan biometric hin jiru", + "toastBiometricNotEnrolled": "Biometric meeshaa irratti hin barreessamne", + "toastBiometricFailed": "Mirkaneessi biometric hin milkoofne", + "toastInvalidPinTitle": "PIN dogoggora", + "toastInvalidPinDescription": "Mee PIN lakkofsa 6 galchi", + "toastAuthErrorTitle": "Dogoggora Mirkaneessaa", + "toastUserNotFound": "Fayyadamaan hin argamne", + "toastPinNotFound": "PIN hin argamne", + "toastIncorrectPinTitle": "PIN sirrii miti", + "toastIncorrectPinDescription": "Irra deebi'ii yaali.", + "authChoiceTitle": "Mala mirkaneessaa fili", + "fingerprintTitle": "Saamunaa qubaa", + "fingerprintSubtitle": "Mirkaneessuuf saamunaa qubaa fayyadami", + "pinTitle": "PIN AmbaPay", + "pinSubtitle": "PIN lakkofsa 6 galchi", + "biometricWaiting": "Mirkaneessa biometric eeggachaa jirra…", + "cancelButton": "Haquu", + "pinVerificationTitle": "Mirkaneessa PIN", + "submitButtonVerifying": "Mirkaneessaa jira...", + "submitButtonConfirm": "Mirkaneessi" + }, + "transactioncard": { + "descriptionSend": "Gara {{recipientName}} ergame", + "descriptionReceive": "Irraa {{senderName}} fudhatame", + "descriptionAddCash": "Maallaqa Dabalame (****{{lastFourDigits}})", + "descriptionCashOut": "Maallaqa gara {{bankProvider}} baafame", + "descriptionDefault": "Hojimaallaqaa", + "detailPhone": "Bilbila: {{phoneNumber}}", + "detailCard": "Kaardii: ****{{lastFourDigits}}", + "detailAccount": "Herrega: {{accountNumber}}", + "detailBankProvider": "via {{bankProvider}}" + } + } +} diff --git a/locales/ti/common.json b/locales/ti/common.json new file mode 100644 index 0000000..b060381 --- /dev/null +++ b/locales/ti/common.json @@ -0,0 +1,718 @@ +{ + "common": { + "back": "ተመለስ" + }, + "signin": { + "phoneLabel": "ቁልፊ ስልኪ", + "phonePlaceholder": "9XXXXXXXX", + "button": "ግባ", + "buttonLoading": "OTP ተላኢኽ ኣሎ...", + "validationErrorTitle": "ስሕተት", + "validationInvalidPhone": "ብቕዋም ዘለዎ ቁልፊ ስልኪ ኣእቱ", + "toastErrorTitle": "ስሕተት", + "toastOtpFailed": "OTP ምስተላእ ምስክር ኣይተሳከሉን። ቅርጺ ቁልፊ ስልኪኻ ተኸታተል።", + "toastAuthErrorTitle": "ምርግጋእ ስልኪ ስሕተት" + }, + "otp": { + "title": "ምርግጋእ", + "description": "ኮድ ናብ ቁልፊ ስልኪኻ ሰዲድናልና", + "codeLabel": "OTP ኮድ", + "codePlaceholder": "123456", + "verifyButton": "ኣረጋግጽ", + "verifyButtonLoading": "ኣረጋጊጽ ኣሎ...", + "resendButton": "ኮድ ደጊም ሰድድ", + "resendButtonCountdown": "ኮድ ደጊም ክትሰድድ ብ {{countdown}} ሰከንድ ተጠቐም", + "validationErrorTitle": "ስሕተት", + "validationInvalidCode": "ብቕዋም 6-ኣሃዳዊ OTP ኮድ ኣእቱ", + "toastErrorTitle": "ስሕተት", + "toastInvalidCode": "OTP ኮድ ሕማቕ እዩ። ሓደ ጊዜ ካልእ ፈትን።", + "toastInfoTitle": "ሓበሬታ", + "toastBackInfo": "ናብ መግቢ ገጽ ኣብ መመለስ ኣሎና", + "toastResendInfo": "ኮድ እንደገና ክትሰድድ ናብ መግቢ ገጽ ተመለስ", + "toastSuccessTitle": "ስራሕ ተፈፂሙ", + "toastDevSuccess": "OTP ብሞዳል ልሙድ ተረጋጊጹ", + "toastVerificationErrorTitle": "OTP ምርግጋእ ስሕተት" + }, + "phoneSetup": { + "title": "መደብ መጨረሻ", + "subtitle": "ከም ትልኣኽ ገንዘብ ድሕሪ ጥያቄታት ኣለና።", + "fullNameLabel": "ሙሉ ስም *", + "fullNamePlaceholder": "ኣበበ ከበደ", + "addressLabel": "ኣድራሻ", + "addressPlaceholder": "ኣዲስ ኣበባ፣ ኢትዮጵያ", + "emailLabel": "ኢሜይል ኣድራሻ *", + "emailPlaceholder": "abebe@example.com", + "pinLabel": "PIN *", + "pinPlaceholder": "6-ኣሃዳዊ PIN ኣእቱ", + "confirmPinLabel": "PIN ኣረጋግጽ *", + "confirmPinPlaceholder": "PINኻ ኣረጋግጽ", + "button": "ተማሪኮ", + "buttonLoading": "ምትካል ኣሎ...", + "validationErrorTitle": "ስሕተት", + "validationFullNameRequired": "ሙሉ ስምኻ ኣእቲ", + "validationEmailRequired": "ኢሜይል ኣድራሻኻ ኣእቲ", + "validationEmailInvalid": "ብቕዋም ኢሜይል ኣድራሻ ኣእቲ", + "validationPinRequired": "PIN ኣእቲ", + "validationPinLength": "PIN ብሙሉ 6 ኣሃዳት ክኸውን ኣለዎ", + "validationConfirmPinRequired": "PINካ ኣረጋግጽ", + "validationPinsMismatch": "PINን ኣረጋጊጺ PINን ኣይሓዙን", + "toastSuccessTitle": "ስራሕ ተፈፂሙ", + "toastDevSuccess": "ትካል ፕሮፋይል ብሞዳል ልሙድ ተዛሪቡ", + "toastNoUserTitle": "ስሕተት", + "toastNoUser": "ተመሪ ተመዝጊቡ ዘለዎ ተጠቃሚ ኣይተረኸበን", + "toastSetupErrorTitle": "ስሕተት መደብ", + "toastSetupError": "ትካል ፕሮፋይል ምጨረስ ኣይተሳከሉን። እንደገና ፈትን።" + }, + "forgot": { + "headerTitle": "ፓስወርድ ኣስተኻኽል", + "title": "ፓስወርድ ኣስተኻኽል", + "description": "ፓስወርድኻ እንድትምለስ ናብ መሳርሒኻ ኮድ ሰዲድናልና።", + "tabsPhone": "ስልኪ", + "tabsEmail": "ኢሜይል", + "phoneSelectorLabel": "ምርጫ ስልኪ", + "phonePlaceholder": "+25112515232", + "otpLabel": "OTP ኮድ", + "otpPlaceholder": "123123", + "passwordLabel": "ፓስወርድ", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "ፓስወርድ ኣረጋግጽ", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "ኣስተኻኽል", + "resendButton": "ኮድ ደጊም ሰድድ", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "test@gmail.com", + "toastSuccessTitle": "ስራሕ ተፈፂሙ", + "toastResetSuccess": "ሕቶ ፓስወርድ ምቕያር ተላኢኹ።", + "toastInfoTitle": "ሓበሬታ", + "toastResendInfo": "ኮድ ምርግጋእ እንደገና ሰዲድናልና።" + }, + "home": { + "balanceLabel": "ቀሪ ሒሳብ", + "accountDetails": "ዝርዝር ኣካውንት", + "addButton": "ምዕቃብ", + "cashOutButton": "ገንዘብ ውጽእ", + "recipientsTitle": "ተቐባሊታት", + "contactsAllowAccess": "መዳረሻ ሃብት", + "contactsGrantAccess": "መዳረሻ ሃብት", + "contactsLoading": "ኣውታር ተመሪታት ተሓክል ኣሎ...", + "contactsError": "ኣውታር ተመሪታት ምምጻእ ኣይተሳከለን", + "contactsNoContacts": "ኣውታር ተመሪ የለን", + "transactionsTitle": "ግብያዊ ስራሕታት", + "transactionsLoading": "ግብያዊ ስራሕታት ተሓክል ኣሎ...", + "transactionsError": "ግብያዊ ስራሕታት ምምጻእ ኣይተሳከለን", + "transactionsNoTransactions": "ግብያዊ ስራሕ የለን እዚ", + "transactionsEmptySubtitle": "ታሪኽ ግብያዊ ስራሕትካ ኣብዚ ክትረኽብ ኢኻ", + "transactionsMore": "+{{count}} ግብያዊ ስራሕታት ካልእ", + "cashoutErrorTitle": "ስሕተት", + "cashoutNoBalance": "ቀሪ ሒሳብ ኣይተረኸበን", + "cashoutMinError": "ቀሪ ሒሳብ ከዚ ዝሓፀረ $10 እዩ", + "addCashNoPaymentMethodTitle": "መንገዲ ክፍሊት የለን", + "addCashNoPaymentMethodMessage": "ገንዘብ ናብ wallet ክትዕቀብ ቅድሚ ካርታ ክትዕቀብ ኣለኻ።", + "addCashCancel": "ሰርዝ", + "addCashAddCard": "ካርታ ዕቅብ" + }, + "history": { + "title": "ታሪኽ", + "subtitle": "ዝርዝር ግብያዊ ስራሕታት።", + "loading": "ግብያዊ ስራሕታት ተሓክል ኣሎ...", + "errorTitle": "ግብያዊ ስራሕታት ምምጻእ ኣይተሳከለን", + "emptyTitle": "ግብያዊ ስራሕ የለን እዚ", + "emptySubtitle": "ታሪኽ ግብያዊ ስራሕትካ ኣብዚ ክትረኽብ ኢኻ", + "filterTitle": "ግብያዊ ስራሕታት ፍለጥ", + "filterSubtitle": "ታሪኽኻ ክትዕጽወ ዕለትን ኣይነትን ምረጽ።", + "dateRangeLabel": "ዝተመረጸ ዕለት", + "fromLabel": "ካብ", + "toLabel": "ክሳብ", + "selectStart": "መጀመርታ ዕለት ምረጽ", + "selectEnd": "መወዳእታ ዕለት ምረጽ", + "clearDates": "ዕለታት ኣጥፍእ", + "typeLabel": "ኣይነት", + "typeAll": "ኩሉ", + "typeIncoming": "ኣብቲ እትቕበል", + "typeOutgoing": "ኣብቲ እትልእ", + "applyFilters": "ፍለጥ ኣመሓይሽ", + "searchPlaceholder": "ታሪኽ ፈልጥ", + "toastErrorTitle": "ስሕተት", + "toastTransactionsError": "ግብያዊ ስራሕታት ምምጻእ ኣይተሳከለን።" + }, + "cardmang": { + "title": "ካርታታት", + "subtitle": "መረዳእታ ክፍሊትካ ኩሉ ኣብዚ ኣቕምጥ።", + "searchPlaceholder": "ስም ኣካውንት ንምንባር፣ ቁልፊ ካርታ ኣእቱ", + "addCardButton": "ካርታ ዕቅብ", + "paymentOptionsTitle": "መንገዲታት ክፍሊት", + "cardTypeFallback": "ካርታ", + "cardExpires": "ዝውድኦ {{date}}", + "loading": "ካርታታት ተሓክሉ ኣለዉ...", + "errorTitle": "ካርታታት ምምጻእ ኣይተሳከለን", + "emptyTitle": "ካርታ ገና ኣይተወሰነን", + "emptySubtitle": "ካርታ መጀመርታ ኣቕምጥ መቆጣጠሪ ክፍሊት ጀምር", + "toastRemoveSuccessTitle": "ስራሕ ተፈፂሙ", + "toastRemoveSuccess": "ካርታ ተኣጥፊኡ!", + "toastRemoveErrorTitle": "ስሕተት", + "toastRemoveError": "ካርታ ምሰረዝ ኣይተሳከለን" + }, + "listrecipient": { + "title": "ተቐባሊ", + "searchPlaceholder": "ስም፣ ቁልፊ ስልኪ፣ ኣካውንት ፈልጥ", + "addButton": "ተቐባሊ ሓድሽ ዕቅብ", + "savedRecipientsTitle": "ተቐባሊታት ተዓቀቡ ({{count}})", + "savedRecipientsLoading": "ተቐባሊታት ተሓክሉ ኣለዉ...", + "savedRecipientsError": "ተቐባሊታት ምምጻእ ኣይተሳከለን", + "savedRecipientsEmpty": "ተቐባሊ ተዓቂቡ የለን", + "contactsTitle": "ናታትካ ኣውታር ({{count}})", + "contactsLoading": "ኣውታር ተሓክሉ ኣለዉ...", + "contactsErrorTitle": "ኣውታር ምምጻእ ኣይተሳከለን", + "contactsErrorSubtitle": "ኣብ ምምጻእ ኣውታር ጉዳይ ተፈጢሩ ኣሎ።", + "contactsPermissionTitle": "መፍቓዕ ኣውታር ይዕቀብ", + "contactsPermissionSubtitle": "ተቐባሊታት ምርኣይ መፍቓዕ ኣውታር ሃበ", + "contactsPermissionButton": "መፍቓዕ ሃብ", + "contactsEmptyTitle": "ኣውታር ኣይተረኸበን", + "contactsEmptySubtitle": "ኣብ መሳርሕካ ቁልፊ ስልኪ ዘለዎ ኣውታር ኣይተረኸበን", + "contactNoPhone": "ቁልፊ ስልኪ የለን", + "toastErrorTitle": "ስሕተት", + "toastContactsError": "ኣውታር ምምጻእ ኣይተሳከለን።", + "toastRecipientsError": "ተቐባሊታት ምምጻእ ኣይተሳከለን።" + }, + "sendorrequestmoney": { + "verifyingSecurity": "ሓፈሻ እዚ እዩ ንምርግጋእ...", + "availableBalanceLabel": "ዝተዘረየ ቀሪ ሒሳብ", + "processingFee": "ክፍሊት ኣገልግሎት: ${{fee}} ({{percent}}%)", + "totalLabel": "ጠቕላላ: ${{total}}", + "validationErrorTitle": "ብዝተሳሰረ መጠን", + "validationEnterAmount": "ብዝተፈለጠ መጠን ኣእቱ", + "validationMinAmount": "ዝንብል መጠን $0.01 እዩ", + "validationMaxAmount": "ዝበዝሐ መጠን $999.99 እዩ", + "validationInsufficientBalance": "ቀሪ ሒሳብ ኣይበቃን። ዝደለየ: ${{required}} (ኣብዚ ኣቐማምሎ ${{fee}} ክፍሊት ኣገልግሎት እዩ). ዝርከብ: ${{available}}", + "requestButton": "ሕቶ ስደድ", + "requestButtonWithAmount": "ሕቶ ስደድ ${{amount}}", + "payButton": "ክፈል", + "payButtonWithAmount": "ክፈል ${{amount}}", + "pinModalTitle": "PIN ገንዘብካ ኣረጋግጽ" + }, + "addcard": { + "title": "ካርታ ዕቅብ", + "sectionCardTitle": "ካርታ", + "sectionCardSubtitle": "ካርታታትካ ኣብዚ ክትዕቀብ ትኽእል።", + "cardNumberLabel": "ቁልፊ ካርታ", + "cardNumberPlaceholder": "1234 5678 9012 3456", + "expiryDateLabel": "ዝውድእ ዕለት", + "expiryDatePlaceholder": "MM/YY", + "cvvLabel": "CVV", + "cvvPlaceholder": "123", + "addButton": "ካርታ ዕቅብ", + "addButtonLoading": "ኣተውዕቲ ኣሎ...", + "validationErrorTitle": "ስሕተት", + "validationCardNumberRequired": "ቁልፊ ካርታ የለብካን እዩ", + "validationExpiryRequired": "ዝውድእ ዕለት ግድነት እዩ", + "validationCvvRequired": "CVV ግድነት እዩ", + "toastErrorTitle": "ስሕተት ካርታ", + "toastAddFailed": "ካርታ ክሬዲት ምዕቃብ ኣይተሳከለን" + }, + "addcash": { + "verifyingSecurity": "ሓፈሻ እዚ እዩ ንምርግጋእ...", + "title": "ዕቃብ", + "validationErrorTitle": "ስሕተት መጠን", + "validationEnterAmount": "ብ $0.00 ዝበለ መጠን ኣእቱ", + "validationMinAmount": "ዝንብል መጠን $10.00 እዩ", + "validationMaxAmount": "ዝበዝሐ መጠን $999.99 እዩ", + "addButton": "ዕቃብ", + "addButtonWithAmount": "ዕቃብ ${{amount}}", + "pinModalTitle": "ገንዘብ ክትዕቀብ ቅድሚ PIN ገንዘብካ ኣረጋግጽ" + }, + "addrecipient": { + "title": "ተቐባሊ ዕቅብ", + "sectionTitle": "ተቐባሊ", + "sectionSubtitle": "ገንዘብ ክትልኣኽሉ ዝደለየ ማንን ኣብዚ ዕቅብ።", + "fullNameLabel": "ሙሉ ስም", + "fullNamePlaceholder": "ኪሩበል ኪብሩ", + "phoneLabel": "ቁልፊ ስልኪ", + "phonePlaceholder": "+251983032475", + "addButton": "ዕቃብ", + "addButtonLoading": "ኣተውዕቲ ኣሎ...", + "validationErrorTitle": "ስሕተት ምርግጋእ", + "validationFullNameRequired": "ሙሉ ስም ተቐባሊ ኣእቱ", + "validationPhoneRequired": "ቁልፊ ስልኪ ኣእቱ", + "validationPhoneInvalid": "ብቕዋም ቁልፊ ስልኪ ኣእቱ", + "toastErrorTitle": "ስሕተት", + "toastAddError": "ተቐባሊ ምዕቃብ ኣይተሳከለን" + }, + "addcashcomp": { + "successNote": "ገንዘብ ናብ wallet ኻ ብስኬት ዕቅብካሉ.", + "addAgainButton": "እንደገና ገንዘብ ዕቅብ", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ" + }, + "cashout": { + "verifyingSecurity": "ሓፈሻ እዚ እዩ ንምርግጋእ...", + "availableBalanceLabel": "ዝተዘረየ ቀሪ ሒሳብ", + "validationErrorTitle": "ስሕተት መጠን", + "validationEnterAmount": "ብዝተፈለጠ መጠን ኣእቱ", + "validationMinAmount": "ዝንብል መጠን $0.01 እዩ", + "validationMaxAmount": "ዝበዝሐ መጠን $999.99 እዩ", + "validationInsufficientBalance": "ቀሪ ሒሳብ ኣይበቃን። ዝደለየ: ${{required}}፣ ዝርከብ: ${{available}}", + "button": "ገንዘብ ውጽእ", + "buttonWithAmount": "ገንዘብ ውጽእ ${{amount}}", + "pinModalTitle": "ገንዘብ ክትወጽእ ቅድሚ PIN ገንዘብካ ኣረጋግጽ" + }, + "cardaddedcomp": { + "title": "ተፈፂሙ", + "description": "ካርታኻ ብስኬት ዕቅብካሉ።", + "addButton": "ዕቃብ", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ", + "shareMessageWithParam": "{{message}} ብ Amba App", + "shareMessageDefault": "ካርታ ሓድሽ ናብ Amba App ዕቅብኩ!", + "shareTitle": "ካርታ ብስኬት ተዕቂቡ", + "toastErrorTitle": "ስሕተት", + "toastShareError": "ምሕባር ኣይተሳከለን። እንደገና ፈትን።" + }, + "cashoutcomp": { + "successNote": "ገንዘብ ናብ ኣካውንትካ ብስኬት ውጽእኻሉ።", + "cashOutAgainButton": "እንደገና ገንዘብ ውጽእ", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ", + "toastErrorTitle": "ስሕተት", + "toastNoBalance": "ቀሪ ሒሳብ ኣይተረኸበን", + "toastMinError": "ቀሪ ሒሳብ ከዚ ዝሓፀረ $10 እዩ" + }, + "crowdfunding": { + "tabsOverview": "ኣጠቃላይ", + "tabsCampaign": "ካምፓይን", + "tabsFaq": "FAQ", + "title": "ኣርዕስቲ ካምፓይኖታት", + "description": "ጽሑፍ ናይ መርኣይ ካምፓይን ኣብዚ ክትረኽብ ኢኻ። ሓላፊነትን ዕላማን ይግለፅ።", + "pledgedAmount": "$123,000", + "pledgedOf": "pledged of {{target}}", + "backersCountLabel": "backers.", + "daysToGoLabel": "days to go.", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "test@gmail.com", + "otpLabel": "OTP ኮድ", + "otpPlaceholder": "123123", + "passwordLabel": "ፓስወርድ", + "passwordPlaceholder": "Kka123#12", + "confirmPasswordLabel": "ፓስወርድ ኣረጋግጽ", + "confirmPasswordPlaceholder": "Kka123#12", + "resetButton": "ኣስተኻኽል", + "resendButton": "ኮድ ደጊም ሰድድ" + }, + "eventdetail": { + "shareMessage": "በዚ ኢቨንት ምስል ንዓብካ!", + "title": "ስም ኢቨንት", + "description": "ጽሑፍ መግለጺ ኢቨንት ኣብዚ ክትረኽብ ኢኻ። ቦታ፣ ጊዜን ጉዳያትን ይግለፅ።", + "location": "Millenium Hall ኣዲስ ኣበባ", + "dateTime": "ሰንበት፣ Jan 10, 2026 ካብ 6:00 ምሸት - 1:00 ናይ ለይቲ (EAT)", + "peopleComing": "እቲ ትምሃር ዝተወደዱ ከምዚ እዚ እዮም እዚኦም!", + "ticketLabel": "ቲከት #{{index}}", + "ticketPrice": "$55.44", + "buyButton": "ሕጂ ግዛ", + "shareButton": "ኣካፍል", + "guestListButton": "ዝርዝር ስደድቲ", + "toastErrorTitle": "ስሕተት", + "toastShareError": "ኢቨንት ምሕባር ኣይተሳከለን። እንደገና ፈትን።" + }, + "eventqrscreen": { + "printButton": "ኣትም", + "goBackButton": "ተመለስ" + }, + "events": { + "title": "ኢቨንታት", + "subtitle": "ብገንዘብ ማሕበርኻ ተሳተፍ", + "searchPlaceholder": "ስም ኢቨንት፣ ቦታ", + "filterButton": "ኣጣር", + "myTicketsButton": "ቲከታተይ", + "featuredTitle": "ዝተመርጹ ኢቨንታት", + "cardTitle": "ስም ኢቨንት", + "cardDescription": "ጽሑፍ መግለጺ ኢቨንት ኣብዚ ክትረኽብ ኢኻ።", + "ticketCountPrefix": "100 - ", + "ticketLocation": "ቲከት Los Angeles", + "ticketDate": "23 Jan26" + }, + "moneydonated": { + "title": "ተፈፂሙ", + "description": "ሕቶ ገንዘብ ብስኬት ኣቐበልካዮ።", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ" + }, + "moneyrequested": { + "title": "ተፈፂሙ", + "description": "ክሬዲት ብስኬት ጠይቕካዮ።", + "descriptionWithName": "ክሬዲት ካብ {{fullName}} ብስኬት ጠይቕካዮ።", + "requestAgainButton": "እንደገና ጠይቕ", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ" + }, + "mytickets": { + "title": "ኢቨንታት", + "subtitle": "መረዳእታ ቲከታትካ ኩሉ ኣብዚ ኣቕምጥ።", + "searchPlaceholder": "ስም ኢቨንት", + "filterButton": "ኣጣር", + "ticketsTitle": "ቲከታት" + }, + "notification": { + "title": "ማስታወቂት", + "sectionToday": "ሎሚ", + "loading": "ማስታወቂታት ተሓክል ኣሎ...", + "errorWithMessage": "ስሕተት: {{error}}", + "emptyTitle": "ማስታወቂት የለን", + "emptySubtitle": "ስለ ሕቶታት ገንዘብን ግብያዊ ስራሕታትን ማስታወቂታትካ ኣብዚ ትረኽብ።", + "toastErrorTitle": "ስሕተት", + "toastRequestNotPending": "ሕቶ ብዝሒ ዚኣሎ ጊዜ ክደርስ ኣይክእልን።", + "toastRequestActionFailed": "ሕቶ {{action}} ምስራሕ ኣይተሳከለን።" + }, + "notificationOption": { + "title": "እነሱን ጠውቕ", + "sectionTitle": "መንገዲ ማስታወቂት", + "sectionSubtitle": "መንገዲ ማስታወቂት ትፈልጡዎ ምረጹ።", + "smsLabel": "SMS ማስታወቂት", + "whatsappLabel": "WhatsApp", + "continueButton": "ቀጽል", + "toastErrorTitle": "ስሕተት", + "toastAuthRequired": "ሕቶ ምስደው ቅድሚ ምቕጻል ኣለካ።", + "toastMissingInfo": "ሓበሬታ ሕቶ ይጎድል።", + "toastInvalidAmount": "ብዝተሳሰረ መጠን።", + "toastCreateFailed": "ሕቶ ገንዘብ መፍጠር ኣይተሳከለን።", + "toastRequestFailed": "ገንዘብ ምጥዋቕ ኣይተሳከለን። እንደገና ፈትን።", + "toastSelectMethod": "መንገዲ ማስታወቂት ምረጽ።" + }, + "points": { + "title": "ፖይንት", + "referTitle": "ኣወዳድራ ሓወታትካ", + "earnSubtitle": "እና ፖይንት ክትርገብ", + "copyButton": "ኮፒ", + "shareButton": "ኣካፍል", + "activityButton": "ንባብ", + "howToEarnButton": "ፖይንት ከመይ ክረክብ?", + "rewardsTitle": "ሽልማት", + "reward1Title": "ግብያዊ ስራሕ", + "reward1Description": "ክፍሊት ግብያዊ ስራሕ ተኣጽዊኡልካ" + }, + "pointsactivity": { + "title": "ንባብ", + "activity1Title": "ተቐባሊ ዕቅብ", + "activity1Date": "13 Jan 24 • 8:00 ንጹሕ", + "activity2Title": "ቅኑዕ ግብያዊ ክፍሊት", + "activity2Date": "13 Jan 24 • 8:00 ንጹሕ", + "pointsPill": "{{sign}} {{points}} ፖይንት" + }, + "profile": { + "title": "ፕሮፋይል", + "pointsBadge": "1200 ፖይንት", + "loadingProfile": "ፕሮፋይል ተሓክል ኣሎ...", + "errorWithMessage": "ስሕተት: {{error}}", + "fullNameLabel": "ሙሉ ስም", + "fullNamePlaceholder": "ሙሉ ስም", + "addressLabel": "ኣድራሻ", + "addressPlaceholder": "ኣድራሻ", + "phoneLabel": "ቁልፊ ስልኪ", + "phonePlaceholder": "ቁልፊ ስልኪ", + "emailLabel": "ኢሜይል", + "emailPlaceholder": "ኢሜይል", + "languageLabel": "ቋንቋ", + "languagePlaceholder": "ቋንቋ ምረጽ", + "languageOptionEnglish": "እንግሊዝኛ", + "languageOptionAmharic": "ኣማርኛ", + "languageOptionFrench": "ፈረንሳይኛ", + "languageOptionTigrinya": "ትግርኛ", + "languageOptionOromo": "Afaan Oromoo", + "accountNumberLabel": "ቁልፊ ኣካውንት", + "accountNumberPlaceholder": "ኣይተረኸበን", + "usernameLabel": "ስም ተጠቃሚ", + "usernamePlaceholder": "@username", + "editButton": "ፕሮፋይል ኣርትዕ", + "saveButton": "ለውጥታት መዝግብ", + "savingButton": "መዝጊብ ኣሎ...", + "cancelButton": "ሰርዝ", + "pointsButton": "ፖይንት", + "logoutButton": "ውጣ", + "toastLoggedOutTitle": "ወጺኻ", + "toastLoggedOutDescription": "ካብ መተግበሪ ወጺኻ።", + "toastErrorTitle": "ስሕተት", + "toastLogoutFailed": "መውጽእ ኣይተሳከለን። እንደገና ፈትን።", + "toastUserNotFound": "ተጠቃሚ ኣይተረኸበን።", + "toastFullNameRequiredTitle": "ሙሉ ስም የድልየካ", + "toastFullNameRequiredDescription": "ሙሉ ስምካ ኣእቲ።", + "toastProfileUpdatedTitle": "ፕሮፋይል ተሓዲሱ", + "toastProfileUpdatedDescription": "ፕሮፋይልካ ብስኬት ተሓዲሱ።", + "toastUpdateErrorTitle": "ስሕተት ምሕዳስ", + "toastUpdateErrorDescription": "ፕሮፋይል ምሕዳስ ኣይተሳከለን።" + }, + "qrscreen": { + "shareMessage": "QR ኣምባይ ክትስካን ገንዘብ ክትልእ ወይ ክትቕበል ትኽእል።", + "shareButton": "ኣካፍል", + "goBackButton": "ተመለስ", + "toastErrorTitle": "ስሕተት", + "toastShareError": "QR ምሕባር ኣይተሳከለን። እንደገና ፈትን።" + }, + "selectacc": { + "title": "ኣካውንት ምረጽ", + "addingAmount": "ዕቃብ: ${{amount}}", + "accountsTitle": "ኣካውንታት", + "accountsDescriptionSelected": "ካርታ ተመሪቱ እዩ! ንመቐጺል Continue ጠውቕ።", + "accountsDescriptionUnselected": "ገንዘብ ክትዕቀብሉ ኣካውንት ካርታ ምረጽ።", + "loadingCards": "ካርታታት ተሓክሉ ኣለዉ...", + "errorTitle": "ካርታታት ምምጻእ ኣይተሳከለን", + "errorWithMessage": "ስሕተት: {{error}}", + "emptyTitle": "ካርታ የለን", + "emptySubtitle": "ገንዘብ ክትዕቀብ ቅድሚ ካርታ ኣቕምጥ።", + "buttonProcessing": "ተኸቲሉ ኣሎ...", + "buttonProceed": "ቀጽል", + "toastErrorTitle": "ስሕተት", + "toastMissingInfo": "ኣስፈላጊ መረጃ ይጎድል።", + "toastAddCashFailed": "ገንዘብ ናብ wallet ምዕቃብ ኣይተሳከለን።", + "toastAddCashFailedWithRetry": "ገንዘብ ናብ wallet ምዕቃብ ኣይተሳከለን። እንደገና ፈትን።" + }, + "recipaddedcomp": { + "title": "ተቐባሊ ሓድሽ", + "description": "ተቐባሊ ሓድሽ ብስኬት ዕቅብካሉ።", + "addButton": "ዕቃብ", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ", + "shareMessageWithParam": "{{message}} ብ Amba App", + "shareMessageDefault": "ተቐባሊ ሓድሽ ናብ Amba App ዕቅብኩ!", + "shareTitle": "ተቐባሊ ተዕቂቡ ብስኬት", + "toastErrorTitle": "ስሕተት", + "toastShareError": "ምሕባር ኣይተሳከለን። እንደገና ፈትን።" + }, + "selectdonor": { + "title": "ተዋፊ ምረጽ", + "requestButton": "ሕቶ", + "requestButtonLoading": "ሕቶ ተላኢኽ ኣሎ...", + "toLabel": "ናብ", + "forLabel": "ብዝኾነሉ", + "searchPlaceholder": "ብስም ወይ ቁልፊ ስልኪ ፈልጥ", + "notePlaceholder": "ማስታወሻ ጨምር (ኣጋዊ)", + "donorsTitle": "ተዋፊታት ({{count}})", + "loadingDonors": "ተዋፊታት ተሓክሉ ኣለዉ...", + "errorTitle": "ተዋፊታት ምምጻእ ኣይተሳከለን", + "errorWithMessage": "ስሕተት: {{error}}", + "contactsPermissionTitle": "መፍቓዕ ኣውታር ይዕቀብ", + "contactsPermissionSubtitle": "ተዋፊታት ምርኣይ መፍቓዕ ኣውታር ሃብ", + "contactsAllowAccess": "መፍቓዕ ሃብ", + "emptyTitleSearch": "ንፍለጋኻ ዝዛረብ ተዋፊ ኣይተረኸበን", + "emptyTitleDefault": "ተዋፊ ኣይተረኸበን", + "emptySubtitleSearch": "ካልእ ቃል ፍለጥ ፈትን", + "emptySubtitleDefault": "ተቐባሊታት ተቐብሎም ወይ መፍቓዕ ኣውታር ሃብ", + "toastErrorTitle": "ስሕተት", + "toastMissingInfo": "ተዋፊ ምረጽካ እና መጠን ምርግጋእ ግድነት ኣሎ።", + "toastInvalidAmount": "ብዝተሳሰረ መጠን።", + "toastDonorNotFound": "ተዋፊ ዝተመረጠ ኣይተረኸበን" + }, + "selectrecip": { + "title": "ተቐባሊ ምረጽ", + "sendButton": "ስደድ", + "sendButtonLoading": "ተላኢኽ ኣሎ...", + "toLabel": "ናብ", + "forLabel": "ብዝኾነሉ", + "searchPlaceholder": "ብስም ወይ ቁልፊ ስልኪ ፈልጥ", + "notePlaceholder": "ማስታወሻ ጨምር (ኣጋዊ)", + "recipientsTitle": "ተቐባሊታት ({{count}})", + "loadingRecipients": "ተቐባሊታት ተሓክሉ ኣለዉ...", + "errorTitle": "ተቐባሊታት ምምጻእ ኣይተሳከለን", + "errorWithMessage": "ስሕተት: {{error}}", + "contactsPermissionTitle": "መፍቓዕ ኣውታር ይዕቀብ", + "contactsPermissionSubtitle": "ተቐባሊታት ምርኣይ መፍቓዕ ኣውታር ሃብ", + "contactsAllowAccess": "መፍቓዕ ሃብ", + "emptyTitleSearch": "ንፍለጋኻ ዝዛረብ ተቐባሊ ኣይተረኸበን", + "emptyTitleDefault": "ተቐባሊ ኣይተረኸበን", + "emptySubtitleSearch": "ካልእ ቃል ፍለጥ ፈትን", + "emptySubtitleDefault": "ተቐባሊታት ተዓቕብ ወይ መፍቓዕ ኣውታር ሃብ", + "toastErrorTitle": "ስሕተት", + "toastMissingInfo": "ተቐባሊ ምረጽካ እና መጠን ምርግጋእ ግድነት ኣሎ።", + "toastInvalidAmount": "ብዝተሳሰረ መጠን።", + "toastWalletNotFound": "wallet ኣይተረኸበን", + "toastInsufficientBalanceTitle": "ቀሪ ሒሳብ ኣይበቃን", + "toastInsufficientBalanceDescription": "ዝደለየ: ${{required}} (ኣብዚ {{fee}} ክፍሊት ኣገልግሎት እዩ). ዝርከብ: ${{available}}", + "toastRecipientNotFound": "ተቐባሊ ዝተመረጠ ኣይተረኸበን" + }, + "sendbank": { + "amountTitleCashOut": "ገንዘብ ውጽእ", + "amountTitleToRecipient": "ናብ {{recipientName}}", + "noteWithText": "ማስታወሻ: {{note}}", + "paymentOptionsTitle": "መንገዲ ክፍሊት", + "paymentOptionsSelected": "ተመሪቱ: {{providerName}}", + "paymentOptionsUnselected": "መንገዲ ክፍሊት ዝትፈልጥ ምረጽ።", + "awashName": "Awash Bank", + "awashSubtitle": "ቅዋም ባንክ", + "telebirrName": "Telebirr", + "telebirrSubtitle": "Mobile Money", + "sendButtonCashOut": "ገንዘብ ${{amount}} ብ {{providerName}} ውጽእ", + "sendButtonSend": "ገንዘብ ${{amount}} ብ {{providerName}} ስደድ", + "processingTitle": "ግብያዊ ስራሕ ኣብ ምትካል ኣሎ...", + "processingSubtitle": "ክፍሊትና ክንፈጽም እየን በጃኻ ተጠቐመ", + "poweredBy": "ብ መሰረት", + "toastErrorTitle": "ስሕተት", + "toastNoMethod": "እቲ መንገዲ ክፍሊት መጀመርያ ምረጽ", + "toastMissingInfo": "መረጃ ግብያዊ ስራሕ ይጎድል።", + "toastInvalidAmount": "ብዝተሳሰረ መጠን።", + "toastInsufficientBalanceTitle": "ቀሪ ሒሳብ ኣይበቃን", + "toastInsufficientBalanceCashoutDescription": "ዝደለየ: ${{required}}፣ ዝርከብ: ${{available}}", + "toastInsufficientBalanceSendDescription": "ዝደለየ: ${{required}} (ኣብዚ {{fee}} ክፍሊት ኣገልግሎት እዩ). ዝርከብ: ${{available}}", + "toastMissingRecipient": "መረጃ ተቐባሊ ይጎድል።", + "toastProcessFailed": "ግብያዊ ስራሕ ምፍጣር ኣይተሳከለን።", + "toastProcessFailedWithRetry": "ግብያዊ ስራሕ ምፍጣር ኣይተሳከለን። እንደገና ፈትን።" + }, + "checkout": { + "title": "Checkout", + "subtitle": "እዚ ክፍሊት ከመይ ክትውዕል ትፈልጥ?", + "recipientLabel": "ተቐባሊ", + "totalLabel": "ጠቕላላ", + "paymentOptionsTitle": "መንገዲ ክፍሊት", + "cardInfoTitle": "መረዳእታ ካርታ", + "paymentEmailTitle": "ኢሜይል ክፍሊት", + "appleIdTitle": "Apple ID", + "contactInfoTitle": "መረዳእታ ምርካብ", + "billingAddressTitle": "ኣድራሻ ቢልልንግ", + "cardNumberPlaceholder": "ቁልፊ ካርታ", + "expiryPlaceholder": "MM/YY", + "cvvPlaceholder": "CVV", + "paymentEmailPlaceholder": "ኢሜይል ክፍሊት", + "appleIdPlaceholder": "Apple ID", + "payButton": "ክፈል", + "payButtonProcessing": "ተኸቲሉ ኣሎ..." + }, + "donation": { + "title": "ምቕፅዓት", + "subtitle": "እዚ ትልኣኽሉ ገንዘብ ምቕፅዓት ከተደለየ እትወድድ ኣምር።", + "chooseAmountTitle": "መጠን ምቕፅዓት ምረጽ", + "donationTypeOneTime": "ሓደ ጊዜ", + "donationTypeMonthly": "ወርሓዊ", + "donationRaisedLabel": "ምቕፅዓት ዝተኣካበተ", + "donateAnonymouslyLabel": "ብስም ኣይትቕፃ", + "anonymousLabel": "ዘለምንም", + "displayNameFallback": "ስምካ", + "skipButton": "ሓፈስ", + "donateButton": "ቕፃ" + }, + "taskcomp": { + "successDescription": "ገንዘብ ብስኬት ላእኻሉ።", + "sendAgainButton": "እንደገና ስደድ", + "shareButton": "ኣካፍል", + "goHomeButton": "ናብ መጀመርያ ገጽ ተመለስ", + "shareMessageWithParam": "{{message}} ብ Amba App", + "shareMessageDefault": "ገንዘብ ብ Amba App ብስኬት ላእኩ!", + "shareTitle": "ስነ ስርዓት ገንዘብ ተፈፂሙ", + "toastErrorTitle": "ስሕተት", + "toastNoBalance": "ቀሪ ሒሳብ ኣይተረኸበን", + "toastMinError": "ቀሪ ሒሳብ ከዚ ዝሓፀረ $10 እዩ", + "toastShareError": "ምሕባር ኣይተሳከለን። እንደገና ፈትን።", + "ratingTitle": "እዚ ስነ ስርዓት ገንዘብ ድምጺ ህብስብስ", + "ratingSubtitle": "ግብያዊ ስራሕቲ እዚ ከመይ ኣጋጢሙካ ንሕና ንቕርብ፣ AmbaPay ንምምሕዳስ ይሕግዝና።", + "ratingOverallLabel": "ጠቕላላ ተሞክሮ", + "ratingPurposeLabel": "እዚ ስነ ስርዓት ገንዘብ ስለምንታይ እዩ?", + "ratingPurposePlaceholder": "ክፍሊ ምረጽ", + "ratingPurposeFamily": "ስግኣት ወገን", + "ratingPurposeMedical": "ሕክምና", + "ratingPurposeLoan": "እቶኒ", + "ratingPurposePurchase": "ግዢ", + "ratingPurposeOther": "ካልእ", + "ratingOtherLabel": "ምክንያትካ ጨምር", + "ratingOtherPlaceholder": "ምክንያትካ ጽፍ", + "ratingSubmitButton": "ኣቕምጥ ንኽብል ተመለስ" + }, + "transconfirm": { + "title": "ዝርዝር ግብያዊ ስራሕ", + "planningDescription": "ገንዘብ ናብ {{recipientName}} ክትልእ ትዘግብ ኣለኻ", + "sectionTitle": "ዝርዝር ግብያዊ ስራሕ", + "noteLabel": "ማስታወሻ", + "processingFeeLabel": "ክፍሊት ኣገልግሎት (1.25%)", + "subtotalLabel": "ንዑስ ጠቕላላ", + "totalLabel": "ጠቕላላ", + "buttonProcessing": "ተኸቲሉ ኣሎ...", + "buttonConfirm": "ኣረጋግጽ", + "toastErrorTitle": "ስሕተት", + "toastMissingDetails": "ዝርዝር ግብያዊ ስራሕ ይጎድል።", + "toastInvalidAmount": "ብዝተሳሰረ መጠን።", + "toastWalletNotFound": "wallet ኣይተረኸበን", + "toastInsufficientBalanceTitle": "ቀሪ ሒሳብ ኣይበቃን", + "toastInsufficientBalanceDescription": "ዝደለየ: ${{required}} (ኣብዚ {{fee}} ክፍሊት ኣገልግሎት እዩ). ዝርከብ: ${{available}}", + "toastRecipientMissing": "መረጃ ተቐባሊ ይጎድል።", + "toastSendFailed": "ገንዘብ ምልኣኽ ኣይተሳከለን", + "toastSendFailedWithRetry": "ገንዘብ ምልኣኽ ኣይተሳከለን። እንደገና ፈትን።" + }, + "transdetail": { + "title": "ዝርዝር ግብያዊ ስራሕ", + "descriptionSend": "ገንዘብ ናብ {{recipientName}} ላእኻሉ", + "descriptionReceive": "ገንዘብ ካብ {{recipientName}} ተቐቢሉ", + "descriptionAddCash": "ገንዘብ ናብ wallet ኻ ዕቅብካሉ", + "descriptionCashOut": "ገንዘብ ናብ ባንክኻ ውጺኻሉ", + "descriptionDefault": "ግብያዊ ስራሕ", + "sectionTitle": "ዝርዝር ግብያዊ ስራሕ", + "dateLabel": "ዕለት", + "statusLabel": "ሁነታ", + "statusUnknown": "ዘይተረዳኡ", + "dateUnknown": "ዘይተረዳኡ", + "noteLabel": "ማስታወሻ", + "processingFeeLabel": "ክፍሊት ኣገልግሎት (1.25%)", + "subtotalLabel": "ንዑስ ጠቕላላ", + "totalLabel": "ጠቕላላ", + "sendAgainButton": "እንደገና ስደድ" + }, + "components": { + "acccard": { + "cardTypeFallback": "ካርታ", + "cardNumberPlaceholder": "**** **** **** ****", + "expiryPlaceholder": "MM/YY", + "expiryLabel": "ዝውድእ {{date}}" + }, + "accordion": {}, + "backbar": {}, + "bottomsheet": {}, + "button": {}, + "card": {}, + "cardcomp": { + "title": "Master Card", + "number": "13131-1313-3131-1313-1312" + }, + "profilecard": { + "emptyLabelContact": "ምርካብ", + "unknownContact": "ዘይተረዳኡ ምርካብ" + }, + "recipcard": { + "name": "ኣበበ ከበደ", + "accountNumber": "1030230213021" + }, + "topbar": { + "greeting": "ሰላም" + }, + "contactmodal": { + "headerTitle": "ዝርዝር ምርካብ", + "phoneNumbersTitle": "ቁልፊ ስልኪታት", + "emailAddressesTitle": "ኢሜይል ኣድራሻታት", + "noAdditionalInfo": "ቁልፊ ስልኪ ወይ ኢሜይል ዘለዎ መረጃ የለን።", + "sendMoneyButton": "ገንዘብ ስደድ", + "closeButton": "ዝጋ", + "unknownContact": "ዘይተረዳኡ ምርካብ" + }, + "pinconfirmationmodal": { + "titleDefault": "PIN ገንዘብካ ኣረጋግጽ", + "toastBiometricErrorTitle": "ባዮሜትሪክ ስሕተት", + "toastBiometricHardwareNotAvailable": "መሳርሒ ባዮሜትሪክ የለን", + "toastBiometricNotEnrolled": "ባዮሜትሪክ ኣብ መሳርሒ ኣይተመዝገበን", + "toastBiometricFailed": "ባዮሜትሪክ ምርግጋእ ኣይተሳከለን", + "toastInvalidPinTitle": "PIN ሕማቕ", + "toastInvalidPinDescription": "PIN 6-ኣሃዳዊ ኣእቲ", + "toastAuthErrorTitle": "ስሕተት ምርግጋእ", + "toastUserNotFound": "ተጠቃሚ ኣይተረኸበን", + "toastPinNotFound": "PIN ኣይተረኸበን", + "toastIncorrectPinTitle": "PIN ሕማቕ እዩ", + "toastIncorrectPinDescription": "እንደገና ፈትን።", + "authChoiceTitle": "መንገዲ ምርግጋእ ምረጽ", + "fingerprintTitle": "እግሪ ጣት", + "fingerprintSubtitle": "ባዮሜትሪክ ምርግጋእ ንምፍጣር እግሪ ጣት ተጠቀም", + "pinTitle": "AmbaPay PIN", + "pinSubtitle": "PIN 6-ኣሃዳዊ ኣእቱ", + "biometricWaiting": "ባዮሜትሪክ ምርግጋእ ንምቕባል ተጠቕሙ…", + "cancelButton": "ሰርዝ", + "pinVerificationTitle": "ምርግጋእ PIN", + "submitButtonVerifying": "ተኸቲሉ ኣሎ...", + "submitButtonConfirm": "ኣረጋግጽ" + }, + "transactioncard": { + "descriptionSend": "ናብ {{recipientName}} ተላእኮ", + "descriptionReceive": "ካብ {{senderName}} ተቐቢሉ", + "descriptionAddCash": "ገንዘብ ዝተዕቀበ (****{{lastFourDigits}})", + "descriptionCashOut": "ገንዘብ ናብ {{bankProvider}} ዝወጸ", + "descriptionDefault": "ግብያዊ ስራሕ", + "detailPhone": "ስልኪ: {{phoneNumber}}", + "detailCard": "ካርታ: ****{{lastFourDigits}}", + "detailAccount": "ኣካውንት: {{accountNumber}}", + "detailBankProvider": "ብ {{bankProvider}}" + } + } +} diff --git a/metro.config.js b/metro.config.js new file mode 100644 index 0000000..f7c74bb --- /dev/null +++ b/metro.config.js @@ -0,0 +1,19 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); + +const config = getDefaultConfig(__dirname); + +// Add resolver for platform-specific files +config.resolver.platforms = ['ios', 'android', 'native', 'web']; + +// Ensure web extensions are properly resolved +// This allows .web.ts files to be picked up for web builds +config.resolver.sourceExts = [ + ...config.resolver.sourceExts, + 'web.ts', + 'web.tsx', + 'web.js', + 'web.jsx', +]; + +module.exports = withNativeWind(config, { input: "./app/global.css" }); diff --git a/nativewind-env.d.ts b/nativewind-env.d.ts new file mode 100644 index 0000000..c0d8380 --- /dev/null +++ b/nativewind-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eac059b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13536 @@ +{ + "name": "amba", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "amba", + "version": "1.0.0", + "dependencies": { + "@chatwoot/react-native-widget": "^0.0.21", + "@expo/metro-runtime": "~5.0.4", + "@expo/vector-icons": "^14.1.0", + "@lottiefiles/dotlottie-react": "^0.17.10", + "@react-native-async-storage/async-storage": "2.1.2", + "@react-native-firebase/app": "^23.5.0", + "@react-native-firebase/auth": "^23.5.0", + "@react-native-firebase/firestore": "^23.5.0", + "@react-native-firebase/functions": "^23.5.0", + "@react-native-firebase/messaging": "^23.5.0", + "@react-native-firebase/storage": "^23.7.0", + "@react-native-google-signin/google-signin": "^16.0.0", + "@react-navigation/material-top-tabs": "^7.4.2", + "@rn-primitives/accordion": "^1.1.0", + "@rn-primitives/checkbox": "^1.2.0", + "@rn-primitives/label": "^1.1.0", + "@rn-primitives/portal": "^1.3.0", + "@rn-primitives/progress": "^1.1.0", + "@rn-primitives/select": "^1.1.0", + "@rn-primitives/slot": "^1.1.0", + "@rn-primitives/tabs": "^1.1.0", + "@rn-primitives/types": "^1.1.0", + "@shopify/flash-list": "^2.2.0", + "big.js": "^7.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "expo": "^53.0.0", + "expo-auth-session": "~6.2.1", + "expo-calendar": "^15.0.8", + "expo-camera": "^16.1.11", + "expo-clipboard": "~7.1.5", + "expo-constants": "~17.1.7", + "expo-contacts": "~14.2.5", + "expo-crypto": "~14.1.5", + "expo-dev-client": "~5.2.4", + "expo-haptics": "~14.1.4", + "expo-image-picker": "~16.1.4", + "expo-linking": "~7.1.7", + "expo-local-authentication": "~14.0.1", + "expo-network": "~7.1.5", + "expo-router": "~5.1.6", + "expo-status-bar": "~2.2.3", + "firebase": "^12.2.1", + "i18next": "^25.7.2", + "libphonenumber-js": "^1.12.22", + "lottie-react-native": "^7.3.4", + "lucide-react-native": "^0.400.0", + "nativewind": "^4.1.23", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-i18next": "^16.5.0", + "react-native": "0.79.5", + "react-native-country-picker-modal": "^2.0.0", + "react-native-pager-view": "^6.9.1", + "react-native-qrcode-svg": "^6.3.21", + "react-native-reanimated": "~3.17.4", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-svg": "^15.11.2", + "react-native-web": "^0.20.0", + "react-native-webview": "13.13.5", + "react-native-wheel-pick": "^1.2.6", + "tailwind-merge": "^3.0.1", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "valibot": "^1.1.0", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@types/big.js": "^6.2.2", + "@types/react": "~19.0.10", + "patch-package": "^8.0.1", + "typescript": "~5.8.3" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", + "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz", + "integrity": "sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", + "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", + "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@callstack/react-theme-provider": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.3.tgz", + "integrity": "sha512-B+9JBK7zsND/AdVkjwHvbb4cR05fJofLFG30hOeoXke8WkKAWN36yFljauAhI8qwlXlGFGZMYE1wQvsqBSccrA==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^3.2.0", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": "^16.3.0" + } + }, + "node_modules/@callstack/react-theme-provider/node_modules/deepmerge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", + "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@chatwoot/react-native-widget": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@chatwoot/react-native-widget/-/react-native-widget-0.0.21.tgz", + "integrity": "sha512-NlsRyRkJz3GyijQ2eGCfNkkOA8mRGDjWkUdJKAmLhbbNtVrhaE0OWur5qhy26c3ekOgzii47s5hfbjHFqLXFPQ==", + "license": "MIT", + "dependencies": { + "react-native-modal": "^13.0.1" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.23.1", + "react": "*", + "react-native": "*", + "react-native-webview": ">=13.8.6" + } + }, + "node_modules/@expo/cli": { + "version": "0.24.21", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.21.tgz", + "integrity": "sha512-DT6K9vgFHqqWL/19mU1ofRcPoO1pn4qmgi76GtuiNU4tbBe/02mRHwFsQw7qRfFAT28If5e/wiwVozgSuZVL8g==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.8", + "@babel/runtime": "^7.20.0", + "@expo/code-signing-certificates": "^0.0.5", + "@expo/config": "~11.0.13", + "@expo/config-plugins": "~10.1.2", + "@expo/devcert": "^1.1.2", + "@expo/env": "~1.0.7", + "@expo/image-utils": "^0.7.6", + "@expo/json-file": "^9.1.5", + "@expo/metro-config": "~0.20.17", + "@expo/osascript": "^2.2.5", + "@expo/package-manager": "^1.8.6", + "@expo/plist": "^0.3.5", + "@expo/prebuild-config": "^9.0.11", + "@expo/schema-utils": "^0.1.0", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "0.79.6", + "@urql/core": "^5.0.6", + "@urql/exchange-retry": "^1.3.0", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "env-editor": "^0.4.1", + "freeport-async": "^2.0.0", + "getenv": "^2.0.0", + "glob": "^10.4.2", + "lan-network": "^0.1.6", + "minimatch": "^9.0.0", + "node-forge": "^1.3.1", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^3.0.1", + "pretty-bytes": "^5.6.0", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", + "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.3", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "tar": "^7.4.3", + "terminal-link": "^2.1.1", + "undici": "^6.18.2", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1" + }, + "bin": { + "expo-internal": "build/bin/cli" + } + }, + "node_modules/@expo/cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@expo/cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/cli/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", + "integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==", + "license": "MIT", + "dependencies": { + "node-forge": "^1.2.1", + "nullthrows": "^1.1.1" + } + }, + "node_modules/@expo/config": { + "version": "11.0.13", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-11.0.13.tgz", + "integrity": "sha512-TnGb4u/zUZetpav9sx/3fWK71oCPaOjZHoVED9NaEncktAd0Eonhq5NUghiJmkUGt3gGSjRAEBXiBbbY9/B1LA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "@expo/config-plugins": "~10.1.2", + "@expo/config-types": "^53.0.5", + "@expo/json-file": "^9.1.5", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^10.4.2", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4", + "sucrase": "3.35.0" + } + }, + "node_modules/@expo/config-plugins": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.1.2.tgz", + "integrity": "sha512-IMYCxBOcnuFStuK0Ay+FzEIBKrwW8OVUMc65+v0+i7YFIIe8aL342l7T4F8lR4oCfhXn7d6M5QPgXvjtc/gAcw==", + "license": "MIT", + "dependencies": { + "@expo/config-types": "^53.0.5", + "@expo/json-file": "~9.1.5", + "@expo/plist": "^0.3.5", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^10.4.2", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slash": "^3.0.0", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/config-types": { + "version": "53.0.5", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-53.0.5.tgz", + "integrity": "sha512-kqZ0w44E+HEGBjy+Lpyn0BVL5UANg/tmNixxaRMLS6nf37YsDrLk2VMAmeKMMk5CKG0NmOdVv3ngeUjRQMsy9g==", + "license": "MIT" + }, + "node_modules/@expo/config/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/config/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/devcert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.0.tgz", + "integrity": "sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA==", + "license": "MIT", + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0", + "glob": "^10.4.2" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/env": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-1.0.7.tgz", + "integrity": "sha512-qSTEnwvuYJ3umapO9XJtrb1fAqiPlmUUg78N0IZXXGwQRt+bkp0OBls+Y5Mxw/Owj8waAM0Z3huKKskRADR5ow==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, + "node_modules/@expo/fingerprint": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.13.4.tgz", + "integrity": "sha512-MYfPYBTMfrrNr07DALuLhG6EaLVNVrY/PXjEzsjWdWE4ZFn0yqI0IdHNkJG7t1gePT8iztHc7qnsx+oo/rDo6w==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "find-up": "^5.0.0", + "getenv": "^2.0.0", + "glob": "^10.4.2", + "ignore": "^5.3.1", + "minimatch": "^9.0.0", + "p-limit": "^3.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/fingerprint/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@expo/fingerprint/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/fingerprint/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.7.6.tgz", + "integrity": "sha512-GKnMqC79+mo/1AFrmAcUcGfbsXXTRqOMNS1umebuevl3aaw+ztsYEFEiuNhHZW7PQ3Xs3URNT513ZxKhznDscw==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "temp-dir": "~2.0.0", + "unique-string": "~2.0.0" + } + }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/json-file": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.1.5.tgz", + "integrity": "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/json-file/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/metro-config": { + "version": "0.20.17", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.17.tgz", + "integrity": "sha512-lpntF2UZn5bTwrPK6guUv00Xv3X9mkN3YYla+IhEHiYXWyG7WKOtDU0U4KR8h3ubkZ6SPH3snDyRyAzMsWtZFA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@babel/parser": "^7.20.0", + "@babel/types": "^7.20.0", + "@expo/config": "~11.0.12", + "@expo/env": "~1.0.7", + "@expo/json-file": "~9.1.5", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0", + "glob": "^10.4.2", + "jsc-safe-url": "^0.2.4", + "lightningcss": "~1.27.0", + "minimatch": "^9.0.0", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + } + }, + "node_modules/@expo/metro-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@expo/metro-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/metro-runtime": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-5.0.4.tgz", + "integrity": "sha512-r694MeO+7Vi8IwOsDIDzH/Q5RPMt1kUDYbiTJwnO15nIqiDwlE8HU55UlRhffKZy6s5FmxQsZ8HA+T8DqUW8cQ==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/@expo/osascript": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.2.5.tgz", + "integrity": "sha512-Bpp/n5rZ0UmpBOnl7Li3LtM7la0AR3H9NNesqL+ytW5UiqV/TbonYW3rDZY38u4u/lG7TnYflVIVQPD+iqZJ5w==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "exec-async": "^2.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.8.6.tgz", + "integrity": "sha512-gcdICLuL+nHKZagPIDC5tX8UoDDB8vNA5/+SaQEqz8D+T2C4KrEJc2Vi1gPAlDnKif834QS6YluHWyxjk0yZlQ==", + "license": "MIT", + "dependencies": { + "@expo/json-file": "^9.1.5", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/plist": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.3.5.tgz", + "integrity": "sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@expo/prebuild-config": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.11.tgz", + "integrity": "sha512-0DsxhhixRbCCvmYskBTq8czsU0YOBsntYURhWPNpkl0IPVpeP9haE5W4OwtHGzXEbmHdzaoDwNmVcWjS/mqbDw==", + "license": "MIT", + "dependencies": { + "@expo/config": "~11.0.13", + "@expo/config-plugins": "~10.1.2", + "@expo/config-types": "^53.0.5", + "@expo/image-utils": "^0.7.6", + "@expo/json-file": "^9.1.5", + "@react-native/normalize-colors": "0.79.5", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/prebuild-config/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/schema-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.0.tgz", + "integrity": "sha512-Me2avOfbcVT/O5iRmPKLCCSvbCfVfxIstGMlzVJOffplaZX1+ut8D18siR1wx5fkLMTWKs14ozEz11cGUY7hcw==", + "license": "MIT" + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", + "license": "MIT" + }, + "node_modules/@expo/server": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@expo/server/-/server-0.6.3.tgz", + "integrity": "sha512-Ea7NJn9Xk1fe4YeJ86rObHSv/bm3u/6WiQPXEqXJ2GrfYpVab2Swoh9/PnSM3KjR64JAgKjArDn1HiPjITCfHA==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "source-map-support": "~0.5.21", + "undici": "^6.18.2 || ^7.0.0" + } + }, + "node_modules/@expo/spawn-async": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", + "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", + "license": "MIT" + }, + "node_modules/@expo/vector-icons": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.1.0.tgz", + "integrity": "sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==", + "license": "MIT", + "peerDependencies": { + "expo-font": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", + "license": "MIT" + }, + "node_modules/@expo/xcpretty": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.3.2.tgz", + "integrity": "sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/code-frame": "7.10.4", + "chalk": "^4.1.0", + "find-up": "^5.0.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@expo/xcpretty/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@firebase/ai": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.4.0.tgz", + "integrity": "sha512-YilG6AJ/nYpCKtxZyvEzBRAQv5bU+2tBOKX4Ps0rNNSdxN39aT37kGhjATbk1kq1z5Lq7mkWglw/ajAF3lOWUg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.19", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", + "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", + "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz", + "integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz", + "integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "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" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.10.tgz", + "integrity": "sha512-ikrN05/q0/KjqIU+n48uNwmE7DeZIC9y3Nd19httcKqe273zoOeNYycEaQzLSdcpEGnWLmHaZpgtoo07aQZAXg==", + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.58.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.58.1.tgz", + "integrity": "sha512-YC4pmScrV0R3rd11gU5xHrjeNczlCic69zlnMH/buDIzYxIbpR88oPUhGtKgu5ln7EJchoLpeRJbA3uLCzSeTA==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", + "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-firebase/app": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.5.0.tgz", + "integrity": "sha512-TOlm6V6fbILwgFP37QZM9Y0nfAW6zqNGVIWlMlepQB6b/BzzFMrCl1FiyknqD5l7i1jgdFQrqX1WH6ZO4ePa/g==", + "license": "Apache-2.0", + "dependencies": { + "firebase": "12.4.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/auth": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/auth/-/auth-23.5.0.tgz", + "integrity": "sha512-lsOIHhvxtGe4dGfsCOwyFux+eGB9b1zaII0/tvrsJMhH7Gm77RxZA8VCGefDqw/UBU3YpQ395LXs997nNok0pQ==", + "license": "Apache-2.0", + "dependencies": { + "plist": "^3.1.0" + }, + "peerDependencies": { + "@react-native-firebase/app": "23.5.0", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/firestore": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/firestore/-/firestore-23.5.0.tgz", + "integrity": "sha512-eazEyJAd1BD47NXeOWU+YUilzHN4zY7YrE0pzevBIBCH4DaLqhH678TZpulMAh0SENkZA8MKBfxkkvqGyAGCsQ==", + "license": "Apache-2.0", + "dependencies": { + "react-native-url-polyfill": "2.0.0" + }, + "peerDependencies": { + "@react-native-firebase/app": "23.5.0" + } + }, + "node_modules/@react-native-firebase/functions": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/functions/-/functions-23.5.0.tgz", + "integrity": "sha512-aHdn6IBYkEyJ8k8syYolvYrOzzkPJDfIlH0FiF5LtNyU6XOlc5GxflLWa0MllYB9RUpVpisLv8XAWQee3y3Jsg==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "23.5.0" + } + }, + "node_modules/@react-native-firebase/messaging": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/messaging/-/messaging-23.5.0.tgz", + "integrity": "sha512-2EM28isDWgqCauar/kOnhpFQZ8ARnq9iE0N093TrS/sr+Mu6PHkPEDJElV9LFfp6nfxxjlm75h+x+nJrEDRkhQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "23.5.0", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/storage": { + "version": "23.7.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/storage/-/storage-23.7.0.tgz", + "integrity": "sha512-nInKRRNSE7vvsmyqdMsPGR48ttOx17B3CKHqX2yvUK/NZFQC0QoMXGHQBpOkaOmD/qDqvtg5GUvZgpOr+D+UzA==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "23.7.0" + } + }, + "node_modules/@react-native-google-signin/google-signin": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-16.0.0.tgz", + "integrity": "sha512-jVuzPo8odREekFc0b4RK3YsqCvedtLIM2P6NSszFr9cYyhKrUNikffPapL6LmkL9qkb8K6pDeb5CXg4qALOc0g==", + "license": "MIT", + "peerDependencies": { + "expo": ">=52.0.40", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", + "integrity": "sha512-N4Kt1cKxO5zgM/BLiyzuuDNquZPiIgfktEQ6TqJ/4nKA8zr4e8KJgU6Tb2eleihDO4E24HmkvGc73naybKRz/w==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.6.tgz", + "integrity": "sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.79.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.6.tgz", + "integrity": "sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.79.6", + "babel-plugin-syntax-hermes-parser": "0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.6.tgz", + "integrity": "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.25.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.5.tgz", + "integrity": "sha512-ApLO1ARS8JnQglqS3JAHk0jrvB+zNW3dvNJyXPZPoygBpZVbf8sjvqeBiaEYpn8ETbFWddebC4HoQelDndnrrA==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.79.5", + "chalk": "^4.0.0", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "metro": "^0.82.0", + "metro-config": "^0.82.0", + "metro-core": "^0.82.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + } + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.5.tgz", + "integrity": "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.5.tgz", + "integrity": "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.79.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.6.tgz", + "integrity": "sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.6.tgz", + "integrity": "sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.79.6", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.5.tgz", + "integrity": "sha512-K3QhfFNKiWKF3HsCZCEoWwJPSMcPJQaeqOmzFP4RL8L3nkpgUwn74PfSCcKHxooVpS6bMvJFQOz7ggUZtNVT+A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.5.tgz", + "integrity": "sha512-a2wsFlIhvd9ZqCD5KPRsbCQmbZi6KxhRN++jrqG0FUTEV5vY7MvjjUqDILwJd2ZBZsf7uiDuClCcKqA+EEdbvw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.5.tgz", + "integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz", + "integrity": "sha512-EUPM2rfGNO4cbI3olAbhPkIt3q7MapwCwAJBzUfWlZ/pu0PRNOnMQ1IvaXTf3TpeozXV52K1OdprLEI/kI5eUA==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.7.tgz", + "integrity": "sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.4", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.17", + "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.12.4", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.12.4.tgz", + "integrity": "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.1", + "escape-string-regexp": "^4.0.0", + "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/elements": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.8.1.tgz", + "integrity": "sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ==", + "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.1.19", + "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/material-top-tabs": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-7.4.2.tgz", + "integrity": "sha512-LB/bCDhdaKsexA5w0otgZEDBysGbiCr2l0hW6z41rJQ0JqAOVybH0cBuFr3Awasv0mQh9iTJNha4VsuUb7Q0Xw==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.8.1", + "color": "^4.2.3", + "react-native-tab-view": "^4.2.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.19", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-pager-view": ">= 6.0.0", + "react-native-safe-area-context": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.17", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.17.tgz", + "integrity": "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.12.4", + "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.3.26", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.26.tgz", + "integrity": "sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.4", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.17", + "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.1", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz", + "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@rn-primitives/accordion": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/accordion/-/accordion-1.2.0.tgz", + "integrity": "sha512-gt1NQC6XTbSN6KEqX3QDbvqO4kXFDKhQGiljL/pq1WO0iJNVaV47/3E8fLOZLEuj2FSkQblNhSvVc4WU/7qHoA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-accordion": "^1.2.11", + "@rn-primitives/hooks": "1.3.0", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/accordion/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/checkbox": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/checkbox/-/checkbox-1.2.0.tgz", + "integrity": "sha512-FU/c81eoo6zGm4J4uCccjnqijrQ0grMkt6W3xhnOFkxqxiU2QrnlXN7ddhm2yN+s8CNnke4bOj/xr78wIaYfiw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.2", + "@rn-primitives/hooks": "1.3.0", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/checkbox/node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/checkbox/node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/checkbox/node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/checkbox/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/hooks": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/hooks/-/hooks-1.3.0.tgz", + "integrity": "sha512-BR97reSu7uVDpyMeQdRJHT0w8KdS6jdYnOL6xQtqS2q3H6N7vXBlX4LFERqJZphD+aziJFIAJ3HJF1vtt6XlpQ==", + "license": "MIT", + "dependencies": { + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/label": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/label/-/label-1.2.0.tgz", + "integrity": "sha512-eThBr6vn2jS81ZS4JNcg0+02TkEircH4bZmjF4IZUDl4XRpevwK95NyOkbfhGYmpVbAuisAVxDmvNOQ4OVjfug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-label": "^2.1.7", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/label/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/label/node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/label/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/portal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/portal/-/portal-1.3.0.tgz", + "integrity": "sha512-a2DSce7TcSfcs0cCngLadAJOvx/+mdH9NRu+GxkX8NPRsGGhJvDEOqouMgDqLwx7z9mjXoUaZcwaVcemUSW9/A==", + "license": "MIT", + "dependencies": { + "zustand": "^5.0.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/progress": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/progress/-/progress-1.2.0.tgz", + "integrity": "sha512-bbO4WGSNAd2idYDW0ma4xCX9UFOjNK3U4F4hLRhMKglz3c/QVYfpKvlGQ0Y0d7kpelA7MQizvFeqGGYfxSuisw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-progress": "^1.1.7", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/progress/node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/progress/node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/select/-/select-1.2.0.tgz", + "integrity": "sha512-W3qFkdSAFPnjNMM7II5MiLCItjWOGXr8f+3obPtLAHcWrcsX/d1KogmplWXwmhBvVStCgE1OpJAD3DE2CHx9Rw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-select": "^2.2.5", + "@rn-primitives/hooks": "1.3.0", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "@rn-primitives/portal": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper/node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/slot/-/slot-1.2.0.tgz", + "integrity": "sha512-cpbn+JLjSeq3wcA4uqgFsUimMrWYWx2Ks7r5rkwd1ds1utxynsGkLOKpYVQkATwWrYhtcoF1raxIKEqXuMN+/w==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/tabs/-/tabs-1.2.0.tgz", + "integrity": "sha512-sKqVYQD1s46eS8kGBsCGxJrtZ4my+sjXD5m2auGSdTJmJ8dNYqZJEI1caiMm64ZvRuQDe2bFsekAFGexBD4qUQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-tabs": "^1.1.12", + "@rn-primitives/slot": "1.2.0", + "@rn-primitives/types": "1.2.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/tabs/node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rn-primitives/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rn-primitives/types/-/types-1.2.0.tgz", + "integrity": "sha512-b+6zKgdKVqAfaFPSfhwlQL0dnPQXPpW890m3eguC0VDI1eOsoEvUfVb6lmgH4bum9MmI0xymq4tOUI/fsKLoCQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/@shopify/flash-list": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.2.0.tgz", + "integrity": "sha512-mL61IofcfBNRZ/qazIf+pghGULkcZUQ7EZNldH1JBbIjtDb25ADSiQrt62ZTnRz0H5+bPFEZUmN9+WChHzX8pw==", + "license": "MIT", + "peerDependencies": { + "@babel/runtime": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "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", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/react": { + "version": "19.0.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz", + "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@urql/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", + "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.13", + "wonka": "^6.3.2" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", + "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", + "license": "MIT", + "dependencies": { + "@urql/core": "^5.1.2", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "@urql/core": "^5.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.19.13", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.13.tgz", + "integrity": "sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-expo": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.4.tgz", + "integrity": "sha512-3IKORo3KR+4qtLdCkZNDj8KeA43oBn7RRQejFGWfiZgu/NeaRUSri8YwYjZqybm7hn3nmMv9OLahlvXBX23o5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.79.6", + "babel-plugin-react-native-web": "~0.19.13", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "react-refresh": "^0.14.2", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "babel-plugin-react-compiler": "^19.0.0-beta-e993439-20250405" + }, + "peerDependenciesMeta": { + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-7.0.1.tgz", + "integrity": "sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "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", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "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.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "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/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "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/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/exec-async": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", + "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==", + "license": "MIT" + }, + "node_modules/expo": { + "version": "53.0.22", + "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.22.tgz", + "integrity": "sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "0.24.21", + "@expo/config": "~11.0.13", + "@expo/config-plugins": "~10.1.2", + "@expo/fingerprint": "0.13.4", + "@expo/metro-config": "0.20.17", + "@expo/vector-icons": "^14.0.0", + "babel-preset-expo": "~13.2.4", + "expo-asset": "~11.1.7", + "expo-constants": "~17.1.7", + "expo-file-system": "~18.1.11", + "expo-font": "~13.3.2", + "expo-keep-awake": "~14.1.4", + "expo-modules-autolinking": "2.1.14", + "expo-modules-core": "2.5.0", + "react-native-edge-to-edge": "1.6.0", + "whatwg-url-without-unicode": "8.0.0-3" + }, + "bin": { + "expo": "bin/cli", + "expo-modules-autolinking": "bin/autolinking", + "fingerprint": "bin/fingerprint" + }, + "peerDependencies": { + "@expo/dom-webview": "*", + "@expo/metro-runtime": "*", + "react": "*", + "react-native": "*", + "react-native-webview": "*" + }, + "peerDependenciesMeta": { + "@expo/dom-webview": { + "optional": true + }, + "@expo/metro-runtime": { + "optional": true + }, + "react-native-webview": { + "optional": true + } + } + }, + "node_modules/expo-application": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.1.5.tgz", + "integrity": "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-asset": { + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz", + "integrity": "sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.7.6", + "expo-constants": "~17.1.7" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-auth-session": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-6.2.1.tgz", + "integrity": "sha512-9KgqrGpW7PoNOhxJ7toofi/Dz5BU2TE4Q+ktJZsmDXLoFcNOcvBokh2+mkhG58Qvd/xJ9Z5sAt/5QoOFaPb9wA==", + "license": "MIT", + "dependencies": { + "expo-application": "~6.1.5", + "expo-constants": "~17.1.7", + "expo-crypto": "~14.1.5", + "expo-linking": "~7.1.7", + "expo-web-browser": "~14.2.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-calendar": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-calendar/-/expo-calendar-15.0.8.tgz", + "integrity": "sha512-i+ojy6zFnWSPb2DYp4L4W4U5iVI+NXnuHr3xysShoV8znNOmixP1TOYuJXt5Lpz+BpHCWseU31gV1E5SSkIKsw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-camera": { + "version": "16.1.11", + "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-16.1.11.tgz", + "integrity": "sha512-etA5ZKoC6nPBnWWqiTmlX//zoFZ6cWQCCIdmpUHTGHAKd4qZNCkhPvBWbi8o32pDe57lix1V4+TPFgEcvPwsaA==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, + "node_modules/expo-clipboard": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz", + "integrity": "sha512-TCANUGOxouoJXxKBW5ASJl2WlmQLGpuZGemDCL2fO5ZMl57DGTypUmagb0CVUFxDl0yAtFIcESd78UsF9o64aw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-constants": { + "version": "17.1.7", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz", + "integrity": "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA==", + "license": "MIT", + "dependencies": { + "@expo/config": "~11.0.12", + "@expo/env": "~1.0.7" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-contacts": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/expo-contacts/-/expo-contacts-14.2.5.tgz", + "integrity": "sha512-yiVmXrLKBCcBkwsaHFlbs0f7UwE2t7Aa1NBOK4Y06ya0Y5WyE6I/P5ZAtWNjXnKmbV7iNKAiPUzqVaNazhCtWA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-crypto": { + "version": "14.1.5", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.1.5.tgz", + "integrity": "sha512-ZXJoUMoUeiMNEoSD4itItFFz3cKrit6YJ/BR0hjuwNC+NczbV9rorvhvmeJmrU9O2cFQHhJQQR1fjQnt45Vu4Q==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-client": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.2.4.tgz", + "integrity": "sha512-s/N/nK5LPo0QZJpV4aPijxyrzV4O49S3dN8D2fljqrX2WwFZzWwFO6dX1elPbTmddxumdcpczsdUPY+Ms8g43g==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "5.1.16", + "expo-dev-menu": "6.1.14", + "expo-dev-menu-interface": "1.10.0", + "expo-manifests": "~0.16.6", + "expo-updates-interface": "~1.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.16.tgz", + "integrity": "sha512-tbCske9pvbozaEblyxoyo/97D6od9Ma4yAuyUnXtRET1CKAPKYS+c4fiZ+I3B4qtpZwN3JNFUjG3oateN0y6Hg==", + "license": "MIT", + "dependencies": { + "ajv": "8.11.0", + "expo-dev-menu": "6.1.14", + "expo-manifests": "~0.16.6", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.14.tgz", + "integrity": "sha512-yonNMg2GHJZtuisVowdl1iQjZfYP85r1D1IO+ar9D9zlrBPBJhq2XEju52jd1rDmDkmDuEhBSbPNhzIcsBNiPg==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "1.10.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz", + "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-file-system": { + "version": "18.1.11", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz", + "integrity": "sha512-HJw/m0nVOKeqeRjPjGdvm+zBi5/NxcdPf8M8P3G2JFvH5Z8vBWqVDic2O58jnT1OFEy0XXzoH9UqFu7cHg9DTQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-font": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz", + "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-haptics": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.1.4.tgz", + "integrity": "sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.1.0.tgz", + "integrity": "sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.1.4.tgz", + "integrity": "sha512-bTmmxtw1AohUT+HxEBn2vYwdeOrj1CLpMXKjvi9FKSoSbpcarT4xxI0z7YyGwDGHbrJqyyic3I9TTdP2J2b4YA==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~5.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, + "node_modules/expo-keep-awake": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", + "integrity": "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-linking": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz", + "integrity": "sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA==", + "license": "MIT", + "dependencies": { + "expo-constants": "~17.1.7", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-local-authentication": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-14.0.1.tgz", + "integrity": "sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-manifests": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", + "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "license": "MIT", + "dependencies": { + "@expo/config": "~11.0.12", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz", + "integrity": "sha512-nT5ERXwc+0ZT/pozDoJjYZyUQu5RnXMk9jDGm5lg+PiKvsrCTSA/2/eftJGMxLkTjVI2MXp5WjSz3JRjbA7UXA==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "find-up": "^5.0.0", + "glob": "^10.4.2", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-2.5.0.tgz", + "integrity": "sha512-aIbQxZE2vdCKsolQUl6Q9Farlf8tjh/ROR4hfN1qT7QBGPl1XrJGnaOKkcgYaGrlzCPg/7IBe0Np67GzKMZKKQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + } + }, + "node_modules/expo-network": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/expo-network/-/expo-network-7.1.5.tgz", + "integrity": "sha512-VNgxNe3Y1xo00zMzFy7Q+35qWnSJnjZ9RRLtW3Nu/ITtv9ak+BIghfWj1PANLYB3ZkWzY5656R1YIkkRkeDukg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-router": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.1.6.tgz", + "integrity": "sha512-Tc7QFurWqLItrHvbL2TB4OLq8WA4y8fCXPkRG+q9zP4Lk4xKIDskO7/8ff3+XAOHJB5Z8GHn5IhXv4Ik89SVUA==", + "license": "MIT", + "dependencies": { + "@expo/metro-runtime": "5.0.4", + "@expo/schema-utils": "^0.1.0", + "@expo/server": "^0.6.3", + "@radix-ui/react-slot": "1.2.0", + "@react-navigation/bottom-tabs": "^7.3.10", + "@react-navigation/native": "^7.1.6", + "@react-navigation/native-stack": "^7.3.10", + "client-only": "^0.0.1", + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "react-native-is-edge-to-edge": "^1.1.6", + "semver": "~7.6.3", + "server-only": "^0.0.1", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "@react-navigation/drawer": "^7.3.9", + "expo": "*", + "expo-constants": "*", + "expo-linking": "*", + "react-native-reanimated": "*", + "react-native-safe-area-context": "*", + "react-native-screens": "*" + }, + "peerDependenciesMeta": { + "@react-navigation/drawer": { + "optional": true + }, + "@testing-library/jest-native": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + } + } + }, + "node_modules/expo-router/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-status-bar": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.2.3.tgz", + "integrity": "sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q==", + "license": "MIT", + "dependencies": { + "react-native-edge-to-edge": "1.6.0", + "react-native-is-edge-to-edge": "^1.1.6" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-updates-interface": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", + "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-web-browser": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz", + "integrity": "sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "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", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/firebase": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.4.0.tgz", + "integrity": "sha512-/chNgDQ6ppPPGOQO4jctxOa/5JeQxuhaxA7Y90K0I+n/wPfoO8mRveedhVUdo7ExLcWUivnnow/ouSLYSI5Icw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.4.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.4", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.4", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "license": "BSD-2-Clause" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.5.tgz", + "integrity": "sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=6" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getenv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", + "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "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/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, + "node_modules/i18next": { + "version": "25.7.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz", + "integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", + "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lan-network": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz", + "integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==", + "license": "MIT", + "bin": { + "lan-network": "dist/lan-network-cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.22", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.22.tgz", + "integrity": "sha512-nzdkDyqlcLV754o1RrOJxh8kycG+63odJVUqnK4dxhw7buNkdTqJc/a/CE0h599dTJgFbzvr6GEOemFBSBryAA==", + "license": "MIT" + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz", + "integrity": "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.27.0", + "lightningcss-darwin-x64": "1.27.0", + "lightningcss-freebsd-x64": "1.27.0", + "lightningcss-linux-arm-gnueabihf": "1.27.0", + "lightningcss-linux-arm64-gnu": "1.27.0", + "lightningcss-linux-arm64-musl": "1.27.0", + "lightningcss-linux-x64-gnu": "1.27.0", + "lightningcss-linux-x64-musl": "1.27.0", + "lightningcss-win32-arm64-msvc": "1.27.0", + "lightningcss-win32-x64-msvc": "1.27.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz", + "integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz", + "integrity": "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz", + "integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz", + "integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz", + "integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz", + "integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz", + "integrity": "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz", + "integrity": "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz", + "integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz", + "integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lottie-react-native": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-7.3.4.tgz", + "integrity": "sha512-XUh7eGFb7ID8JRdU6U4N4cYQeYmjtdQRvd8ZXJ6xrdSsn5gZD0c79ITOREPcwJg4YupBFHgyV1GXdAHQP+KYUQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@lottiefiles/dotlottie-react": "^0.13.5", + "react": "*", + "react-native": ">=0.46", + "react-native-windows": ">=0.63.x" + }, + "peerDependenciesMeta": { + "@lottiefiles/dotlottie-react": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react-native": { + "version": "0.400.0", + "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.400.0.tgz", + "integrity": "sha512-hb8aJdTEHlv7Ywj0QilUwcpi8yuprCw/q9wQItCHUf1TBXbz0hesqCnc18z+O6XmXT5fBZDDL3ifK5TkkuB0iQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0", + "react-native": "*", + "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "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", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz", + "integrity": "sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "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.29.1", + "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.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-config": "0.82.5", + "metro-core": "0.82.5", + "metro-file-map": "0.82.5", + "metro-resolver": "0.82.5", + "metro-runtime": "0.82.5", + "metro-source-map": "0.82.5", + "metro-symbolicate": "0.82.5", + "metro-transform-plugins": "0.82.5", + "metro-transform-worker": "0.82.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.5.tgz", + "integrity": "sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.29.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.5.tgz", + "integrity": "sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.5.tgz", + "integrity": "sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.5.tgz", + "integrity": "sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.82.5", + "metro-cache": "0.82.5", + "metro-core": "0.82.5", + "metro-runtime": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-core": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz", + "integrity": "sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.5.tgz", + "integrity": "sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.5.tgz", + "integrity": "sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.5.tgz", + "integrity": "sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.5.tgz", + "integrity": "sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.5.tgz", + "integrity": "sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.82.5", + "nullthrows": "^1.1.1", + "ob1": "0.82.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.5.tgz", + "integrity": "sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.82.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.5.tgz", + "integrity": "sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.5.tgz", + "integrity": "sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.82.5", + "metro-babel-transformer": "0.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-minify-terser": "0.82.5", + "metro-source-map": "0.82.5", + "metro-transform-plugins": "0.82.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/modal-react-native-web": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/modal-react-native-web/-/modal-react-native-web-0.2.0.tgz", + "integrity": "sha512-sC0/jL3ZL4bGtv1VS43TnrH7/FHUqgb7IU3VYWNDzuR223fYlpG5Gc974GsTP172Vi+lnnBL/G70xONmaggxeQ==", + "license": "MIT", + "dependencies": { + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": "16.x.x", + "react-art": "16.x.x", + "react-dom": "16.x.x", + "react-native-web": "0.9.x" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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/nativewind": { + "version": "4.1.23", + "resolved": "https://registry.npmjs.org/nativewind/-/nativewind-4.1.23.tgz", + "integrity": "sha512-oLX3suGI6ojQqWxdQezOSM5GmJ4KvMnMtmaSMN9Ggb5j7ysFt4nHxb1xs8RDjZR7BWc+bsetNJU8IQdQMHqRpg==", + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.5", + "debug": "^4.3.7", + "react-native-css-interop": "0.1.22" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "tailwindcss": ">3.3.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "license": "MIT", + "dependencies": { + "lodash.toarray": "^4.4.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.5.tgz", + "integrity": "sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ora/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", + "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", + "license": "MIT", + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/prop-types/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/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "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", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-async-hook": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-async-hook/-/react-async-hook-3.6.1.tgz", + "integrity": "sha512-YWBB2feVQF79t5u2raMPHlZ8975Jds+guCvkWVC4kRLDlSCouLsYpQm4DGSqPeHvoHYVVcDfqNayLZAXQmnxnw==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "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-i18next": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz", + "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.5.tgz", + "integrity": "sha512-jVihwsE4mWEHZ9HkO1J2eUZSwHyDByZOqthwnGrVZCh6kTQBCm4v8dicsyDa6p0fpWNE5KicTcpX/XXl0ASJFg==", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.79.5", + "@react-native/codegen": "0.79.5", + "@react-native/community-cli-plugin": "0.79.5", + "@react-native/gradle-plugin": "0.79.5", + "@react-native/js-polyfills": "0.79.5", + "@react-native/normalize-colors": "0.79.5", + "@react-native/virtualized-lists": "0.79.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.25.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.82.0", + "metro-source-map": "^0.82.0", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.25.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-animatable": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.3.tgz", + "integrity": "sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + } + }, + "node_modules/react-native-country-picker-modal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-native-country-picker-modal/-/react-native-country-picker-modal-2.0.0.tgz", + "integrity": "sha512-p0wfkKx1mOCEkn3Qm2/FA8TrxSdUw/nqaCcT6KYvIGmPtPdia1Ce+iWh5G0j2hZcDs6yJSkYAx+7lrZ0HwixAw==", + "license": "MIT", + "dependencies": { + "@callstack/react-theme-provider": "3.0.3", + "fuse.js": "3.4.5", + "modal-react-native-web": "0.2.0", + "node-emoji": "1.10.0", + "prop-types": "15.7.2", + "react-async-hook": "3.6.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-web": "*" + } + }, + "node_modules/react-native-css-interop": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz", + "integrity": "sha512-Mu01e+H9G+fxSWvwtgWlF5MJBJC4VszTCBXopIpeR171lbeBInHb8aHqoqRPxmJpi3xIHryzqKFOJYAdk7PBxg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.3.7", + "lightningcss": "^1.27.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">=18", + "react-native": "*", + "react-native-reanimated": ">=3.6.2", + "tailwindcss": "~3" + }, + "peerDependenciesMeta": { + "react-native-safe-area-context": { + "optional": true + }, + "react-native-svg": { + "optional": true + } + } + }, + "node_modules/react-native-css-interop/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-edge-to-edge": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", + "integrity": "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-modal": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-13.0.1.tgz", + "integrity": "sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.2", + "react-native-animatable": "1.3.3" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.65.0" + } + }, + "node_modules/react-native-pager-view": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.9.1.tgz", + "integrity": "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-qrcode-svg": { + "version": "6.3.21", + "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.21.tgz", + "integrity": "sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.0", + "qrcode": "^1.5.4", + "text-encoding": "^0.7.0" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.63.4", + "react-native-svg": ">=14.0.0" + } + }, + "node_modules/react-native-qrcode-svg/node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/react-native-qrcode-svg/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/react-native-reanimated": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", + "integrity": "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0", + "invariant": "^2.2.4", + "react-native-is-edge-to-edge": "1.1.7" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", + "integrity": "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz", + "integrity": "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.11.1.tgz", + "integrity": "sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.1.7", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg": { + "version": "15.11.2", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", + "integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-tab-view": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.2.0.tgz", + "integrity": "sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ==", + "license": "MIT", + "dependencies": { + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*", + "react-native-pager-view": ">= 6.0.0" + } + }, + "node_modules/react-native-url-polyfill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz", + "integrity": "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==", + "license": "MIT", + "dependencies": { + "whatwg-url-without-unicode": "8.0.0-3" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/react-native-web": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", + "integrity": "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@react-native/normalize-colors": "^0.74.1", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^7.0.1", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "styleq": "^0.1.3" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.89", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", + "license": "MIT" + }, + "node_modules/react-native-web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/react-native-webview": { + "version": "13.13.5", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.13.5.tgz", + "integrity": "sha512-MfC2B+woL4Hlj2WCzcb1USySKk+SteXnUKmKktOk/H/AQy5+LuVdkPKm8SknJ0/RxaxhZ48WBoTRGaqgR137hw==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-wheel-pick": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-native-wheel-pick/-/react-native-wheel-pick-1.2.6.tgz", + "integrity": "sha512-ElFE1o/fnATVxf+T4U8qZ8B0MBtX6MHNADvOLuxaxbdTlC0tSWHAQVSoefhpnLzvyzgIKGLAOZOuskuTMIPUnA==", + "license": "Apache-2.0", + "dependencies": { + "moment": "^2.19.1" + }, + "peerDependencies": { + "@react-native-community/datetimepicker": ">=6.1.3", + "@react-native-picker/picker": ">=2.4.1", + "moment": ">=2.0.0", + "prop-types": "*" + } + }, + "node_modules/react-native/node_modules/@react-native/codegen": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.5.tgz", + "integrity": "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.1", + "hermes-parser": "0.25.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-native/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/react-native/node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/react-native/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.0.tgz", + "integrity": "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw==", + "license": "MIT" + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "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-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", + "license": "MIT" + }, + "node_modules/styleq": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "deprecated": "no longer maintained", + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz", + "integrity": "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "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/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "license": "MIT", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wonka": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", + "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xcode/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", + "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "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 new file mode 100644 index 0000000..128cd39 --- /dev/null +++ b/package.json @@ -0,0 +1,89 @@ +{ + "name": "amba", + "version": "1.0.0", + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "android": "expo run:android", + "build:android": "eas build --platform android --profile preview", + "ios": "expo run:ios", + "web": "expo start --web" + }, + "dependencies": { + "@chatwoot/react-native-widget": "^0.0.21", + "@expo/metro-runtime": "~5.0.4", + "@expo/vector-icons": "^14.1.0", + "@lottiefiles/dotlottie-react": "^0.17.10", + "@react-native-async-storage/async-storage": "2.1.2", + "@react-native-firebase/app": "^23.5.0", + "@react-native-firebase/auth": "^23.5.0", + "@react-native-firebase/firestore": "^23.5.0", + "@react-native-firebase/functions": "^23.5.0", + "@react-native-firebase/messaging": "^23.5.0", + "@react-native-firebase/storage": "^23.7.0", + "@react-native-google-signin/google-signin": "^16.0.0", + "@react-navigation/material-top-tabs": "^7.4.2", + "@rn-primitives/accordion": "^1.1.0", + "@rn-primitives/checkbox": "^1.2.0", + "@rn-primitives/label": "^1.1.0", + "@rn-primitives/portal": "^1.3.0", + "@rn-primitives/progress": "^1.1.0", + "@rn-primitives/select": "^1.1.0", + "@rn-primitives/slot": "^1.1.0", + "@rn-primitives/tabs": "^1.1.0", + "@rn-primitives/types": "^1.1.0", + "@shopify/flash-list": "^2.2.0", + "big.js": "^7.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "expo": "^53.0.0", + "expo-auth-session": "~6.2.1", + "expo-calendar": "^15.0.8", + "expo-camera": "^16.1.11", + "expo-clipboard": "~7.1.5", + "expo-constants": "~17.1.7", + "expo-contacts": "~14.2.5", + "expo-crypto": "~14.1.5", + "expo-dev-client": "~5.2.4", + "expo-haptics": "~14.1.4", + "expo-image-picker": "~16.1.4", + "expo-linking": "~7.1.7", + "expo-local-authentication": "~14.0.1", + "expo-network": "~7.1.5", + "expo-router": "~5.1.6", + "expo-status-bar": "~2.2.3", + "firebase": "^12.2.1", + "i18next": "^25.7.2", + "libphonenumber-js": "^1.12.22", + "lottie-react-native": "^7.3.4", + "lucide-react-native": "^0.400.0", + "nativewind": "^4.1.23", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-i18next": "^16.5.0", + "react-native": "0.79.5", + "react-native-country-picker-modal": "^2.0.0", + "react-native-pager-view": "^6.9.1", + "react-native-qrcode-svg": "^6.3.21", + "react-native-reanimated": "~3.17.4", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-svg": "^15.11.2", + "react-native-web": "^0.20.0", + "react-native-webview": "13.13.5", + "react-native-wheel-pick": "^1.2.6", + "tailwind-merge": "^3.0.1", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "valibot": "^1.1.0", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@types/big.js": "^6.2.2", + "@types/react": "~19.0.10", + "patch-package": "^8.0.1", + "typescript": "~5.8.3" + }, + "private": true +} diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..913b433 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,9 @@ +module.exports = { + dependencies: { + 'expo-firebase-core': { + platforms: { android: null, ios: null }, + }, + }, +}; + + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..63969ee --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,76 @@ +const { hairlineWidth } = require('nativewind/theme'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'], + presets: [require('nativewind/preset')], + theme: { + extend: { + + fontFamily: { + "dmsans": ['DMSans-Regular', 'sans-serif'], + "dmsans-extrabold": ['DMSans-ExtraBold', 'sans-serif'], + "dmsans-bold": ['DMSans-Bold', 'sans-serif'], + "dmsans-medium": ['DMSans-Medium', 'sans-serif'], + "dmsans-light": ['DMSans-Light', 'sans-serif'], + "dmsans-thin": ['DMSans-Thin', 'sans-serif'], + "dmsans-semibold": ['DMSans-SemiBold', 'sans-serif'], + "dmsans-extralight": ['DMSans-ExtraLight', 'sans-serif'], + "dmsans-black": ['DMSans-Black', 'sans-serif'], + }, + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderWidth: { + hairline: hairlineWidth(), + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cb47cc6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "jsx": "react", + "strict": true, + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "~/*": [ + "*" + ], + "@firebase/auth": [ + "./node_modules/@firebase/auth/dist/index.rn.d.ts" + ] + } + } +} \ No newline at end of file