vibefast-cli 1.1.5 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +63 -169
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +547 -589
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/checklist.d.ts +1 -1
- package/dist/commands/checklist.d.ts.map +1 -1
- package/dist/commands/checklist.js +40 -39
- package/dist/commands/checklist.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +22 -22
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/env.d.ts +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +58 -53
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/health.d.ts +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +101 -93
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +416 -296
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/remove.d.ts +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +77 -64
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/core/__tests__/detect.test.js +68 -34
- package/dist/core/__tests__/detect.test.js.map +1 -1
- package/dist/core/ast.d.ts +14 -0
- package/dist/core/ast.d.ts.map +1 -0
- package/dist/core/ast.js +239 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/core/codemod.d.ts.map +1 -1
- package/dist/core/codemod.js +62 -44
- package/dist/core/codemod.js.map +1 -1
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +51 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/detect.d.ts +8 -2
- package/dist/core/detect.d.ts.map +1 -1
- package/dist/core/detect.js +52 -21
- package/dist/core/detect.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -8
- package/dist/core/errors.js.map +1 -1
- package/dist/core/exec.d.ts +16 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +48 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/manualSteps.d.ts +7 -0
- package/dist/core/manualSteps.d.ts.map +1 -0
- package/dist/core/manualSteps.js +59 -0
- package/dist/core/manualSteps.js.map +1 -0
- package/dist/core/paths.d.ts +3 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +14 -10
- package/dist/core/paths.js.map +1 -1
- package/dist/core/spinner.d.ts +1 -1
- package/dist/core/spinner.d.ts.map +1 -1
- package/dist/core/spinner.js +38 -8
- package/dist/core/spinner.js.map +1 -1
- package/dist/core/vosk.d.ts.map +1 -1
- package/dist/core/vosk.js +50 -39
- package/dist/core/vosk.js.map +1 -1
- package/docs/manual-testing.md +91 -0
- package/package.json +6 -3
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/recipe.json +3 -3
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
- package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
- package/recipes/audio-recorder-supabase/recipe.json +35 -0
- package/recipes/audio-recorder-supabase@latest.zip +0 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
- package/recipes/charts/recipe.json +4 -13
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
- package/recipes/chatbot/recipe.json +26 -92
- package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
- package/recipes/chatbot-supabase/recipe.json +79 -0
- package/recipes/chatbot-supabase@latest.zip +0 -0
- package/recipes/chatbot.zip +0 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis.ts +0 -1
- package/recipes/image-analysis/recipe.json +15 -55
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
- package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
- package/recipes/image-analysis-supabase/recipe.json +57 -0
- package/recipes/image-analysis-supabase@latest.zip +0 -0
- package/recipes/image-analysis@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
- package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
- package/recipes/image-generator/recipe.json +16 -39
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
- package/recipes/image-generator-supabase/recipe.json +59 -0
- package/recipes/image-generator-supabase@latest.zip +0 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/ios-widget/recipe.json +15 -24
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
- package/recipes/onboarding/recipe.json +15 -0
- package/recipes/onboarding@latest.zip +0 -0
- package/recipes/payments/recipe.json +28 -61
- package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
- package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
- package/recipes/payments-supabase/recipe.json +51 -0
- package/recipes/payments-supabase@latest.zip +0 -0
- package/recipes/payments@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
- package/recipes/quiz/recipe.json +6 -9
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
- package/recipes/tracker-app/recipe.json +7 -10
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/recipe.json +8 -68
- package/recipes/voice-bot.zip +0 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/recipes/wake-word/recipe.json +10 -9
- package/recipes/wake-word.zip +0 -0
- package/recipes/wake-word@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
- package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
- package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
- package/recipes/image-analysis/packages/backend/convex/imageAnalysisFunctions.ts +0 -325
- package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
- package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
- package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
- package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
- package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
- /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
- /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
- /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/index.tsx +0 -0
package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import Purchases, {
|
|
3
|
+
LOG_LEVEL,
|
|
4
|
+
type PurchasesOffering,
|
|
5
|
+
type PurchasesPackage,
|
|
6
|
+
} from 'react-native-purchases';
|
|
7
|
+
|
|
8
|
+
import { ConfigService } from '@/core/config';
|
|
9
|
+
import * as Logger from '@/core/logging';
|
|
10
|
+
import type {
|
|
11
|
+
PaymentService,
|
|
12
|
+
ProductOffering,
|
|
13
|
+
} from '@/core/payments/payment-service';
|
|
14
|
+
|
|
15
|
+
// Ensure a log handler is registered ASAP to avoid
|
|
16
|
+
// "TypeError: customLogHandler is not a function" on Android Hermes
|
|
17
|
+
try {
|
|
18
|
+
// If a handler is already set later via configure, this will be overwritten.
|
|
19
|
+
Purchases.setLogHandler((logLevel, message) => {
|
|
20
|
+
switch (logLevel) {
|
|
21
|
+
case LOG_LEVEL.DEBUG:
|
|
22
|
+
// console.debug(`[RevenueCat] ${message}`);
|
|
23
|
+
break;
|
|
24
|
+
case LOG_LEVEL.INFO:
|
|
25
|
+
// console.info(`[RevenueCat] ${message}`);
|
|
26
|
+
break;
|
|
27
|
+
case LOG_LEVEL.WARN:
|
|
28
|
+
// console.warn(`[RevenueCat] ${message}`);
|
|
29
|
+
break;
|
|
30
|
+
case LOG_LEVEL.ERROR:
|
|
31
|
+
// console.error(`[RevenueCat] ${message}`);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
// console.log(`[RevenueCat] ${message}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
// best-effort: ignore if native module isn't ready yet
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* RevenueCat adapter service implementing the PaymentService interface
|
|
43
|
+
*/
|
|
44
|
+
export class RevenueCatAdapter implements PaymentService {
|
|
45
|
+
private static isInitialized = false;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the RevenueCat SDK
|
|
49
|
+
* Should be called once during app startup
|
|
50
|
+
*/
|
|
51
|
+
static async initialize(): Promise<void> {
|
|
52
|
+
if (RevenueCatAdapter.isInitialized) {
|
|
53
|
+
Logger.info('[RevenueCatAdapter] SDK already initialized, skipping');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Check if RevenueCat is already configured (Fast Refresh protection)
|
|
59
|
+
try {
|
|
60
|
+
await Purchases.getCustomerInfo();
|
|
61
|
+
Logger.info(
|
|
62
|
+
'[RevenueCatAdapter] RevenueCat already configured from previous session',
|
|
63
|
+
);
|
|
64
|
+
RevenueCatAdapter.isInitialized = true;
|
|
65
|
+
return;
|
|
66
|
+
} catch {
|
|
67
|
+
// Not configured yet, proceed with configuration
|
|
68
|
+
Logger.debug(
|
|
69
|
+
'[RevenueCatAdapter] RevenueCat not configured, proceeding with initialization',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get the appropriate API key based on platform
|
|
74
|
+
const apiKey = Platform.select({
|
|
75
|
+
ios: ConfigService.getRevenueCatApiKeyApple(),
|
|
76
|
+
android: ConfigService.getRevenueCatApiKeyGoogle(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
Logger.warn(
|
|
81
|
+
`[RevenueCatAdapter] No API key found for platform: ${Platform.OS}`,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set debug logs level (only in development)
|
|
87
|
+
if (ConfigService.getAppEnv() !== 'production') {
|
|
88
|
+
Purchases.setLogLevel(LOG_LEVEL.DEBUG);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Configure and initialize the SDK
|
|
92
|
+
await Purchases.configure({ apiKey });
|
|
93
|
+
|
|
94
|
+
RevenueCatAdapter.isInitialized = true;
|
|
95
|
+
Logger.info('[RevenueCatAdapter] SDK initialized successfully');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
Logger.error(
|
|
98
|
+
'[RevenueCatAdapter] Failed to initialize SDK: ' + String(error),
|
|
99
|
+
);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if the SDK is initialized
|
|
106
|
+
*/
|
|
107
|
+
static getIsInitialized(): boolean {
|
|
108
|
+
return RevenueCatAdapter.isInitialized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch product offerings from RevenueCat
|
|
113
|
+
*/
|
|
114
|
+
async fetchProductOfferings(
|
|
115
|
+
_configType: 'local' | 'remote',
|
|
116
|
+
): Promise<ProductOffering[]> {
|
|
117
|
+
this.ensureInitialized();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const offerings = await Purchases.getOfferings();
|
|
121
|
+
Logger.debug('[RevenueCatAdapter] Offerings retrieved');
|
|
122
|
+
|
|
123
|
+
const productOfferings: ProductOffering[] = [];
|
|
124
|
+
|
|
125
|
+
// Process current offering
|
|
126
|
+
if (offerings.current) {
|
|
127
|
+
const currentOffering = offerings.current;
|
|
128
|
+
|
|
129
|
+
// Convert packages to ProductOffering format
|
|
130
|
+
Object.values(currentOffering.availablePackages).forEach(
|
|
131
|
+
(pkg: PurchasesPackage) => {
|
|
132
|
+
productOfferings.push({
|
|
133
|
+
id: pkg.identifier,
|
|
134
|
+
priceString: pkg.product.priceString,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Process all offerings if needed
|
|
141
|
+
Object.values(offerings.all).forEach((offering: PurchasesOffering) => {
|
|
142
|
+
Object.values(offering.availablePackages).forEach(
|
|
143
|
+
(pkg: PurchasesPackage) => {
|
|
144
|
+
// Avoid duplicates
|
|
145
|
+
if (!productOfferings.find((p) => p.id === pkg.identifier)) {
|
|
146
|
+
productOfferings.push({
|
|
147
|
+
id: pkg.identifier,
|
|
148
|
+
priceString: pkg.product.priceString,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return productOfferings;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
Logger.error(
|
|
158
|
+
'[RevenueCatAdapter] Failed to fetch offerings: ' + String(error),
|
|
159
|
+
);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Purchase a product
|
|
166
|
+
*/
|
|
167
|
+
async purchaseProduct(
|
|
168
|
+
productId: string,
|
|
169
|
+
offeringIdentifier?: string,
|
|
170
|
+
onConsumablePurchase?: (details: {
|
|
171
|
+
productId: string;
|
|
172
|
+
quantity: number;
|
|
173
|
+
}) => Promise<void>,
|
|
174
|
+
): Promise<{ success: boolean; error?: string; customerInfo?: any }> {
|
|
175
|
+
this.ensureInitialized();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Get offerings to find the package
|
|
179
|
+
const offerings = await Purchases.getOfferings();
|
|
180
|
+
let packageToPurchase: PurchasesPackage | null = null;
|
|
181
|
+
|
|
182
|
+
// Find the package by productId
|
|
183
|
+
if (offeringIdentifier && offerings.all[offeringIdentifier]) {
|
|
184
|
+
packageToPurchase =
|
|
185
|
+
offerings.all[offeringIdentifier].availablePackages.find(
|
|
186
|
+
(pkg) => pkg.identifier === productId,
|
|
187
|
+
) || null;
|
|
188
|
+
} else if (offerings.current) {
|
|
189
|
+
packageToPurchase =
|
|
190
|
+
offerings.current.availablePackages.find(
|
|
191
|
+
(pkg) => pkg.identifier === productId,
|
|
192
|
+
) || null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!packageToPurchase) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: `Package with ID ${productId} not found`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { customerInfo } =
|
|
203
|
+
await Purchases.purchasePackage(packageToPurchase);
|
|
204
|
+
Logger.info(`[RevenueCatAdapter] Purchase successful: ${productId}`);
|
|
205
|
+
|
|
206
|
+
// Check if this is a consumable product and handle it
|
|
207
|
+
if (onConsumablePurchase && this.isConsumableProduct(productId)) {
|
|
208
|
+
const quantity = this.getConsumableQuantity(productId);
|
|
209
|
+
try {
|
|
210
|
+
await onConsumablePurchase({ productId, quantity });
|
|
211
|
+
Logger.info(
|
|
212
|
+
`[RevenueCatAdapter] Consumable purchase recorded: ${productId} (${quantity} credits)`,
|
|
213
|
+
);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
Logger.error(
|
|
216
|
+
`[RevenueCatAdapter] Failed to record consumable purchase: ${String(error)}`,
|
|
217
|
+
);
|
|
218
|
+
// Continue with successful purchase response even if backend recording fails
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
customerInfo,
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
Logger.error('[RevenueCatAdapter] Purchase failed: ' + String(error));
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: String(error),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get user subscription status
|
|
237
|
+
*/
|
|
238
|
+
async getUserSubscriptionStatus(
|
|
239
|
+
entitlementId: string,
|
|
240
|
+
): Promise<{ isActive: boolean; details?: any }> {
|
|
241
|
+
this.ensureInitialized();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const customerInfo = await Purchases.getCustomerInfo();
|
|
245
|
+
const entitlement = customerInfo.entitlements.active[entitlementId];
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
isActive: !!entitlement,
|
|
249
|
+
details: entitlement || null,
|
|
250
|
+
};
|
|
251
|
+
} catch (error) {
|
|
252
|
+
Logger.error(
|
|
253
|
+
'[RevenueCatAdapter] Failed to get subscription status: ' +
|
|
254
|
+
String(error),
|
|
255
|
+
);
|
|
256
|
+
return {
|
|
257
|
+
isActive: false,
|
|
258
|
+
details: { error: String(error) },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Restore purchases
|
|
265
|
+
*/
|
|
266
|
+
async restorePurchases(): Promise<{
|
|
267
|
+
success: boolean;
|
|
268
|
+
error?: string;
|
|
269
|
+
restoredEntitlements?: string[];
|
|
270
|
+
}> {
|
|
271
|
+
this.ensureInitialized();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const customerInfo = await Purchases.restorePurchases();
|
|
275
|
+
Logger.info('[RevenueCatAdapter] Purchases restored');
|
|
276
|
+
|
|
277
|
+
const restoredEntitlements = Object.keys(
|
|
278
|
+
customerInfo.entitlements.active,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
restoredEntitlements,
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
Logger.error(
|
|
287
|
+
'[RevenueCatAdapter] Failed to restore purchases: ' + String(error),
|
|
288
|
+
);
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: String(error),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Record consumable purchase (not typically used with RevenueCat subscriptions)
|
|
298
|
+
*/
|
|
299
|
+
async recordConsumablePurchase(_details: {
|
|
300
|
+
productId: string;
|
|
301
|
+
quantity: number;
|
|
302
|
+
transactionDetails: any;
|
|
303
|
+
}): Promise<{ success: boolean; error?: string }> {
|
|
304
|
+
Logger.warn(
|
|
305
|
+
'[RevenueCatAdapter] recordConsumablePurchase not implemented for RevenueCat',
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
error: 'Consumable purchases not supported by RevenueCat adapter',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Set user ID for RevenueCat
|
|
315
|
+
*/
|
|
316
|
+
static async setUserId(userId: string) {
|
|
317
|
+
RevenueCatAdapter.ensureInitialized();
|
|
318
|
+
try {
|
|
319
|
+
const { customerInfo } = await Purchases.logIn(userId);
|
|
320
|
+
Logger.info(`[RevenueCatAdapter] User logged in: ${userId}`);
|
|
321
|
+
return customerInfo;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
Logger.error(
|
|
324
|
+
'[RevenueCatAdapter] Failed to set user ID: ' + String(error),
|
|
325
|
+
);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Log out the current user
|
|
332
|
+
*/
|
|
333
|
+
static async logOut() {
|
|
334
|
+
RevenueCatAdapter.ensureInitialized();
|
|
335
|
+
try {
|
|
336
|
+
const customerInfo = await Purchases.logOut();
|
|
337
|
+
Logger.info('[RevenueCatAdapter] User logged out');
|
|
338
|
+
return customerInfo;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
Logger.error(
|
|
341
|
+
'[RevenueCatAdapter] Failed to log out user: ' + String(error),
|
|
342
|
+
);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Ensure the SDK is initialized before making calls
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
private ensureInitialized(): void {
|
|
352
|
+
if (!RevenueCatAdapter.isInitialized) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
'[RevenueCatAdapter] SDK not initialized. Call initialize() first.',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Static version for class-level calls
|
|
361
|
+
* @private
|
|
362
|
+
*/
|
|
363
|
+
private static ensureInitialized(): void {
|
|
364
|
+
if (!RevenueCatAdapter.isInitialized) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
'[RevenueCatAdapter] SDK not initialized. Call initialize() first.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check if a product ID represents a consumable purchase (credits)
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
private isConsumableProduct(productId: string): boolean {
|
|
376
|
+
// Define patterns for consumable products (credits)
|
|
377
|
+
return productId.includes('credits') || productId.includes('credit');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get the number of credits for a consumable product
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
private getConsumableQuantity(productId: string): number {
|
|
385
|
+
// Extract quantity from product ID patterns
|
|
386
|
+
const patterns = [
|
|
387
|
+
/credits?[_-]?(\d+)/i, // credits_100, credit_50, credits100
|
|
388
|
+
/(\d+)[_-]?credits?/i, // 100_credits, 50credit, 100credits
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
for (const pattern of patterns) {
|
|
392
|
+
const match = productId.match(pattern);
|
|
393
|
+
if (match && match[1]) {
|
|
394
|
+
const quantity = Number.parseInt(match[1], 10);
|
|
395
|
+
if (!isNaN(quantity) && quantity > 0) {
|
|
396
|
+
return quantity;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Default to 1 credit if pattern not recognized
|
|
402
|
+
Logger.warn(
|
|
403
|
+
`[RevenueCatAdapter] Could not parse quantity from productId: ${productId}, defaulting to 1`,
|
|
404
|
+
);
|
|
405
|
+
return 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payments Service
|
|
3
|
+
*
|
|
4
|
+
* Handles credit purchases and purchase history operations.
|
|
5
|
+
* Uses the record_purchase database function for atomic credit updates.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 9.1, 9.2
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SupabaseClientType } from '../lib/supabase';
|
|
11
|
+
import type { Purchase } from '../types';
|
|
12
|
+
|
|
13
|
+
export interface RecordPurchaseInput {
|
|
14
|
+
productId: string;
|
|
15
|
+
quantity: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RecordPurchaseResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
purchase?: Purchase;
|
|
21
|
+
creditsAdded?: number;
|
|
22
|
+
newBalance?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GetPurchaseHistoryResult {
|
|
27
|
+
success: boolean;
|
|
28
|
+
purchases?: Purchase[];
|
|
29
|
+
hasMore?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Record a consumable purchase and add credits to the user
|
|
35
|
+
*
|
|
36
|
+
* Premium products (containing 'premium' in product_id) receive 2x credits.
|
|
37
|
+
* Uses the record_purchase database function for atomic operations.
|
|
38
|
+
*
|
|
39
|
+
* @param supabase - Supabase client (authenticated)
|
|
40
|
+
* @param input - Purchase details (productId, quantity)
|
|
41
|
+
* @returns The purchase record and updated credit balance
|
|
42
|
+
*
|
|
43
|
+
* Requirements: 9.1
|
|
44
|
+
*/
|
|
45
|
+
export async function recordConsumablePurchase(
|
|
46
|
+
supabase: SupabaseClientType,
|
|
47
|
+
input: RecordPurchaseInput
|
|
48
|
+
): Promise<RecordPurchaseResult> {
|
|
49
|
+
// Get the current auth user
|
|
50
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
51
|
+
|
|
52
|
+
if (authError || !authData.user) {
|
|
53
|
+
return { success: false, error: 'Not authenticated' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { productId, quantity } = input;
|
|
57
|
+
|
|
58
|
+
// Validate input
|
|
59
|
+
if (!productId || productId.trim() === '') {
|
|
60
|
+
return { success: false, error: 'Product ID is required' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!quantity || quantity < 1) {
|
|
64
|
+
return { success: false, error: 'Quantity must be at least 1' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Call the record_purchase database function for atomic operation
|
|
68
|
+
const { data, error } = await supabase.rpc('record_purchase', {
|
|
69
|
+
p_user_id: authData.user.id,
|
|
70
|
+
p_product_id: productId,
|
|
71
|
+
p_quantity: quantity,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
return { success: false, error: `Failed to record purchase: ${error.message}` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The function returns an array with one row
|
|
79
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
80
|
+
|
|
81
|
+
if (!result) {
|
|
82
|
+
return { success: false, error: 'Failed to record purchase: no result returned' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fetch the created purchase record
|
|
86
|
+
const { data: purchase, error: fetchError } = await supabase
|
|
87
|
+
.from('purchases')
|
|
88
|
+
.select('*')
|
|
89
|
+
.eq('id', result.purchase_id)
|
|
90
|
+
.single();
|
|
91
|
+
|
|
92
|
+
if (fetchError || !purchase) {
|
|
93
|
+
// Purchase was recorded but we couldn't fetch it - still return success
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
creditsAdded: result.credits_added,
|
|
97
|
+
newBalance: result.new_balance,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
purchase,
|
|
104
|
+
creditsAdded: result.credits_added,
|
|
105
|
+
newBalance: result.new_balance,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get purchase history for the current user
|
|
112
|
+
*
|
|
113
|
+
* Returns purchases ordered by date descending (newest first) with pagination.
|
|
114
|
+
*
|
|
115
|
+
* @param supabase - Supabase client (authenticated)
|
|
116
|
+
* @param options - Pagination options
|
|
117
|
+
* @returns List of purchases with pagination info
|
|
118
|
+
*
|
|
119
|
+
* Requirements: 9.2
|
|
120
|
+
*/
|
|
121
|
+
export async function getPurchaseHistory(
|
|
122
|
+
supabase: SupabaseClientType,
|
|
123
|
+
options: {
|
|
124
|
+
limit?: number;
|
|
125
|
+
offset?: number;
|
|
126
|
+
} = {}
|
|
127
|
+
): Promise<GetPurchaseHistoryResult> {
|
|
128
|
+
// Get the current auth user
|
|
129
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
130
|
+
|
|
131
|
+
if (authError || !authData.user) {
|
|
132
|
+
return { success: false, error: 'Not authenticated' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { limit = 20, offset = 0 } = options;
|
|
136
|
+
|
|
137
|
+
// Fetch purchases with pagination
|
|
138
|
+
const { data: purchases, error: listError, count } = await supabase
|
|
139
|
+
.from('purchases')
|
|
140
|
+
.select('*', { count: 'exact' })
|
|
141
|
+
.eq('user_id', authData.user.id)
|
|
142
|
+
.order('purchase_date', { ascending: false })
|
|
143
|
+
.range(offset, offset + limit - 1);
|
|
144
|
+
|
|
145
|
+
if (listError) {
|
|
146
|
+
return { success: false, error: `Failed to get purchase history: ${listError.message}` };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hasMore = count !== null && offset + (purchases?.length || 0) < count;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
purchases: purchases || [],
|
|
154
|
+
hasMore,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a single purchase by ID
|
|
160
|
+
*
|
|
161
|
+
* @param supabase - Supabase client (authenticated)
|
|
162
|
+
* @param purchaseId - The ID of the purchase
|
|
163
|
+
* @returns The purchase record or error
|
|
164
|
+
*/
|
|
165
|
+
export async function getPurchaseById(
|
|
166
|
+
supabase: SupabaseClientType,
|
|
167
|
+
purchaseId: string
|
|
168
|
+
): Promise<{ success: boolean; purchase?: Purchase; error?: string }> {
|
|
169
|
+
// Get the current auth user
|
|
170
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
171
|
+
|
|
172
|
+
if (authError || !authData.user) {
|
|
173
|
+
return { success: false, error: 'Not authenticated' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!purchaseId) {
|
|
177
|
+
return { success: false, error: 'Purchase ID is required' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { data: purchase, error: getError } = await supabase
|
|
181
|
+
.from('purchases')
|
|
182
|
+
.select('*')
|
|
183
|
+
.eq('id', purchaseId)
|
|
184
|
+
.eq('user_id', authData.user.id)
|
|
185
|
+
.single();
|
|
186
|
+
|
|
187
|
+
if (getError) {
|
|
188
|
+
if (getError.code === 'PGRST116') {
|
|
189
|
+
return { success: false, error: 'Purchase not found' };
|
|
190
|
+
}
|
|
191
|
+
return { success: false, error: `Failed to get purchase: ${getError.message}` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { success: true, purchase };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const paymentsService = {
|
|
198
|
+
recordConsumablePurchase,
|
|
199
|
+
getPurchaseHistory,
|
|
200
|
+
getPurchaseById,
|
|
201
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- Payments Feature Migration
|
|
2
|
+
-- Creates tables for purchases/credits tracking
|
|
3
|
+
-- Run this migration when adding the payments feature via CLI
|
|
4
|
+
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
-- PURCHASES TABLE
|
|
7
|
+
-- Records credit purchases and transactions
|
|
8
|
+
-- ============================================================================
|
|
9
|
+
CREATE TABLE IF NOT EXISTS purchases (
|
|
10
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
12
|
+
product_id TEXT NOT NULL,
|
|
13
|
+
quantity INTEGER NOT NULL,
|
|
14
|
+
credits_added INTEGER NOT NULL,
|
|
15
|
+
purchase_date TIMESTAMPTZ DEFAULT now()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- ============================================================================
|
|
19
|
+
-- INDEXES
|
|
20
|
+
-- ============================================================================
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_purchases_user_id ON purchases(user_id);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_purchases_purchase_date ON purchases(purchase_date);
|
|
23
|
+
|
|
24
|
+
-- ============================================================================
|
|
25
|
+
-- RLS POLICIES
|
|
26
|
+
-- ============================================================================
|
|
27
|
+
ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
|
|
28
|
+
|
|
29
|
+
CREATE POLICY "purchases_select_own" ON purchases
|
|
30
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
31
|
+
|
|
32
|
+
CREATE POLICY "purchases_insert_own" ON purchases
|
|
33
|
+
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
34
|
+
|
|
35
|
+
-- Note: UPDATE and DELETE not allowed for purchases (immutable records)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payments",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "RevenueCat payments with Supabase sync",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/features/payments",
|
|
8
|
+
"to": "apps/native/src/features/payments"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "packages/backend/supabase/migrations/payments.sql",
|
|
12
|
+
"to": "packages/backend/supabase/migrations/payments.sql"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"from": "packages/backend/src/services/payments.ts",
|
|
16
|
+
"to": "packages/backend/src/services/payments.ts"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"nav": {
|
|
20
|
+
"href": "/(root)/(protected)/payments",
|
|
21
|
+
"label": "Payments",
|
|
22
|
+
"icon": "💳",
|
|
23
|
+
"color": "#EC4899"
|
|
24
|
+
},
|
|
25
|
+
"target": "native",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"expo": [
|
|
28
|
+
"@supabase/supabase-js",
|
|
29
|
+
"react-native-purchases",
|
|
30
|
+
"react-native-purchases-ui"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"env": [
|
|
34
|
+
{
|
|
35
|
+
"key": "REVENUECAT_API_KEY_APPLE",
|
|
36
|
+
"description": "RevenueCat API key for iOS",
|
|
37
|
+
"example": "appl_..."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"key": "REVENUECAT_API_KEY_GOOGLE",
|
|
41
|
+
"description": "RevenueCat API key for Android",
|
|
42
|
+
"example": "goog_..."
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"manualSteps": [
|
|
46
|
+
{
|
|
47
|
+
"title": "Apply Supabase migration",
|
|
48
|
+
"description": "Run: cd packages/backend && supabase db push"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -3,9 +3,8 @@ import { useRouter } from 'expo-router';
|
|
|
3
3
|
import { AnimatePresence, MotiView } from 'moti';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { View } from 'react-native';
|
|
6
|
-
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
7
6
|
|
|
8
|
-
import { Button, Text } from '@/components/ui';
|
|
7
|
+
import { Button, SafeAreaView, Text } from '@/components/ui';
|
|
9
8
|
|
|
10
9
|
import Question from './components/question';
|
|
11
10
|
import { QuestionMode, quizConfig } from './config';
|