vibefast-cli 1.1.3 → 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.
Files changed (300) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +63 -169
  3. package/dist/__tests__/recipes.test.js +25 -3
  4. package/dist/__tests__/recipes.test.js.map +1 -1
  5. package/dist/commands/add.d.ts +1 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +547 -543
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/checklist.d.ts +1 -1
  10. package/dist/commands/checklist.d.ts.map +1 -1
  11. package/dist/commands/checklist.js +40 -39
  12. package/dist/commands/checklist.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +1 -1
  14. package/dist/commands/doctor.js +22 -22
  15. package/dist/commands/doctor.js.map +1 -1
  16. package/dist/commands/env.d.ts +1 -1
  17. package/dist/commands/env.d.ts.map +1 -1
  18. package/dist/commands/env.js +58 -53
  19. package/dist/commands/env.js.map +1 -1
  20. package/dist/commands/health.d.ts +1 -1
  21. package/dist/commands/health.d.ts.map +1 -1
  22. package/dist/commands/health.js +101 -93
  23. package/dist/commands/health.js.map +1 -1
  24. package/dist/commands/init.d.ts +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +416 -296
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/remove.d.ts +1 -1
  29. package/dist/commands/remove.d.ts.map +1 -1
  30. package/dist/commands/remove.js +77 -64
  31. package/dist/commands/remove.js.map +1 -1
  32. package/dist/commands/status.d.ts +1 -1
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +15 -14
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/core/__tests__/detect.test.js +68 -34
  37. package/dist/core/__tests__/detect.test.js.map +1 -1
  38. package/dist/core/ast.d.ts +14 -0
  39. package/dist/core/ast.d.ts.map +1 -0
  40. package/dist/core/ast.js +239 -0
  41. package/dist/core/ast.js.map +1 -0
  42. package/dist/core/codemod.d.ts.map +1 -1
  43. package/dist/core/codemod.js +62 -44
  44. package/dist/core/codemod.js.map +1 -1
  45. package/dist/core/config.d.ts +10 -0
  46. package/dist/core/config.d.ts.map +1 -0
  47. package/dist/core/config.js +51 -0
  48. package/dist/core/config.js.map +1 -0
  49. package/dist/core/detect.d.ts +8 -2
  50. package/dist/core/detect.d.ts.map +1 -1
  51. package/dist/core/detect.js +52 -21
  52. package/dist/core/detect.js.map +1 -1
  53. package/dist/core/errors.d.ts.map +1 -1
  54. package/dist/core/errors.js +9 -8
  55. package/dist/core/errors.js.map +1 -1
  56. package/dist/core/exec.d.ts +16 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +48 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/manualSteps.d.ts +7 -0
  61. package/dist/core/manualSteps.d.ts.map +1 -0
  62. package/dist/core/manualSteps.js +59 -0
  63. package/dist/core/manualSteps.js.map +1 -0
  64. package/dist/core/paths.d.ts +3 -1
  65. package/dist/core/paths.d.ts.map +1 -1
  66. package/dist/core/paths.js +14 -10
  67. package/dist/core/paths.js.map +1 -1
  68. package/dist/core/spinner.d.ts +1 -1
  69. package/dist/core/spinner.d.ts.map +1 -1
  70. package/dist/core/spinner.js +38 -8
  71. package/dist/core/spinner.js.map +1 -1
  72. package/dist/core/vosk.d.ts.map +1 -1
  73. package/dist/core/vosk.js +50 -39
  74. package/dist/core/vosk.js.map +1 -1
  75. package/docs/manual-testing.md +91 -0
  76. package/package.json +6 -3
  77. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  78. package/recipes/audio-recorder/recipe.json +3 -3
  79. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  80. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  81. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  82. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  83. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  84. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  85. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  86. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  87. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  88. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  89. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  90. package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
  91. package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
  92. package/recipes/audio-recorder-supabase/recipe.json +35 -0
  93. package/recipes/audio-recorder-supabase@latest.zip +0 -0
  94. package/recipes/audio-recorder@latest.zip +0 -0
  95. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
  96. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
  97. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
  98. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
  99. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
  100. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
  101. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
  102. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
  103. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
  104. package/recipes/charts/recipe.json +4 -13
  105. package/recipes/charts@latest.zip +0 -0
  106. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  107. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
  108. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
  109. package/recipes/chatbot/recipe.json +26 -92
  110. package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
  111. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
  112. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  113. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
  114. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  115. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
  116. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  117. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
  118. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  119. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  120. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  121. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  122. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
  123. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  124. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  125. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  126. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
  127. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  128. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
  129. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
  130. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
  131. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  132. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
  133. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  134. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  135. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
  136. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
  137. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
  138. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
  139. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
  140. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
  141. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
  142. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
  143. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  144. package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
  145. package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
  146. package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
  147. package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
  148. package/recipes/chatbot-supabase/recipe.json +79 -0
  149. package/recipes/chatbot-supabase@latest.zip +0 -0
  150. package/recipes/chatbot.zip +0 -0
  151. package/recipes/chatbot@latest.zip +0 -0
  152. package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
  153. package/recipes/image-analysis/packages/backend/convex/{imageAnalysisFunctions.ts → imageAnalysis.ts} +5 -5
  154. package/recipes/image-analysis/recipe.json +15 -55
  155. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
  156. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
  157. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
  158. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
  159. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
  160. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
  161. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
  162. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
  163. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
  164. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
  165. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
  166. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
  167. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
  168. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
  169. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
  170. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
  171. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
  172. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
  173. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
  174. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
  175. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
  176. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
  177. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
  178. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
  179. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
  180. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
  181. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
  182. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
  183. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
  184. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
  185. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
  186. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
  187. package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
  188. package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
  189. package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
  190. package/recipes/image-analysis-supabase/recipe.json +57 -0
  191. package/recipes/image-analysis-supabase@latest.zip +0 -0
  192. package/recipes/image-analysis@latest.zip +0 -0
  193. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
  194. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
  195. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
  196. package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
  197. package/recipes/image-generator/recipe.json +16 -39
  198. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
  199. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  200. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
  201. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
  202. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  203. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
  204. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  205. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  206. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  207. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
  208. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
  209. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
  210. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
  211. package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
  212. package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
  213. package/recipes/image-generator-supabase/recipe.json +59 -0
  214. package/recipes/image-generator-supabase@latest.zip +0 -0
  215. package/recipes/image-generator@latest.zip +0 -0
  216. package/recipes/ios-widget/recipe.json +15 -24
  217. package/recipes/ios-widget@latest.zip +0 -0
  218. package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
  219. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
  220. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
  221. package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
  222. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
  223. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
  224. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
  225. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
  226. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
  227. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
  228. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
  229. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
  230. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
  231. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
  232. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
  233. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
  234. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
  235. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
  236. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
  237. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
  238. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
  239. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
  240. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
  241. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
  242. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
  243. package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
  244. package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
  245. package/recipes/onboarding/recipe.json +15 -0
  246. package/recipes/onboarding@latest.zip +0 -0
  247. package/recipes/payments/recipe.json +28 -61
  248. package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
  249. package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  250. package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  251. package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  252. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  253. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  254. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  255. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  256. package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  257. package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
  258. package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  259. package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
  260. package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
  261. package/recipes/payments-supabase/recipe.json +51 -0
  262. package/recipes/payments-supabase@latest.zip +0 -0
  263. package/recipes/payments@latest.zip +0 -0
  264. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
  265. package/recipes/quiz/recipe.json +6 -9
  266. package/recipes/quiz@latest.zip +0 -0
  267. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
  268. package/recipes/tracker-app/recipe.json +7 -10
  269. package/recipes/tracker-app@latest.zip +0 -0
  270. package/recipes/voice-bot/recipe.json +8 -68
  271. package/recipes/voice-bot.zip +0 -0
  272. package/recipes/voice-bot@latest.zip +0 -0
  273. package/recipes/wake-word/recipe.json +10 -9
  274. package/recipes/wake-word.zip +0 -0
  275. package/recipes/wake-word@latest.zip +0 -0
  276. package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
  277. package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
  278. package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
  279. package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
  280. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
  281. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
  282. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
  283. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
  284. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
  285. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
  286. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
  287. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
  288. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
  289. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
  290. package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
  291. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
  292. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
  293. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
  294. package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
  295. package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
  296. package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
  297. package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
  298. /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
  299. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
  300. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/index.tsx +0 -0
@@ -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
@@ -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';