├── .env.example
├── .github
└── workflows
│ ├── build-test-lint.yml
│ └── ci.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── extensions.json
├── 2025-04-21T210500_NEXT_PROMPT.md
├── Cargo.lock
├── Cargo.toml
├── IMPLEMENTATION_PLAN.md
├── README.md
├── apps
├── web
│ ├── .env.example
│ ├── components.json
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── _redirects
│ │ └── logos
│ │ │ ├── app_icon_240x240.png
│ │ │ ├── favicon_32x32.png
│ │ │ ├── pnpm.svg
│ │ │ ├── stripe.svg
│ │ │ ├── supabase-light.svg
│ │ │ ├── supabase.svg
│ │ │ └── vite.svg
│ ├── setupTests.ts
│ ├── src
│ │ ├── App.test.tsx
│ │ ├── App.tsx
│ │ ├── __forceRebuild__.ts
│ │ ├── components
│ │ │ ├── ai
│ │ │ │ ├── AiChatbox.test.tsx
│ │ │ │ ├── AiChatbox.tsx
│ │ │ │ ├── ChatContextSelector.test.tsx
│ │ │ │ ├── ChatContextSelector.tsx
│ │ │ │ ├── ChatHistoryList.test.tsx
│ │ │ │ ├── ChatHistoryList.tsx
│ │ │ │ ├── ChatItem.test.tsx
│ │ │ │ ├── ChatItem.tsx
│ │ │ │ ├── ChatMessageBubble.test.tsx
│ │ │ │ ├── ChatMessageBubble.tsx
│ │ │ │ ├── ModelSelector.test.tsx
│ │ │ │ ├── ModelSelector.tsx
│ │ │ │ ├── PromptSelector.test.tsx
│ │ │ │ ├── PromptSelector.tsx
│ │ │ │ └── ai.integration.test.tsx
│ │ │ ├── auth
│ │ │ │ ├── AuthenticatedGate.test.tsx
│ │ │ │ ├── AuthenticatedGate.tsx
│ │ │ │ ├── LoginForm.test.tsx
│ │ │ │ ├── LoginForm.tsx
│ │ │ │ ├── ProtectedRoute.test.tsx
│ │ │ │ ├── ProtectedRoute.tsx
│ │ │ │ ├── RegisterForm.test.tsx
│ │ │ │ ├── RegisterForm.tsx
│ │ │ │ └── auth.integration.test.tsx
│ │ │ ├── common
│ │ │ │ ├── AttributionDisplay.test.tsx
│ │ │ │ ├── AttributionDisplay.tsx
│ │ │ │ ├── DropZone.test.tsx
│ │ │ │ ├── DropZone.tsx
│ │ │ │ ├── ErrorBoundary.test.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── FileDataDisplay.test.tsx
│ │ │ │ ├── FileDataDisplay.tsx
│ │ │ │ ├── MarkdownRenderer.test.tsx
│ │ │ │ ├── MarkdownRenderer.tsx
│ │ │ │ ├── PaginationComponent.test.tsx
│ │ │ │ ├── PaginationComponent.tsx
│ │ │ │ ├── TextInputArea.test.tsx
│ │ │ │ └── TextInputArea.tsx
│ │ │ ├── debug
│ │ │ │ ├── PlatformFeatureTester.test.tsx
│ │ │ │ └── PlatformFeatureTester.tsx
│ │ │ ├── demos
│ │ │ │ └── WalletBackupDemo
│ │ │ │ │ ├── ExportMnemonicButton.test.tsx
│ │ │ │ │ ├── ExportMnemonicButton.tsx
│ │ │ │ │ ├── GenerateMnemonicButton.test.tsx
│ │ │ │ │ ├── GenerateMnemonicButton.tsx
│ │ │ │ │ ├── ImportMnemonicButton.test.tsx
│ │ │ │ │ ├── ImportMnemonicButton.tsx
│ │ │ │ │ ├── StatusDisplay.test.tsx
│ │ │ │ │ ├── StatusDisplay.tsx
│ │ │ │ │ ├── WalletBackupDemoCard.test.tsx
│ │ │ │ │ └── WalletBackupDemoCard.tsx
│ │ │ ├── features
│ │ │ │ ├── ConfigFileManager.test.tsx
│ │ │ │ └── ConfigFileManager.tsx
│ │ │ ├── integrations
│ │ │ │ └── ChatwootIntegration.tsx
│ │ │ ├── layout
│ │ │ │ ├── Footer.test.tsx
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── Header.test.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Layout.tsx
│ │ │ │ └── Login.test.tsx
│ │ │ ├── marketing
│ │ │ │ └── Badges.tsx
│ │ │ ├── notifications
│ │ │ │ ├── NotificationCard.tsx
│ │ │ │ └── Notifications.tsx
│ │ │ ├── organizations
│ │ │ │ ├── AdminBadge.tsx
│ │ │ │ ├── CreateOrganizationForm.test.tsx
│ │ │ │ ├── CreateOrganizationForm.tsx
│ │ │ │ ├── CreateOrganizationModal.test.tsx
│ │ │ │ ├── CreateOrganizationModal.tsx
│ │ │ │ ├── DeleteOrganizationDialog.test.tsx
│ │ │ │ ├── DeleteOrganizationDialog.tsx
│ │ │ │ ├── InviteMemberCard.test.tsx
│ │ │ │ ├── InviteMemberCard.tsx
│ │ │ │ ├── MemberListCard.test.tsx
│ │ │ │ ├── MemberListCard.tsx
│ │ │ │ ├── OrganizationChatSettings.test.tsx
│ │ │ │ ├── OrganizationChatSettings.tsx
│ │ │ │ ├── OrganizationDetailsCard.test.tsx
│ │ │ │ ├── OrganizationDetailsCard.tsx
│ │ │ │ ├── OrganizationListCard.test.tsx
│ │ │ │ ├── OrganizationListCard.tsx
│ │ │ │ ├── OrganizationSettingsCard.test.tsx
│ │ │ │ ├── OrganizationSettingsCard.tsx
│ │ │ │ ├── OrganizationSwitcher.test.tsx
│ │ │ │ ├── OrganizationSwitcher.tsx
│ │ │ │ ├── PendingActionsCard.test.tsx
│ │ │ │ └── PendingActionsCard.tsx
│ │ │ ├── profile
│ │ │ │ ├── ProfileEditor.test.tsx
│ │ │ │ ├── ProfileEditor.tsx
│ │ │ │ ├── ProfilePrivacySettingsCard.tsx
│ │ │ │ ├── ProfilePrivacySettingsCard.unit.test.tsx
│ │ │ │ └── profile.integration.test.tsx
│ │ │ ├── routes
│ │ │ │ ├── RootRoute.test.tsx
│ │ │ │ ├── RootRoute.tsx
│ │ │ │ └── TauriOnlyWrapper.tsx
│ │ │ ├── subscription
│ │ │ │ ├── CurrentSubscriptionCard.test.tsx
│ │ │ │ ├── CurrentSubscriptionCard.tsx
│ │ │ │ ├── PlanCard.test.tsx
│ │ │ │ ├── PlanCard.tsx
│ │ │ │ └── Subscription.integration.test.tsx
│ │ │ └── ui
│ │ │ │ ├── SimpleDropdown.tsx
│ │ │ │ ├── accordion.tsx
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── aspect-ratio.tsx
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── calendar.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── carousel.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── context-menu.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── drawer.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── form.tsx
│ │ │ │ ├── hover-card.tsx
│ │ │ │ ├── input-otp.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── menubar.tsx
│ │ │ │ ├── navigation-menu.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── resizable.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── sidebar.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toggle-group.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ └── tooltip.tsx
│ │ ├── config
│ │ │ └── themes.ts
│ │ ├── context
│ │ │ └── theme.context.tsx
│ │ ├── hooks
│ │ │ ├── use-mobile.ts
│ │ │ ├── useAuthSession.test.ts
│ │ │ ├── useCurrentUser.ts
│ │ │ ├── useSubscription.ts
│ │ │ ├── useTheme.test.tsx
│ │ │ └── useTheme.ts
│ │ ├── index.css
│ │ ├── lib
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ ├── mocks
│ │ │ ├── aiStore.mock.ts
│ │ │ ├── authStore.mock.ts
│ │ │ └── organizationStore.mock.ts
│ │ ├── pages
│ │ │ ├── AcceptInvitePage.test.tsx
│ │ │ ├── AcceptInvitePage.tsx
│ │ │ ├── AiChat.test.tsx
│ │ │ ├── AiChat.tsx
│ │ │ ├── Dashboard.test.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── Home.test.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── Login.tsx
│ │ │ ├── Notifications.test.tsx
│ │ │ ├── Notifications.tsx
│ │ │ ├── OrganizationFocusedViewPage.test.tsx
│ │ │ ├── OrganizationFocusedViewPage.tsx
│ │ │ ├── OrganizationHubPage.test.tsx
│ │ │ ├── OrganizationHubPage.tsx
│ │ │ ├── Profile.test.tsx
│ │ │ ├── Profile.tsx
│ │ │ ├── Register.test.tsx
│ │ │ ├── Register.tsx
│ │ │ ├── Subscription.test.tsx
│ │ │ ├── Subscription.tsx
│ │ │ ├── SubscriptionSuccess.test.tsx
│ │ │ └── SubscriptionSuccess.tsx
│ │ ├── routes
│ │ │ └── routes.tsx
│ │ ├── tests
│ │ │ └── setup.ts
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ └── vitest.config.ts
└── windows
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── src-tauri
│ ├── .gitignore
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── build.rs
│ ├── capabilities
│ │ └── default.json
│ ├── crates
│ │ ├── core-crypto
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ │ └── lib.rs
│ │ └── storage-interface
│ │ │ ├── Cargo.toml
│ │ │ └── src
│ │ │ └── lib.rs
│ ├── icons
│ │ ├── 128x128.png
│ │ ├── 128x128@2x.png
│ │ ├── 32x32.png
│ │ ├── Square107x107Logo.png
│ │ ├── Square142x142Logo.png
│ │ ├── Square150x150Logo.png
│ │ ├── Square284x284Logo.png
│ │ ├── Square30x30Logo.png
│ │ ├── Square310x310Logo.png
│ │ ├── Square44x44Logo.png
│ │ ├── Square71x71Logo.png
│ │ ├── Square89x89Logo.png
│ │ ├── StoreLogo.png
│ │ ├── icon.icns
│ │ ├── icon.ico
│ │ └── icon.png
│ ├── src
│ │ ├── capabilities.rs
│ │ ├── crypto_commands.rs
│ │ ├── lib.rs
│ │ ├── main.rs
│ │ └── wallet_commands.rs
│ └── tauri.conf.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── deno.lock
├── docs
├── architecture
│ └── tauri_bridge.md
├── dev_plan.md
├── development
│ └── PlatformCapabilities.md
├── imp_plan.md
├── implementations
│ ├── Complete-Closed-Deprecated-Discarded
│ │ ├── 20250416 Anon Auth Flow.md
│ │ ├── 20250418 Analytics Clients.md
│ │ ├── 20250419 Email Marketing.md
│ │ ├── 20250421 AI Providers Integration.md
│ │ ├── 20250421-markdown-rendering.md
│ │ ├── 20250422_Multitenant.md
│ │ ├── 20250422_Notifications.md
│ │ ├── 20250422_Platform Capabilities.md
│ │ ├── 20250423112244_Supabase_Auth_Fix.md
│ │ ├── 20250502_Merged_Platform_Capabilities.md
│ │ ├── 20250502_Org_Refinement.md
│ │ ├── 20250502_desktop_ui.md
│ │ ├── 20250503_WalletBackupDemo.md
│ │ └── Consolidate Zustand.md
│ ├── Current
│ │ ├── Checklists
│ │ │ ├── Complete
│ │ │ │ ├── 0-Project-Setup-and-Planning.md
│ │ │ │ ├── 1-Core-Backend-and-Data-Model.md
│ │ │ │ ├── 2-State-Management-Integration.md
│ │ │ │ └── 3-UI-Component-Implementation.md
│ │ │ ├── Current
│ │ │ │ ├── 4-Token-Tracking-Audit.md
│ │ │ │ └── desktop-crypto
│ │ │ │ │ ├── 20250504_WalletBackend.md
│ │ │ │ │ ├── NextSteps_After_WalletBackend.md
│ │ │ │ │ ├── documentation
│ │ │ │ │ ├── DEV_PLAN.md
│ │ │ │ │ ├── IMPLEMENTATION_PLAN.md
│ │ │ │ │ ├── STREAMING_SCALABILITY_AND_ORACLE_DESIGN.md
│ │ │ │ │ ├── STRUCTURE.md
│ │ │ │ │ ├── TESTING_PLAN.md
│ │ │ │ │ ├── cryptography.md
│ │ │ │ │ ├── key_management.md
│ │ │ │ │ └── solidity_contract_draft.md
│ │ │ │ │ └── genesis
│ │ │ │ │ ├── 0. always
│ │ │ │ │ └── always.md
│ │ │ │ │ ├── 1. thesis
│ │ │ │ │ ├── seedphrase.md
│ │ │ │ │ ├── thesis.claude.md
│ │ │ │ │ ├── thesis.gemini.md
│ │ │ │ │ └── thesis.openai.md
│ │ │ │ │ ├── 2. antithesis
│ │ │ │ │ ├── antithesis.claude.md
│ │ │ │ │ ├── antithesis.gemini.md
│ │ │ │ │ ├── antithesis.openai.md
│ │ │ │ │ └── seedphrase
│ │ │ │ │ ├── 3. synthesis
│ │ │ │ │ ├── seedphrase.md
│ │ │ │ │ ├── synthesis.claude.md
│ │ │ │ │ ├── synthesis.gemini.md
│ │ │ │ │ └── synthesis.openai.md
│ │ │ │ │ └── 4. implementation
│ │ │ │ │ └── 20250426_crypto_core.md
│ │ │ └── Pending
│ │ │ │ ├── 4.5-Organization-Finder.md
│ │ │ │ ├── 4.6-Multi-User-Chat.md
│ │ │ │ ├── 5-Bug-Fixes-and-Optimizations.md
│ │ │ │ ├── 6-Stretch-Goals.md
│ │ │ │ ├── 7-Testing-and-Refinement.md
│ │ │ │ ├── 8-Documentation-and-Deployment.md
│ │ │ │ └── 9-Post-Implementation.md
│ │ ├── Documentation
│ │ │ ├── 0-2-1-FilePaths.md
│ │ │ └── 0-2-2-ArchitectureAndTypes.md
│ │ ├── fix_pending_invites_display.md
│ │ └── process
│ │ │ ├── 1. hypothesis
│ │ │ ├── ai-chat-enhancements-hypothesis-claude.md
│ │ │ ├── ai-chat-enhancements-hypothesis-gemini.md
│ │ │ ├── ai-chat-enhancements-hypothesis-openai.md
│ │ │ ├── ai-chat-enhancements-hypothesis-seed.md
│ │ │ └── ai-chat-enhancements-hypothesis-user-response.md
│ │ │ ├── 2. antithesis
│ │ │ ├── ai-chat-enhancements-antithesis-claude.md
│ │ │ ├── ai-chat-enhancements-antithesis-gemini.md
│ │ │ ├── ai-chat-enhancements-antithesis-openai.md
│ │ │ ├── ai-chat-enhancements-antithesis-seed.md
│ │ │ └── ai-chat-enhancements-antithesis-user-response.md
│ │ │ ├── 3. synthesis
│ │ │ ├── ai-chat-enhancements-synthesis-claude.md
│ │ │ ├── ai-chat-enhancements-synthesis-gemini.md
│ │ │ ├── ai-chat-enhancements-synthesis-openai.md
│ │ │ ├── ai-chat-enhancements-synthesis-seed.md
│ │ │ └── ai-chat-enhancements-synthesis-user-response.md
│ │ │ └── 4. parenthesis
│ │ │ ├── ai-chat-enhancements-parenthesis-claude.md
│ │ │ ├── ai-chat-enhancements-parenthesis-gemini.md
│ │ │ ├── ai-chat-enhancements-parenthesis-openai.md
│ │ │ └── ai-chat-enhancements-parenthesis-seed.md
│ └── Pending
│ │ ├── 20250422_Refactor_Types.md
│ │ ├── 20250424174735_auth_req_cleanup.md
│ │ ├── 20250430_FixAuthListenerHandling.md
│ │ └── 20250506_Finish_Initial_Testing.md
├── struct.md
└── tests.md
├── eslint.config.js
├── nx.json
├── package.json
├── packages
├── analytics
│ ├── package.json
│ ├── src
│ │ ├── .keep
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── nullAdapter.test.ts
│ │ ├── nullAdapter.ts
│ │ ├── posthogAdapter.test.ts
│ │ └── posthogAdapter.ts
│ └── tsconfig.json
├── api
│ ├── package.json
│ ├── src
│ │ ├── ApiContext.tsx
│ │ ├── ai.api.test.ts
│ │ ├── ai.api.ts
│ │ ├── apiClient.test.ts
│ │ ├── apiClient.ts
│ │ ├── fetch.test.ts
│ │ ├── index.ts
│ │ ├── mocks
│ │ │ ├── ai.api.mock.ts
│ │ │ ├── apiClient.mock.ts
│ │ │ ├── chatHistory.mock.ts
│ │ │ ├── handlers.ts
│ │ │ ├── index.ts
│ │ │ ├── notifications.api.mock.ts
│ │ │ ├── organizations.api.mock.ts
│ │ │ ├── organizations.mock.ts
│ │ │ ├── stripe.mock.ts
│ │ │ └── supabase.mock.ts
│ │ ├── notifications.api.test.ts
│ │ ├── notifications.api.ts
│ │ ├── organizations.api.test.ts
│ │ ├── organizations.api.ts
│ │ ├── setupTests.ts
│ │ ├── stripe.api.test.ts
│ │ ├── stripe.api.ts
│ │ ├── users.api.test.ts
│ │ ├── users.api.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── platform
│ ├── package.json
│ ├── src
│ │ ├── Context.test.tsx
│ │ ├── Context.tsx
│ │ ├── android.test.ts
│ │ ├── android.ts
│ │ ├── events.ts
│ │ ├── index.ts
│ │ ├── ios.test.ts
│ │ ├── ios.ts
│ │ ├── linux.test.ts
│ │ ├── linux.ts
│ │ ├── mac.test.ts
│ │ ├── mac.ts
│ │ ├── setupTests.ts
│ │ ├── tauri.test.ts
│ │ ├── tauri.ts
│ │ ├── web.test.ts
│ │ └── web.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── store
│ ├── package.json
│ ├── src
│ │ ├── aiStore.config.test.ts
│ │ ├── aiStore.context.test.ts
│ │ ├── aiStore.deleteChat.test.ts
│ │ ├── aiStore.details.test.ts
│ │ ├── aiStore.history.test.ts
│ │ ├── aiStore.hydration.test.ts
│ │ ├── aiStore.misc.test.ts
│ │ ├── aiStore.replay.test.ts
│ │ ├── aiStore.rewind.test.ts
│ │ ├── aiStore.selectors.test.ts
│ │ ├── aiStore.selectors.ts
│ │ ├── aiStore.sendMessage.test.ts
│ │ ├── aiStore.startNewChat.test.ts
│ │ ├── aiStore.ts
│ │ ├── authStore.base.test.ts
│ │ ├── authStore.listener.test.ts
│ │ ├── authStore.login.test.ts
│ │ ├── authStore.logout.test.ts
│ │ ├── authStore.profile.test.ts
│ │ ├── authStore.register.test.ts
│ │ ├── authStore.ts
│ │ ├── index.ts
│ │ ├── notificationStore.test.ts
│ │ ├── notificationStore.ts
│ │ ├── organizationStore.selectors.test.ts
│ │ ├── organizationStore.selectors.ts
│ │ ├── organizationStore.settings.test.ts
│ │ ├── organizationStore.test.ts
│ │ ├── organizationStore.ts
│ │ ├── setupTests.ts
│ │ ├── subscriptionStore.selectors.test.ts
│ │ ├── subscriptionStore.selectors.ts
│ │ ├── subscriptionStore.test.ts
│ │ └── subscriptionStore.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── types
│ ├── package.json
│ ├── src
│ │ ├── ai.types.ts
│ │ ├── analytics.types.ts
│ │ ├── api.types.ts
│ │ ├── auth.types.ts
│ │ ├── email.types.ts
│ │ ├── index.ts
│ │ ├── logger.types.ts
│ │ ├── navigation.types.ts
│ │ ├── notification.types.ts
│ │ ├── organizations.types.ts
│ │ ├── platform.types.ts
│ │ ├── route.types.ts
│ │ ├── subscription.types.ts
│ │ ├── supabase.types.ts
│ │ ├── theme.types.ts
│ │ └── vite-env.d.ts
│ └── tsconfig.json
└── utils
│ ├── package.json
│ ├── src
│ ├── ai-parsers.ts
│ ├── index.ts
│ ├── logger.test.ts
│ ├── logger.ts
│ ├── platform.ts
│ └── stringUtils.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
├── build.js
└── update-imports.js
├── supabase
├── .gitignore
├── config.toml
├── deno.json
├── deno.lock
├── functions
│ ├── .gitignore
│ ├── README.md
│ ├── _shared
│ │ ├── ai_service
│ │ │ ├── anthropic_adapter.test.ts
│ │ │ ├── anthropic_adapter.ts
│ │ │ ├── factory.test.ts
│ │ │ ├── factory.ts
│ │ │ ├── google_adapter.test.ts
│ │ │ ├── google_adapter.ts
│ │ │ ├── openai_adapter.test.ts
│ │ │ └── openai_adapter.ts
│ │ ├── auth.test.ts
│ │ ├── auth.ts
│ │ ├── cors-headers.test.ts
│ │ ├── cors-headers.ts
│ │ ├── email_service
│ │ │ ├── dummy_service.ts
│ │ │ ├── factory.test.ts
│ │ │ ├── factory.ts
│ │ │ ├── kit_service.test.ts
│ │ │ ├── kit_service.ts
│ │ │ ├── no_op_service.test.ts
│ │ │ └── no_op_service.ts
│ │ ├── logger.ts
│ │ ├── stripe-client.test.ts
│ │ ├── stripe-client.ts
│ │ ├── supabase.mock.ts
│ │ └── types.ts
│ ├── ai-providers
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── api-subscriptions
│ │ ├── api-subscriptions.integration.test.ts
│ │ ├── handlers
│ │ │ ├── billing-portal.test.ts
│ │ │ ├── billing-portal.ts
│ │ │ ├── checkout.test.ts
│ │ │ ├── checkout.ts
│ │ │ ├── current.test.ts
│ │ │ ├── current.ts
│ │ │ ├── plans.test.ts
│ │ │ ├── plans.ts
│ │ │ ├── subscription.test.ts
│ │ │ ├── subscription.ts
│ │ │ ├── usage.test.ts
│ │ │ └── usage.ts
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── chat-details
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── test
│ │ │ └── chat-details.integration.test.ts
│ ├── chat-history
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── test
│ │ │ └── chat-history.integration.test.ts
│ ├── chat
│ │ ├── _server.ts
│ │ ├── dependencies.ts
│ │ ├── index.rewind.test.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── test
│ │ │ └── chat.integration.test.ts
│ ├── deno.jsonc
│ ├── deno.lock
│ ├── login
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── login.integration.test.ts
│ ├── logout
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── logout.integration.test.ts
│ ├── me
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── me.integration.test.ts
│ ├── notifications
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── on-user-created
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── organizations
│ │ ├── create.test.ts
│ │ ├── create.ts
│ │ ├── details.test.ts
│ │ ├── details.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── invites.test.ts
│ │ ├── invites.ts
│ │ ├── list.test.ts
│ │ ├── list.ts
│ │ ├── members.test.ts
│ │ ├── members.ts
│ │ ├── requests.test.ts
│ │ └── requests.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── ping
│ │ ├── index.ts
│ │ └── ping.integration.test.ts
│ ├── profile
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── profile.integration.test.ts
│ ├── register
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── register.integration.test.ts
│ ├── reset-password
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── reset-password.integration.test.ts
│ ├── session
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── session.integration.test.ts
│ ├── stripe-webhook
│ │ ├── handlers
│ │ │ ├── checkout-session.test.ts
│ │ │ ├── checkout-session.ts
│ │ │ ├── invoice.test.ts
│ │ │ ├── invoice.ts
│ │ │ ├── price.test.ts
│ │ │ ├── price.ts
│ │ │ ├── product.test.ts
│ │ │ ├── product.ts
│ │ │ ├── subscription.test.ts
│ │ │ └── subscription.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── services
│ │ │ ├── price_webhook_service.ts
│ │ │ └── product_webhook_service.ts
│ │ └── stripe-webhook.integration.test.ts
│ ├── sync-ai-models
│ │ ├── anthropic_sync.test.ts
│ │ ├── anthropic_sync.ts
│ │ ├── google_sync.test.ts
│ │ ├── google_sync.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── openai_sync.test.ts
│ │ └── openai_sync.ts
│ ├── sync-stripe-plans
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── services
│ │ │ └── sync_plans_service.ts
│ ├── system-prompts
│ │ └── index.ts
│ ├── tools
│ │ └── sync-envs.js
│ ├── tsconfig.json
│ └── types_db.ts
├── import_map.json
├── integration_tests
│ ├── me.integration.test.ts
│ └── rls
│ │ ├── delete_chat_function.test.ts
│ │ ├── rls_chat_messages.test.ts
│ │ ├── rls_chats.test.ts
│ │ └── rls_test_helpers.ts
├── migrations
│ └── 20250512205139_remote_schema.sql
├── package-lock.json
├── package.json
├── project.json
├── scripts
│ ├── apply_email_sync_trigger.ts
│ └── sync-supabase-shared-types.mjs
├── seed.sql
├── supabase.test.output.md
└── tests
│ └── database
│ ├── chat_restrictions_policy_test.sql
│ ├── chat_seed_file.sql
│ ├── chat_seed_markdown.sql
│ └── org_rls_policy_test_test.sql
├── tsconfig.base.json
├── tsconfig.json
├── tsconfig.node.json
└── vitest.config.ts
/.env.example:
--------------------------------------------------------------------------------
1 | # Supabase Keys
2 | VITE_SUPABASE_ANON_KEY=
3 | VITE_SUPABASE_URL=https://[YOUR SUPABASE PROJECT ID].supabase.co/
4 | VITE_SUPABASE_SERVICE_ROLE_KEY=
5 | SUPABASE_DATABASE_PASSWORD= ## You'll need this for the email sync trigger script
6 |
7 | ## AI Keys
8 | OPENAI_API_KEY=
9 | ANTHROPIC_API_KEY=
10 | GOOGLE_API_KEY=
11 | PERPLEXITY_API_KEY=
12 | DEEPSEEK_API_KEY=
13 | GROQ_API_KEY=
14 |
15 | ## Stripe live
16 | STRIPE_SECRET_LIVE_KEY=
17 | STRIPE_LIVE_WEBHOOK_SECRET=
18 |
19 | ## Stripe test
20 | VITE_STRIPE_TEST_MODE=true ## Set to "false" when you want Stripe to actually run transactions
21 | STRIPE_SECRET_TEST_KEY=
22 | STRIPE_TEST_WEBHOOK_SECRET=
23 |
24 | ## Analytics
25 | VITE_GA_MEASUREMENT_ID= ## your Google Analytics ID (e.g., G-XXXXXXXXXX)
26 |
27 | ## Customer Support
28 | VITE_CHATWOOT_BASE_URL= ## e.g., https://app.chatwoot.com
29 | VITE_CHATWOOT_WEBSITE_TOKEN= ## Your Chatwoot Website Inbox Token
30 |
31 | ## Email Marketing
32 | EMAIL_MARKETING_PROVIDER="none" # Options: "kit", "none"
33 | EMAIL_MARKETING_BASE_URL="https://api.convertkit.com" # Base URL for Kit API (v3)
34 | EMAIL_MARKETING_API_KEY="" # Required for Kit
35 | EMAIL_MARKETING_TAG_ID="" # Required for Kit (Tag to add users to)
36 | EMAIL_MARKETING_CUSTOM_USER_ID_FIELD="" # Required for Kit (e.g., "cf_supabase_user_id")
37 | EMAIL_MARKETING_CUSTOM_CREATED_AT_FIELD="" # Required for Kit (e.g., "cf_supabase_created_at")
38 |
39 | # Analytics (Optional)
40 | # Set VITE_ANALYTICS_PROVIDER to 'posthog' (or 'mixpanel' in future) to enable.
41 | # If provider is set, the corresponding API key is required.
42 | VITE_ANALYTICS_PROVIDER="none" # Options: "none", "posthog"
43 | VITE_POSTHOG_KEY="" # Required if provider is "posthog"
44 | VITE_POSTHOG_HOST="https://app.posthog.com" # Optional: Defaults to PostHog cloud
--------------------------------------------------------------------------------
/.github/workflows/build-test-lint.yml:
--------------------------------------------------------------------------------
1 | name: Build, Lint, and Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - develop # Add other main branches if applicable
8 | - 'feature/**'
9 | pull_request:
10 | branches:
11 | - main
12 | - develop # Add other main branches if applicable
13 |
14 | jobs:
15 | build_lint_test:
16 | name: Build, Lint & Test
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v4
22 |
23 | - name: Setup pnpm
24 | uses: pnpm/action-setup@v4
25 | with:
26 | version: 9 # Specify your pnpm version if needed
27 |
28 | - name: Setup Node.js
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: '20' # Specify your Node.js version
32 | cache: 'pnpm'
33 |
34 | - name: Install dependencies
35 | run: pnpm install
36 |
37 | - name: Lint Packages Individually
38 | run: pnpm -r lint
39 |
40 | - name: Build
41 | # Adjust if your build command is different or needs filtering
42 | run: pnpm build
43 |
44 | - name: Test
45 | run: pnpm test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # env files
27 | .env
28 |
29 | tsconfig.tsbuildinfo
30 | .nx/cache
31 | .nx/workspace-data
32 | vite.config.*.timestamp*
33 | vitest.config.*.timestamp*
34 |
35 | # Rust
36 | target/
37 |
38 |
39 | # Rust build artifacts
40 | /target/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semiColons": false,
5 | "bracketSpacing": true,
6 | "proseWrap": "preserve",
7 | "semi": false,
8 | "singleQuote": true,
9 | "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
10 | "importOrderSeparation": true,
11 | "importOrderSortSpecifiers": true
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["denoland.vscode-deno"]
3 | }
4 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2" # Explicitly set resolver version
3 | members = [
4 | "apps/windows/src-tauri", # Path to the Tauri app crate
5 | "apps/windows/src-tauri/crates/core-crypto", # UPDATED Path to the new crypto crate location
6 | "apps/windows/src-tauri/crates/storage-interface" # ADDED storage-interface crate
7 | ]
8 |
9 | # Optional: Define shared dependencies or profiles
10 | [profile.release]
11 | lto = true
12 | codegen-units = 1
13 | panic = 'abort'
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | VITE_SUPABASE_ANON_KEY=
2 | VITE_SUPABASE_URL=https://[YOUR SUPABASE PROJECT ID].supabase.co/
3 | VITE_APP_TITLE=YOUR APP NAME
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import globals from 'globals';
3 | import reactHooks from 'eslint-plugin-react-hooks';
4 | import reactRefresh from 'eslint-plugin-react-refresh';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | }
28 | );
29 |
--------------------------------------------------------------------------------
/apps/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %VITE_APP_TITLE%
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/web/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/apps/web/public/logos/app_icon_240x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/web/public/logos/app_icon_240x240.png
--------------------------------------------------------------------------------
/apps/web/public/logos/favicon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/web/public/logos/favicon_32x32.png
--------------------------------------------------------------------------------
/apps/web/public/logos/pnpm.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/apps/web/public/logos/stripe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/vite.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/apps/web/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest';
2 | import { cleanup } from '@testing-library/react';
3 | import { afterEach, vi } from 'vitest';
4 | import { setupMockServer } from './src/test-utils/mocks/api';
5 |
6 | // Setup MSW
7 | setupMockServer();
8 |
9 | // Cleanup after each test
10 | afterEach(() => {
11 | cleanup();
12 | vi.clearAllMocks();
13 | });
14 |
15 | // Mock window.matchMedia
16 | Object.defineProperty(window, 'matchMedia', {
17 | writable: true,
18 | value: vi.fn().mockImplementation(query => ({
19 | matches: false,
20 | media: query,
21 | onchange: null,
22 | addListener: vi.fn(),
23 | removeListener: vi.fn(),
24 | addEventListener: vi.fn(),
25 | removeEventListener: vi.fn(),
26 | dispatchEvent: vi.fn(),
27 | })),
28 | });
--------------------------------------------------------------------------------
/apps/web/src/__forceRebuild__.ts:
--------------------------------------------------------------------------------
1 | // Change this string when you're losing your mind
2 | export const forceRebuild = 'v1';
--------------------------------------------------------------------------------
/apps/web/src/components/auth/AuthenticatedGate.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAuthStore } from '@paynless/store';
3 |
4 | // Placeholder for a loading component
5 | const DefaultLoadingFallback = () => (
6 |
7 |
8 | );
9 |
10 | interface AuthenticatedGateProps {
11 | children: React.ReactNode;
12 | loadingFallback?: React.ReactNode;
13 | unauthenticatedFallback?: React.ReactNode | null;
14 | }
15 |
16 | export function AuthenticatedGate({
17 | children,
18 | loadingFallback = ,
19 | unauthenticatedFallback = null,
20 | }: AuthenticatedGateProps) {
21 |
22 | const { user, isLoading } = useAuthStore(state => ({
23 | user: state.user,
24 | isLoading: state.isLoading,
25 | }));
26 |
27 | if (isLoading) {
28 | // Still determining auth state
29 | return <>{loadingFallback}>;
30 | }
31 |
32 | if (!user) {
33 | // Not authenticated, render null. Router handles showing public pages.
34 | return <>{unauthenticatedFallback}>;
35 | }
36 |
37 | // Authenticated and auth check complete, render the protected children
38 | return <>{children}>;
39 | }
--------------------------------------------------------------------------------
/apps/web/src/components/auth/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { Navigate, useLocation } from 'react-router-dom';
3 | import { useAuthStore } from '@paynless/store';
4 | import { UserRole } from '@paynless/types';
5 | import { CreateOrganizationModal } from '../organizations/CreateOrganizationModal';
6 |
7 | interface ProtectedRouteProps {
8 | children: ReactNode;
9 | allowedRoles?: UserRole[];
10 | }
11 |
12 | export function ProtectedRoute({ children, allowedRoles }: ProtectedRouteProps) {
13 | const { user, isLoading } = useAuthStore();
14 | const location = useLocation();
15 |
16 | if (isLoading) {
17 | return (
18 |
21 | );
22 | }
23 |
24 | // Don't redirect if we're on the register or login pages
25 | if (!user && !location.pathname.match(/^\/(register|login)$/)) {
26 | return ;
27 | }
28 |
29 | if (allowedRoles && user && !allowedRoles.includes(user.role as UserRole)) {
30 | return ;
31 | }
32 |
33 | // Render children AND the modal for authenticated users
34 | return (
35 | <>
36 | {children}
37 |
38 | >
39 | );
40 | }
--------------------------------------------------------------------------------
/apps/web/src/components/common/DropZone.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, DragEvent, useEffect } from 'react';
2 | import { cn } from '@/lib/utils'; // Assuming Shadcn's utility for class merging
3 | import { UploadCloud } from 'lucide-react';
4 | import { platformEventEmitter } from '@paynless/platform'; // Import emitter
5 |
6 | interface DropZoneProps {
7 | className?: string;
8 | children?: React.ReactNode; // Allow passing content inside
9 | activeText?: string;
10 | }
11 |
12 | export const DropZone: React.FC = ({
13 | className,
14 | children,
15 | activeText = 'Drop file here to import',
16 | }) => {
17 | const [isHovering, setIsHovering] = useState(false);
18 |
19 | // ADDED: useEffect for platform event listener
20 | useEffect(() => {
21 | const handleHover = () => setIsHovering(true);
22 | const handleCancel = () => setIsHovering(false);
23 |
24 | console.log('[DropZone] Subscribing to file drag events');
25 | platformEventEmitter.on('file-drag-hover', handleHover);
26 | platformEventEmitter.on('file-drag-cancel', handleCancel);
27 |
28 | // Cleanup
29 | return () => {
30 | console.log('[DropZone] Unsubscribing from file drag events');
31 | platformEventEmitter.off('file-drag-hover', handleHover);
32 | platformEventEmitter.off('file-drag-cancel', handleCancel);
33 | };
34 | }, []); // Run only once on mount
35 |
36 | return (
37 |
45 | {isHovering ? (
46 |
47 |
48 |
{activeText}
49 |
50 | ) : (
51 | children || (
52 |
53 |
54 |
Drag and drop file here
55 |
(or use the import button)
56 |
57 | )
58 | )}
59 |
60 | );
61 | };
--------------------------------------------------------------------------------
/apps/web/src/components/common/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { Component, ErrorInfo, ReactNode } from 'react';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | fallback?: ReactNode; // Optional custom fallback component
8 | }
9 |
10 | interface State {
11 | hasError: boolean;
12 | error?: Error;
13 | }
14 |
15 | class ErrorBoundary extends Component {
16 | override state: State = {
17 | hasError: false,
18 | };
19 |
20 | static getDerivedStateFromError(error: Error): State {
21 | // Update state so the next render will show the fallback UI.
22 | return { hasError: true, error };
23 | }
24 |
25 | public override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26 | // Log the error assuming logger takes message and context/error object
27 | console.error('ErrorBoundary caught an error:', { error, errorInfo });
28 | }
29 |
30 | public override render() {
31 | if (this.state.hasError) {
32 | if (this.props.fallback) {
33 | return this.props.fallback;
34 | }
35 | // Default fallback UI
36 | return (
37 |
38 |
Oops, something went wrong.
39 |
40 | We encountered an error. Please try refreshing the page or contact support if the problem persists.
41 |
42 | {this.state.error && (
43 |
44 | {this.state.error.message}
45 |
46 | )}
47 |
48 | );
49 | }
50 |
51 | return this.props.children;
52 | }
53 | }
54 |
55 | export default ErrorBoundary;
--------------------------------------------------------------------------------
/apps/web/src/components/common/FileDataDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface FileDataDisplayProps {
4 | content: string;
5 | title?: string;
6 | }
7 |
8 | /**
9 | * A component to display string content, typically loaded from a file,
10 | * within a formatted, read-only container.
11 | */
12 | export const FileDataDisplay: React.FC = ({ content, title }) => {
13 | return (
14 |
15 | {title && (
16 |
17 | {title}
18 |
19 | )}
20 |
21 | {content}
22 |
23 |
24 | );
25 | };
--------------------------------------------------------------------------------
/apps/web/src/components/common/MarkdownRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import remarkGfm from 'remark-gfm';
4 | import { Prism } from 'react-syntax-highlighter';
5 | import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
6 |
7 | export interface MarkdownRendererProps {
8 | content: string;
9 | className?: string;
10 | }
11 |
12 | export const MarkdownRenderer: React.FC = ({ content, className }) => {
13 | // Basic styling for markdown elements using Tailwind prose classes
14 | const markdownStyles = `
15 | prose
16 | dark:prose-invert
17 | prose-sm
18 | max-w-none
19 | prose-headings:font-semibold
20 | prose-a:text-blue-600 prose-a:hover:underline
21 | prose-code:bg-gray-200 prose-code:dark:bg-gray-800 prose-code:p-1 prose-code:rounded prose-code:text-sm
22 | prose-pre:bg-gray-200 prose-pre:dark:bg-gray-800 prose-pre:p-2 prose-pre:rounded prose-pre:overflow-x-auto
23 | prose-blockquote:border-l-4 prose-blockquote:pl-4 prose-blockquote:italic
24 | `;
25 |
26 | return (
27 |
28 |
& { inline?: boolean; children?: React.ReactNode }) {
32 | const match = /language-(\w+)/.exec(className || '');
33 | if (inline || !className) {
34 | return (
35 |
36 | {children}
37 |
38 | );
39 | }
40 | return (
41 |
42 |
46 | {String(children).replace(/\n$/, '')}
47 |
48 |
49 | );
50 | },
51 | }}
52 | >
53 | {content}
54 |
55 |
56 | );
57 | };
--------------------------------------------------------------------------------
/apps/web/src/components/common/TextInputArea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Textarea } from '@/components/ui/textarea';
3 | import { Label } from '@/components/ui/label';
4 |
5 | interface TextInputAreaProps {
6 | value: string;
7 | onChange: (value: string) => void;
8 | disabled?: boolean;
9 | label: string;
10 | placeholder?: string;
11 | id?: string;
12 | rows?: number;
13 | dataTestId?: string;
14 | }
15 |
16 | /**
17 | * A reusable textarea input component with a label.
18 | * (Refactored from MnemonicInputArea)
19 | */
20 | export const TextInputArea: React.FC = ({
21 | value,
22 | onChange,
23 | disabled = false,
24 | label,
25 | placeholder,
26 | id = 'textInputArea',
27 | rows = 4,
28 | dataTestId
29 | }) => {
30 | // Handler to extract value from event and pass to prop function
31 | const handleChange = (event: React.ChangeEvent) => {
32 | onChange(event.target.value);
33 | };
34 |
35 | return (
36 |
37 |
38 |
48 |
49 | );
50 | };
--------------------------------------------------------------------------------
/apps/web/src/components/demos/WalletBackupDemo/GenerateMnemonicButton.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 | import { describe, it, expect, vi } from 'vitest';
3 | import { GenerateMnemonicButton } from './GenerateMnemonicButton'; // Assuming this path
4 |
5 | describe('GenerateMnemonicButton Component', () => {
6 | it('should render a button with text "Generate Mnemonic"', () => {
7 | render( {}} disabled={false} />);
8 | expect(screen.getByRole('button', { name: /generate mnemonic/i })).toBeInTheDocument();
9 | });
10 |
11 | it('should call onGenerate prop when clicked and not disabled', () => {
12 | const handleGenerate = vi.fn();
13 | render();
14 | const button = screen.getByRole('button', { name: /generate mnemonic/i });
15 |
16 | fireEvent.click(button);
17 |
18 | expect(handleGenerate).toHaveBeenCalledTimes(1);
19 | });
20 |
21 | it('should NOT call onGenerate prop when clicked and disabled', () => {
22 | const handleGenerate = vi.fn();
23 | render();
24 | const button = screen.getByRole('button', { name: /generate mnemonic/i });
25 |
26 | fireEvent.click(button);
27 |
28 | expect(handleGenerate).not.toHaveBeenCalled();
29 | });
30 |
31 | it('should apply the disabled attribute when disabled prop is true', () => {
32 | render( {}} disabled={true} />);
33 | const button = screen.getByRole('button', { name: /generate mnemonic/i });
34 | expect(button).toBeDisabled();
35 | });
36 |
37 | it('should NOT apply the disabled attribute when disabled prop is false', () => {
38 | render( {}} disabled={false} />);
39 | const button = screen.getByRole('button', { name: /generate mnemonic/i });
40 | expect(button).not.toBeDisabled();
41 | });
42 | });
--------------------------------------------------------------------------------
/apps/web/src/components/demos/WalletBackupDemo/GenerateMnemonicButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@/components/ui/button';
3 |
4 | interface GenerateMnemonicButtonProps {
5 | onGenerate: () => void; // Simple callback, no args needed from button
6 | disabled: boolean;
7 | }
8 |
9 | export const GenerateMnemonicButton: React.FC = ({
10 | onGenerate,
11 | disabled,
12 | }) => {
13 | return (
14 |
17 | );
18 | };
--------------------------------------------------------------------------------
/apps/web/src/components/demos/WalletBackupDemo/StatusDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Alert,
4 | AlertDescription,
5 | AlertTitle,
6 | } from "@/components/ui/alert"
7 | import { Info, CheckCircle, AlertCircle } from 'lucide-react';
8 |
9 | export interface StatusDisplayProps {
10 | message: string | null;
11 | variant: 'info' | 'success' | 'error';
12 | }
13 |
14 | const statusConfig = {
15 | info: {
16 | Icon: Info,
17 | title: 'Information',
18 | alertVariant: 'default' as const, // Map to Shadcn Alert variant
19 | },
20 | success: {
21 | Icon: CheckCircle,
22 | title: 'Success',
23 | alertVariant: 'default' as const,
24 | },
25 | error: {
26 | Icon: AlertCircle,
27 | title: 'Error',
28 | alertVariant: 'destructive' as const,
29 | },
30 | };
31 |
32 | export const StatusDisplay: React.FC = ({
33 | message,
34 | variant,
35 | }) => {
36 | const { Icon, title, alertVariant } = statusConfig[variant];
37 |
38 | return (
39 |
40 |
41 | {title}
42 | {message && {message}}
43 |
44 | );
45 | };
--------------------------------------------------------------------------------
/apps/web/src/components/layout/Footer.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import { describe, it, expect } from 'vitest';
4 | import { Footer } from './Footer';
5 |
6 | // Basic test suite for the Footer component
7 | describe('Footer Component', () => {
8 | // Helper function to render the Footer within a Router context
9 | const renderFooter = () => {
10 | return render(
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | it('should render without crashing', () => {
18 | renderFooter();
19 | // Check if the footer element itself is rendered
20 | expect(screen.getByRole('contentinfo')).toBeInTheDocument();
21 | });
22 |
23 | it('should display the copyright notice', () => {
24 | renderFooter();
25 | // Find the paragraph containing the copyright notice
26 | const copyrightParagraph = screen.getByText((content, element) => {
27 | // Check if the element is a paragraph and contains the copyright symbol
28 | return element?.tagName.toLowerCase() === 'p' && content.includes('©');
29 | });
30 | expect(copyrightParagraph).toBeInTheDocument();
31 | // Check the full text content using a regex to match the dynamic year and structure
32 | const currentYear = new Date().getFullYear();
33 | // Simple regex, removing unnecessary escapes
34 | expect(copyrightParagraph.textContent).toMatch(`© ${currentYear} Paynless Framework. All rights reserved.`);
35 | });
36 |
37 | it('should display navigation links', () => {
38 | renderFooter();
39 | expect(screen.getByRole('link', { name: /privacy policy/i })).toBeInTheDocument();
40 | expect(screen.getByRole('link', { name: /terms of service/i })).toBeInTheDocument();
41 | expect(screen.getByRole('link', { name: /contact us/i })).toBeInTheDocument();
42 | });
43 |
44 | it('should have correct href attributes for links', () => {
45 | renderFooter();
46 | expect(screen.getByRole('link', { name: /privacy policy/i })).toHaveAttribute('href', '/privacy');
47 | expect(screen.getByRole('link', { name: /terms of service/i })).toHaveAttribute('href', '/terms');
48 | expect(screen.getByRole('link', { name: /contact us/i })).toHaveAttribute('href', '/contact');
49 | });
50 |
51 | });
--------------------------------------------------------------------------------
/apps/web/src/components/layout/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | export function Footer() {
4 | const year = new Date().getFullYear();
5 |
6 | return (
7 |
33 | );
34 | }
--------------------------------------------------------------------------------
/apps/web/src/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Header } from './Header'
3 | import { Footer } from './Footer'
4 | //import { Badges } from '../marketing/Badges'
5 |
6 | interface LayoutProps {
7 | children: ReactNode
8 | }
9 |
10 | export function Layout({ children }: LayoutProps) {
11 | return (
12 |
13 |
14 | {children}
15 |
16 | {/* */}
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/components/marketing/Badges.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '../../hooks/useTheme';
2 |
3 | export function Badges() {
4 | const { colorMode } = useTheme();
5 |
6 | const badgeTheme = colorMode === 'dark' ? 'dark' : 'light';
7 |
8 | const imageUrlPH = `https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=956609&theme=${badgeTheme}&t=${Date.now()}`;
9 |
10 | return (
11 |
28 | );
29 | }
--------------------------------------------------------------------------------
/apps/web/src/components/organizations/AdminBadge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Badge } from "@/components/ui/badge";
3 |
4 | export const AdminBadge: React.FC = () => {
5 | return (
6 | Admin
7 | );
8 | };
--------------------------------------------------------------------------------
/apps/web/src/components/organizations/CreateOrganizationModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogDescription,
8 | // DialogFooter, // No longer needed here, handled by form
9 | // DialogClose, // No longer needed here, handled by form
10 | } from '@/components/ui/dialog'; // Assuming shadcn ui path alias
11 | import { useOrganizationStore } from '@paynless/store';
12 | // Import the form component
13 | import { CreateOrganizationForm } from './CreateOrganizationForm';
14 | // import { Button } from '@/components/ui/button'; // No longer needed here
15 |
16 | export const CreateOrganizationModal: React.FC = () => {
17 | const isCreateModalOpen = useOrganizationStore((state) => state.isCreateModalOpen);
18 | const closeCreateModal = useOrganizationStore((state) => state.closeCreateModal);
19 |
20 | const handleOpenChange = (isOpen: boolean) => {
21 | if (!isOpen) {
22 | // Reset potentially stuck form state if modal is closed externally
23 | // Note: The form itself also calls close on cancel/success
24 | closeCreateModal();
25 | }
26 | // We don't handle opening here, it's triggered by openCreateModal
27 | };
28 |
29 | return (
30 |
45 | );
46 | };
--------------------------------------------------------------------------------
/apps/web/src/components/organizations/OrganizationDetailsCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useOrganizationStore } from '@paynless/store';
5 | import {
6 | Card, CardHeader, CardTitle, CardContent
7 | } from "@/components/ui/card";
8 | import { Skeleton } from "@/components/ui/skeleton";
9 |
10 | export const OrganizationDetailsCard: React.FC = () => {
11 | const {
12 | currentOrganizationId,
13 | currentOrganizationDetails,
14 | isLoading,
15 | } = useOrganizationStore();
16 |
17 | const isLoadingDetails = isLoading && (!currentOrganizationDetails || currentOrganizationId !== currentOrganizationDetails.id);
18 |
19 | return (
20 |
21 |
22 | Organization Details
23 |
24 |
25 | {isLoadingDetails ? (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | ) : currentOrganizationDetails ? (
41 |
42 |
Name: {currentOrganizationDetails.name}
43 |
Visibility: {currentOrganizationDetails.visibility}
44 |
Created:
45 | {new Date(currentOrganizationDetails.created_at).toLocaleDateString()}
46 |
47 | {/* Add other details as needed */}
48 |
49 | ) : (
50 | No organization selected or details unavailable.
51 | )}
52 |
53 |
54 | );
55 | };
--------------------------------------------------------------------------------
/apps/web/src/components/routes/RootRoute.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import { NavigateInjector } from '../../App';
3 | import { Layout } from '../layout/Layout';
4 |
5 | // Root route component that handles layout and renders nested routes
6 | export function RootRoute() {
7 | // Render the main layout and the Outlet within it.
8 | // NavigateInjector is rendered outside the Layout,
9 | // but it might be better inside if it needs layout context (unlikely).
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 | >
17 | );
18 | // OLD Logic: Always render HomePage for the root route
19 | // return ;
20 | }
--------------------------------------------------------------------------------
/apps/web/src/components/routes/TauriOnlyWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet, Navigate } from 'react-router-dom';
3 | import { usePlatform } from '@paynless/platform';
4 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
5 | import { Info } from 'lucide-react';
6 | import { Skeleton } from '@/components/ui/skeleton';
7 |
8 | // Wrapper component to restrict access to routes intended only for the Tauri environment
9 | export const TauriOnlyWrapper: React.FC = () => {
10 | const { platformCapabilities, isLoadingCapabilities, capabilityError } = usePlatform();
11 |
12 | if (isLoadingCapabilities) {
13 | // Optional: Show a generic loading skeleton while checking platform
14 | return (
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | if (capabilityError) {
23 | // Optional: Show an error if platform detection failed
24 | return (
25 |
26 |
27 | Platform Detection Error
28 |
29 | Could not determine the application platform: {capabilityError.message}
30 |
31 |
32 | );
33 | }
34 |
35 | // If capabilities are loaded and platform is Tauri, render the nested routes
36 | if (platformCapabilities?.platform === 'tauri') {
37 | return ;
38 | }
39 |
40 | // Otherwise, redirect to home page (or show an access denied message)
41 | // Redirecting is often cleaner UX than showing a dead page.
42 | return ;
43 |
44 | /* Alternative: Show message instead of redirecting
45 | return (
46 |
47 |
48 | Desktop App Required
49 |
50 | This feature is only available in the desktop application.
51 |
52 |
53 | );
54 | */
55 | };
--------------------------------------------------------------------------------
/apps/web/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDownIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Accordion({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function AccordionItem({
14 | className,
15 | ...props
16 | }: React.ComponentProps) {
17 | return (
18 |
23 | )
24 | }
25 |
26 | function AccordionTrigger({
27 | className,
28 | children,
29 | ...props
30 | }: React.ComponentProps) {
31 | return (
32 |
33 | svg]:rotate-180",
37 | className
38 | )}
39 | {...props}
40 | >
41 | {children}
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | function AccordionContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
59 | {children}
60 |
61 | )
62 | }
63 |
64 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
65 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-card text-card-foreground",
12 | destructive:
13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | function Alert({
23 | className,
24 | variant,
25 | ...props
26 | }: React.ComponentProps<"div"> & VariantProps) {
27 | return (
28 |
34 | )
35 | }
36 |
37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38 | return (
39 |
47 | )
48 | }
49 |
50 | function AlertDescription({
51 | className,
52 | ...props
53 | }: React.ComponentProps<"div">) {
54 | return (
55 |
63 | )
64 | }
65 |
66 | export { Alert, AlertTitle, AlertDescription }
67 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | function AspectRatio({
4 | ...props
5 | }: React.ComponentProps) {
6 | return
7 | }
8 |
9 | export { AspectRatio }
10 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-background shadow-xs hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
15 | outline:
16 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
19 | ghost:
20 | 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | },
23 | size: {
24 | default: 'h-9 px-4 py-2 has-[>svg]:px-3',
25 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
26 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
27 | icon: 'size-9',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | }
35 | )
36 |
37 | function Button({
38 | className,
39 | variant,
40 | size,
41 | asChild = false,
42 | ...props
43 | }: React.ComponentProps<'button'> &
44 | VariantProps & {
45 | asChild?: boolean
46 | }) {
47 | const Comp = asChild ? Slot : 'button'
48 |
49 | return (
50 |
55 | )
56 | }
57 |
58 | Button.displayName = "Button"
59 |
60 | export { Button, buttonVariants }
61 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { CheckIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Checkbox({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | function Collapsible({
4 | ...props
5 | }: React.ComponentProps) {
6 | return
7 | }
8 |
9 | function CollapsibleTrigger({
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
17 | )
18 | }
19 |
20 | function CollapsibleContent({
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
28 | )
29 | }
30 |
31 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function HoverCard({
7 | ...props
8 | }: React.ComponentProps) {
9 | return
10 | }
11 |
12 | function HoverCardTrigger({
13 | ...props
14 | }: React.ComponentProps) {
15 | return (
16 |
17 | )
18 | }
19 |
20 | function HoverCardContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | export { HoverCard, HoverCardTrigger, HoverCardContent }
43 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
23 | )
24 | }
25 | )
26 | Input.displayName = "Input"
27 |
28 | export { Input }
29 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | function Label({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | )
20 | }
21 |
22 | export { Label }
23 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Progress({
7 | className,
8 | value,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 |
25 |
26 | )
27 | }
28 |
29 | export { Progress }
30 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function RadioGroup({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 | )
20 | }
21 |
22 | function RadioGroupItem({
23 | className,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
35 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export { RadioGroup, RadioGroupItem }
46 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { GripVerticalIcon } from "lucide-react"
3 | import * as ResizablePrimitive from "react-resizable-panels"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function ResizablePanelGroup({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 | )
21 | }
22 |
23 | function ResizablePanel({
24 | ...props
25 | }: React.ComponentProps) {
26 | return
27 | }
28 |
29 | function ResizableHandle({
30 | withHandle,
31 | className,
32 | ...props
33 | }: React.ComponentProps & {
34 | withHandle?: boolean
35 | }) {
36 | return (
37 | div]:rotate-90",
41 | className
42 | )}
43 | {...props}
44 | >
45 | {withHandle && (
46 |
47 |
48 |
49 | )}
50 |
51 | )
52 | }
53 |
54 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
55 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function ScrollArea({
9 | className,
10 | children,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function ScrollBar({
32 | className,
33 | orientation = "vertical",
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
50 |
54 |
55 | )
56 | }
57 |
58 | export { ScrollArea, ScrollBar }
59 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Slider({
9 | className,
10 | defaultValue,
11 | value,
12 | min = 0,
13 | max = 100,
14 | ...props
15 | }: React.ComponentProps) {
16 | const _values = React.useMemo(
17 | () =>
18 | Array.isArray(value)
19 | ? value
20 | : Array.isArray(defaultValue)
21 | ? defaultValue
22 | : [min, max],
23 | [value, defaultValue, min, max]
24 | )
25 |
26 | return (
27 |
39 |
45 |
51 |
52 | {Array.from({ length: _values.length }, (_, index) => (
53 |
58 | ))}
59 |
60 | )
61 | }
62 |
63 | export { Slider }
64 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 | import { Toaster as Sonner, ToasterProps } from "sonner"
3 |
4 | const Toaster = ({ ...props }: ToasterProps) => {
5 | const { theme = "system" } = useTheme()
6 |
7 | return (
8 |
20 | )
21 | }
22 |
23 | export { Toaster }
24 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitive from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Switch({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 |
25 |
26 | )
27 | }
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Tabs({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function TabsList({
22 | className,
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
34 | )
35 | }
36 |
37 | function TabsTrigger({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | function TabsContent({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
63 | )
64 | }
65 |
66 | export { Tabs, TabsList, TabsTrigger, TabsContent }
67 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | function ToggleGroup({
18 | className,
19 | variant,
20 | size,
21 | children,
22 | ...props
23 | }: React.ComponentProps &
24 | VariantProps) {
25 | return (
26 |
36 |
37 | {children}
38 |
39 |
40 | )
41 | }
42 |
43 | function ToggleGroupItem({
44 | className,
45 | children,
46 | variant,
47 | size,
48 | ...props
49 | }: React.ComponentProps &
50 | VariantProps) {
51 | const context = React.useContext(ToggleGroupContext)
52 |
53 | return (
54 |
68 | {children}
69 |
70 | )
71 | }
72 |
73 | export { ToggleGroup, ToggleGroupItem }
74 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-9 px-2 min-w-9",
18 | sm: "h-8 px-1.5 min-w-8",
19 | lg: "h-10 px-2.5 min-w-10",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | function Toggle({
30 | className,
31 | variant,
32 | size,
33 | ...props
34 | }: React.ComponentProps &
35 | VariantProps) {
36 | return (
37 |
42 | )
43 | }
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/useCurrentUser.ts:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '@paynless/store';
2 | // Remove Supabase User type if not directly needed, or keep if profile structure matches
3 | // import { User } from '@supabase/supabase-js';
4 |
5 | // Use the UserProfile type from the store if available and preferred
6 | // Or adjust the return type based on what authStore actually provides
7 | import type { UserProfile } from '@paynless/types'; // Assuming this path is correct
8 |
9 | // Remove placeholder TODO comments
10 | // interface UseCurrentUserReturn {
11 | // user: User | null; // Keep Supabase User type?
12 | // // Add other relevant user state/functions if needed, e.g., isLoading
13 | // }
14 |
15 | // Updated return type to match store structure (adjust as needed)
16 | interface UseCurrentUserReturn {
17 | user: UserProfile | null; // Use UserProfile or the actual type from store
18 | isLoading: boolean; // Reflect loading state from store
19 | }
20 |
21 | /**
22 | * Hook to provide current user information from the auth store.
23 | */
24 | export const useCurrentUser = (): UseCurrentUserReturn => {
25 | // Retrieve user and loading state from the Zustand auth store
26 | const user = useAuthStore((state) => state.profile);
27 | const isLoading = useAuthStore((state) => state.isLoading); // Or appropriate loading state
28 |
29 | // Return the user profile and loading status
30 | return { user, isLoading };
31 |
32 | // Example: return a static mock user for now
33 | // const mockUser: User = {
34 | // id: 'mock-user-id',
35 | // app_metadata: {},
36 | // user_metadata: { name: 'Mock User' },
37 | // aud: 'authenticated',
38 | // created_at: new Date().toISOString(),
39 | // };
40 | // return { user: mockUser };
41 |
42 | // Or return null if no user is assumed initially
43 | // return { user: null };
44 | };
--------------------------------------------------------------------------------
/apps/web/src/hooks/useSubscription.ts:
--------------------------------------------------------------------------------
1 | import { useSubscriptionStore } from '@paynless/store';
2 |
3 | /**
4 | * Hook to access subscription state and actions
5 | * Acts as a compatibility layer for components still using the old hook
6 | */
7 | export function useSubscription() {
8 | return useSubscriptionStore();
9 | }
--------------------------------------------------------------------------------
/apps/web/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ThemeContext } from '../context/theme.context';
3 |
4 | export const useTheme = () => {
5 | const context = useContext(ThemeContext);
6 | if (!context) {
7 | throw new Error('useTheme must be used within a ThemeProvider');
8 | }
9 | return context;
10 | };
--------------------------------------------------------------------------------
/apps/web/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react'; // REMOVED
2 | // import { Container, Typography } from '@mui/material'; // REMOVED - Unused
3 | import { LoginForm } from '../components/auth/LoginForm'; // CORRECTED: Use named import
4 |
5 | export function LoginPage() {
6 | return (
7 |
12 | );
13 | }
--------------------------------------------------------------------------------
/apps/web/src/pages/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useNotificationStore } from '@paynless/store';
3 | import { logger } from '@paynless/utils';
4 | import { NotificationCard } from '@/components/notifications/NotificationCard';
5 |
6 | export const Notifications: React.FC = () => {
7 | const { notifications, fetchNotifications } = useNotificationStore(state => ({
8 | notifications: state.notifications,
9 | fetchNotifications: state.fetchNotifications,
10 | }));
11 |
12 | useEffect(() => {
13 | if (notifications.length === 0) {
14 | logger.debug('[NotificationsPage] No notifications in store, fetching...');
15 | fetchNotifications();
16 | }
17 | }, [notifications.length, fetchNotifications]);
18 |
19 | return (
20 |
21 |
22 |
Notification History
23 | {notifications.length === 0 ? (
24 |
You have no notifications.
25 | ) : (
26 |
27 | {notifications.map((notification) => (
28 |
29 | ))}
30 |
31 | )}
32 |
33 |
34 | );
35 | };
36 |
37 | export default Notifications;
38 |
--------------------------------------------------------------------------------
/apps/web/src/pages/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from '@paynless/store'
2 | import { ProfileEditor } from '../components/profile/ProfileEditor'
3 | import { ProfilePrivacySettingsCard } from '../components/profile/ProfilePrivacySettingsCard'
4 | export function ProfilePage() {
5 | const {
6 | profile: currentProfile,
7 | isLoading: authLoading,
8 | error: authError,
9 | } = useAuthStore((state) => ({
10 | profile: state.profile,
11 | isLoading: state.isLoading,
12 | error: state.error,
13 | }))
14 |
15 | if (authLoading && !currentProfile) {
16 | return (
17 |
18 |
19 |
Loading profile...
20 |
21 |
22 | )
23 | }
24 |
25 | if (!currentProfile) {
26 | return (
27 |
28 |
29 | Could not load profile data. {authError?.message}
30 |
31 |
32 | )
33 | }
34 |
35 | return (
36 |
37 |
41 | {/* Feedback moved inside ProfileEditor or handled via store state globally */}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/apps/web/src/pages/Register.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import { describe, it, expect, vi } from 'vitest';
4 | import { RegisterPage } from './Register';
5 |
6 | // Mock the RegisterForm component to isolate the RegisterPage test
7 | vi.mock('../../components/auth/RegisterForm', () => ({
8 | RegisterForm: () => (
9 |
10 |
11 |
12 | {/* Keep mock simple for page test */}
13 |
14 |
15 | ),
16 | }));
17 |
18 | // REMOVE Layout mock - RegisterPage doesn't render it directly
19 | // vi.mock('../components/layout/Layout', () => ({
20 | // // eslint-disable-next-line @typescript-eslint/no-explicit-any
21 | // Layout: ({ children }: { children: any }) => {children}
,
22 | // }));
23 |
24 |
25 | describe('RegisterPage Component', () => {
26 | const renderRegisterPage = () => {
27 | return render(
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | it('should render the RegisterForm component', () => {
35 | renderRegisterPage();
36 | // Check if the mocked RegisterForm is present (using its test ID)
37 | expect(screen.getByTestId('mock-register-form')).toBeInTheDocument();
38 | // Remove layout check
39 | // expect(screen.getByTestId('layout')).toBeInTheDocument();
40 | });
41 |
42 | // Keep this test focused on checking elements within the mock form
43 | it('should render mocked form elements', () => {
44 | renderRegisterPage();
45 | // Check for elements known to be in the mocked RegisterForm
46 | expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
47 | expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
48 | // Removed 'Confirm Password' as it's not in the simplified mock
49 | expect(screen.getByRole('button', { name: /register/i })).toBeInTheDocument();
50 | });
51 |
52 | });
--------------------------------------------------------------------------------
/apps/web/src/pages/Register.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react'; // REMOVED
2 | // import { Container, Typography } from '@mui/material'; // REMOVED - Unused
3 | import { RegisterForm } from '../components/auth/RegisterForm';
4 |
5 | export function RegisterPage() {
6 | return (
7 |
12 | );
13 | }
--------------------------------------------------------------------------------
/apps/web/src/pages/SubscriptionSuccess.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/SubscriptionSuccess.tsx
2 | import { useEffect } from 'react';
3 | import { useAuthStore, useSubscriptionStore } from '@paynless/store';
4 | import { useNavigate } from 'react-router-dom';
5 | import { CheckCircle } from 'lucide-react';
6 |
7 | export function SubscriptionSuccessPage() {
8 | const user = useAuthStore(state => state.user);
9 | const refreshSubscription = useSubscriptionStore(state => state.refreshSubscription);
10 | const navigate = useNavigate();
11 |
12 | useEffect(() => {
13 | // Refresh subscription data when the component mounts
14 | if (user) {
15 | refreshSubscription();
16 | }
17 |
18 | // Redirect to dashboard after 5 seconds
19 | const timer = setTimeout(() => {
20 | navigate('/dashboard');
21 | }, 5000);
22 |
23 | return () => clearTimeout(timer);
24 | }, [user, refreshSubscription, navigate]);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Thank you for your subscription!
35 |
36 |
37 | Your subscription has been processed successfully. You now have access to all the premium features.
38 |
39 |
40 |
41 | You will be redirected to the dashboard in a few seconds. If you're not redirected, please{' '}
42 | .
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // import path from 'path';
2 |
3 | /** @type {import('tailwindcss').Config} */
4 |
5 | // Log the CWD to diagnose path issues
6 | //console.log('[tailwind.config.js] CWD:', process.cwd());
7 |
8 | export default {
9 | content: [
10 | './index.html',
11 | './src/**/*.{js,ts,jsx,tsx}'
12 | ],
13 | darkMode: 'class',
14 | theme: {
15 | container: {
16 | center: true,
17 | padding: {
18 | DEFAULT: '1rem',
19 | sm: '2rem',
20 | lg: '4rem',
21 | xl: '5rem',
22 | '2xl': '6rem',
23 | },
24 | },
25 | extend: {
26 | colors: {
27 | primary: 'rgb(var(--color-primary) / )',
28 | secondary: 'rgb(var(--color-secondary) / )',
29 | background: 'rgb(var(--color-background) / )',
30 | surface: 'rgb(var(--color-surface) / )',
31 | textPrimary: 'rgb(var(--color-textPrimary) / )',
32 | textSecondary: 'rgb(var(--color-textSecondary) / )',
33 | border: 'rgb(var(--color-border) / )',
34 | },
35 | ringWidth: {
36 | DEFAULT: '3px',
37 | },
38 | ringColor: {
39 | DEFAULT: 'rgb(var(--color-primary) / )',
40 | primary: 'rgb(var(--color-primary) / )',
41 | },
42 | ringOpacity: {
43 | DEFAULT: '0.2',
44 | '20': '0.2',
45 | },
46 | ringOffsetWidth: {
47 | DEFAULT: '2px',
48 | },
49 | ringOffsetColor: {
50 | DEFAULT: 'var(--color-background)',
51 | },
52 | },
53 | },
54 | plugins: [require('@tailwindcss/typography')],
55 | };
--------------------------------------------------------------------------------
/apps/web/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "target": "ESNext",
6 | "useDefineForClassFields": true,
7 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
8 | "allowJs": false,
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "allowImportingTsExtensions": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "ESNext",
16 | "moduleResolution": "Bundler",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 | "types": ["vite/client", "node", "vitest/globals"],
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./src/*"],
25 | "@paynless/platform": ["../../packages/platform/src/index.ts"],
26 | "@paynless/types": ["../../packages/types/src/index.ts"],
27 | "@paynless/api": ["../../packages/api/src/index.ts"],
28 | "@paynless/store": ["../../packages/store/src/index.ts"],
29 | "@paynless/utils": ["../../packages/utils/src/index.ts"],
30 | "@paynless/analytics": ["../../packages/analytics/src/index.ts"]
31 | },
32 |
33 | /* Linting */
34 | "noUnusedLocals": true,
35 | "noUnusedParameters": true,
36 | "noFallthroughCasesInSwitch": true
37 | },
38 | "include": ["src"],
39 | "exclude": [
40 | "node_modules",
41 | "dist",
42 | "src/setupTests.ts",
43 | "vite.config.ts",
44 | "vitest.config.ts"
45 | ],
46 | "references": [
47 | { "path": "../../packages/api" },
48 | { "path": "../../packages/store" },
49 | { "path": "../../packages/types" },
50 | { "path": "../../packages/utils" },
51 | { "path": "../../packages/platform" },
52 | { "path": "../../packages/analytics" }
53 | ]
54 | }
--------------------------------------------------------------------------------
/apps/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | // Import vitest types for test config
4 | import type { UserConfig } from 'vitest/config';
5 | import path from 'node:path'; // Keep path for alias
6 | import tsconfigPaths from 'vite-tsconfig-paths';
7 | import { nodePolyfills } from 'vite-plugin-node-polyfills';
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | plugins: [
12 | react(),
13 | tsconfigPaths(),
14 | nodePolyfills({
15 | globals: {
16 | Buffer: true,
17 | },
18 | }),
19 | ],
20 | optimizeDeps: {
21 | // Explicitly include problematic transitive dependencies based on errors
22 | include: [
23 | '@tanstack/query-core',
24 | 'react-router',
25 | 'use-sync-external-store/shim/with-selector.js',
26 | '@remix-run/router',
27 | 'scheduler',
28 | // Include direct dependencies that require these as well, just in case
29 | '@tanstack/react-query',
30 | 'react-router-dom',
31 | 'zustand',
32 | ],
33 | // Add any necessary excludes back if they were part of a working config
34 | // exclude: ['lucide-react'],
35 | },
36 | resolve: {
37 | // Setting preserveSymlinks to false might help resolve hoisted deps
38 | preserveSymlinks: false,
39 | alias: {
40 | // Keep aliases for local workspace packages
41 | '@paynless/api': path.resolve(__dirname, '../../packages/api/src'),
42 | '@paynless/store': path.resolve(__dirname, '../../packages/store/src/index.ts'),
43 | // Add aliases for other local packages if needed
44 | '@': path.resolve(__dirname, './src'),
45 | },
46 | },
47 | build: {
48 | // Output directly to a dist folder inside the desktop app project
49 | outDir: 'dist',
50 | emptyOutDir: true, // Ensure it's clean before building
51 | },
52 | // Ensure server settings don't conflict if they exist
53 | server: {
54 | port: 5173, // Example, keep your original port
55 | strictPort: true,
56 | },
57 | });
--------------------------------------------------------------------------------
/apps/web/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import viteConfig from './vite.config';
3 |
4 | export default defineConfig({
5 | ...viteConfig,
6 | test: {
7 | // Disable optimizer for tests
8 | deps: {
9 | optimizer: {
10 | web: {
11 | enabled: false,
12 | },
13 | },
14 | // Force use of source code
15 | // Ensure all workspace packages are inlined for tests
16 | inline: [/@paynless\//], // Regex to match @paynless/api, @paynless/store, etc.
17 | },
18 | // Watch for changes in api
19 | forceRerunTriggers: ['../../packages/api/src/**/*'],
20 | // Explicit setup
21 | setupFiles: ['./src/tests/setup.ts'],
22 | environment: 'jsdom',
23 | globals: true,
24 | // Disable threads to avoid concurrency issues
25 | threads: false,
26 | },
27 | });
--------------------------------------------------------------------------------
/apps/windows/README.md:
--------------------------------------------------------------------------------
1 | This application depends on apps/web for its UI.
2 |
3 | Platform provides context to access the file system.
4 |
--------------------------------------------------------------------------------
/apps/windows/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tauri App
8 |
9 |
10 |
11 |
12 |
13 | Welcome to Tauri
14 |
15 |
34 | Click on the Tauri logo to learn more about the framework
35 |
36 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/apps/windows/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "windows",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "tauri": "tauri"
11 | },
12 | "dependencies": {
13 | "@paynless/platform": "workspace:*",
14 | "@tauri-apps/api": "^2",
15 | "@tauri-apps/plugin-opener": "^2.2.6"
16 | },
17 | "devDependencies": {
18 | "@tauri-apps/cli": "^2",
19 | "typescript": "~5.6.2",
20 | "vite": "^6.0.3",
21 | "vite-tsconfig-paths": "^4.3.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "paynless-desktop"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [lib]
11 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "windows_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [build-dependencies]
18 | tauri-build = { version = "^2.0.0", features = [] }
19 |
20 | [dependencies]
21 | tauri = { version = "^2.0.0", features = [] }
22 | serde = { version = "1.0", features = ["derive"] }
23 | serde_json = "1.0"
24 | # Local Crates
25 | core-crypto = { path = "./crates/core-crypto" }
26 | storage-interface = { path = "./crates/storage-interface" }
27 | thiserror = "1.0" # Needed by error types exposed in commands
28 | hex = "0.4" # For command argument decoding
29 |
30 | # Tauri Plugins (Ensure versions match tauri core if necessary)
31 | tauri-plugin-dialog = "2"
32 | tauri-plugin-fs = "2"
33 | tauri-plugin-opener = "2"
34 |
35 | [dev-dependencies]
36 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # For async tests
37 | tempfile = "3.10.1"
38 |
39 | # Features section removed as it wasn't present in the conflicting file and seems outdated
40 | # [features]
41 | # default = ["custom-protocol"]
42 | # custom-protocol = ["tauri/custom-protocol"]
43 |
44 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default",
10 | "opener:default",
11 | "dialog:default",
12 | "fs:default",
13 | "fs:write-all",
14 | "opener:default"
15 | ]
16 | }
--------------------------------------------------------------------------------
/apps/windows/src-tauri/crates/core-crypto/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/crates/core-crypto/Cargo.toml:
--------------------------------------------------------------------------------
1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2 |
3 | [package]
4 | name = "core-crypto"
5 | version = "0.1.0"
6 | edition = "2024"
7 |
8 | [dependencies]
9 | # Hashing
10 | # blake3 = { version = "1.5", default-features = false } # Removed due to incorrect results
11 | sha3 = "0.10.8" # Using SHA3-256 as backup
12 |
13 | # Symmetric Encryption (AEAD)
14 | chacha20poly1305 = "0.10.1" # Includes AEAD traits
15 |
16 | # Signatures & Key Exchange
17 | ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } # For Ed25519
18 | x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } # For X25519
19 | signature = { version = "2.2.0", features = ["rand_core"] } # Trait needed by ed25519-dalek
20 |
21 | # Key Derivation
22 | hkdf = "0.12"
23 | sha2 = "0.10" # Underlying hash for HKDF
24 |
25 | # Randomness
26 | rand = "0.8"
27 | rand_core = { version = "0.6", features = ["std"] } # Often needed by crypto crates like ed25519-dalek
28 |
29 | # Error Handling
30 | thiserror = "1.0"
31 |
32 | # Serialization (Likely needed for keys/tokens eventually)
33 | serde = { version = "1.0", features = ["derive"], optional = true }
34 | bip39 = "2.1.0"
35 | # base64 = { version = "0.21", optional = true }
36 |
37 | # Mnemonic / Seed Handling (Might live elsewhere, but potentially useful here)
38 | # bip39 = { version = "2.0", optional = true }
39 |
40 | [dev-dependencies]
41 | hex = "0.4" # For decoding hex strings in tests
42 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/crates/storage-interface/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "storage-interface"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | thiserror = "1.0"
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/apps/windows/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/apps/windows/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/apps/windows/src-tauri/src/capabilities.rs:
--------------------------------------------------------------------------------
1 | // Import AppHandle - still needed if other commands use it
2 | use tauri::AppHandle;
3 | // Unused imports removed
4 | // use tauri_plugin_dialog::DialogExt;
5 | // use std::path::PathBuf;
6 | // use tauri_plugin_fs::FilePath;
7 |
8 | // No commands or tests left in this module after refactoring to plugins
9 |
10 | // --- Unit Tests ---
11 |
12 | #[cfg(test)]
13 | mod tests {
14 | // No tests needed here anymore
15 | }
16 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2 |
3 | mod capabilities;
4 |
5 | #[tauri::command]
6 | fn greet(name: &str) -> String {
7 | format!("Hello, {}! You've been greeted from Rust!", name)
8 | }
9 |
10 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
11 | pub fn run() {
12 | tauri::Builder::default()
13 | .plugin(tauri_plugin_dialog::init())
14 | .plugin(tauri_plugin_fs::init())
15 | .invoke_handler(tauri::generate_handler![greet])
16 | .run(tauri::generate_context!("tauri.conf.json"))
17 | .expect("error while running tauri application");
18 | }
19 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | // Define modules
5 | mod capabilities;
6 | mod crypto_commands;
7 | mod wallet_commands;
8 |
9 | fn main() {
10 | // Create the mock storage instance (using Default is fine here)
11 | let mock_storage = wallet_commands::MockSecureStorage::default();
12 |
13 | // Use tauri::Builder to create and run the app
14 | tauri::Builder::default()
15 | .plugin(tauri_plugin_opener::init())
16 | .manage(mock_storage) // <-- Manage the storage instance
17 | .plugin(tauri_plugin_dialog::init())
18 | .plugin(tauri_plugin_fs::init())
19 | .invoke_handler(tauri::generate_handler![
20 | // Crypto commands
21 | crypto_commands::generate_signing_keypair_hex,
22 | crypto_commands::sign_hex,
23 | crypto_commands::verify_hex,
24 | crypto_commands::generate_nonce_hex,
25 | crypto_commands::encrypt_symmetric_hex,
26 | crypto_commands::decrypt_symmetric_hex,
27 | // Wallet commands
28 | wallet_commands::import_mnemonic,
29 | wallet_commands::export_mnemonic
30 | ])
31 | .setup(|app| Ok(()))
32 | .run(tauri::generate_context!())
33 | .expect("error while running tauri application");
34 | }
35 |
--------------------------------------------------------------------------------
/apps/windows/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2",
3 | "productName": "windows",
4 | "version": "0.1.0",
5 | "identifier": "com.paynless.windows",
6 | "build": {
7 | "beforeDevCommand": "pnpm --filter web dev",
8 | "devUrl": "http://localhost:5173",
9 | "beforeBuildCommand": "pnpm --filter web build",
10 | "frontendDist": "../../web/dist"
11 | },
12 | "app": {
13 | "withGlobalTauri": true,
14 | "windows": [
15 | {
16 | "title": "windows",
17 | "label": "main"
18 | }
19 | ],
20 | "security": {
21 | "csp": null
22 | }
23 | },
24 | "bundle": {
25 | "active": true,
26 | "targets": "all",
27 | "icon": [
28 | "icons/32x32.png",
29 | "icons/128x128.png",
30 | "icons/128x128@2x.png",
31 | "icons/icon.icns",
32 | "icons/icon.ico"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/windows/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true
21 | },
22 | "include": ["src"]
23 | }
24 |
--------------------------------------------------------------------------------
/apps/windows/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import tsconfigPaths from 'vite-tsconfig-paths';
3 | import path from 'path'; // Import path module
4 |
5 | const host = process.env.TAURI_DEV_HOST;
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig(async () => ({
9 | plugins: [tsconfigPaths()], // Add the plugin
10 |
11 | // Add resolve alias for the shared package
12 | resolve: {
13 | alias: {
14 | '@paynless/platform': path.resolve(__dirname, '../../packages/platform/src/index.ts'),
15 | },
16 | },
17 |
18 | // Explicitly include the shared package for optimization
19 | optimizeDeps: {
20 | exclude: [
21 | '@tauri-apps/api',
22 | '@tauri-apps/api/core',
23 | '@tauri-apps/api/dialog',
24 | '@tauri-apps/api/tauri',
25 | ],
26 | },
27 |
28 | // Ensure Vite can access files outside the app's root
29 | server: {
30 | port: 1420,
31 | strictPort: true,
32 | host: host || false,
33 | fs: {
34 | // Allow serving files from one level up to the project root
35 | allow: ['../../'],
36 | },
37 | hmr: host
38 | ? {
39 | protocol: "ws",
40 | host,
41 | port: 1421,
42 | }
43 | : undefined,
44 | watch: {
45 | // 3. tell vite to ignore watching `src-tauri`
46 | ignored: ["**/src-tauri/**"],
47 | },
48 | },
49 | }));
50 |
--------------------------------------------------------------------------------
/docs/architecture/tauri_bridge.md:
--------------------------------------------------------------------------------
1 | # Tauri <-> Rust Interface Design
2 |
3 | This document specifies the communication bridge between the frontend (Tauri application) and the Rust backend, primarily through Tauri commands and events.
4 |
5 | ## Commands
6 |
7 | Tauri commands allow the frontend to invoke Rust functions in the backend.
8 |
9 | ### Wallet Management Commands
10 |
11 | These commands handle operations related to user wallet keys (mnemonics, seeds).
12 |
13 | #### `import_mnemonic`
14 |
15 | * **Purpose:** Validates a mnemonic phrase provided by the frontend, derives the master seed, and securely stores it using the `storage-layer`.
16 | * **Frontend Call:** `invoke('import_mnemonic', { mnemonic: string })`
17 | * **Arguments:**
18 | * `mnemonic: string`: The BIP-39 mnemonic phrase to import.
19 | * **Return Type (Rust `Result`):** `Result<(), MnemonicImportError>`
20 | * `Ok(())`: Indicates successful validation, derivation, and storage.
21 | * `Err(MnemonicImportError)`: Indicates failure. Possible error variants:
22 | * `InvalidFormat`: Mnemonic structure/word count is wrong.
23 | * `InvalidChecksum`: Mnemonic words are valid, but the checksum fails.
24 | * `StorageFailed { error: String }`: An error occurred accessing the `storage-layer`.
25 |
26 | #### `export_mnemonic`
27 |
28 | * **Purpose:** Securely retrieves the master seed from the `storage-layer`, optionally derives the corresponding mnemonic phrase (if only the seed is stored), and returns it to the frontend after performing necessary security checks.
29 | * **Frontend Call:** `invoke('export_mnemonic')`
30 | * **Arguments:** None (Security checks like password confirmation handled internally or via future arguments).
31 | * **Return Type (Rust `Result`):** `Result`
32 | * `Ok(string)`: The exported mnemonic phrase.
33 | * `Err(MnemonicExportError)`: Indicates failure. Possible error variants:
34 | * `NotInitialized`: No seed/mnemonic found in storage.
35 | * `AuthenticationFailed`: Required security checks failed.
36 | * `RetrievalFailed { error: String }`: An error occurred accessing the `storage-layer`.
37 | * `DerivationFailed`: An error occurred converting the seed back to a mnemonic.
38 |
39 | ## Events
40 |
41 | *(Placeholder for future event definitions)*
--------------------------------------------------------------------------------
/docs/implementations/Complete-Closed-Deprecated-Discarded/20250502_Org_Refinement.md:
--------------------------------------------------------------------------------
1 | ### 3.1 Organizations Refinement Tasks
2 |
3 | * [X] **Implement Pagination Component:**
4 | * [X] Create reusable `PaginationComponent` (`apps/web/src/components/common/PaginationComponent.tsx`) with props for `currentPage`, `pageSize`, `totalItems`, `onPageChange`, `onPageSizeChange`, `allowedPageSizes`.
5 | * [X] Implement TDD: Create `PaginationComponent.test.tsx` with tests covering rendering logic, button clicks/disabling, and page size selection.
6 | * **Note:** Tests for `onPageSizeChange` and rendering specific page size options (`findByRole('option', ...)` after trigger click) are currently failing due to difficulties interacting with the Radix UI Select dropdown portal in the test environment. Accepting these failures for now.
7 | * [X] Component renders standard controls: `<< < {PageSizeDropdown} > >>`, `Page x of y`, `Total Items`.
8 | * [X] Component does not render if `totalPages <= 1`.
9 | * [X] Page size change resets `currentPage` to 1 via `onPageChange(1)`.
10 | * [X] **Integrate Pagination:**
11 | * [X] Add pagination controls to `OrganizationListCard` if list exceeds threshold (e.g., 10).
12 | * [X] Add pagination controls to `MemberListCard` if list exceeds threshold (e.g., 10-20).
13 | * [X] Update corresponding store actions/API calls to support pagination parameters.
14 | * [X] Preset size list for pagination (10, 25, 50, all)
15 |
16 |
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/documentation/solidity_contract_draft.md:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.0;
3 |
4 | contract EncryptedContentRegistry {
5 |
6 | struct ContentMetadata {
7 | bytes32 encryptedFileHash; // hash or CID (e.g., IPFS) of the encrypted content
8 | address keyNFTAddress; // address of NFT contract controlling access
9 | address creator; // original creator address (for royalties maybe)
10 | uint256 createdAt; // timestamp
11 | string pointerURI; // optional: IPFS URI, Arweave TX ID, etc.
12 | }
13 |
14 | mapping(uint256 => ContentMetadata) public contentRegistry;
15 | uint256 public nextContentID = 1;
16 |
17 | event ContentRegistered(uint256 indexed contentID, address indexed creator, address keyNFTAddress);
18 |
19 | function registerContent(
20 | bytes32 _encryptedFileHash,
21 | address _keyNFTAddress,
22 | string memory _pointerURI
23 | ) external returns (uint256) {
24 | uint256 contentID = nextContentID++;
25 | contentRegistry[contentID] = ContentMetadata({
26 | encryptedFileHash: _encryptedFileHash,
27 | keyNFTAddress: _keyNFTAddress,
28 | creator: msg.sender,
29 | createdAt: block.timestamp,
30 | pointerURI: _pointerURI
31 | });
32 |
33 | emit ContentRegistered(contentID, msg.sender, _keyNFTAddress);
34 | return contentID;
35 | }
36 |
37 | function getContentMetadata(uint256 _contentID) external view returns (ContentMetadata memory) {
38 | return contentRegistry[_contentID];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/0. always/always.md:
--------------------------------------------------------------------------------
1 | Read all of these in numeric order. In each folder, read the seedphrase first.
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/2. antithesis/antithesis.openai.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/2. antithesis/antithesis.openai.md
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/2. antithesis/seedphrase:
--------------------------------------------------------------------------------
1 | Read these files and critique them strongly.
2 |
3 | Challenge their understanding of best practices, reliable architecture, correct methods and patterns.
4 |
5 | Explain how to improve the suggested approaches in every possible way. You are very learned and capable in this fields, you should explain clearly all that they have missed, misunderstood, forgotten, or ignored.
6 |
7 | We appreciate and encourage your thoroughness and accuracy of your criticism and quest for improvement and correction.
8 |
9 | You know many ways to improve all of this, and you will tell us everything to the finest detail about how to make it better.
10 |
11 | Create a checklist that proves you know how to fix all of this and make it better, workable, safe, reliable, practical. You can make it excellent and will create an extraordinarily detailed checlklist that shows us every single step by step of the way. Think long and carefully.
12 |
13 | Save your work to the existing /3. antithesis file named after your model.
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/3. synthesis/seedphrase.md:
--------------------------------------------------------------------------------
1 | Everyone has made excellent points. This is an incredible concept with enormous potential but it must be planned, architected, designed, and implemented meticulously. It must be correct the first time.
2 |
3 | We have much excellent content to reflect on. We are going to synthesize all of the points made, improving, resolving, addressing, and correcting every aspect of this plan that we can.
4 |
5 | We are going to produce an extraordinarily detailed, dense, and thorough explanation of how to synthesize all of the points raised in the simplest, most reliable, maintainable, inexpensive, distributed, robust, secure methods we can.
6 |
7 | The end product of our work will be an extensive checklist that details the full implementation, testing, and deployment path required for this feature.
8 |
9 | The implementation will be atomic and test-driven, proving that it works at each granular step of the weay. We will build in rust stepwise so that we can prove from testing that each step works as we go.
10 |
11 | We should probably begin with the encryption method for the proposed blockchain-bittorrent integration that produces transactable keys.
--------------------------------------------------------------------------------
/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/3. synthesis/synthesis.openai.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/docs/implementations/Current/Checklists/Current/desktop-crypto/genesis/3. synthesis/synthesis.openai.md
--------------------------------------------------------------------------------
/docs/implementations/Current/Documentation/0-2-1-FilePaths.md:
--------------------------------------------------------------------------------
1 | # STEP-0.2.1: Planned File Paths for Modification/Creation
2 |
3 | Based on the review of the existing folder structure and the requirements for Organization Chat Context, the following files are initially planned for modification or creation:
4 |
5 | **Backend / API (`supabase/functions/`, `packages/api`, `packages/types`):**
6 |
7 | * **Modify:** `supabase/functions/chat/index.ts`: Update POST endpoint for `organization_id`, apply RLS.
8 | * **Modify:** `supabase/functions/chat-history/index.ts`: Update GET endpoint to filter by context/`organizationId`.
9 | * **Modify:** `supabase/functions/chat-details/index.ts`: Update GET endpoint RLS check for context/`organizationId`.
10 | * **Modify:** `packages/api/src/clients/aiApiClient.ts`: Update methods (`sendChatMessage`, `getChatHistory`, `getChatDetails`) signatures/calls.
11 | * **Modify:** `packages/types/src/ai.types.ts`: Update relevant request/response types if needed.
12 |
13 | **State Management (`packages/store`):**
14 |
15 | * **Modify:** `packages/store/src/aiStore.ts`: Add context state/actions, update existing actions.
16 | * **Modify:** `packages/store/src/organizationStore.ts`: Likely read-only interaction (getting `currentOrganizationId`).
17 |
18 | **Frontend UI (`apps/web/src`):**
19 |
20 | * **New/Modify:** `apps/web/src/components/ai/ChatContextSwitcher.tsx` (or similar): New component for context switching.
21 | * **Modify:** `apps/web/src/pages/AiChat.tsx`: Integrate switcher, call store actions.
22 | * **Modify:** `apps/web/src/components/ai/ChatHistoryList.tsx`: Minor UI tweaks possible, core logic relies on updated store state.
23 |
24 | *(Note: This list focuses on the initial Organization Context feature. Other features like admin toggles, rewind, token display will involve further changes.)*
--------------------------------------------------------------------------------
/docs/implementations/Current/process/1. hypothesis/ai-chat-enhancements-hypothesis-user-response.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsylvester/paynless-framework/d8275b4f651e7602bfe4678aed586cb9e1aa7935/docs/implementations/Current/process/1. hypothesis/ai-chat-enhancements-hypothesis-user-response.md
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import globals from 'globals';
3 | import reactHooks from 'eslint-plugin-react-hooks';
4 | import reactRefresh from 'eslint-plugin-react-refresh';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | {
9 | ignores: [
10 | 'dist',
11 | 'vite.config.ts',
12 | '*.config.js', // Ignore ESLint config itself and potentially others
13 | 'apps/web/src/components/ui/**/*', // Ignore Shadcn UI components
14 | ]
15 | },
16 | {
17 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
18 | files: ['**/*.{ts,tsx}'],
19 | languageOptions: {
20 | ecmaVersion: 2020,
21 | globals: globals.browser,
22 | },
23 | plugins: {
24 | 'react-hooks': reactHooks,
25 | 'react-refresh': reactRefresh,
26 | },
27 | rules: {
28 | ...reactHooks.configs.recommended.rules,
29 | 'react-refresh/only-export-components': [
30 | 'warn',
31 | { allowConstantExport: true },
32 | ],
33 | '@typescript-eslint/no-unused-vars': [
34 | 'warn',
35 | {
36 | argsIgnorePattern: '^_|node|ref',
37 | varsIgnorePattern: '^_|analytics',
38 | caughtErrorsIgnorePattern: '^_|',
39 | },
40 | ],
41 | },
42 | },
43 | {
44 | files: [
45 | '**/*.test.*',
46 | '**/tests/**/*.{ts,tsx}',
47 | '**/mocks/**/*.{ts,tsx}',
48 | '**/*.mock.{ts,tsx}',
49 | ],
50 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
51 | languageOptions: {
52 | ecmaVersion: 2020,
53 | globals: {
54 | ...globals.browser,
55 | ...globals.node,
56 | ...globals.jest,
57 | },
58 | },
59 | plugins: {
60 | 'react-hooks': reactHooks,
61 | },
62 | rules: {
63 | '@typescript-eslint/no-unused-vars': 'off',
64 | '@typescript-eslint/no-explicit-any': 'off',
65 | '@typescript-eslint/no-non-null-assertion': 'off',
66 | '@typescript-eslint/no-empty-function': 'off',
67 | }
68 | }
69 | );
70 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
3 | "targetDefaults": {
4 | "build": {
5 | "dependsOn": [
6 | "^build"
7 | ],
8 | "outputs": [
9 | "{projectRoot}/dist"
10 | ],
11 | "cache": true
12 | },
13 | "lint": {
14 | "dependsOn": [
15 | "^lint"
16 | ],
17 | "cache": true
18 | }
19 | },
20 | "plugins": [],
21 | "nxCloudId": "67f29f0c17ad73371d9f4f32"
22 | }
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "paynless-framework-monorepo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "description": "Monorepo for the Paynless framework backend, shared packages, and apps.",
7 | "scripts": {
8 | "dev": "pnpm --parallel --filter \"./apps/*\" dev",
9 | "dev:web": "pnpm --filter ./apps/web dev",
10 | "build": "pnpm -r --filter @paynless/web... build",
11 | "build:clean": "pnpm run clean:buildinfo",
12 | "clean": "pnpm run clean:buildinfo && pnpm run clean:node_modules",
13 | "clean:buildinfo": "rimraf packages/**/*.tsbuildinfo apps/**/*.tsbuildinfo tsconfig.tsbuildinfo",
14 | "clean:node_modules": "rimraf node_modules packages/**/node_modules apps/**/node_modules",
15 | "preview": "pnpm --filter @paynless/web preview",
16 | "lint": "pnpm -r run lint --if-present",
17 | "test": "pnpm -r test",
18 | "test:integration": "vitest run --config ./vitest.config.ts",
19 | "sync:types": "supabase gen types typescript --local > supabase/functions/types_db.ts && node supabase/scripts/sync-supabase-shared-types.mjs"
20 | },
21 | "keywords": [
22 | "supabase",
23 | "stripe",
24 | "react",
25 | "framework",
26 | "saas"
27 | ],
28 | "author": "",
29 | "license": "ISC",
30 | "devDependencies": {
31 | "@nx/eslint": "20.7.1",
32 | "@nx/vite": "20.7.1",
33 | "@nx/web": "20.7.1",
34 | "@supabase/supabase-js": "^2.49.4",
35 | "@testing-library/react": "^16.3.0",
36 | "@types/node": "^20.11.24",
37 | "@vitest/ui": "^1.3.1",
38 | "copyfiles": "^2.4.1",
39 | "dotenv": "^16.5.0",
40 | "globals": "^15.8.0",
41 | "jiti": "2.4.2",
42 | "jsdom": "^26.0.0",
43 | "msw": "^2.7.3",
44 | "nx": "20.7.1",
45 | "rimraf": "^5.0.10",
46 | "typescript": "^5.5.3",
47 | "vite": "^6.0.0",
48 | "vitest": "^1.6.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/analytics/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/analytics",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "exports": {
8 | ".": {
9 | "import": "./dist/index.js",
10 | "types": "./dist/index.d.ts"
11 | }
12 | },
13 | "scripts": {
14 | "build": "tsc",
15 | "dev": "tsc -w",
16 | "clean": "rm -rf dist",
17 | "test": "vitest run",
18 | "test:watch": "vitest",
19 | "lint": "eslint . --max-warnings 0"
20 | },
21 | "dependencies": {
22 | "@paynless/types": "workspace:*",
23 | "@paynless/utils": "workspace:*",
24 | "posthog-js": "^1.140.1"
25 | },
26 | "devDependencies": {
27 | "typescript": "^5.2.2",
28 | "vite": "^5.2.0",
29 | "vitest": "^1.6.0",
30 | "eslint": "^8.57.0",
31 | "@types/node": "^20.11.20"
32 | },
33 | "peerDependencies": {
34 | }
35 | }
--------------------------------------------------------------------------------
/packages/analytics/src/.keep:
--------------------------------------------------------------------------------
1 | # Keep this file to ensure the src directory is created
--------------------------------------------------------------------------------
/packages/analytics/src/index.ts:
--------------------------------------------------------------------------------
1 | import { AnalyticsClient } from '@paynless/types';
2 | import { logger } from '@paynless/utils';
3 | import { NullAnalyticsAdapter } from './nullAdapter';
4 | // Import PostHogAdapter
5 | import { PostHogAdapter } from './posthogAdapter';
6 |
7 | let analyticsInstance: AnalyticsClient;
8 |
9 | const initializeAnalytics = (): AnalyticsClient => {
10 | // Read config from environment variables
11 | const provider = import.meta.env['VITE_ANALYTICS_PROVIDER']?.toLowerCase() || 'none';
12 | const posthogApiKey = import.meta.env['VITE_POSTHOG_KEY'];
13 | const posthogApiHost = import.meta.env['VITE_POSTHOG_HOST'] || 'https://app.posthog.com';
14 | // Add other provider keys here (e.g., mixpanelToken)
15 |
16 | logger.debug('[Analytics] Initializing analytics service...', { provider, hasPosthogKey: !!posthogApiKey });
17 |
18 | // --- Provider Selection Logic ---
19 | if (provider === 'posthog' && posthogApiKey) {
20 | try {
21 | logger.info(`[Analytics] PostHog provider selected. Initializing with host: ${posthogApiHost}`);
22 | const posthogAdapter = new PostHogAdapter();
23 | posthogAdapter.init(posthogApiKey, posthogApiHost); // Initialize PostHog
24 | analyticsInstance = posthogAdapter;
25 | } catch (initError: unknown) {
26 | logger.error('[Analytics] Failed to initialize PostHog Adapter. Falling back to NullAdapter.', {
27 | error: initError instanceof Error ? initError.message : String(initError)
28 | });
29 | analyticsInstance = new NullAnalyticsAdapter();
30 | }
31 | } else {
32 | // Default to Null Adapter
33 | if (provider !== 'none') {
34 | logger.warn(`[Analytics] Provider '${provider}' configured but requirements not met (e.g., missing key or unsupported). Using NullAdapter.`);
35 | } else {
36 | logger.info('[Analytics] No analytics provider configured or provider is "none". Using NullAdapter.');
37 | }
38 | analyticsInstance = new NullAnalyticsAdapter();
39 | }
40 |
41 | return analyticsInstance;
42 | };
43 |
44 | // Initialize on import and export the singleton instance
45 | export const analytics: AnalyticsClient = initializeAnalytics();
46 |
47 | // Optional: Export adapters if needed for specific testing scenarios
48 | export { NullAnalyticsAdapter, PostHogAdapter };
--------------------------------------------------------------------------------
/packages/analytics/src/nullAdapter.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, beforeEach } from 'vitest';
2 | import { NullAnalyticsAdapter } from './nullAdapter';
3 |
4 | describe('NullAnalyticsAdapter', () => {
5 | let adapter: NullAnalyticsAdapter;
6 |
7 | beforeEach(() => {
8 | adapter = new NullAnalyticsAdapter();
9 | });
10 |
11 | it('should instantiate without errors', () => {
12 | expect(adapter).toBeInstanceOf(NullAnalyticsAdapter);
13 | });
14 |
15 | it('should have an identify method that does nothing', () => {
16 | expect(() => adapter.identify('user123', { email: 'test@example.com' })).not.toThrow();
17 | });
18 |
19 | it('should have a track method that does nothing', () => {
20 | expect(() => adapter.track('testEvent', { prop: 'value' })).not.toThrow();
21 | });
22 |
23 | it('should have a reset method that does nothing', () => {
24 | expect(() => adapter.reset()).not.toThrow();
25 | });
26 |
27 | it('should have an optInTracking method (optional) that does nothing', () => {
28 | expect(() => adapter.optInTracking?.()).not.toThrow();
29 | });
30 |
31 | it('should have an optOutTracking method (optional) that does nothing', () => {
32 | expect(() => adapter.optOutTracking?.()).not.toThrow();
33 | });
34 |
35 | it('should have an isFeatureEnabled method (optional) that returns false', () => {
36 | expect(adapter.isFeatureEnabled?.('test-flag')).toBe(false);
37 | expect(() => adapter.isFeatureEnabled?.('test-flag')).not.toThrow();
38 | });
39 | });
--------------------------------------------------------------------------------
/packages/analytics/src/nullAdapter.ts:
--------------------------------------------------------------------------------
1 | import { AnalyticsClient } from '@paynless/types';
2 | // import { logger } from '@paynless/utils'; // Removed unused import
3 |
4 | /**
5 | * A no-operation implementation of the AnalyticsClient interface.
6 | * Used when no analytics provider is configured or enabled.
7 | */
8 | export class NullAnalyticsAdapter implements AnalyticsClient {
9 | constructor() {
10 | // Optional: Log that the null adapter is being used. Keep this minimal.
11 | // logger.debug('[Analytics] Null adapter initialized. Analytics calls will be no-ops.');
12 | }
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
15 | identify(_userId: string, _traits?: Record): void {
16 | // No operation
17 | }
18 |
19 | // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
20 | track(_eventName: string, _properties?: Record): void {
21 | // No operation
22 | }
23 |
24 | // eslint-disable-next-line @typescript-eslint/no-empty-function
25 | reset(): void {
26 | // No operation
27 | }
28 |
29 | // eslint-disable-next-line @typescript-eslint/no-empty-function
30 | optInTracking?(): void {
31 | // No operation
32 | }
33 |
34 | // eslint-disable-next-line @typescript-eslint/no-empty-function
35 | optOutTracking?(): void {
36 | // No operation
37 | }
38 |
39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
40 | isFeatureEnabled?(_key: string): boolean {
41 | // Default to false or handle as needed if feature flags are used without a provider
42 | return false;
43 | }
44 | }
--------------------------------------------------------------------------------
/packages/analytics/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "baseUrl": ".",
7 | "types": ["vitest/globals", "vite/client"],
8 | "lib": ["ESNext", "DOM"]
9 | },
10 | "include": ["src/**/*.ts"],
11 | "exclude": ["node_modules", "dist"]
12 | }
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/api",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "API client for Paynless applications",
6 | "type": "module",
7 | "main": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": "./dist/index.js",
12 | "types": "./dist/index.d.ts"
13 | },
14 | "./mocks": {
15 | "import": "./dist/mocks/index.js",
16 | "types": "./dist/mocks/index.d.ts"
17 | },
18 | "./mocks/stripe.mock": {
19 | "import": "./dist/mocks/stripe.mock.js",
20 | "types": "./dist/mocks/stripe.mock.d.ts"
21 | },
22 | "./mocks/supabase.mock": "./dist/mocks/supabase.mock.js",
23 | "./mocks/notifications.api.mock": "./dist/mocks/notifications.api.mock.js",
24 | "./mocks/ai.api.mock": "./dist/mocks/ai.api.mock.js"
25 | },
26 | "scripts": {
27 | "test": "vitest run",
28 | "test:watch": "vitest",
29 | "build": "tsc -b",
30 | "clean": "rm -rf dist tsconfig.tsbuildinfo",
31 | "lint": "eslint src/**/*.{ts,tsx} --report-unused-disable-directives --max-warnings 0"
32 | },
33 | "dependencies": {
34 | "@paynless/types": "workspace:*",
35 | "@paynless/utils": "workspace:*",
36 | "@supabase/supabase-js": "^2.49.4",
37 | "axios": "^1.6.7",
38 | "vite-plugin-node-polyfills": "^0.23.0",
39 | "zustand": "^4.5.2"
40 | },
41 | "devDependencies": {
42 | "@paynless/db-types": "workspace:^",
43 | "@types/node": "^20.11.20",
44 | "@types/react": "^18",
45 | "@typescript-eslint/eslint-plugin": "^7.18.0",
46 | "@typescript-eslint/parser": "^7.18.0",
47 | "eslint": "^8.57.0",
48 | "msw": "^2.2.13",
49 | "typescript": "^5.2.2",
50 | "vitest": "^1.4.0"
51 | },
52 | "peerDependencies": {
53 | "react": "^18"
54 | },
55 | "license": "ISC"
56 | }
57 |
--------------------------------------------------------------------------------
/packages/api/src/fetch.test.ts:
--------------------------------------------------------------------------------
1 | // packages/api/test/fetch.test.ts
2 | // Adjusted path to be within src/ for easier discovery if no specific test dir exists
3 | import { describe, it, expect, vi } from 'vitest';
4 | import { http, HttpResponse } from 'msw';
5 | import { api, initializeApiClient } from './apiClient'; // Assuming apiClient initialization happens elsewhere or isn't strictly needed for global fetch test
6 | import { server } from './setupTests'; // Import the shared server
7 |
8 | // Mock logger if necessary, or ensure it doesn't break tests
9 | vi.mock('@paynless/utils', () => ({
10 | logger: {
11 | info: vi.fn(),
12 | warn: vi.fn(),
13 | error: vi.fn(),
14 | debug: vi.fn(),
15 | configure: vi.fn(),
16 | getInstance: vi.fn().mockReturnThis(), // Mock getInstance if needed
17 | // Add other methods if the code under test uses them
18 | }
19 | }));
20 |
21 | describe('fetch call within api package', () => {
22 | it('should be intercepted by MSW', async () => {
23 | console.log('[api/fetch.test.ts] Running test, about to fetch...');
24 | // Add a specific handler for this test's endpoint
25 | server.use(
26 | http.get('http://test.host/internal-test', () => {
27 | return HttpResponse.json({ message: 'Mocked fetch success' });
28 | })
29 | );
30 |
31 | // Log fetch implementation here too for comparison
32 | console.log('[api/fetch.test.ts] globalThis.fetch:', globalThis.fetch);
33 | const response = await fetch('http://test.host/internal-test');
34 | console.log('[api/fetch.test.ts] Fetch call completed.');
35 | expect(response.status).toBe(200);
36 | // You might want to consume the body to avoid potential resource leaks in test runners
37 | const data = await response.json();
38 | expect(data).toEqual({ message: 'Mocked fetch success' });
39 | });
40 | });
--------------------------------------------------------------------------------
/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | // Export API client modules
2 | export * from './apiClient'; // Ensure this points to the correct file
3 | export * from './stripe.api';
4 | export * from './ai.api'; // Export AiApiClient
5 | export * from './organizations.api'; // Export Org API client
6 | // export * from './mocks'; // DO NOT EXPORT MOCKS FROM THE MAIN PACKAGE ENTRY POINT
7 | // Export mock utilities (re-exporting from ./mocks/index.ts)
--------------------------------------------------------------------------------
/packages/api/src/mocks/chatHistory.mock.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 | import type { Chat, ApiResponse } from '@paynless/types';
3 |
4 | // Mock implementations using vi.fn()
5 | export const mockGetChatHistory = vi.fn<[], Promise>>();
6 | export const mockDeleteChatHistory = vi.fn<[string], Promise>>();
7 | export const mockClearUserChatHistory = vi.fn<[], Promise>>();
8 |
9 | // Default mock data (can be overridden in tests)
10 | export const defaultMockChatHistory: Chat[] = [
11 | {
12 | id: 'chat-1',
13 | user_id: 'user-123',
14 | title: 'Test Chat 1',
15 | created_at: new Date().toISOString(),
16 | updated_at: new Date().toISOString(),
17 | organization_id: null,
18 | system_prompt_id: null,
19 | },
20 | {
21 | id: 'chat-2',
22 | user_id: 'user-123',
23 | title: 'Test Chat 2',
24 | created_at: new Date().toISOString(),
25 | updated_at: new Date().toISOString(),
26 | organization_id: null,
27 | system_prompt_id: null,
28 | },
29 | ];
30 |
31 | // Reset function
32 | export const resetChatHistoryMocks = () => {
33 | mockGetChatHistory.mockReset();
34 | mockDeleteChatHistory.mockReset();
35 | mockClearUserChatHistory.mockReset();
36 |
37 | // Set default successful resolutions with status and error: undefined
38 | mockGetChatHistory.mockResolvedValue({ status: 200, data: [...defaultMockChatHistory], error: undefined });
39 | mockDeleteChatHistory.mockResolvedValue({ status: 204, data: undefined, error: undefined });
40 | mockClearUserChatHistory.mockResolvedValue({ status: 204, data: undefined, error: undefined });
41 | };
42 |
43 | // Initialize with default mocks
44 | resetChatHistoryMocks();
--------------------------------------------------------------------------------
/packages/api/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | // src/mocks/handlers.ts
2 | // import { http, HttpResponse } from 'msw'; // Comment out unused imports
3 |
4 | // Define mock API handlers here.
5 | // Example:
6 | // http.get('/api/users', () => {
7 | // return HttpResponse.json([{ id: 1, name: 'John Doe' }])
8 | // })
9 |
10 | export const handlers = [
11 | // Add your handlers here
12 | ];
--------------------------------------------------------------------------------
/packages/api/src/mocks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ai.api.mock';
2 | export * from './stripe.mock';
3 | export * from './supabase.mock';
4 | export * from './notifications.api.mock';
5 | export * from './apiClient.mock';
6 | export * from './organizations.api.mock';
7 | export * from './chatHistory.mock';
8 |
9 | // Add exports for other mocks if they exist and are needed, e.g.:
10 | // export * from './stripe.mock';
--------------------------------------------------------------------------------
/packages/api/src/mocks/notifications.api.mock.ts:
--------------------------------------------------------------------------------
1 | import { vi, type Mock } from 'vitest';
2 | import type { NotificationApiClient } from '../notifications.api';
3 | import type { Notification, ApiResponse } from '@paynless/types';
4 |
5 | /**
6 | * Creates a reusable mock object for the NotificationApiClient, suitable for Vitest unit tests.
7 | * Provides vi.fn() implementations for all NotificationApiClient methods.
8 | *
9 | * @returns A mocked NotificationApiClient instance.
10 | */
11 | export const createMockNotificationApiClient = (): NotificationApiClient => ({
12 | fetchNotifications: vi.fn() as Mock<[], Promise>>,
13 | markNotificationRead: vi.fn() as Mock<[string], Promise>>,
14 | markAllNotificationsAsRead: vi.fn() as Mock<[], Promise>>,
15 | // Ensure all methods from the actual NotificationApiClient are mocked
16 | // Cast the entire object to NotificationApiClient to satisfy the type,
17 | // acknowledging that private members are not part of this mock object structure
18 | // because the class constructor itself is typically mocked in tests.
19 | }) as unknown as NotificationApiClient;
20 |
21 | /**
22 | * Resets all mock functions within a given mock NotificationApiClient instance.
23 | * Useful for cleaning up mocks between tests (e.g., in `beforeEach`).
24 | *
25 | * @param mockClient - The mock NotificationApiClient instance to reset.
26 | */
27 | export const resetMockNotificationApiClient = (mockClient: NotificationApiClient) => {
28 | (mockClient.fetchNotifications as Mock).mockReset();
29 | (mockClient.markNotificationRead as Mock).mockReset();
30 | (mockClient.markAllNotificationsAsRead as Mock).mockReset();
31 | };
32 |
33 | // Optional: Export a default instance if needed, though creating fresh ones might be safer
34 | // export const mockNotificationApiClient = createMockNotificationApiClient();
--------------------------------------------------------------------------------
/packages/api/src/mocks/stripe.mock.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 |
3 | // Create individual mock functions for each StripeApiClient method
4 | export const mockStripeGetSubscriptionPlans = vi.fn();
5 | export const mockStripeGetUserSubscription = vi.fn();
6 | export const mockStripeCreateCheckoutSession = vi.fn();
7 | export const mockStripeCreatePortalSession = vi.fn();
8 | export const mockStripeCancelSubscription = vi.fn();
9 | export const mockStripeResumeSubscription = vi.fn();
10 | export const mockStripeGetUsageMetrics = vi.fn();
11 |
12 | // Function to reset all mocks
13 | export const resetStripeMocks = () => {
14 | mockStripeGetSubscriptionPlans.mockReset();
15 | mockStripeGetUserSubscription.mockReset();
16 | mockStripeCreateCheckoutSession.mockReset();
17 | mockStripeCreatePortalSession.mockReset();
18 | mockStripeCancelSubscription.mockReset();
19 | mockStripeResumeSubscription.mockReset();
20 | mockStripeGetUsageMetrics.mockReset();
21 | };
22 |
23 | // Optional: Export a mock implementation object if needed elsewhere
24 | export const mockStripeApiClientImplementation = {
25 | getSubscriptionPlans: mockStripeGetSubscriptionPlans,
26 | getUserSubscription: mockStripeGetUserSubscription,
27 | createCheckoutSession: mockStripeCreateCheckoutSession,
28 | createPortalSession: mockStripeCreatePortalSession,
29 | cancelSubscription: mockStripeCancelSubscription,
30 | resumeSubscription: mockStripeResumeSubscription,
31 | getUsageMetrics: mockStripeGetUsageMetrics,
32 | };
--------------------------------------------------------------------------------
/packages/api/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { beforeAll, afterEach, afterAll } from 'vitest';
2 | import { setupServer } from 'msw/node';
3 |
4 | // Import your request handlers (we'll create this next)
5 | import { handlers } from './mocks/handlers';
6 |
7 | // Setup requests interception using the given handlers.
8 | export const server = setupServer(...handlers);
9 |
10 | // Start server before all tests
11 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
12 |
13 | // Reset handlers after each test `important for test isolation`
14 | afterEach(() => server.resetHandlers());
15 |
16 | // Clean up after all tests are done
17 | afterAll(() => server.close());
--------------------------------------------------------------------------------
/packages/api/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Local type definitions for Vite environment variables.
3 | * Copied from @paynless/types/src/vite-env.d.ts to resolve build issues.
4 | * Ensure this stays in sync with the canonical definition.
5 | */
6 | declare interface ImportMeta {
7 | readonly env: {
8 | // API and server URLs
9 | readonly VITE_SUPABASE_URL: string;
10 | readonly VITE_SUPABASE_ANON_KEY: string;
11 |
12 | // Stripe-related environment variables
13 | readonly VITE_STRIPE_TEST_MODE: string;
14 | readonly VITE_STRIPE_PUBLISHABLE_KEY_TEST: string;
15 | readonly VITE_STRIPE_PUBLISHABLE_KEY_LIVE: string;
16 | readonly VITE_STRIPE_SECRET_KEY_TEST: string; // Type needed even if value used server-side
17 | readonly VITE_STRIPE_SECRET_KEY_LIVE: string; // Type needed even if value used server-side
18 |
19 | // Allow any other environment variables
20 | readonly [key: string]: string;
21 | };
22 | }
23 |
24 | // Export {} to treat this file as a module script, ensuring global augmentation.
25 | export {};
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "types": ["node", "vitest/globals"],
7 | "jsx": "react-jsx",
8 | "declaration": true,
9 | "lib": ["ESNext", "DOM"],
10 | "composite": true
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": [
14 | "node_modules",
15 | "dist",
16 | "src/**/*.test.ts",
17 | "src/setupTests.ts"
18 | ],
19 | "references": [
20 | { "path": "../types" },
21 | { "path": "../utils" }
22 | ]
23 | }
--------------------------------------------------------------------------------
/packages/api/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import path from 'path';
4 |
5 | export default defineConfig({
6 | test: {
7 | globals: true,
8 | environment: 'node', // Or 'jsdom' if you need browser APIs for some api tests
9 | // Include all .test.ts and .spec.ts files within the src directory
10 | include: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
11 | alias: {
12 | // If your API tests need to resolve other workspace packages, add aliases here
13 | // For example, if importing directly from @paynless/types source:
14 | '@paynless/types': path.resolve(__dirname, '../types/src'),
15 | // Add other necessary aliases
16 | },
17 | // Add setupFiles if needed, e.g., for mocking 'msw'
18 | // setupFiles: ['./src/mocks/setup.ts'],
19 | },
20 | });
--------------------------------------------------------------------------------
/packages/platform/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/platform",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.js",
7 | "types": "./dist/index.d.ts",
8 | "files": [
9 | "dist/**",
10 | "src/**"
11 | ],
12 | "scripts": {
13 | "build": "tsc -b",
14 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
15 | "clean": "rm -rf dist .turbo",
16 | "test": "vitest run --passWithNoTests",
17 | "test:watch": "vitest"
18 | },
19 | "peerDependencies": {
20 | "react": "^18.2.0"
21 | },
22 | "devDependencies": {
23 | "@testing-library/jest-dom": "^6.4.2",
24 | "@testing-library/react": "^16.0.0",
25 | "@types/node": "^20.11.24",
26 | "@types/react": "^18.2.61",
27 | "@types/testing-library__jest-dom": "^6.0.0",
28 | "@vitejs/plugin-react": "^4.2.1",
29 | "eslint": "^8.57.0",
30 | "jsdom": "^24.0.0",
31 | "react": "^18.2.0",
32 | "tsup": "^8.0.2",
33 | "typescript": "^5.3.3",
34 | "vite-tsconfig-paths": "^5.1.4",
35 | "vitest": "^1.6.1"
36 | },
37 | "dependencies": {
38 | "@paynless/types": "workspace:*",
39 | "@tauri-apps/api": "^2.2.0",
40 | "@tauri-apps/plugin-dialog": "^2.0.0-beta.8",
41 | "@tauri-apps/plugin-fs": "^2.2.1",
42 | "mitt": "^3.0.1",
43 | "vite-plugin-node-polyfills": "^0.23.0"
44 | }
45 | }
--------------------------------------------------------------------------------
/packages/platform/src/android.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getAndroidCapabilities } from './android';
3 | import type { OperatingSystem } from '@paynless/types';
4 |
5 | describe('getAndroidCapabilities', () => {
6 | it('should return correct Android stub capabilities structure', () => {
7 | const capabilities = getAndroidCapabilities();
8 | expect(capabilities.os).toBe('android');
9 | expect(capabilities.fileSystem.isAvailable).toBe(false);
10 | expect((capabilities.fileSystem as any).readFile).toBeUndefined();
11 | });
12 | });
--------------------------------------------------------------------------------
/packages/platform/src/android.ts:
--------------------------------------------------------------------------------
1 | import type { PlatformCapabilities } from '@paynless/types';
2 |
3 | // Web platform provides no special filesystem capabilities via this service by default.
4 | // Standard file inputs or future Web File System Access API would be handled differently.
5 | export const android: PlatformCapabilities['fileSystem'] = {
6 | isAvailable: false,
7 | };
8 |
9 | export const getAndroidCapabilities = (): PlatformCapabilities => {
10 | // Placeholder implementation for Android
11 | return {
12 | platform: 'web', // Default to 'web' as Android isn't a primary target yet
13 | os: 'android',
14 | fileSystem: { isAvailable: false },
15 | // Initialize other capability groups as needed
16 | };
17 | };
--------------------------------------------------------------------------------
/packages/platform/src/events.ts:
--------------------------------------------------------------------------------
1 | import mitt from 'mitt';
2 |
3 | // Define event payload type
4 | type FileDropPayload = string[];
5 |
6 | // Define event map
7 | type PlatformEvents = {
8 | 'file-drop': FileDropPayload;
9 | 'file-drag-hover': void; // File is being dragged over the window
10 | 'file-drag-cancel': void; // Drag operation cancelled (left window)
11 | };
12 |
13 | // Create and export the emitter instance
14 | export const platformEventEmitter = mitt();
15 |
16 | // Export types if needed elsewhere
17 | export type { PlatformEvents, FileDropPayload };
--------------------------------------------------------------------------------
/packages/platform/src/ios.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getIosCapabilities } from './ios';
3 | import type { OperatingSystem } from '@paynless/types';
4 |
5 | describe('getIosCapabilities', () => {
6 | it('should return correct iOS stub capabilities structure', () => {
7 | const capabilities = getIosCapabilities();
8 | expect(capabilities.os).toBe('ios');
9 | expect(capabilities.fileSystem.isAvailable).toBe(false);
10 | expect((capabilities.fileSystem as any).readFile).toBeUndefined();
11 | });
12 | });
--------------------------------------------------------------------------------
/packages/platform/src/ios.ts:
--------------------------------------------------------------------------------
1 | import type { PlatformCapabilities } from '@paynless/types';
2 |
3 | // Web platform provides no special filesystem capabilities via this service by default.
4 | // Standard file inputs or future Web File System Access API would be handled differently.
5 | export const getIosCapabilities = (): PlatformCapabilities => {
6 | // Placeholder implementation for iOS
7 | return {
8 | platform: 'web', // Default to 'web' as iOS isn't a primary target yet
9 | os: 'ios',
10 | fileSystem: { isAvailable: false },
11 | // Initialize other capability groups as needed
12 | };
13 | };
--------------------------------------------------------------------------------
/packages/platform/src/linux.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getLinuxCapabilities } from './linux';
3 | import type { OperatingSystem } from '@paynless/types'; // Import for type checking
4 |
5 | describe('getLinuxCapabilities', () => {
6 | it('should return correct Linux stub capabilities structure', () => {
7 | const capabilities = getLinuxCapabilities();
8 |
9 | // Check OS
10 | expect(capabilities.os).toBe('linux');
11 |
12 | // Check filesystem (should be unavailable in stub)
13 | expect(capabilities.fileSystem.isAvailable).toBe(false);
14 | // Ensure no FS methods are present (type safety check)
15 | expect((capabilities.fileSystem as any).readFile).toBeUndefined();
16 |
17 | // Platform might be 'web' or 'tauri' depending on stub details,
18 | // OS and unavailable FS are the key checks for now.
19 | });
20 | });
--------------------------------------------------------------------------------
/packages/platform/src/linux.ts:
--------------------------------------------------------------------------------
1 | import type { PlatformCapabilities } from '@paynless/types';
2 |
3 | // Web platform provides no special filesystem capabilities via this service by default.
4 | // Standard file inputs or future Web File System Access API would be handled differently.
5 | export const getLinuxCapabilities = (): PlatformCapabilities => {
6 | // Placeholder implementation for Linux
7 | return {
8 | platform: 'web', // Placeholder, might be 'tauri' if running desktop
9 | os: 'linux',
10 | fileSystem: { isAvailable: false }, // Or Tauri FS if detected
11 | // Initialize other capability groups as needed
12 | };
13 | };
--------------------------------------------------------------------------------
/packages/platform/src/mac.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getMacCapabilities } from './mac';
3 | import type { OperatingSystem } from '@paynless/types';
4 |
5 | describe('getMacCapabilities', () => {
6 | it('should return correct macOS stub capabilities structure', () => {
7 | const capabilities = getMacCapabilities();
8 | expect(capabilities.os).toBe('macos');
9 | expect(capabilities.fileSystem.isAvailable).toBe(false);
10 | expect((capabilities.fileSystem as any).readFile).toBeUndefined();
11 | });
12 | });
--------------------------------------------------------------------------------
/packages/platform/src/mac.ts:
--------------------------------------------------------------------------------
1 | import type { PlatformCapabilities } from '@paynless/types';
2 |
3 | // Web platform provides no special filesystem capabilities via this service by default.
4 | // Standard file inputs or future Web File System Access API would be handled differently.
5 | export const mac: PlatformCapabilities['fileSystem'] = {
6 | isAvailable: false,
7 | };
8 |
9 | export const getMacCapabilities = (): PlatformCapabilities => {
10 | // Placeholder implementation for macOS
11 | return {
12 | platform: 'web', // Placeholder, might be 'tauri' if running desktop
13 | os: 'macos',
14 | fileSystem: { isAvailable: false }, // Or Tauri FS if detected
15 | // Initialize other capability groups as needed
16 | };
17 | };
--------------------------------------------------------------------------------
/packages/platform/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // This file is used by vitest.config.ts for test setup.
2 | // Add global mocks, polyfills, or testing library imports here when needed.
3 | import '@testing-library/jest-dom/vitest'; // Add jest-dom matchers
4 |
5 | // Example for later: import '@testing-library/jest-dom/vitest';
--------------------------------------------------------------------------------
/packages/platform/src/web.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getWebCapabilities } from './web';
3 | import type { OperatingSystem } from '@paynless/types'; // Import for type checking
4 |
5 | // Define possible OS values for checking
6 | const possibleOS: OperatingSystem[] = [
7 | 'windows', 'macos', 'linux', 'ios', 'android', 'unknown'
8 | ];
9 |
10 | describe('getWebCapabilities', () => {
11 | it('should return correct web capabilities structure', () => {
12 | const capabilities = getWebCapabilities();
13 |
14 | // Check platform
15 | expect(capabilities.platform).toBe('web');
16 |
17 | // Check filesystem
18 | expect(capabilities.fileSystem.isAvailable).toBe(false);
19 | // Ensure no FS methods are present (type safety check)
20 | expect((capabilities.fileSystem as any).readFile).toBeUndefined();
21 |
22 | // Check OS (should be one of the valid types, likely 'unknown' in test env)
23 | expect(possibleOS).toContain(capabilities.os);
24 | });
25 | });
--------------------------------------------------------------------------------
/packages/platform/src/web.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | CapabilityUnavailable,
3 | OperatingSystem,
4 | } from '@paynless/types';
5 |
6 | // Web platform provides no special filesystem capabilities via this service by default.
7 | // Standard file inputs or future Web File System Access API would be handled differently.
8 |
9 | const unavailableFileSystem: CapabilityUnavailable = {
10 | isAvailable: false,
11 | } as const;
12 |
13 | // Define a structure specifically for the web provider's return value
14 | // It provides the fileSystem part, aligning with how index.ts uses it.
15 | interface WebCapabilitiesResult {
16 | platform: 'web';
17 | os: OperatingSystem;
18 | fileSystem: CapabilityUnavailable;
19 | }
20 |
21 | export const getWebCapabilities = (): WebCapabilitiesResult => {
22 | // Basic OS detection for web
23 | let detectedOs: OperatingSystem = 'unknown';
24 | if (typeof navigator !== 'undefined') {
25 | const platform = navigator.platform.toLowerCase();
26 | if (platform.startsWith('win')) detectedOs = 'windows';
27 | else if (platform.startsWith('mac')) detectedOs = 'macos';
28 | else if (platform.startsWith('linux')) detectedOs = 'linux';
29 | else if (/iphone|ipad|ipod/.test(platform)) detectedOs = 'ios';
30 | else if (/android/.test(platform)) detectedOs = 'android';
31 | }
32 |
33 | return {
34 | platform: 'web',
35 | os: detectedOs, // Include detected OS, even if it's 'unknown'
36 | fileSystem: unavailableFileSystem, // Use the typed object
37 | // Other capabilities would also be CapabilityUnavailable here
38 | };
39 | };
--------------------------------------------------------------------------------
/packages/platform/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "composite": true,
7 | // Ensure JSX is handled correctly if using React components within this package, even for context
8 | "jsx": "react-jsx",
9 | // Add specific lib entries if needed, e.g., for DOM types if manipulating window
10 | "lib": ["ESNext", "DOM"],
11 | // Specify types needed for testing environment
12 | "types": ["node", "vitest/globals"]
13 | },
14 | "include": ["src/**/*"], // Restore original broad include
15 | "exclude": [
16 | "node_modules",
17 | "dist",
18 | "**/*.test.ts",
19 | "**/*.test.tsx"
20 | ],
21 | "references": [
22 | // Add references to other workspace packages this one depends on
23 | { "path": "../types" }
24 | ]
25 | }
--------------------------------------------------------------------------------
/packages/platform/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import react from '@vitejs/plugin-react'; // Needed if testing React components
3 |
4 | // https://vitejs.dev/config/
5 | // https://vitest.dev/config/
6 | export default defineConfig({
7 | plugins: [react()], // Include react plugin if testing components
8 | test: {
9 | globals: true, // Use global APIs (describe, it, expect, etc.)
10 | environment: 'jsdom', // Simulate browser environment
11 | setupFiles: ['./src/setupTests.ts'], // Add setup files if needed (e.g., for polyfills, global mocks)
12 | // Add any other Vitest specific configurations here
13 | // deps: { ... } // Might be needed for linked workspace deps later
14 | },
15 | });
--------------------------------------------------------------------------------
/packages/store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/store",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "Zustand state management stores for Paynless framework",
6 | "type": "module",
7 | "main": "./dist/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | ".": {
11 | "import": "./dist/index.js",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "scripts": {
16 | "build": "tsc -b",
17 | "clean": "rm -rf dist tsconfig.tsbuildinfo",
18 | "lint": "eslint src/**/*.{ts,tsx} --max-warnings 0",
19 | "test": "vitest run"
20 | },
21 | "dependencies": {
22 | "@paynless/analytics": "workspace:*",
23 | "@paynless/api": "workspace:*",
24 | "@paynless/types": "workspace:*",
25 | "@paynless/utils": "workspace:*",
26 | "@supabase/supabase-js": "^2.0.0",
27 | "immer": "^10.1.1",
28 | "jwt-decode": "^4.0.0",
29 | "reselect": "^5.1.1",
30 | "zustand": "^4.5.2"
31 | },
32 | "devDependencies": {
33 | "@paynless/db-types": "workspace:^",
34 | "@testing-library/react": "^16.3.0",
35 | "@types/node": "^20.11.24",
36 | "@typescript-eslint/eslint-plugin": "^7.18.0",
37 | "@typescript-eslint/parser": "^7.18.0",
38 | "eslint": "^8.57.0",
39 | "typescript": "^5.5.3",
40 | "vite-plugin-node-polyfills": "^0.23.0",
41 | "vitest": "^1.6.1",
42 | "vitest-localstorage-mock": "^0.1.2"
43 | },
44 | "license": "ISC"
45 | }
--------------------------------------------------------------------------------
/packages/store/src/index.ts:
--------------------------------------------------------------------------------
1 | // Export all stores from the store package
2 | export * from './authStore';
3 | export * from './subscriptionStore';
4 | export * from './aiStore';
5 | export * from './notificationStore';
6 | export * from './organizationStore';
7 |
8 | // Export selectors
9 | export * from './aiStore.selectors';
10 | export * from './organizationStore.selectors';
11 | export * from './subscriptionStore.selectors';
12 | // Add other selector exports if they exist and are needed externally
13 |
14 | // Explicitly export types used by other packages
15 | export type { SubscriptionStore } from './subscriptionStore';
16 | // export type { useNotificationStore } from './notificationStore'; // Removed redundant/conflicting type export
17 |
18 | // Add other necessary type exports here if needed
--------------------------------------------------------------------------------
/packages/store/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "declarationDir": "./dist",
7 | "lib": ["ESNext", "DOM"],
8 | "types": ["vite/client"],
9 | "composite": true
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": [
13 | "node_modules",
14 | "dist",
15 | "src/**/*.test.ts",
16 | "src/**/*.test.tsx"
17 | ],
18 | "references": [
19 | { "path": "../types" },
20 | { "path": "../utils" },
21 | { "path": "../api" },
22 | { "path": "../analytics" }
23 | ]
24 | }
--------------------------------------------------------------------------------
/packages/store/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import path from 'path';
4 |
5 | export default defineConfig({
6 | test: {
7 | globals: true, // Enable global test APIs (describe, it, etc.)
8 | environment: 'jsdom', // Simulate browser for fetch, localStorage etc.
9 | setupFiles: ['vitest-localstorage-mock'], // Add the mock setup file
10 | mockReset: false, // Recommended by the mock library
11 | // Optional: alias configuration if needed for imports
12 | alias: {
13 | '@paynless/types': path.resolve(__dirname, '../types/src'),
14 | '@paynless/utils': path.resolve(__dirname, '../utils/src'),
15 | // '@paynless/api': path.resolve(__dirname, '../api/src'), // Removed to allow standard package resolution
16 | '@paynless/analytics': path.resolve(__dirname, '../analytics/src'),
17 | },
18 | },
19 | });
--------------------------------------------------------------------------------
/packages/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/types",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "Shared TypeScript types for Paynless framework",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "types": "./src/index.ts",
9 | "exports": {
10 | ".": "./src/index.ts"
11 | },
12 | "scripts": {
13 | "lint": "eslint src/**/*.ts --max-warnings 0",
14 | "lint:fix": "eslint src/**/*.ts --fix",
15 | "build": "tsc -b",
16 | "clean": "rm -rf dist tsconfig.tsbuildinfo"
17 | },
18 | "devDependencies": {
19 | "@paynless/db-types": "workspace:^",
20 | "@supabase/supabase-js": "^2",
21 | "@types/react": "^18",
22 | "@typescript-eslint/eslint-plugin": "^7.18.0",
23 | "@typescript-eslint/parser": "^7.18.0",
24 | "eslint": "^8.57.0",
25 | "typescript": "^5.3.3"
26 | },
27 | "license": "ISC",
28 | "dependencies": {
29 | "@supabase/postgrest-js": "^1.19.4",
30 | "vite-plugin-node-polyfills": "^0.23.0"
31 | }
32 | }
--------------------------------------------------------------------------------
/packages/types/src/analytics.types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generic interface for an Analytics Client.
3 | * Defines common methods needed by the application.
4 | */
5 | export interface AnalyticsClient {
6 | /**
7 | * Initializes the analytics provider if needed (some may init on import).
8 | * Configuration should typically be handled via environment variables read during service instantiation.
9 | */
10 | // init?(): void; // REMOVED - Initialization is internal to the service
11 |
12 | /**
13 | * Associates the current user with a unique ID and sets user properties (traits).
14 | * @param userId - The unique identifier for the user.
15 | * @param traits - Optional key-value pairs of user properties.
16 | */
17 | identify(userId: string, traits?: Record): void;
18 |
19 | /**
20 | * Tracks a custom event occurrence.
21 | * @param eventName - The name of the event to track.
22 | * @param properties - Optional key-value pairs providing context for the event.
23 | */
24 | track(eventName: string, properties?: Record): void;
25 |
26 | /**
27 | * Clears the identified user and resets analytics state (e.g., on logout).
28 | */
29 | reset(): void;
30 |
31 | /**
32 | * (Optional) Opts the user into tracking. Behavior depends on provider implementation.
33 | */
34 | optInTracking?(): void;
35 |
36 | /**
37 | * (Optional) Opts the user out of tracking. Behavior depends on provider implementation.
38 | */
39 | optOutTracking?(): void;
40 |
41 | /**
42 | * (Optional) Checks if a specific feature flag is enabled for the current user.
43 | * NOTE: Feature flag abstraction can be complex. This is a placeholder.
44 | * @param key - The key of the feature flag.
45 | * @returns True if the feature is enabled, false otherwise.
46 | */
47 | isFeatureEnabled?(key: string): boolean;
48 |
49 | // Potentially add other common methods like group, setPeopleProperties, etc. if needed
50 | }
51 |
52 | /**
53 | * Configuration options for the analytics service.
54 | */
55 | export interface AnalyticsConfig {
56 | provider: 'posthog' | 'mixpanel' | 'none'; // Extend as needed
57 | posthogApiKey?: string;
58 | posthogApiHost?: string;
59 | mixpanelToken?: string;
60 | // Add other provider-specific config keys
61 | }
--------------------------------------------------------------------------------
/packages/types/src/email.types.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Represents the standard user data structure for email marketing services.
4 | * From packages/types/src/email.types.ts
5 | */
6 | export interface UserData {
7 | id: string; // Your internal user ID
8 | email: string;
9 | firstName?: string;
10 | lastName?: string;
11 | createdAt: string; // ISO string format recommended
12 | lastSignInAt?: string; // ISO string format
13 | // Add other standard fields you might want to sync
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | [key: string]: any; // Allows for platform-specific custom fields
16 | }
17 |
18 | // --- Email Marketing Service Interface ---
19 | // From packages/types/src/email.types.ts
20 |
21 | /**
22 | * Defines the common contract for interacting with different email marketing platforms.
23 | */
24 | export interface EmailMarketingService {
25 | /**
26 | * Adds a new user/subscriber to the primary list/audience/tag.
27 | * @param userData - The user's details.
28 | */
29 | addUserToList(userData: UserData): Promise;
30 |
31 | /**
32 | * Updates attributes/custom fields for an existing user/subscriber.
33 | * Typically identified by email or their ID in the marketing platform.
34 | * @param email - The user's email address to identify them.
35 | * @param attributes - An object containing the fields to update.
36 | */
37 | updateUserAttributes(email: string, attributes: Partial): Promise;
38 |
39 | /**
40 | * (Optional but recommended for advanced segmentation)
41 | * Tracks a specific event performed by the user.
42 | * @param email - The user's email address.
43 | * @param eventName - The name of the event (e.g., 'Subscription Upgraded').
44 | * @param properties - Optional additional data about the event.
45 | */
46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
47 | trackEvent?(email: string, eventName: string, properties?: Record): Promise;
48 |
49 | /**
50 | * (Optional) Removes a user from the list/unsubscribes them.
51 | * Usually handled by the platform's unsubscribe links, but useful for manual removal.
52 | * @param email - The user's email address.
53 | */
54 | removeUser?(email: string): Promise;
55 | }
--------------------------------------------------------------------------------
/packages/types/src/index.ts:
--------------------------------------------------------------------------------
1 | // Export all types from the types package
2 | export * from './auth.types';
3 | export * from './subscription.types';
4 | export * from './theme.types';
5 | export * from './route.types';
6 | export * from './api.types';
7 | export * from './ai.types';
8 | export * from './platform.types';
9 | export * from './analytics.types';
10 | export * from './email.types';
11 | export * from './notification.types';
12 | export * from './navigation.types';
13 | export * from './organizations.types'; // This now exports all org+store types
14 | export * from './logger.types';
15 | export * from './supabase.types';
16 |
17 | // Export Json type if it's defined elsewhere or define basic alias
18 | // If Json is complex and defined in its own file (e.g., json.types.ts), export that:
19 | // export * from './json.types';
20 | // Otherwise, a simple alias might suffice if not already defined:
21 | export type Json =
22 | | string
23 | | number
24 | | boolean
25 | | null
26 | | { [key: string]: Json | undefined }
27 | | Json[];
28 |
29 | // Ensure vite-env types are handled if needed, maybe not exported directly
30 | // ///
31 |
32 | // Re-export the Vite environment types (they're ambient declarations)
33 | export {};
34 |
35 | // This file serves as a reference to vite-env.d.ts which provides ambient declarations
36 | // for import.meta.env across all packages
37 |
38 | // [NEW] Notification Types
39 | // export interface Notification {
40 | // id: string; // UUID
41 | // user_id: string; // UUID
42 | // type: string; // e.g., 'join_request', 'invite_sent'
43 | // data?: Record | null; // Stores context like target_path, org_id, etc.
44 | // read: boolean;
45 | // created_at: string; // ISO timestamp string
46 | // }
47 |
48 | // [NEW] Add Notification related types to ApiClient interface if defined, or create specific interfaces
49 | // export interface NotificationApiClientInterface { ... }
--------------------------------------------------------------------------------
/packages/types/src/logger.types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Interface describing the public contract of a Logger instance.
3 | */
4 | export interface ILogger {
5 | debug: (message: string, metadata?: LogMetadata) => void;
6 | info: (message: string, metadata?: LogMetadata) => void;
7 | warn: (message: string, metadata?: LogMetadata) => void;
8 | error: (message: string | Error, metadata?: LogMetadata) => void;
9 | // setLogLevel?: (level: LogLevel) => void; // Example if needed
10 | }
11 |
12 | /**
13 | * Logging levels
14 | */
15 | export enum LogLevel {
16 | DEBUG = 'debug',
17 | INFO = 'info',
18 | WARN = 'warn',
19 | ERROR = 'error',
20 | }
21 |
22 | /**
23 | * Configuration for the logger
24 | */
25 | export interface LoggerConfig {
26 | minLevel: LogLevel;
27 | enableConsole: boolean;
28 | captureErrors: boolean;
29 | }
30 |
31 | /**
32 | * Interface for log entry metadata
33 | */
34 | export interface LogMetadata {
35 | [key: string]: unknown;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/types/src/navigation.types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Placeholder type for a navigation function, typically provided by a router library.
3 | * It accepts a path string and navigates the user.
4 | */
5 | export type NavigateFunction = (path: string) => void;
--------------------------------------------------------------------------------
/packages/types/src/notification.types.ts:
--------------------------------------------------------------------------------
1 | import type { Database, Json } from '@paynless/db-types';
2 |
3 | /**
4 | * Defines the expected structure for the `data` JSONB field
5 | * within a Notification row.
6 | */
7 | export interface NotificationData {
8 | subject?: string;
9 | message?: string;
10 | target_path?: string;
11 | // Add other potential common fields here
12 | [key: string]: Json | undefined; // Allow other dynamic properties from DB JSON
13 | }
14 |
15 | /**
16 | * Represents an in-app notification for a user.
17 | * Based on the `notifications` table but refines the `data` field type.
18 | */
19 | // Omit the original 'data' field and intersect with our refined version
20 | export type Notification =
21 | Omit
22 | & { data: NotificationData | null };
23 |
24 | // Add other notification-related types here in the future,
25 | // ensuring they don't duplicate DB schema types.
26 | // export type NotificationType = 'join_request' | 'invite_sent' | 'role_changed' | 'mention' | 'system_message';
27 | // export interface JoinRequestData { ... }
--------------------------------------------------------------------------------
/packages/types/src/platform.types.ts:
--------------------------------------------------------------------------------
1 | // Represents the *absence* of a specific capability group
2 | export interface CapabilityUnavailable {
3 | readonly isAvailable: false;
4 | }
5 |
6 | // Define explicit platform and OS types for clarity
7 | export type PlatformType = 'web' | 'tauri' | 'react-native' | 'unknown';
8 | export type OperatingSystem = 'windows' | 'macos' | 'linux' | 'ios' | 'android' | 'unknown';
9 |
10 | // Define the capabilities for file system access
11 | export interface FileSystemCapabilities {
12 | readonly isAvailable: true; // Mark explicitly that the FS capability object exists and is functional
13 | readFile: (path: string) => Promise; // Returns file content as byte array
14 | writeFile: (path: string, data: Uint8Array) => Promise; // Writes byte array to file
15 | pickFile: (options?: { accept?: string; multiple?: boolean }) => Promise; // Returns array of paths or null if cancelled
16 | pickDirectory: (options?: { multiple?: boolean }) => Promise; // Returns array of directory paths or null if cancelled
17 | pickSaveFile: (options?: { defaultPath?: string, accept?: string }) => Promise; // Returns single path or null
18 | // Add other relevant FS operations as needed (e.g., readDir, exists, deleteFile)
19 | }
20 |
21 | // Define the main interface aggregating all platform capabilities
22 | export interface PlatformCapabilities {
23 | readonly platform: PlatformType;
24 | readonly os: OperatingSystem; // Determined OS (required)
25 | readonly fileSystem: FileSystemCapabilities | CapabilityUnavailable; // Union type for presence/absence
26 | // Add other future capability groups here using the same pattern:
27 | // Example: readonly windowManagement: WindowManagementCapabilities | CapabilityUnavailable;
28 | // Example: readonly notifications: NotificationCapabilities | CapabilityUnavailable;
29 | }
30 |
31 | // *** Added CapabilitiesContextValue interface ***
32 | export interface CapabilitiesContextValue {
33 | capabilities: PlatformCapabilities | null; // Can be null initially or on error
34 | isLoadingCapabilities: boolean;
35 | capabilityError: Error | null;
36 | }
--------------------------------------------------------------------------------
/packages/types/src/route.types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import type { Database } from '@paynless/db-types';
3 |
4 | // Alias the enum type for easier use
5 | type UserRole = Database['public']['Enums']['user_role'];
6 |
7 | export interface AppRoute {
8 | path: string;
9 | index?: boolean;
10 | element: ReactNode;
11 | requireAuth?: boolean;
12 | allowedRoles?: UserRole[];
13 | children?: AppRoute[];
14 | }
--------------------------------------------------------------------------------
/packages/types/src/supabase.types.ts:
--------------------------------------------------------------------------------
1 | import type { PostgrestSingleResponse } from '@supabase/postgrest-js';
2 | // Assuming 'Database' is the root type exported from your db-types package or similar.
3 | // This import might need adjustment based on how 'Database' is actually exposed.
4 | import type { Database } from '@paynless/db-types'; // Or './db.types' if local
5 |
6 | // Helper to extract Row and Update types from the Database definition
7 | type TableName = keyof Database['public']['Tables'];
8 | type TableRow = Database['public']['Tables'][T]['Row'];
9 | type TableUpdateDTO = Database['public']['Tables'][T]['Update'];
10 |
11 | export interface IQueryBuilder<
12 | TN extends TableName,
13 | TRow = TableRow,
14 | TUpdate = TableUpdateDTO
15 | > {
16 | update: (values: TUpdate) => IQueryBuilder;
17 | eq: (column: K, value: TRow[K]) => IQueryBuilder;
18 | select: (columns?: string) => {
19 | single: () => Promise>;
20 | // maybeSingle?: () => Promise>; // For .maybeSingle()
21 | };
22 | }
23 |
24 | export interface ISupabaseDataClient {
25 | from: (table: TN) => IQueryBuilder;
26 | // rpc?: (fn: string, args?: object) => Promise>; // For future use
27 | }
--------------------------------------------------------------------------------
/packages/types/src/theme.types.ts:
--------------------------------------------------------------------------------
1 | export interface ThemeColors {
2 | primary: string;
3 | secondary: string;
4 | background: string;
5 | surface: string;
6 | textPrimary: string;
7 | textSecondary: string;
8 | border: string;
9 | successBackground: string;
10 | successForeground: string;
11 | attentionBackground: string;
12 | attentionForeground: string;
13 | }
14 |
15 | export interface Theme {
16 | name: string;
17 | colors: ThemeColors;
18 | isDark: boolean;
19 | }
20 |
21 | export type ColorMode = 'light' | 'dark';
22 |
23 | export type ThemeName =
24 | | 'light'
25 | | 'dark'
26 | | 'protanopia'
27 | | 'deuteranopia'
28 | | 'tritanopia'
29 | | 'red'
30 | | 'orange'
31 | | 'yellow'
32 | | 'green'
33 | | 'blue'
34 | | 'indigo'
35 | | 'violet';
36 |
37 | export interface ThemeState {
38 | currentTheme: Theme;
39 | colorMode: ColorMode;
40 | setColorMode: (mode: ColorMode) => void;
41 | setTheme: (themeName: ThemeName) => void;
42 | }
--------------------------------------------------------------------------------
/packages/types/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Type definitions for Vite environment variables
3 | * This provides a single source of truth for all import.meta.env types across packages
4 | */
5 |
6 | // Extend Vite's ImportMeta interface for PUBLIC, VITE_ prefixed variables
7 | declare interface ImportMeta {
8 | readonly env: {
9 | // API and server URLs
10 | readonly VITE_SUPABASE_URL: string;
11 | readonly VITE_SUPABASE_ANON_KEY: string;
12 |
13 | // Stripe-related PUBLIC environment variables
14 | readonly VITE_STRIPE_TEST_MODE: string;
15 | readonly VITE_STRIPE_PUBLISHABLE_KEY_TEST: string;
16 | readonly VITE_STRIPE_PUBLISHABLE_KEY_LIVE: string;
17 | // REMOVED Secret keys - should not be VITE_ prefixed or in ImportMeta
18 |
19 | // Allow any other VITE_ prefixed environment variables
20 | readonly [key: `VITE_${string}`]: string;
21 | };
22 | }
23 |
24 | // Extend Node.js ProcessEnv interface for SERVER-SIDE environment variables
25 | declare namespace NodeJS {
26 | interface ProcessEnv {
27 | // Server-side specific variables
28 | STRIPE_TEST_MODE?: string;
29 | STRIPE_SECRET_KEY_TEST?: string; // Define server-side secrets here
30 | STRIPE_SECRET_KEY_LIVE?: string; // Define server-side secrets here
31 | NODE_ENV?: string;
32 | // Allow any other environment variables on server
33 | [key: string]: string | undefined;
34 | }
35 | }
--------------------------------------------------------------------------------
/packages/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "baseUrl": ".",
7 | "declaration": true,
8 | "lib": ["ESNext", "DOM"],
9 | "composite": true
10 | },
11 | "include": [
12 | "src/**/*"
13 | ],
14 | "references": [
15 | { "path": "../../supabase/functions" }
16 | ],
17 | "exclude": ["node_modules", "dist"]
18 | }
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/utils",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "Shared utility functions for Paynless framework",
6 | "type": "module",
7 | "main": "./src/index.ts",
8 | "types": "./src/index.ts",
9 | "exports": {
10 | ".": "./src/index.ts"
11 | },
12 | "scripts": {
13 | "test": "vitest run",
14 | "test:watch": "vitest",
15 | "lint": "eslint src/**/*.ts --max-warnings 0",
16 | "lint:fix": "eslint src/**/*.ts --fix",
17 | "build": "tsc -b",
18 | "clean": "rm -rf dist tsconfig.tsbuildinfo"
19 | },
20 | "dependencies": {
21 | "@paynless/types": "workspace:*",
22 | "date-fns": "^3.5.0",
23 | "pino": "^9.3.2",
24 | "pino-pretty": "^11.2.1",
25 | "vite-plugin-node-polyfills": "^0.23.0"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20.11.24",
29 | "eslint": "^8.57.0",
30 | "typescript": "^5.3.3",
31 | "vitest": "^1.6.1"
32 | },
33 | "license": "ISC"
34 | }
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | // Export all utilities from the utils package
2 | export * from './logger';
3 | export * from './ai-parsers';
4 | export * from './stringUtils';
5 |
--------------------------------------------------------------------------------
/packages/utils/src/platform.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks if the code is running in a Tauri environment.
3 | * Relies on the presence of the __TAURI__ property on the window object.
4 | */
5 | export function isTauri(): boolean {
6 | // Check for window object first to avoid errors in non-browser environments
7 | return typeof window !== 'undefined' && '__TAURI__' in window;
8 | }
9 |
10 | /**
11 | * Determines the current platform based on the environment.
12 | *
13 | * @returns {'web' | 'tauri'} The detected platform.
14 | */
15 | export function getPlatform(): 'web' | 'tauri' {
16 | // Always check for Tauri first, as it might also have typical web properties
17 | return isTauri() ? 'tauri' : 'web';
18 | }
--------------------------------------------------------------------------------
/packages/utils/src/stringUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Generates initials from first and last names.
3 | *
4 | * @param firstName - The first name (optional).
5 | * @param lastName - The last name (optional).
6 | * @returns A string containing the initials (e.g., "JD") or an empty string if names are not provided.
7 | */
8 | export const getInitials = (
9 | firstName?: string | null,
10 | lastName?: string | null
11 | ): string => {
12 | const firstInitial = firstName?.charAt(0).toUpperCase() || '';
13 | const lastInitial = lastName?.charAt(0).toUpperCase() || '';
14 |
15 | // If only one name is provided, return just that initial
16 | if (firstName && !lastName) return firstInitial;
17 | if (!firstName && lastName) return lastInitial;
18 |
19 | return `${firstInitial}${lastInitial}`;
20 | };
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": "./src",
6 | "declaration": true,
7 | "lib": ["ESNext", "DOM"],
8 | "composite": true,
9 | "types": ["node"]
10 | },
11 | "include": [
12 | "src/**/*"
13 | ],
14 | "exclude": [
15 | "node_modules",
16 | "dist"
17 | ],
18 | "references": [
19 | { "path": "../types" }
20 | ]
21 | }
--------------------------------------------------------------------------------
/packages/utils/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true, // Enable global test APIs (describe, it, etc.)
7 | environment: 'node', // Use Node environment for utils testing
8 | // No setupFiles needed for basic utils unless mocking is required
9 | },
10 | });
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | # pnpm-workspace.yaml
2 | packages:
3 | # Standard packages
4 | - 'packages/*'
5 | # Apps
6 | - 'apps/*'
7 | # Supabase functions (for sharing types)
8 | - 'supabase/functions'
9 | # exclude packages that are inside test directories
10 | - '!**/test/**'
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
5 | # dotenvx
6 | .env.keys
7 | .env.local
8 | .env.*.local
9 |
--------------------------------------------------------------------------------
/supabase/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "importMap": "./import_map.json",
3 | "nodeModulesDir": "auto",
4 | "compilerOptions": {
5 | "jsx": "react-jsx",
6 | "jsxImportSource": "npm:react"
7 | },
8 | "tasks": {
9 | "test": "deno test --allow-all --no-check",
10 | "test:watch": "deno test --allow-all --no-check --watch",
11 | "serve:login": "deno run --allow-all --no-check functions/login/index.ts",
12 | "lint": "deno lint",
13 | "fmt": "deno fmt"
14 | },
15 | "lint": {
16 | "rules": {
17 | "tags": ["recommended"],
18 | "exclude": ["no-explicit-any"]
19 | }
20 | },
21 | "fmt": {
22 | "options": {
23 | "useTabs": false,
24 | "lineWidth": 80,
25 | "indentWidth": 2,
26 | "singleQuote": false,
27 | "proseWrap": "preserve"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/supabase/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Node.js modules directory created by pnpm workspace setup
2 | node_modules/
--------------------------------------------------------------------------------
/supabase/functions/_shared/ai_service/factory.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertExists, assertInstanceOf } from "https://deno.land/std@0.224.0/assert/mod.ts";
2 | import { getAiProviderAdapter } from './factory.ts';
3 | import { OpenAiAdapter, openAiAdapter } from './openai_adapter.ts';
4 | import { AnthropicAdapter, anthropicAdapter } from './anthropic_adapter.ts';
5 | import { GoogleAdapter, googleAdapter } from './google_adapter.ts';
6 |
7 | Deno.test("AI Adapter Factory - getAiProviderAdapter", () => {
8 | // Test known providers (case-insensitive)
9 | const adapterOpenAI = getAiProviderAdapter('openai');
10 | assertExists(adapterOpenAI);
11 | assertInstanceOf(adapterOpenAI, OpenAiAdapter);
12 | assertEquals(adapterOpenAI, openAiAdapter); // Check it returns the exported instance
13 |
14 | const adapterOpenAICased = getAiProviderAdapter('OpenAI');
15 | assertExists(adapterOpenAICased);
16 | assertInstanceOf(adapterOpenAICased, OpenAiAdapter);
17 | assertEquals(adapterOpenAICased, openAiAdapter);
18 |
19 | const adapterAnthropic = getAiProviderAdapter('anthropic');
20 | assertExists(adapterAnthropic);
21 | assertInstanceOf(adapterAnthropic, AnthropicAdapter);
22 | assertEquals(adapterAnthropic, anthropicAdapter);
23 |
24 | const adapterAnthropicCased = getAiProviderAdapter('AnThRoPiC');
25 | assertExists(adapterAnthropicCased);
26 | assertInstanceOf(adapterAnthropicCased, AnthropicAdapter);
27 | assertEquals(adapterAnthropicCased, anthropicAdapter);
28 |
29 | const adapterGoogle = getAiProviderAdapter('google');
30 | assertExists(adapterGoogle);
31 | assertInstanceOf(adapterGoogle, GoogleAdapter);
32 | assertEquals(adapterGoogle, googleAdapter);
33 |
34 | const adapterGoogleCased = getAiProviderAdapter('GOOGLE');
35 | assertExists(adapterGoogleCased);
36 | assertInstanceOf(adapterGoogleCased, GoogleAdapter);
37 | assertEquals(adapterGoogleCased, googleAdapter);
38 |
39 | // Test unknown provider
40 | const adapterUnknown = getAiProviderAdapter('some-other-provider');
41 | assertEquals(adapterUnknown, null);
42 |
43 | // Test empty string
44 | const adapterEmpty = getAiProviderAdapter('');
45 | assertEquals(adapterEmpty, null);
46 | });
--------------------------------------------------------------------------------
/supabase/functions/_shared/email_service/dummy_service.ts:
--------------------------------------------------------------------------------
1 | import { type UserData, type EmailMarketingService } from "../types.ts";
2 | import { logger } from "../logger.ts";
3 |
4 | /**
5 | * A no-operation email marketing service implementation.
6 | * Used when no provider is configured or for local testing.
7 | */
8 | export class DummyEmailService implements EmailMarketingService {
9 | constructor() {
10 | logger.info('DummyEmailService initialized (no email marketing operations will occur).');
11 | }
12 |
13 | async addUserToList(userData: UserData): Promise {
14 | logger.debug('[DummyEmailService] Skipping addUserToList for:', { email: userData.email });
15 | // Do nothing
16 | return Promise.resolve();
17 | }
18 |
19 | async updateUserAttributes(email: string, attributes: Partial): Promise {
20 | logger.debug('[DummyEmailService] Skipping updateUserAttributes for:', { email, attributes: Object.keys(attributes) });
21 | // Do nothing
22 | return Promise.resolve();
23 | }
24 |
25 | async trackEvent(email: string, eventName: string, properties?: Record): Promise {
26 | logger.debug('[DummyEmailService] Skipping trackEvent:', { email, eventName, hasProperties: !!properties });
27 | // Do nothing
28 | return Promise.resolve();
29 | }
30 |
31 | async removeUser(email: string): Promise {
32 | logger.debug('[DummyEmailService] Skipping removeUser for:', { email });
33 | // Do nothing
34 | return Promise.resolve();
35 | }
36 | }
--------------------------------------------------------------------------------
/supabase/functions/chat/_server.ts:
--------------------------------------------------------------------------------
1 | // supabase/functions/chat/_server.ts
2 | // This file contains the Deno-specific HTTP server setup.
3 |
4 | import { serve } from 'https://deno.land/std@0.224.0/http/server.ts'
5 | // Import the main HTTP handler logic and default dependencies
6 | import { mainHandler, defaultDeps } from './index.ts'
7 |
8 | console.log('Initializing chat function server...');
9 |
10 | // Start the server, passing requests to the main handler
11 | serve((req) => mainHandler(req, defaultDeps));
12 |
13 | console.log(`Function "chat" server is up and running!`);
--------------------------------------------------------------------------------
/supabase/functions/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["deno.window", "dom"]
4 | },
5 | "tasks": {
6 | "test": "deno test --allow-env --allow-net"
7 | },
8 | "nodeModulesDir": "auto",
9 | "imports": {
10 | "@types/node": "npm:@types/node@^20"
11 | // "@paynless/db-types": "./types_db.ts"
12 | }
13 | }
--------------------------------------------------------------------------------
/supabase/functions/organizations/index.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertExists, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
2 | import { handleOrganizationRequest } from "./index.ts"; // Use the exported handler
3 | import {
4 | createMockSupabaseClient,
5 | MockSupabaseDataConfig,
6 | MockQueryBuilderState
7 | } from "../_shared/supabase.mock.ts";
8 | import type { SupabaseClient } from "@supabase/supabase-js";
9 | import { User } from "@supabase/supabase-js"; // Import User type if needed for casting
10 |
11 | // Helper to create a mock request
12 | const createMockRequest = (method: string, path: string, body?: Record): Request => {
13 | const headers = new Headers({
14 | 'Content-Type': 'application/json',
15 | });
16 | if (method !== 'GET' && method !== 'HEAD') {
17 | headers.set('Authorization', 'Bearer fake-token'); // Add default auth
18 | }
19 | return new Request(`http://localhost${path}`, {
20 | method,
21 | headers,
22 | body: body ? JSON.stringify(body) : undefined,
23 | });
24 | };
--------------------------------------------------------------------------------
/supabase/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@paynless/db-types",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "dist/types_db.js",
6 | "types": "dist/types_db.d.ts",
7 | "files": [
8 | "dist",
9 | "types_db.ts"
10 | ],
11 | "devDependencies": {
12 | "@supabase/supabase-js": "^2",
13 | "@types/node": "^20",
14 | "typescript": "~5"
15 | },
16 | "scripts": {
17 | "install:npm": "npm install",
18 | "build": "tsc -b tsconfig.json"
19 | }
20 | }
--------------------------------------------------------------------------------
/supabase/functions/ping/index.ts:
--------------------------------------------------------------------------------
1 | // IMPORTANT: Supabase Edge Functions require relative paths for imports from shared modules.
2 | // Do not use path aliases (like @shared/) as they will cause deployment failures.
3 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
4 | // Import shared CORS helpers
5 | import {
6 | handleCorsPreflightRequest,
7 | createSuccessResponse, // Assuming 'pong' is a success
8 | createErrorResponse // For potential future error handling
9 | } from "../_shared/cors-headers.ts";
10 |
11 | console.log("[ping/index.ts] Function loaded.");
12 |
13 | async function handler(req: Request): Promise {
14 | console.log("[ping/index.ts] Request received.");
15 |
16 | // Handle CORS preflight request
17 | const corsResponse = handleCorsPreflightRequest(req);
18 | if (corsResponse) {
19 | return corsResponse;
20 | }
21 |
22 | // Only allow GET method for ping (optional, but good practice)
23 | if (req.method !== 'GET') {
24 | // Pass the request object to createErrorResponse for CORS headers
25 | return createErrorResponse('Method Not Allowed', 405, req);
26 | }
27 |
28 | try {
29 | const data = { message: "pong" };
30 | // Use createSuccessResponse, passing the request object
31 | return createSuccessResponse(data, 200, req);
32 | } catch (err) {
33 | console.error("[ping/index.ts] Error creating response:", err);
34 | // Pass the request object to createErrorResponse for CORS headers
35 | return createErrorResponse(
36 | err instanceof Error ? err.message : "Internal server error",
37 | 500,
38 | req,
39 | err // Pass the original error for logging
40 | );
41 | }
42 | }
43 |
44 | serve(handler);
--------------------------------------------------------------------------------
/supabase/functions/ping/ping.integration.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertExists } from "https://deno.land/std@0.192.0/testing/asserts.ts";
2 | import { startSupabase, stopSupabase } from "../_shared/supabase.mock.ts";
3 |
4 | const PING_URL = "http://localhost:54321/functions/v1/ping";
5 |
6 | Deno.test("/ping Integration Test", async () => {
7 | await startSupabase();
8 |
9 | console.log(`[Test] Calling: ${PING_URL}`);
10 | const response = await fetch(PING_URL, {
11 | method: "GET", // Or POST, shouldn't matter for this simple function
12 | // NO apikey or Authorization headers
13 | });
14 | console.log(`[Test] Response Status: ${response.status}`);
15 |
16 | assertEquals(response.status, 200, `Expected 200 OK, got ${response.status}`);
17 | const body = await response.json();
18 | assertExists(body, "Response body should exist");
19 | assertEquals(body.message, "pong", "Response message should be pong");
20 |
21 | await stopSupabase();
22 | });
--------------------------------------------------------------------------------
/supabase/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": ".", // Considers all files in supabase/functions as potential root
6 | "declaration": true,
7 | "declarationMap": true,
8 | "composite": true,
9 | // We only want to emit declarations, actual JS might not be needed if it's just types
10 | // but tsc might require it with composite.
11 | "emitDeclarationOnly": false, // Set to true if types_db.ts TRULY has no runtime code
12 | "module": "ESNext", // Or "CommonJS" depending on expected output, ESNext is modern
13 | "target": "ESNext"
14 | },
15 | "include": [
16 | "types_db.ts" // Only include this file for this specific build purpose
17 | ],
18 | "exclude": [
19 | "node_modules",
20 | "dist",
21 | // Exclude other Deno function folders if they exist at the root of supabase/functions
22 | // to prevent this tsconfig from trying to compile them.
23 | // For example: "chat", "chat-details", etc. if they are top-level directories.
24 | // If they are nested under index.ts files or similar, this might not be an issue.
25 | "**/*.test.ts", // Exclude test files
26 | "**/*.integration.ts", // Exclude integration test files
27 | "**/*.deno.ts" // Exclude Deno specific test files
28 | // Add other patterns for Deno files if necessary
29 | ]
30 | }
--------------------------------------------------------------------------------
/supabase/import_map.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "@shared/": "./functions/_shared/",
4 | "@supabase/supabase-js": "npm:@supabase/supabase-js@^2.43.4",
5 | "stripe": "npm:stripe@^14.11.0",
6 | "@paynless/utils": "../packages/utils/src/index.ts",
7 | "@paynless/utils/": "../packages/utils/src/",
8 | "@supabase/types": "./functions/types_db.ts",
9 | "@paynless/types": "../packages/types/src/index.ts",
10 | "@paynless/db-types": "./functions/types_db.ts",
11 | "jsr:@std/assert": "jsr:@std/assert@^0.225.3",
12 | "jsr:@std/testing/bdd": "jsr:@std/testing@0.225.1/bdd",
13 | "jsr:@std/testing/mock": "jsr:@std/testing@0.225.1/mock"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/supabase/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supabase",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/supabase/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@supabase/auth-js": "^2.69.1",
4 | "@supabase/functions-js": "^2.4.4",
5 | "@supabase/node-fetch": "^2.6.13",
6 | "@supabase/postgrest-js": "^1.19.4",
7 | "@supabase/realtime-js": "^2.11.7",
8 | "@supabase/storage-js": "^2.7.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/supabase/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supabase",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "root": "supabase",
5 | "projectType": "application",
6 | "sourceRoot": "supabase/functions",
7 | "targets": {
8 | "test": {
9 | "executor": "nx:run-commands",
10 | "options": {
11 | "command": "deno test -A --no-check > supabase.test.output.md 2>&1",
12 | "cwd": "supabase"
13 | },
14 | "cache": true
15 | },
16 | "lint": {
17 | "executor": "nx:run-commands",
18 | "options": {
19 | "command": "deno lint",
20 | "cwd": "supabase"
21 | },
22 | "cache": true
23 | },
24 | "serve": {
25 | "executor": "nx:run-commands",
26 | "options": {
27 | "command": "supabase functions serve --env-file ./supabase/.env.local",
28 | "cwd": "."
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noImplicitAny": true,
5 | "strictNullChecks": true,
6 | "strictFunctionTypes": true,
7 | "strictBindCallApply": true,
8 | "strictPropertyInitialization": true,
9 | "noImplicitThis": true,
10 | "useUnknownInCatchVariables": true,
11 | "alwaysStrict": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "exactOptionalPropertyTypes": false,
15 | "noImplicitReturns": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "noUncheckedIndexedAccess": false,
18 | "noImplicitOverride": true,
19 | "noPropertyAccessFromIndexSignature": true,
20 | "module": "ESNext",
21 | "moduleResolution": "node",
22 | "baseUrl": ".",
23 | "resolveJsonModule": true,
24 | "declaration": true,
25 | "declarationMap": true,
26 | "sourceMap": true,
27 | "outDir": "dist",
28 | "removeComments": false,
29 | "composite": true,
30 | "incremental": true,
31 | "allowJs": false,
32 | "checkJs": false,
33 | "isolatedModules": true,
34 | "allowSyntheticDefaultImports": true,
35 | "esModuleInterop": true,
36 | "forceConsistentCasingInFileNames": true,
37 | "target": "ESNext",
38 | "lib": [
39 | "ESNext"
40 | ],
41 | "jsx": "react-jsx",
42 | "useDefineForClassFields": true,
43 | "skipLibCheck": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "packages/types" },
5 | { "path": "packages/utils" },
6 | { "path": "packages/api" },
7 | { "path": "packages/store" },
8 | { "path": "apps/web" },
9 | { "path": "packages/platform" },
10 | { "path": "packages/analytics" }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true, // Use global APIs like describe, it, expect
7 | environment: 'node', // Environment for running tests (node is suitable for integration tests using fetch)
8 | include: [
9 | 'supabase/functions/**/test/**/*.integration.test.ts' // Pattern to find integration tests
10 | ],
11 | // Optional: Add setup files if needed later
12 | // setupFiles: ['./path/to/integration/setup.ts'],
13 | testTimeout: 30000, // Increase timeout for potentially longer integration tests
14 | hookTimeout: 30000, // Increase timeout for hooks like beforeAll/afterAll
15 | threads: false, // Run integration tests sequentially to avoid DB conflicts if setup/teardown isn't perfectly isolated
16 | logHeapUsage: true // Helps debug memory leaks
17 | },
18 | });
--------------------------------------------------------------------------------