diff --git a/MANUAL_TESTING_CHECKLIST.md b/MANUAL_TESTING_CHECKLIST.md deleted file mode 100644 index 2cad6039..00000000 --- a/MANUAL_TESTING_CHECKLIST.md +++ /dev/null @@ -1,71 +0,0 @@ -# Manual Testing Checklist for app/pricing.tsx - -This checklist is to manually verify the functionality and appearance of the updated pricing page. - -## A. Prerequisites -1. **Build and run the application** on a device or simulator. -2. **Navigate to the Pricing Page.** -3. Open developer tools to monitor console logs and analytics events if possible. - -## B. Initial State Verification -* [ ] **Page Title:** Is the title "Choose Your Plan"? -* [ ] **Default Selected Plan:** Is the Monthly plan ($20/month) selected by default? - * [ ] Does it appear elevated (e.g., stronger shadow, border) compared to other cards? - * [ ] Does it show the "✓ Selected" indicator? -* [ ] **Monthly Plan Badge:** Does the Monthly plan correctly display the "Most Popular" badge (purple background, white text) in the top-right corner? -* [ ] **CTA Button Text:** Is the main call-to-action button text "Try Free Now"? -* [ ] **CTA Subtext (Monthly Plan Trial):** Does the subtext "1 week free trial, then $20/month" appear below the "Try Free Now" button? - -## C. Yearly Plan Card Verification -* [ ] **Display:** Is the Yearly plan card visible with the correct name ("Yearly Plan"), price ("$149.99/year"), and subtext ("SAVE 38%")? -* [ ] **Features:** Are the features listed correctly for the Yearly plan (should be same as Monthly)? -* [ ] **Badge:** Does it correctly display the "SAVE 38%" badge (green background, white text) in the top-right corner? -* [ ] **Selection Indicator:** Does it show "Select" (or similar unselected state indicator, gray text)? - -## D. Plan Selection Behavior & Dynamic UI Changes -* [ ] **Select Yearly Plan:** - * [ ] Tap the Yearly plan card. Does it become selected (elevated, "✓ Selected" indicator)? - * [ ] Does the Monthly plan become unselected (no elevation, "Select" indicator)? - * [ ] Does the CTA subtext ("1 week free trial...") disappear? -* [ ] **Reselect Monthly Plan:** - * [ ] Tap the Monthly plan card. Does it become selected again (elevated, "✓ Selected" indicator)? - * [ ] Does the Yearly plan (previously selected) become unselected? - * [ ] Does the CTA subtext ("1 week free trial...") reappear? - -## E. Styling, Layout, and Visuals -* [ ] **Card Appearance:** Do all cards have consistent styling (padding, rounded corners, background color)? -* [ ] **Selected Card Style:** Is the selected card clearly differentiated (e.g., border color `#8B5CF6`, increased elevation/shadow)? -* [ ] **Badge Appearance & Positioning:** - * [ ] Are badges styled correctly (background colors: purple for "Most Popular", green for "SAVE 38%"; white text; padding; border radius)? - * [ ] Are badges positioned correctly in the top-right area of their respective cards? -* [ ] **Text Elements:** - * [ ] **Plan Names:** Are they clear and consistently styled (`fontSize: 22`, `fontWeight: '600'`)? - * [ ] **Prices & Suffixes:** Are prices prominent (`fontSize: 40`, bold) and suffixes clear (`fontSize: 18`)? - * [ ] **Subtexts:** Are they styled consistently and legibly (`fontSize: 14`)? - * [ ] **Feature Lists:** Is the styling of feature items consistent? -* [ ] **Selection Indicators:** Is the text ("✓ Selected" vs "Select") clear and styled as expected (brand color and bold when selected, gray when not)? -* [ ] **Spacing:** - * [ ] Is there adequate and consistent vertical `marginBottom` (24px) between the plan cards? - * [ ] Is spacing within cards (around text, features, price) appropriate? -* [ ] **Responsiveness (if possible to test):** - * [ ] If tested on different screen sizes or orientations, do the cards stack correctly and remain readable (cards should be `width: '100%'` up to `maxWidth: 400`)? - -## F. "Try Free Now" Button Interaction (Visual/State Check) -*(Actual checkout flow depends on backend Stripe setup and API responses, focus on UI changes)* -* [ ] **With Monthly Plan Selected:** - * [ ] Click "Try Free Now". Does the button text change to "Processing..." temporarily? -* [ ] **With Yearly Plan Selected:** - * [ ] Click "Try Free Now". Does the button text also change to "Processing..." temporarily? -* [ ] **Error Message Display:** If an error occurs during `initiateStripeCheckout` (e.g., Stripe not initialized, API error), is the `errorMessage` state updated and displayed correctly in `styles.errorText`? - -## G. Analytics Event Logging (Developer Tool Check) -* [ ] **View Pricing Page:** When the page initially loads, is an analytics event for `VIEW_PRICING_PAGE` logged? -* [ ] **Initiate Upgrade (per plan):** - * [ ] With Monthly plan selected, click "Try Free Now". Is an `INITIATE_UPGRADE` event logged with parameters like `{ plan: 'monthly' }`? - * [ ] With Yearly plan selected, click "Try Free Now". Is an `INITIATE_UPGRADE` event logged with parameters like `{ plan: 'yearly' }`? -* [ ] **Upgrade Failure (if mockable):** If an upgrade fails, is an `UPGRADE_FAILURE` event logged with the correct plan ID and error message? - -## H. Developer Console Verification -* [ ] **Stripe Placeholder Warning:** When the component mounts, is the following `console.warn` message visible in the developer console: `"TODO: Replace placeholder Stripe Price IDs ('price_yearly_placeholder') in pricingPlansData with actual Price IDs from your Stripe dashboard."`? - -This checklist should help ensure all implemented features and changes on the pricing page are working as expected. diff --git a/POWER_USER_FIX_TESTING.md b/POWER_USER_FIX_TESTING.md index 5daabd71..214c455f 100644 --- a/POWER_USER_FIX_TESTING.md +++ b/POWER_USER_FIX_TESTING.md @@ -1,26 +1,12 @@ # Power User Promotion Fix - Testing Instructions -## Issue Fixed -The power user promotion modal was appearing after just 1 dose instead of the intended 4+ doses. +## Status: Deprecated -## Root Causes Identified & Fixed +**Note**: This document is now deprecated as SafeDose has transitioned to a fully free and open-source model. Power user promotions have been disabled as there are no subscription tiers to promote. -### 1. Misleading Modal Messages -**Problem**: Log limit modal said "You've become a SafeDose power user!" even when triggered by storage limits -**Fix**: Distinct messages for each scenario: -- **Log Limit**: "Dose History Storage Limit Reached" -- **Power User**: "You've Become a SafeDose Power User!" (only when legitimately triggered) +## Historical Context (for reference only) -### 2. Missing Safety Checks -**Problem**: No hardcoded safety net to prevent premature display -**Fix**: Added multiple safety layers: -- Hard-coded check preventing promotion if dose count < 4 -- Storage key update (`powerUserPromotion_v2`) to force fresh start -- Suspicious data detection and warnings - -### 3. Race Conditions & Timing Issues -**Problem**: Async state updates could cause incorrect evaluations -**Fix**: Added timing safeguards and comprehensive debugging +The power user promotion modal was appearing after just 1 dose instead of the intended 4+ doses. This has been resolved by disabling promotions entirely as part of SafeDose's commitment to being free and focused on long-term safety. ## Testing Validation @@ -53,11 +39,9 @@ All 9 test cases pass, including: - `lib/hooks/useDoseCalculator.ts` - Enhanced modal trigger logic - `lib/hooks/useLogUsageTracking.ts` - Added data validation -## Expected Behavior After Fix -- **New users**: Never see promotion modal on first use -- **Active users**: Promotion only after legitimate 4+ doses -- **Storage issues**: Clear messaging about limits, not "power user" confusion -- **Subscribers**: No promotion modals (already have premium features) +## Current Behavior +- **All users**: Power user promotions are disabled +- **Free and open source**: SafeDose is focused on long-term safety, not monetization ## Debug Information Enhanced logging will show exactly which modal is triggered and why: diff --git a/POWER_USER_PROMOTION_TESTING.md b/POWER_USER_PROMOTION_TESTING.md deleted file mode 100644 index 9f000970..00000000 --- a/POWER_USER_PROMOTION_TESTING.md +++ /dev/null @@ -1,114 +0,0 @@ -# Power User Promotion Feature Testing Guide - -## Overview -This guide outlines how to test the new power user promotion modal functionality that addresses issue #333. - -## What Changed -- **Before**: "You've become a SafeDose power user!" modal showed when hitting log storage limits (10 logs for free users) -- **After**: Modal shows based on dose completion count (4+ doses), subscription status, and time cycling (every 2 weeks) - -## Testing Scenarios - -### ✅ Scenario 1: New User - No Promotion Yet -**Setup**: Fresh user, completed 1-3 doses -**Expected**: No promotion modal should appear -**How to Test**: -1. Start fresh dose calculation -2. Complete dose successfully -3. Go through feedback flow -4. **Verify**: No power user promotion modal appears - -### ✅ Scenario 2: Eligible User - First Time Promotion -**Setup**: User with 4+ completed doses, never shown promotion -**Expected**: Power user promotion modal appears -**How to Test**: -1. Complete 4th dose successfully -2. Go through feedback flow -3. **Verify**: Modal appears with title "You've Become a SafeDose Power User!" -4. **Verify**: Shows "Great job on completing multiple doses!" message -5. **Verify**: Shows "Upgrade to Pro" and "Maybe later" buttons - -### ✅ Scenario 3: Premium User - No Promotion -**Setup**: User with Pro/Plus subscription and 10+ doses -**Expected**: No promotion modal (already subscribed) -**How to Test**: -1. Set user to Pro/Plus plan in Firebase/storage -2. Complete multiple doses -3. **Verify**: No power user promotion modal appears - -### ✅ Scenario 4: Recently Shown - No Promotion -**Setup**: User shown promotion within last 14 days -**Expected**: No promotion modal (too soon) -**How to Test**: -1. Manually set `lastShownDate` to 7 days ago in AsyncStorage -2. Complete doses (should have 4+) -3. **Verify**: No power user promotion modal appears - -### ✅ Scenario 5: Cycling - Show Again After 2 Weeks -**Setup**: User shown promotion >14 days ago, still non-subscriber -**Expected**: Promotion modal appears again -**How to Test**: -1. Manually set `lastShownDate` to 20 days ago in AsyncStorage -2. Complete dose successfully -3. **Verify**: Modal appears again - -### ✅ Scenario 6: Log Limit vs Power User Promotion -**Setup**: User hits actual log storage limit (10 logs) -**Expected**: Traditional log limit modal (different from promotion) -**How to Test**: -1. Fill user's log storage to limit (10 entries) -2. Try to save another dose -3. **Verify**: Modal shows "Unlock Your Full Dosing History" title -4. **Verify**: Shows "Continue without saving" button (not "Maybe later") - -## Modal Content Differences - -### Power User Promotion Modal -- **Title**: "You've Become a SafeDose Power User!" -- **Message**: "Great job on completing multiple doses! Upgrade to Pro..." -- **Buttons**: "Upgrade to Pro" + "Maybe later" -- **Behavior**: Dose is already logged, continues navigation on close - -### Log Limit Modal -- **Title**: "Unlock Your Full Dosing History" -- **Message**: "You've become a SafeDose power user! Upgrade to Pro..." -- **Buttons**: "Upgrade to Pro" + "Continue without saving" -- **Behavior**: Dose not logged yet, offers to continue without saving - -## Key Implementation Details - -### Tracking Data Stored -```javascript -{ - doseCount: number, // Incremented on each successful dose - lastShownDate: string|null, // ISO date when promotion was last shown - hasActiveSubscription: boolean, - plan: string // 'free', 'plus', 'pro' -} -``` - -### Trigger Conditions -1. `doseCount >= 4` -2. `hasActiveSubscription === false` -3. `lastShownDate === null` OR `daysSinceLastShown >= 14` - -### Storage Keys -- AsyncStorage: `powerUserPromotion_${userId}` -- Firestore: User document `plan` field for subscription status - -## Analytics Events -- `LIMIT_MODAL_VIEW` with type: `'power_user_promotion'` or `'log_limit'` -- `LIMIT_MODAL_ACTION` with action and type - -## Testing Tools -- Use browser dev tools to modify AsyncStorage values -- Check console logs for detailed promotion logic decisions -- Run `validate-power-user-promotion.js` script for logic testing - -## Edge Cases Covered -- Anonymous vs authenticated users -- Offline functionality (cached data) -- Network failures (graceful degradation) -- Multiple rapid dose completions -- User upgrading mid-session -- App state persistence across sessions \ No newline at end of file diff --git a/STRIPE_SETUP.md b/STRIPE_SETUP.md deleted file mode 100644 index 338eea43..00000000 --- a/STRIPE_SETUP.md +++ /dev/null @@ -1,98 +0,0 @@ -# Stripe Configuration - -This project supports both Stripe test and live modes through a feature flag system. - -## Environment Variables - -Create a `.env` file in the root directory with the following variables: - -```bash -# Stripe Mode (test or live) -STRIPE_MODE=test - -# Test Mode Keys -STRIPE_TEST_PUBLISHABLE_KEY=pk_test_... -STRIPE_TEST_SECRET_KEY=sk_test_... -STRIPE_TEST_PRICE_ID=price_1REyzMPE5x6FmwJPyJVJIEXe - -# Live Mode Keys (for production) -STRIPE_LIVE_PUBLISHABLE_KEY=pk_live_... -STRIPE_LIVE_SECRET_KEY=sk_live_... -STRIPE_LIVE_PRICE_ID=price_1RUHgxAY2p4W374Yb5EWEtZ0 -``` - -## Usage - -### Switching Modes - -- **Test Mode**: Set `STRIPE_MODE=test` to use test keys and test price IDs -- **Live Mode**: Set `STRIPE_MODE=live` to use live keys and live price IDs - -### Backward Compatibility - -The system maintains backward compatibility with legacy environment variables: -- `STRIPE_PUBLISHABLE_KEY` (fallback for test mode) -- `STRIPE_SECRET_KEY` (fallback for test mode) - -### Security - -- The `.env` file is automatically excluded from version control -- Keys are only logged in truncated form for debugging -- Live keys should be stored in your hosting platform's environment variables (e.g., Vercel) - -### Testing Payments - -#### Test Mode -1. Set `STRIPE_MODE=test` -2. Use Stripe test card: `4242 4242 4242 4242` -3. Any future expiry date and CVC - -#### Live Mode -1. Set `STRIPE_MODE=live` -2. Use real credit card -3. Complete actual transaction (remember to refund if testing) - -## Configuration Files - -- `lib/stripeConfig.js` - Client-side configuration -- `lib/stripeConfig.server.js` - Server-side configuration -- `app.config.js` - Expo configuration with environment variables -- `types/env.d.ts` - TypeScript definitions - -## Troubleshooting - -### Error: "This API call cannot be made with a publishable API key" - -This error occurs when the server-side Stripe configuration is using a publishable key instead of a secret key, or when the secret key is not properly configured. - -**Common causes:** -1. **Missing secret key**: The `STRIPE_LIVE_SECRET_KEY` (for live mode) or `STRIPE_TEST_SECRET_KEY` (for test mode) environment variable is not set -2. **Wrong key type**: A publishable key (`pk_*`) was accidentally used as a secret key -3. **Environment variable mismatch**: The keys don't match the current `STRIPE_MODE` setting - -**To debug:** -1. Check the server logs for detailed configuration information -2. Verify that your environment variables are properly set: - ```bash - # For live mode - STRIPE_MODE=live - STRIPE_LIVE_SECRET_KEY=sk_live_... # Must start with 'sk_live_' - STRIPE_LIVE_PUBLISHABLE_KEY=pk_live_... - - # For test mode - STRIPE_MODE=test - STRIPE_TEST_SECRET_KEY=sk_test_... # Must start with 'sk_test_' - STRIPE_TEST_PUBLISHABLE_KEY=pk_test_... - ``` -3. Ensure secret keys start with `sk_` and publishable keys start with `pk_` -4. In production (e.g., Vercel), set environment variables in your hosting platform's dashboard - -**Quick fix:** -- If using live mode, ensure `STRIPE_LIVE_SECRET_KEY` is set to a valid secret key starting with `sk_live_` -- If using test mode, ensure `STRIPE_TEST_SECRET_KEY` is set to a valid secret key starting with `sk_test_` - -### Other Common Issues - -- **CORS errors**: Make sure your API endpoints are properly configured for your domain -- **Price ID mismatch**: Verify that test/live price IDs match your Stripe dashboard -- **Environment loading**: In serverless environments, ensure `.env` files are properly loaded or environment variables are set in the platform \ No newline at end of file diff --git a/api/create-checkout-session.js b/api/create-checkout-session.js deleted file mode 100644 index 7a3d133c..00000000 --- a/api/create-checkout-session.js +++ /dev/null @@ -1,121 +0,0 @@ -// Note: Ensure STRIPE_LIVE_SECRET_KEY is set in your deployment environment (e.g., Vercel). -// In Vercel, go to Settings > Environment Variables, add STRIPE_LIVE_SECRET_KEY with your live secret key (sk_live_...), and redeploy the app. - -const Stripe = require('stripe'); -const stripeConfig = require('../lib/stripeConfig'); - -// Enhanced error checking and logging -console.log('create-checkout-session.js Version: 1.6 (deployed with process.env fix)'); -console.log('Loading create-checkout-session with config:', { - mode: stripeConfig.mode, - hasSecretKey: !!stripeConfig.secretKey, - secretKeyFormat: stripeConfig.secretKey ? (stripeConfig.secretKey.startsWith('sk_') ? 'valid' : 'invalid') : 'missing' -}); - -module.exports = async (req, res) => { - // Direct environment variable logging for diagnostics - const stripeMode = process.env.STRIPE_MODE || 'test'; - const isLiveMode = stripeMode === 'live'; - - console.log('Environment variable diagnostic:', { - STRIPE_MODE: stripeMode, - isLiveMode: isLiveMode, - STRIPE_LIVE_SECRET_KEY: isLiveMode ? - (process.env.STRIPE_LIVE_SECRET_KEY ? `Set (value: ${process.env.STRIPE_LIVE_SECRET_KEY.substring(0, 12)}...)` : 'NOT SET') : - 'Not applicable (test mode)', - STRIPE_TEST_SECRET_KEY: !isLiveMode ? - (process.env.STRIPE_TEST_SECRET_KEY ? `Set (value: ${process.env.STRIPE_TEST_SECRET_KEY.substring(0, 12)}...)` : 'NOT SET') : - 'Not applicable (live mode)' - }); - - console.log('Received request to /api/create-checkout-session:', req.body); - console.log('Stripe config being used:', { - mode: stripeConfig.mode, - hasSecretKey: !!stripeConfig.secretKey, - secretKeyPrefix: stripeConfig.secretKey ? stripeConfig.secretKey.substring(0, 7) : 'N/A' - }); - - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - - // Validate Stripe configuration before proceeding - if (!stripeConfig.secretKey) { - const envVarName = stripeConfig.mode === 'live' ? 'STRIPE_LIVE_SECRET_KEY' : 'STRIPE_TEST_SECRET_KEY'; - console.error(`Stripe secret key not configured for ${stripeConfig.mode} mode. Environment variable ${envVarName} is missing or undefined.`); - console.error('Deployment guidance: In Vercel, go to Settings > Environment Variables, add the missing key under the Production environment, and redeploy with `vercel --prod`.'); - return res.status(500).json({ - error: `Payment system not configured for ${stripeConfig.mode} mode. Please contact support.`, - details: `Missing environment variable: ${envVarName}. Please check your Vercel Production environment configuration.` - }); - } - - // Additional validation to ensure we're not using a publishable key as secret key - if (stripeConfig.secretKey.startsWith('pk_')) { - console.error('CRITICAL: A publishable key was provided as secret key'); - return res.status(500).json({ - error: 'CRITICAL: A publishable key was provided as secret key. This will cause API failures. Please check your environment configuration.' - }); - } - - if (!stripeConfig.secretKey.startsWith('sk_')) { - console.error('Invalid secret key format:', stripeConfig.secretKey.substring(0, 3)); - return res.status(500).json({ - error: `Invalid secret key format. Secret keys should start with 'sk_'. Current key starts with: ${stripeConfig.secretKey.substring(0, 3)}` - }); - } - - // Initialize Stripe only after validation - let stripe; - try { - stripe = new Stripe(stripeConfig.secretKey, { - apiVersion: '2022-11-15', - }); - } catch (stripeInitError) { - console.error('Failed to initialize Stripe:', stripeInitError.message); - return res.status(500).json({ - error: 'Failed to initialize payment system. Please contact support.' - }); - } - - const { priceId, successUrl, cancelUrl, hasTrial } = req.body; - if (!priceId || !successUrl || !cancelUrl) { - console.log('Missing parameters:', { priceId, successUrl, cancelUrl }); - return res.status(400).json({ error: 'Missing parameters' }); - } - - try { - console.log('Attempting to create checkout session with Stripe...'); - - // Base session configuration - const sessionConfig = { - mode: 'subscription', - payment_method_types: ['card'], - line_items: [{ price: priceId, quantity: 1 }], - success_url: successUrl, - cancel_url: cancelUrl, - metadata: { - source: hasTrial ? 'full_pro_trial' : 'standard_subscription' - }, - }; - - // Add trial period for Full Pro plans (both monthly and yearly) - if (hasTrial) { - console.log('Adding 7-day trial period for Full Pro plan'); - sessionConfig.subscription_data = { - trial_period_days: 7, - }; - } - - const session = await stripe.checkout.sessions.create(sessionConfig); - console.log('Created checkout session:', session.id); - res.status(200).json({ sessionId: session.id }); - } catch (err) { - console.error('Error creating checkout session:', err.message); - console.error('Error type:', err.type); - console.error('Error code:', err.code); - console.error('Full error object:', err); - res.status(500).json({ error: err.message }); - } -}; \ No newline at end of file diff --git a/api/create-portal-session.js b/api/create-portal-session.js deleted file mode 100644 index 0a03fcf0..00000000 --- a/api/create-portal-session.js +++ /dev/null @@ -1,78 +0,0 @@ -const Stripe = require('stripe'); -const stripeConfig = require('../lib/stripeConfig.server.js'); - -module.exports = async (req, res) => { - console.log('Received request to /api/create-portal-session'); - - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - - // Validate Stripe configuration before proceeding - if (!stripeConfig.secretKey) { - console.error('Stripe secret key not configured for', stripeConfig.mode, 'mode'); - return res.status(500).json({ - error: `Stripe secret key is not configured for ${stripeConfig.mode} mode. Please set the appropriate environment variables.`, - }); - } - - if (!stripeConfig.secretKey.startsWith('sk_')) { - console.error('Invalid secret key format:', stripeConfig.secretKey.substring(0, 3)); - return res.status(500).json({ - error: `Invalid secret key format. Secret keys should start with 'sk_'. Current key starts with: ${stripeConfig.secretKey.substring(0, 3)}`, - }); - } - - // Initialize Stripe only after validation - let stripe; - try { - stripe = new Stripe(stripeConfig.secretKey, { - apiVersion: '2022-11-15', - }); - } catch (error) { - console.error('Failed to initialize Stripe:', error); - return res.status(500).json({ - error: 'Failed to initialize Stripe configuration', - }); - } - - try { - const { customerId, returnUrl } = req.body; - - if (!customerId) { - return res.status(400).json({ error: 'Customer ID is required' }); - } - - // Create the portal session - const portalSession = await stripe.billingPortal.sessions.create({ - customer: customerId, - return_url: returnUrl || `${req.headers.origin || 'https://safedose.app'}/settings`, - }); - - console.log('Portal session created successfully:', { - sessionId: portalSession.id, - customerId: customerId, - url: portalSession.url.substring(0, 50) + '...' - }); - - res.status(200).json({ - url: portalSession.url, - sessionId: portalSession.id - }); - } catch (error) { - console.error('Error creating portal session:', error); - - if (error.type === 'StripeInvalidRequestError') { - return res.status(400).json({ - error: 'Invalid request to Stripe', - details: error.message - }); - } - - res.status(500).json({ - error: 'Failed to create portal session', - details: error.message - }); - } -}; \ No newline at end of file diff --git a/api/get-subscription-status.js b/api/get-subscription-status.js deleted file mode 100644 index 72e0080d..00000000 --- a/api/get-subscription-status.js +++ /dev/null @@ -1,183 +0,0 @@ -const Stripe = require('stripe'); -const stripeConfig = require('../lib/stripeConfig.server.js'); - -// Try to initialize Firebase Admin if environment variables are available -let admin = null; -let db = null; - -try { - admin = require('firebase-admin'); - - // Only initialize if credentials are available - if (process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_CLIENT_EMAIL && process.env.FIREBASE_PRIVATE_KEY) { - if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert({ - projectId: process.env.FIREBASE_PROJECT_ID, - clientEmail: process.env.FIREBASE_CLIENT_EMAIL, - privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), - }), - }); - } - db = admin.firestore(); - console.log('Firebase Admin initialized successfully'); - } else { - console.warn('Firebase Admin credentials not available, user authentication will be limited'); - } -} catch (error) { - console.warn('Firebase Admin not available:', error.message); -} - -module.exports = async (req, res) => { - console.log('Received request to /api/get-subscription-status'); - - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - - // Validate Stripe configuration before proceeding - if (!stripeConfig.secretKey) { - console.error('Stripe secret key not configured for', stripeConfig.mode, 'mode'); - return res.status(500).json({ - error: `Stripe secret key is not configured for ${stripeConfig.mode} mode. Please set the appropriate environment variables.`, - }); - } - - if (!stripeConfig.secretKey.startsWith('sk_')) { - console.error('Invalid secret key format:', stripeConfig.secretKey.substring(0, 3)); - return res.status(500).json({ - error: `Invalid secret key format. Secret keys should start with 'sk_'. Current key starts with: ${stripeConfig.secretKey.substring(0, 3)}`, - }); - } - - // Initialize Stripe only after validation - let stripe; - try { - stripe = new Stripe(stripeConfig.secretKey, { - apiVersion: '2022-11-15', - }); - } catch (error) { - console.error('Failed to initialize Stripe:', error); - return res.status(500).json({ - error: 'Failed to initialize Stripe configuration', - }); - } - - try { - const { idToken } = req.body; - - if (!idToken) { - return res.status(400).json({ error: 'ID token is required' }); - } - - if (!admin || !db) { - return res.status(500).json({ error: 'Firebase Admin not configured properly' }); - } - - // Verify the Firebase ID token - let decodedToken; - try { - decodedToken = await admin.auth().verifyIdToken(idToken); - } catch (error) { - console.error('Invalid ID token:', error); - return res.status(401).json({ error: 'Invalid authentication token' }); - } - - const userId = decodedToken.uid; - - // Get user data from Firestore - const userDoc = await db.collection('users').doc(userId).get(); - - if (!userDoc.exists) { - return res.status(200).json({ - hasActiveSubscription: false, - subscriptionStatus: 'none', - plan: 'free', - customerId: null - }); - } - - const userData = userDoc.data(); - const customerId = userData.stripeCustomerId; - - if (!customerId) { - return res.status(200).json({ - hasActiveSubscription: false, - subscriptionStatus: 'none', - plan: userData.plan || 'free', - customerId: null - }); - } - - // Get customer and subscription data from Stripe - const customer = await stripe.customers.retrieve(customerId, { - expand: ['subscriptions'] - }); - - let hasActiveSubscription = false; - let subscriptionStatus = 'none'; - let currentPlan = userData.plan || 'free'; - let subscriptionData = null; - - if (customer.subscriptions && customer.subscriptions.data.length > 0) { - const subscription = customer.subscriptions.data[0]; // Get the first subscription - subscriptionStatus = subscription.status; - hasActiveSubscription = ['active', 'trialing'].includes(subscription.status); - - if (hasActiveSubscription && subscription.items.data.length > 0) { - const priceId = subscription.items.data[0].price.id; - - // Map price IDs to plan names (you can extend this based on your pricing) - const planMapping = { - 'price_1RYgx7AY2p4W374YR9UxS0vr': 'starter', // Monthly Starter - 'price_1RYgx7AY2p4W374Yy23EtyIm': 'starter', // Annual Starter - 'price_1RYgyPAY2p4W374YNbpBpbqv': 'basic-pro', // Monthly Basic Pro - 'price_1RYgyPAY2p4W374YJOhwDafY': 'basic-pro', // Annual Basic Pro - 'price_1RUHgxAY2p4W374Yb5EWEtZ0': 'full-pro', // Monthly Full Pro - 'price_1RYgzUAY2p4W374YHiBBHvuX': 'full-pro', // Annual Full Pro - }; - - currentPlan = planMapping[priceId] || 'pro'; - } - - subscriptionData = { - id: subscription.id, - status: subscription.status, - currentPeriodEnd: subscription.current_period_end, - cancelAtPeriodEnd: subscription.cancel_at_period_end, - priceId: subscription.items.data[0]?.price?.id, - }; - } - - console.log('Subscription status retrieved:', { - userId, - customerId, - hasActiveSubscription, - subscriptionStatus, - plan: currentPlan - }); - - res.status(200).json({ - hasActiveSubscription, - subscriptionStatus, - plan: currentPlan, - customerId, - subscription: subscriptionData - }); - } catch (error) { - console.error('Error getting subscription status:', error); - - if (error.type === 'StripeInvalidRequestError') { - return res.status(400).json({ - error: 'Invalid request to Stripe', - details: error.message - }); - } - - res.status(500).json({ - error: 'Failed to get subscription status', - details: error.message - }); - } -}; \ No newline at end of file diff --git a/api/stripe-webhook.js b/api/stripe-webhook.js deleted file mode 100644 index 8efe8941..00000000 --- a/api/stripe-webhook.js +++ /dev/null @@ -1,70 +0,0 @@ -const Stripe = require('stripe'); -const { buffer } = require('micro'); -const stripeConfig = require('../lib/stripeConfig.server.js'); - -if (!stripeConfig.secretKey) { - throw new Error(`Stripe secret key is not configured for ${stripeConfig.mode} mode. Please set the appropriate environment variables.`); -} - -const stripe = new Stripe(stripeConfig.secretKey, { - apiVersion: '2025-03-31.basil', // Use a stable, supported version -}); - -module.exports = async (req, res) => { - const buf = await buffer(req); - const sig = req.headers['stripe-signature']; - let event; - - try { - event = stripe.webhooks.constructEvent( - buf, - sig, - process.env.STRIPE_WEBHOOK_SECRET - ); - } catch (err) { - return res.status(400).send(`Webhook Error: ${err.message}`); - } - - // Handle different webhook events - switch (event.type) { - case 'checkout.session.completed': - const session = event.data.object; - console.log('Received event:', event.type, session.id); - console.log('Session metadata:', session.metadata); - // TODO: Update database (e.g., Firestore) with subscription info - break; - - case 'customer.subscription.trial_will_end': - const subscription = event.data.object; - console.log('Trial ending soon for subscription:', subscription.id); - // TODO: Send email reminder at day 5 of trial - // TODO: Track trial conversion analytics - break; - - case 'customer.subscription.created': - const newSubscription = event.data.object; - console.log('New subscription created:', newSubscription.id); - if (newSubscription.trial_end) { - console.log('Subscription has trial ending at:', new Date(newSubscription.trial_end * 1000)); - // TODO: Track trial sign-up analytics with source: full_pro_trial - } - break; - - case 'customer.subscription.updated': - const updatedSubscription = event.data.object; - console.log('Subscription updated:', updatedSubscription.id); - // TODO: Handle trial to paid conversion - break; - - case 'customer.subscription.deleted': - const deletedSubscription = event.data.object; - console.log('Subscription cancelled:', deletedSubscription.id); - // TODO: Track cancellation analytics - break; - - default: - console.log(`Unhandled event type: ${event.type}`); - } - - res.status(200).json({ received: true }); -}; \ No newline at end of file diff --git a/api/validate-session.js b/api/validate-session.js deleted file mode 100644 index 5ecf6025..00000000 --- a/api/validate-session.js +++ /dev/null @@ -1,91 +0,0 @@ -const Stripe = require('stripe'); -const stripeConfig = require('../lib/stripeConfig.server.js'); - -// Enhanced error checking and logging -console.log('Loading validate-session with config:', { - mode: stripeConfig.mode, - hasSecretKey: !!stripeConfig.secretKey, - secretKeyFormat: stripeConfig.secretKey ? (stripeConfig.secretKey.startsWith('sk_') ? 'valid' : 'invalid') : 'missing' -}); - -module.exports = async (req, res) => { - console.log('Received request to /api/validate-session'); - - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ error: 'Method Not Allowed' }); - } - - // Validate Stripe configuration before proceeding - if (!stripeConfig.secretKey) { - console.error('Stripe secret key not configured for', stripeConfig.mode, 'mode'); - return res.status(500).json({ - error: `Stripe secret key is not configured for ${stripeConfig.mode} mode. Please set the appropriate environment variables.`, - isValid: false - }); - } - - // Additional validation to ensure we're not using a publishable key as secret key - if (stripeConfig.secretKey.startsWith('pk_')) { - console.error('CRITICAL: A publishable key was provided as secret key'); - return res.status(500).json({ - error: 'CRITICAL: A publishable key was provided as secret key. This will cause API failures. Please check your environment configuration.', - isValid: false - }); - } - - if (!stripeConfig.secretKey.startsWith('sk_')) { - console.error('Invalid secret key format:', stripeConfig.secretKey.substring(0, 3)); - return res.status(500).json({ - error: `Invalid secret key format. Secret keys should start with 'sk_'. Current key starts with: ${stripeConfig.secretKey.substring(0, 3)}`, - isValid: false - }); - } - - // Initialize Stripe only after validation - let stripe; - try { - stripe = new Stripe(stripeConfig.secretKey, { - apiVersion: '2022-11-15', - }); - } catch (stripeInitError) { - console.error('Failed to initialize Stripe:', stripeInitError.message); - return res.status(500).json({ - error: 'Failed to initialize payment system. Please contact support.', - isValid: false - }); - } - - const { session_id } = req.body; - if (!session_id) { - console.log('Missing session_id parameter'); - return res.status(400).json({ error: 'Missing session_id parameter', isValid: false }); - } - - try { - // Retrieve the session from Stripe - const session = await stripe.checkout.sessions.retrieve(session_id); - - // Validate that the payment was successful and it's a subscription - const isValid = session && - session.payment_status === 'paid' && - session.mode === 'subscription'; - - console.log(`Session ${session_id} validation result:`, { - isValid, - paymentStatus: session.payment_status, - mode: session.mode - }); - - // Return the validation result - res.status(200).json({ - isValid, - paymentStatus: session.payment_status, - mode: session.mode - }); - - } catch (err) { - console.error('Error validating session:', err.message); - res.status(500).json({ error: err.message, isValid: false }); - } -}; \ No newline at end of file diff --git a/app.config.js b/app.config.js index 89ded9dc..b9dbc64e 100644 --- a/app.config.js +++ b/app.config.js @@ -35,16 +35,6 @@ module.exports = { extra: { NEXTPUBLIC_ENVIRONMENT: process.env.NEXTPUBLIC_ENVIRONMENT || 'production', OPENAI_API_KEY: process.env.OPENAI_API_KEY, - // Legacy Stripe keys for backward compatibility - STRIPE_PUBLISHABLE_KEY: process.env.STRIPE_PUBLISHABLE_KEY, - // New Stripe configuration with feature flag support - STRIPE_MODE: process.env.STRIPE_MODE || 'test', - STRIPE_TEST_PUBLISHABLE_KEY: process.env.STRIPE_TEST_PUBLISHABLE_KEY || process.env.STRIPE_PUBLISHABLE_KEY, - STRIPE_TEST_SECRET_KEY: process.env.STRIPE_TEST_SECRET_KEY || process.env.STRIPE_SECRET_KEY, - STRIPE_LIVE_PUBLISHABLE_KEY: process.env.STRIPE_LIVE_PUBLISHABLE_KEY, - STRIPE_LIVE_SECRET_KEY: process.env.STRIPE_LIVE_SECRET_KEY, - STRIPE_TEST_PRICE_ID: process.env.STRIPE_TEST_PRICE_ID || 'price_1REyzMPE5x6FmwJPyJVJIEXe', - STRIPE_LIVE_PRICE_ID: process.env.STRIPE_LIVE_PRICE_ID, TEST_LOGIN: process.env.REACT_APP_TEST_LOGIN === 'true', // Environment flag for auto-login testing firebase: { apiKey: process.env.FIREBASE_API_KEY || "AIzaSyCOcwQe3AOdanV43iSwYlNxhzSKSRIOq34", diff --git a/app/(tabs)/new-dose.tsx b/app/(tabs)/new-dose.tsx index 4e078309..d5a2e100 100644 --- a/app/(tabs)/new-dose.tsx +++ b/app/(tabs)/new-dose.tsx @@ -1153,13 +1153,11 @@ export default function NewDoseScreen() { setShowLimitModal(false)} /> (null); - const [isLoadingSubscription, setIsLoadingSubscription] = useState(false); - const [isManagingSubscription, setIsManagingSubscription] = useState(false); const [isSigningIn, setIsSigningIn] = useState(false); - useEffect(() => { - if (user && !user.isAnonymous) { - fetchSubscriptionStatus(); - } - }, [user]); - - const fetchSubscriptionStatus = async () => { - if (!user || user.isAnonymous) return; - - setIsLoadingSubscription(true); - try { - const idToken = await user.getIdToken(); - const response = await fetch('/api/get-subscription-status', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ idToken }), - }); - - if (response.ok) { - const data = await response.json(); - setSubscriptionStatus(data); - console.log('Subscription status loaded:', data); - } else { - console.error('Failed to fetch subscription status:', response.statusText); - } - } catch (error) { - console.error('Error fetching subscription status:', error); - } finally { - setIsLoadingSubscription(false); - } - }; - - const handleManageSubscription = async () => { - if (!subscriptionStatus?.customerId) { - Alert.alert( - 'No Subscription Found', - 'You don\'t have an active subscription to manage. You can upgrade from the pricing page.', - [{ text: 'OK' }] - ); - return; - } - - setIsManagingSubscription(true); - logAnalyticsEvent(ANALYTICS_EVENTS.CANCEL_SUBSCRIPTION); - - try { - const response = await fetch('/api/create-portal-session', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - customerId: subscriptionStatus.customerId, - returnUrl: typeof window !== 'undefined' ? window.location.href : 'https://safedose.app/settings', - }), - }); - - if (response.ok) { - const { url } = await response.json(); - - // Handle opening portal based on platform - if (typeof window !== 'undefined') { - window.open(url, '_blank'); - } else { - // For React Native, you could use Linking.openURL(url) - console.log('Portal URL:', url); - Alert.alert('Subscription Management', 'Portal URL generated. Check console for details.'); - } - } else { - const errorData = await response.json(); - Alert.alert('Error', errorData.error || 'Failed to open subscription management'); - } - } catch (error) { - console.error('Error creating portal session:', error); - Alert.alert('Error', 'Failed to open subscription management. Please try again.'); - } finally { - setIsManagingSubscription(false); - } - }; - const handleSignOut = () => { Alert.alert( 'Sign Out', @@ -187,19 +88,7 @@ export default function SettingsScreen() { }); }, [auth, user]); - const getPlanDisplayName = (plan: string) => { - switch (plan) { - case 'starter': return 'Starter'; - case 'basic-pro': return 'Basic Pro'; - case 'full-pro': return 'Full Pro'; - case 'free': return 'Free'; - default: return plan; - } - }; - const formatDate = (timestamp: number) => { - return new Date(timestamp * 1000).toLocaleDateString(); - }; return ( @@ -224,9 +113,6 @@ export default function SettingsScreen() { )} - Current Plan - {getPlanDisplayName(usageData?.plan || 'free')} - Usage This Month {usageData?.scansUsed || 0} / {usageData?.limit === Infinity ? '∞' : (usageData?.limit || 3)} scans @@ -234,73 +120,7 @@ export default function SettingsScreen() { - {/* Subscription Management */} - {!user?.isAnonymous && ( - - - - Subscription - - - {isLoadingSubscription ? ( - - - Loading subscription... - - ) : subscriptionStatus ? ( - - Status - - {subscriptionStatus.hasActiveSubscription ? 'Active' : 'No Active Subscription'} - - - {subscriptionStatus.subscription && ( - <> - Next Billing Date - - {formatDate(subscriptionStatus.subscription.currentPeriodEnd)} - - - {subscriptionStatus.subscription.cancelAtPeriodEnd && ( - - - - Subscription will cancel at the end of the current period - - - )} - - )} - - - {isManagingSubscription ? ( - - ) : ( - - {subscriptionStatus.hasActiveSubscription ? 'Manage Subscription' : 'View Billing'} - - )} - - - ) : ( - - No subscription information available - - Refresh - - - )} - - )} + {/* Sign In/Out */} @@ -411,19 +231,6 @@ const styles = StyleSheet.create({ marginLeft: 8, flex: 1, }, - subscriptionButton: { - backgroundColor: '#007AFF', - paddingVertical: 12, - paddingHorizontal: 20, - borderRadius: 8, - marginTop: 16, - alignItems: 'center', - }, - subscriptionButtonText: { - color: '#FFFFFF', - fontSize: 16, - fontWeight: '600', - }, signOutButton: { backgroundColor: '#FF3B30', paddingVertical: 16, diff --git a/app/pricing.tsx b/app/pricing.tsx deleted file mode 100644 index c1a078ec..00000000 --- a/app/pricing.tsx +++ /dev/null @@ -1,792 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { loadStripe } from "@stripe/stripe-js"; -import stripeConfig from "../lib/stripeConfig"; -import { View, Text, StyleSheet, TouchableOpacity, Animated, Dimensions, Image } from "react-native"; -import { router } from "expo-router"; -import { logAnalyticsEvent, ANALYTICS_EVENTS } from "../lib/analytics"; -import { useUserProfile } from "../contexts/UserProfileContext"; -import { CheckCircle, Shield, Zap, Clock } from "lucide-react-native"; - -// Initialize Stripe.js with the configuration, handling missing publishable key gracefully -const stripePromise = stripeConfig.publishableKey - ? loadStripe(stripeConfig.publishableKey) - : null; - -// Base URL for your API -const API_BASE_URL = "https://app.safedoseai.com"; - -// Get screen dimensions for responsive design -const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); - -export default function PricingPage() { - const { profile } = useUserProfile(); - - // Animation refs - const fadeAnim = useRef(new Animated.Value(0)).current; - const scaleAnim = useRef(new Animated.Value(0.95)).current; - const pulseAnim = useRef(new Animated.Value(1)).current; - // Updated pricing plans with new 4-tier structure and Stripe Price IDs - const pricingPlansData = [ - { - id: 'starter_monthly', - name: "Starter", - price: 4.99, - priceSuffix: "/month", - subtext: "20 saved doses, 10 AI scans", - priceId: 'price_1RYgx7AY2p4W374YR9UxS0vr', - badgeText: "Entry Level", - isDefault: false, - planType: 'starter', - isMonthly: true, - }, - { - id: 'starter_yearly', - name: "Starter", - price: 44.99, - originalPrice: 59.88, - priceSuffix: "/year", - subtext: "Save 25%", - priceId: 'price_1RYgx7AY2p4W374Yy23EtyIm', - badgeText: "Entry Level", - isDefault: false, - savings: 25, - planType: 'starter', - isMonthly: false, - }, - { - id: 'basic_pro_monthly', - name: "Basic Pro", - price: 9.99, - priceSuffix: "/month", - subtext: "Unlimited logs, 20 AI scans", - priceId: 'price_1RYgyPAY2p4W374YNbpBpbqv', - badgeText: "Popular", - isDefault: true, - planType: 'basic_pro', - isMonthly: true, - }, - { - id: 'basic_pro_yearly', - name: "Basic Pro", - price: 89.99, - originalPrice: 119.88, - priceSuffix: "/year", - subtext: "Save 25%", - priceId: 'price_1RYgyPAY2p4W374YJOhwDafY', - badgeText: "Popular", - isDefault: false, - savings: 25, - planType: 'basic_pro', - isMonthly: false, - }, - { - id: 'full_pro_monthly', - name: "Full Pro", - price: 20, - priceSuffix: "/month", - subtext: "7-day free trial", - priceId: 'price_1RUHgxAY2p4W374Yb5EWEtZ0', - badgeText: "Best Value", - isDefault: false, - planType: 'full_pro', - isMonthly: true, - hasTrial: true, - }, - { - id: 'full_pro_yearly', - name: "Full Pro", - price: 179.99, - originalPrice: 240, - priceSuffix: "/year", - subtext: "7-day free trial, Save 25%", - priceId: 'price_1RYgzUAY2p4W374YHiBBHvuX', - badgeText: "Best Value", - isDefault: false, - savings: 25, - planType: 'full_pro', - isMonthly: false, - hasTrial: true, - }, - ]; - - // Key features for compact display - updated for new pricing structure - const getKeyFeatures = (planType) => { - switch (planType) { - case 'starter': - return [ - { icon: Zap, text: "10 AI scans/month", color: "#FF6B6B" }, - { icon: CheckCircle, text: "20 saved doses", color: "#4ECDC4" }, - { icon: Clock, text: "Manual calculations", color: "#45B7D1" }, - { icon: Shield, text: "Basic support", color: "#96CEB4" }, - ]; - case 'basic_pro': - return [ - { icon: Zap, text: "20 AI scans/month", color: "#FF6B6B" }, - { icon: CheckCircle, text: "Unlimited logs", color: "#4ECDC4" }, - { icon: Clock, text: "Manual calculations", color: "#45B7D1" }, - { icon: Shield, text: "Priority support", color: "#96CEB4" }, - ]; - case 'full_pro': - return [ - { icon: Zap, text: "Unlimited AI scans", color: "#FF6B6B" }, - { icon: CheckCircle, text: "Unlimited logs", color: "#4ECDC4" }, - { icon: Clock, text: "Priority processing", color: "#45B7D1" }, - { icon: Shield, text: "Premium support", color: "#96CEB4" }, - ]; - default: - return [ - { icon: Zap, text: "3 AI scans/month", color: "#FF6B6B" }, - { icon: CheckCircle, text: "10 saved doses", color: "#4ECDC4" }, - { icon: Clock, text: "Manual calculations", color: "#45B7D1" }, - { icon: Shield, text: "Basic support", color: "#96CEB4" }, - ]; - } - }; - - // Dynamic headline based on user profile - const getPersonalizedHeadline = () => { - if (profile?.isLicensedProfessional) { - return "Professional Tools for Accurate Dosing"; - } else if (profile?.isPersonalUse && !profile?.isCosmeticUse) { - return "Safe, Reliable Dose Calculations"; - } else { - return "Precision Dosing Made Simple"; - } - }; - - const defaultPlan = pricingPlansData.find(plan => plan.isDefault) || pricingPlansData[0]; - const [selectedPlan, setSelectedPlan] = useState(defaultPlan); - const [selectedPlanType, setSelectedPlanType] = useState(defaultPlan.planType); - const [isLoading, setIsLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - - // Get current key features based on selected plan type - const keyFeatures = getKeyFeatures(selectedPlanType); - - // Log view_pricing_page event when component mounts - useEffect(() => { - logAnalyticsEvent(ANALYTICS_EVENTS.VIEW_PRICING_PAGE); - - // Start entrance animations - Animated.parallel([ - Animated.timing(fadeAnim, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - Animated.timing(scaleAnim, { - toValue: 1, - duration: 500, - useNativeDriver: true, - }), - ]).start(); - - // Start pulse animation for icon - const pulseLoop = () => { - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1.1, - duration: 1000, - useNativeDriver: true, - }), - Animated.timing(pulseAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }), - ]).start(() => pulseLoop()); - }; - - const pulseTimer = setTimeout(pulseLoop, 1000); - return () => clearTimeout(pulseTimer); - }, [fadeAnim, scaleAnim, pulseAnim]); - - const initiateStripeCheckout = async () => { - console.log(`initiateStripeCheckout called for ${selectedPlan.name}`); - logAnalyticsEvent(ANALYTICS_EVENTS.INITIATE_UPGRADE, { plan: selectedPlan.id }); - - setIsLoading(true); - setErrorMessage(""); - - try { - // Check if publishable key is missing before proceeding - if (!stripeConfig.publishableKey) { - console.error("Stripe publishable key is missing"); - setErrorMessage("Payment system configuration error. Please contact support - Stripe publishable key is missing."); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: selectedPlan.id, - error: 'Stripe publishable key is missing' - }); - return; - } - - const stripe = await stripePromise; - console.log("Stripe instance:", stripe); - if (!stripe) { - console.error("Stripe is not initialized"); - setErrorMessage("Stripe is not initialized. Please try again later."); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: selectedPlan.id, - error: 'Stripe not initialized' - }); - return; - } - - const priceId = selectedPlan.priceId; - console.log("Using priceId:", priceId); - - // Debug: show payload - console.log(`Calling ${API_BASE_URL}/api/create-checkout-session with:`, { - priceId, - successUrl: `${API_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`, - cancelUrl: `${API_BASE_URL}/pricing`, - hasTrial: selectedPlan.hasTrial, - }); - - const res = await fetch( - `${API_BASE_URL}/api/create-checkout-session`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - priceId, - successUrl: `${API_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`, - cancelUrl: `${API_BASE_URL}/pricing`, - hasTrial: selectedPlan.hasTrial, - }), - } - ); - console.log("create-checkout-session status:", res.status); - - const text = await res.text(); - console.log("create-checkout-session raw response:", text); - let data: any; - try { - data = JSON.parse(text); - } catch (e) { - console.error("Failed to parse JSON:", e); - throw new Error("Invalid JSON from checkout session"); - } - - if (!res.ok) { - console.error("Error response from API:", data); - - // Provide specific error messages for common configuration issues - let userFriendlyMessage = data.error || "Failed to create checkout session"; - - if (data.error && data.error.includes("publishable API key")) { - userFriendlyMessage = "Payment system configuration error. Please contact support - this issue has been logged for immediate attention."; - console.error("CRITICAL: Stripe configuration error detected - publishable key used as secret key"); - } else if (data.error && data.error.includes("secret key")) { - userFriendlyMessage = "Payment system temporarily unavailable. Please try again later or contact support."; - console.error("CRITICAL: Stripe secret key configuration error"); - } else if (res.status === 500) { - userFriendlyMessage = "Payment system temporarily unavailable. Please try again in a few moments."; - } - - setErrorMessage(userFriendlyMessage); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: selectedPlan.id, - error: data.error || 'Failed to create checkout session', - userMessage: userFriendlyMessage - }); - return; - } - - const { sessionId } = data; - console.log("Received sessionId:", sessionId); - - console.log("Redirecting to Stripe Checkout..."); - const result = await stripe.redirectToCheckout({ sessionId }); - console.log("redirectToCheckout result:", result); - if (result?.error) { - console.error("Stripe redirectToCheckout error:", result.error); - setErrorMessage(result.error.message); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: selectedPlan.id, - error: result.error.message - }); - } - } catch (error: any) { - console.error("Checkout error caught:", error); - setErrorMessage(error.message || "Unable to initiate checkout. Please try again."); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: selectedPlan.id, - error: error.message || 'Unable to initiate checkout' - }); - } finally { - setIsLoading(false); - } - }; - - const handleCancel = () => { - router.back(); - }; - - return ( - - {/* App Icon with Pulse Animation */} - - - - - - - {/* Personalized Headline */} - {getPersonalizedHeadline()} - Upgrade to unlock premium features - - {/* Key Features Grid */} - - {keyFeatures.map((feature, index) => ( - - - - - {feature.text} - - ))} - - - {/* Plan Type Selection */} - - {['starter', 'basic_pro', 'full_pro'].map((planType) => { - const planData = pricingPlansData.find(p => p.planType === planType && p.isMonthly); - return ( - { - setSelectedPlanType(planType); - const newPlan = pricingPlansData.find(p => p.planType === planType && p.isMonthly); - setSelectedPlan(newPlan); - }} - activeOpacity={0.8} - > - {planData.name} - ${planData.price}/mo - {planData.hasTrial && ( - Free Trial - )} - - ); - })} - - - {/* Monthly/Yearly Toggle */} - - {pricingPlansData.filter(plan => plan.planType === selectedPlanType).map((plan) => ( - setSelectedPlan(plan)} - activeOpacity={0.8} - > - {plan.badgeText && plan.id === selectedPlan.id && ( - - {plan.badgeText} - - )} - - {plan.isMonthly ? 'Monthly' : 'Yearly'} - - - {plan.originalPrice && ( - ${plan.originalPrice} - )} - ${plan.price} - - - {plan.priceSuffix} - {plan.subtext} - - {plan.savings && ( - - Save {plan.savings}% - - )} - - ))} - - - {/* Primary CTA */} - - - {isLoading ? "Processing..." : selectedPlan.hasTrial ? "Start Free Trial" : `Upgrade to ${selectedPlan.name}`} - - - - {/* Trial Info */} - - {selectedPlan.hasTrial - ? "7-day free trial • Cancel anytime • No payment required today" - : `Start with ${selectedPlan.name} plan • Cancel anytime` - } - - - {/* Error Message */} - {errorMessage ? ( - {errorMessage} - ) : null} - - {/* Footer Actions */} - - - Maybe Later - - - - - - Restore Purchase - - - - {/* Privacy Links */} - - - Privacy Policy - - - - Terms of Service - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#FFFFFF', - paddingHorizontal: 24, - paddingTop: screenHeight <= 667 ? 20 : Math.max(50, screenHeight * 0.08), // Further reduced top padding for iPhone SE - paddingBottom: screenHeight <= 667 ? 10 : 30, // Further reduced bottom padding for iPhone SE - justifyContent: screenHeight <= 667 ? 'flex-start' : 'space-between', // Change layout strategy for small screens - alignItems: 'center', - }, - - // App Icon Section - iconContainer: { - marginBottom: screenHeight <= 667 ? 8 : 20, // Further reduced margin for iPhone SE - }, - iconBackground: { - width: screenHeight <= 667 ? 64 : 80, // Smaller icon for iPhone SE - height: screenHeight <= 667 ? 64 : 80, - borderRadius: screenHeight <= 667 ? 16 : 20, - backgroundColor: '#F8F9FA', - justifyContent: 'center', - alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.1, - shadowRadius: 12, - elevation: 8, - }, - appIcon: { - width: screenHeight <= 667 ? 48 : 60, // Smaller icon for iPhone SE - height: screenHeight <= 667 ? 48 : 60, - }, - - // Headlines - headline: { - fontSize: screenHeight <= 667 ? 20 : 24, // Smaller font for iPhone SE - fontWeight: '700', - color: '#1A1A1A', - textAlign: 'center', - marginBottom: 6, // Reduced margin - letterSpacing: -0.5, - }, - subheadline: { - fontSize: screenHeight <= 667 ? 14 : 16, // Smaller font for iPhone SE - color: '#666666', - textAlign: 'center', - marginBottom: screenHeight <= 667 ? 16 : 32, // Further reduced margin for iPhone SE - lineHeight: screenHeight <= 667 ? 18 : 22, - }, - - // Features Section - featuresContainer: { - width: '100%', - marginBottom: screenHeight <= 667 ? 16 : 32, // Further reduced margin for iPhone SE - }, - featureItem: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: screenHeight <= 667 ? 6 : 12, // Further reduced margin for iPhone SE - paddingHorizontal: 8, - }, - featureIcon: { - width: screenHeight <= 667 ? 28 : 32, // Smaller icons for iPhone SE - height: screenHeight <= 667 ? 28 : 32, - borderRadius: screenHeight <= 667 ? 14 : 16, - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - featureText: { - fontSize: screenHeight <= 667 ? 14 : 15, // Smaller font for iPhone SE - color: '#333333', - fontWeight: '500', - flex: 1, - }, - featureIconDisabled: { - opacity: 0.5, - }, - featureTextDisabled: { - opacity: 0.5, - textDecorationLine: 'line-through', - }, - - // Plan Types Section - planTypesContainer: { - width: '100%', - marginBottom: screenHeight <= 667 ? 12 : 16, - }, - planTypeBox: { - backgroundColor: '#F8F9FA', - borderRadius: 12, - padding: 16, - marginBottom: 8, - borderWidth: 2, - borderColor: 'transparent', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - selectedPlanTypeBox: { - backgroundColor: '#FFFFFF', - borderColor: '#007AFF', - shadowColor: '#007AFF', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, - }, - planTypeName: { - fontSize: 16, - fontWeight: '600', - color: '#1A1A1A', - flex: 1, - }, - selectedPlanTypeName: { - color: '#007AFF', - }, - planTypePrice: { - fontSize: 14, - fontWeight: '600', - color: '#666666', - marginRight: 8, - }, - planTypeTrialBadge: { - fontSize: 10, - color: '#34C759', - fontWeight: '600', - backgroundColor: '#34C759' + '20', - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - }, - - // Plans Section - plansContainer: { - flexDirection: 'row', - width: '100%', - marginBottom: screenHeight <= 667 ? 12 : 24, // Further reduced margin for iPhone SE - gap: 12, - }, - planBox: { - flex: 1, - backgroundColor: '#F8F9FA', - borderRadius: 16, - padding: screenHeight <= 667 ? 12 : 16, // Reduced padding for iPhone SE - alignItems: 'center', - borderWidth: 2, - borderColor: 'transparent', - position: 'relative', - minHeight: screenHeight <= 667 ? 110 : 140, // Reduced height for iPhone SE - }, - selectedPlanBox: { - backgroundColor: '#FFFFFF', - borderColor: '#007AFF', - shadowColor: '#007AFF', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 12, - elevation: 8, - }, - planBadge: { - position: 'absolute', - top: -8, - backgroundColor: '#007AFF', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 12, - zIndex: 1, - }, - planName: { - fontSize: 16, - fontWeight: '600', - color: '#1A1A1A', - marginBottom: 4, - marginTop: 8, - }, - priceRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 2, - }, - originalPrice: { - fontSize: 14, - color: '#999999', - textDecorationLine: 'line-through', - marginRight: 4, - }, - planPrice: { - fontSize: 20, - fontWeight: '700', - color: '#1A1A1A', - }, - planPeriod: { - fontSize: 12, - color: '#666666', - marginBottom: 4, - }, - planSubtext: { - fontSize: 11, - color: '#007AFF', - fontWeight: '500', - textAlign: 'center', - }, - savingsContainer: { - position: 'absolute', - bottom: 8, - right: 8, - backgroundColor: '#34C759', - paddingHorizontal: 6, - paddingVertical: 2, - borderRadius: 8, - }, - savingsText: { - fontSize: 10, - color: '#FFFFFF', - fontWeight: '600', - }, - - // CTA Button - ctaButton: { - width: '100%', - backgroundColor: '#007AFF', - paddingVertical: screenHeight <= 667 ? 14 : 16, // Slightly reduced padding for iPhone SE - borderRadius: 12, - alignItems: 'center', - marginBottom: screenHeight <= 667 ? 8 : 12, // Reduced margin for iPhone SE - marginTop: screenHeight <= 667 ? 8 : 0, // Add small top margin for iPhone SE - shadowColor: '#007AFF', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 6, - }, - ctaButtonText: { - color: '#FFFFFF', - fontSize: 17, - fontWeight: '600', - letterSpacing: -0.3, - }, - - // Trial Info - trialInfo: { - fontSize: screenHeight <= 667 ? 12 : 13, // Smaller font for iPhone SE - color: '#666666', - textAlign: 'center', - marginBottom: screenHeight <= 667 ? 12 : 20, // Reduced margin for iPhone SE - lineHeight: screenHeight <= 667 ? 16 : 18, - }, - - // Error - errorText: { - color: '#FF3B30', - fontSize: 14, - marginBottom: 16, - textAlign: 'center', - paddingHorizontal: 16, - }, - - // Footer - footerActions: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: screenHeight <= 667 ? 8 : 16, // Reduced margin for iPhone SE - }, - footerLink: { - color: '#007AFF', - fontSize: screenHeight <= 667 ? 14 : 15, // Smaller font for iPhone SE - fontWeight: '500', - }, - footerSeparator: { - width: 1, - height: 16, - backgroundColor: '#E5E5EA', - marginHorizontal: 16, - }, - - // Privacy - privacyLinks: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginBottom: screenHeight <= 667 ? 10 : 0, // Add small bottom margin for iPhone SE - }, - privacyText: { - color: '#8E8E93', - fontSize: 12, - }, - privacySeparator: { - color: '#8E8E93', - fontSize: 12, - }, - - // Badge text (shared) - badgeText: { - color: '#FFFFFF', - fontSize: 10, - fontWeight: '700', - textTransform: 'uppercase', - letterSpacing: 0.5, - }, -}); diff --git a/app/success.tsx b/app/success.tsx deleted file mode 100644 index 8e9df555..00000000 --- a/app/success.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; -import { router, useLocalSearchParams } from 'expo-router'; -import { doc, setDoc } from 'firebase/firestore'; -import { getAuth } from 'firebase/auth'; -import { db } from '../lib/firebase'; -import Constants from 'expo-constants'; -import { logAnalyticsEvent, setAnalyticsUserProperties, ANALYTICS_EVENTS, USER_PROPERTIES } from '../lib/analytics'; - -// Base URL for API -const API_BASE_URL = "https://app.safedoseai.com"; - -export default function SuccessScreen() { - const { session_id } = useLocalSearchParams(); - const [error, setError] = useState(null); - const [isValid, setIsValid] = useState(false); - const [shouldRedirect, setShouldRedirect] = useState(false); - - // Handle redirection separately from validation logic - useEffect(() => { - if (shouldRedirect) { - // Use setTimeout to ensure navigation happens after component is fully mounted - setTimeout(() => { - router.replace('/(tabs)/new-dose'); - }, 100); - } - }, [shouldRedirect]); - - useEffect(() => { - const validatePaymentAndUpdatePlan = async () => { - // Set redirect flag if no session_id is provided - if (!session_id) { - console.log('[SuccessScreen] No session_id provided, setting redirect flag'); - setShouldRedirect(true); - return; - } - - try { - // Call our API to validate the payment session - const response = await fetch(`${API_BASE_URL}/api/validate-session`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ session_id }), - }); - - const data = await response.json(); - console.log('[SuccessScreen] Session validation response:', data); - - // Check if the response indicates a valid payment - if (!response.ok || !data.isValid) { - console.log('[SuccessScreen] Invalid session or payment not completed, setting redirect flag'); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: 'plus', - error: 'Invalid session or payment not completed' - }); - setShouldRedirect(true); - return; - } - - // Payment is valid, mark as valid - setIsValid(true); - - // Log successful upgrade - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_SUCCESS, { plan: 'plus' }); - - // Update user plan in Firestore since payment is confirmed - const auth = getAuth(); - const user = auth.currentUser; - if (user) { - const userRef = doc(db, 'users', user.uid); - await setDoc(userRef, { plan: 'plus', limit: 150, scansUsed: 0 }, { merge: true }); - - // Set user properties for analytics - setAnalyticsUserProperties({ - [USER_PROPERTIES.PLAN_TYPE]: 'plus', - [USER_PROPERTIES.IS_ANONYMOUS]: user.isAnonymous, - }); - - console.log('[SuccessScreen] User plan updated to premium'); - } else { - throw new Error('No authenticated user found'); - } - } catch (err) { - console.error('[SuccessScreen] Error validating payment or updating user:', err); - logAnalyticsEvent(ANALYTICS_EVENTS.UPGRADE_FAILURE, { - plan: 'plus', - error: err instanceof Error ? err.message : 'Unknown error' - }); - setError('Failed to process your upgrade. Please try again.'); - setShouldRedirect(true); - } - }; - - validatePaymentAndUpdatePlan(); - }, [session_id]); - - const handleContinue = () => { - router.push('/(tabs)/new-dose'); - }; - - // Show nothing while validating or redirecting - if (!isValid) { - return null; - } - - return ( - - Upgrade Successful! - You're now a Premium user with 150 scans per month. - {error && {error}} - - Continue to SafeDose - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 20, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#F2F2F7' - }, - title: { - fontSize: 24, - fontWeight: 'bold', - color: '#34C759', - marginBottom: 20 - }, - message: { - fontSize: 16, - textAlign: 'center', - marginBottom: 30, - color: '#333333' - }, - error: { - fontSize: 14, - color: '#f87171', - marginBottom: 20, - textAlign: 'center' - }, - button: { - backgroundColor: '#007AFF', - paddingVertical: 14, - paddingHorizontal: 30, - borderRadius: 12 - }, - buttonText: { - color: '#FFFFFF', - fontSize: 16, - fontWeight: '600' - }, -}); \ No newline at end of file diff --git a/components/IntroScreen.tsx b/components/IntroScreen.tsx index 0fccace9..70b2f2e6 100644 --- a/components/IntroScreen.tsx +++ b/components/IntroScreen.tsx @@ -179,9 +179,7 @@ export default function IntroScreen({ }); }, [auth, user]); - const handleUpgradePress = useCallback(() => { - router.push('/pricing'); - }, [router]); + const handleLogoutPress = useCallback(async () => { if (isLoggingOut) return; @@ -288,9 +286,13 @@ export default function IntroScreen({ // Check if user has remaining scans const scansRemaining = usageData ? usageData.limit - usageData.scansUsed : 3; - if (scansRemaining <= 0) { - // If no scans remaining, redirect to pricing page - router.push('/pricing'); + if (scansRemaining <= 0 && user?.isAnonymous) { + // If anonymous user is out of scans, encourage sign in + Alert.alert( + 'Sign In to Continue', + 'You\'ve used your free scans. Sign in to continue using SafeDose.', + [{ text: 'OK' }] + ); return; } @@ -412,7 +414,7 @@ export default function IntroScreen({ > - {isOutOfScans ? 'Upgrade' : 'Scan'} + Scan ); @@ -480,12 +482,7 @@ export default function IntroScreen({ return "You've used all your scans"; } return `You have ${scansRemaining} scans remaining.`; - })()}{' '} - {(!usageData || usageData.plan === 'free') && ( - - Upgrade - - )} + })()} @@ -726,7 +723,7 @@ const styles = StyleSheet.create({ backgroundColor: '#007AFF', }, outOfScansButton: { - backgroundColor: '#f59e0b', // Orange color to indicate upgrade needed + backgroundColor: '#8E8E93', // Gray color to indicate disabled }, secondaryButton: { backgroundColor: '#6366f1', @@ -769,12 +766,6 @@ const styles = StyleSheet.create({ color: '#666', textAlign: 'center', }, - upgradeLink: { - fontSize: 14, - color: '#007AFF', - textDecorationLine: 'underline', - fontWeight: '500', - }, /* Reconstitution Link */ reconstitutionLinkContainer: { diff --git a/components/LimitModal.tsx b/components/LimitModal.tsx index 0c188397..fbef8ebc 100644 --- a/components/LimitModal.tsx +++ b/components/LimitModal.tsx @@ -28,13 +28,6 @@ export default function LimitModal({ visible, isAnonymous, isPremium = false, on onClose(); }; - const handleUpgrade = () => { - console.log('[LimitModal] Upgrade button pressed'); - logAnalyticsEvent(ANALYTICS_EVENTS.LIMIT_MODAL_ACTION, { action: 'upgrade' }); - router.push('/pricing'); - onClose(); - }; - const handleCancel = () => { console.log('[LimitModal] Cancel button pressed'); logAnalyticsEvent(ANALYTICS_EVENTS.LIMIT_MODAL_ACTION, { action: 'cancel' }); @@ -51,12 +44,12 @@ export default function LimitModal({ visible, isAnonymous, isPremium = false, on - {isAnonymous ? 'Free Scan Limit Reached' : 'Plan Limit Reached'} + {isAnonymous ? 'Free Scan Limit Reached' : 'Scan Limit Reached'} {isAnonymous - ? 'You’ve used all 3 free scans. Sign in to get 10 scans per month or upgrade for more.' - : 'You’ve reached your plan’s scan limit. Upgrade to a premium plan for additional scans.'} + ? "You've used your free scans for this session. Sign in to continue using SafeDose and save your dose history." + : "You've reached your scan limit for this period. SafeDose is committed to long-term safety and reliability. Your usage helps us improve the system for everyone."} {isAnonymous && ( @@ -64,13 +57,8 @@ export default function LimitModal({ visible, isAnonymous, isPremium = false, on Sign In )} - {!isPremium && ( - - Upgrade - - )} - Cancel + OK @@ -121,9 +109,6 @@ const styles = StyleSheet.create({ signInButton: { backgroundColor: '#007AFF', }, - upgradeButton: { - backgroundColor: '#34C759', - }, cancelButton: { backgroundColor: '#8E8E93', }, @@ -132,4 +117,4 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '600', }, -}); \ No newline at end of file +}); diff --git a/components/LogLimitModal.tsx b/components/LogLimitModal.tsx index 39c0da35..9fae5766 100644 --- a/components/LogLimitModal.tsx +++ b/components/LogLimitModal.tsx @@ -30,13 +30,6 @@ export default function LogLimitModal({ } }, [visible, triggerReason]); - const handleUpgrade = () => { - console.log('[LogLimitModal] Upgrade to Pro button pressed'); - logAnalyticsEvent(ANALYTICS_EVENTS.LIMIT_MODAL_ACTION, { action: 'upgrade_pro', type: triggerReason }); - router.push('/pricing'); - onClose(); - }; - const handleContinueWithoutSaving = () => { console.log('[LogLimitModal] Continue without saving pressed'); logAnalyticsEvent(ANALYTICS_EVENTS.LIMIT_MODAL_ACTION, { action: 'continue_without_saving', type: triggerReason }); @@ -63,9 +56,9 @@ export default function LogLimitModal({ const getMessage = () => { if (isPowerUserPromotion) { - return "Great job on completing multiple doses! Upgrade to Pro to unlock unlimited logs, access the AI vial scanner, and support the ongoing development of the tool."; + return "Great job on completing multiple doses! Your usage helps us improve SafeDose for everyone. Continue building safe, reliable dosing habits."; } - return "You've reached your monthly dose logging limit. Upgrade to Pro to save unlimited dose logs, access the AI vial scanner, and support ongoing development."; + return "You've reached your dose logging limit for this period. SafeDose is free and open source, focused on long-term safety. You can continue without saving this dose."; }; return ( @@ -84,19 +77,14 @@ export default function LogLimitModal({ {getMessage()} - - Upgrade to Pro - {isLogLimit && ( - - Continue without saving - - )} - {isPowerUserPromotion && ( - - Maybe later + + Continue without saving )} + + OK + @@ -143,7 +131,7 @@ const styles = StyleSheet.create({ borderRadius: 8, alignItems: 'center', }, - upgradeButton: { + primaryButton: { backgroundColor: '#34C759', }, secondaryButton: { diff --git a/components/pricing/PaymentProviders.tsx b/components/pricing/PaymentProviders.tsx deleted file mode 100644 index 3d3c5976..00000000 --- a/components/pricing/PaymentProviders.tsx +++ /dev/null @@ -1,95 +0,0 @@ - -import { useState } from "react"; - -export type PaymentProvider = "stripe" | "lemonsqueezy" | "revenuecat" | "paddle"; - -interface PaymentProvidersProps { - availableProviders: PaymentProvider[]; - selectedProvider: PaymentProvider; - onSelectProvider: (provider: PaymentProvider) => void; - showApplePay?: boolean; - showGooglePay?: boolean; -} - -const PaymentProviders = ({ - availableProviders, - selectedProvider, - onSelectProvider, - showApplePay = true, - showGooglePay = true, -}: PaymentProvidersProps) => { - // Provider icons would be imported or loaded from your assets folder - const providerIcons = { - stripe: "/placeholder.svg", // Replace with actual paths - lemonsqueezy: "/placeholder.svg", - revenuecat: "/placeholder.svg", - paddle: "/placeholder.svg", - applepay: "/placeholder.svg", - googlepay: "/placeholder.svg", - }; - - const getProviderName = (provider: PaymentProvider): string => { - switch (provider) { - case "stripe": return "Stripe"; - case "lemonsqueezy": return "Lemon Squeezy"; - case "revenuecat": return "RevenueCat"; - case "paddle": return "Paddle"; - default: return ""; - } - }; - - return ( -
-
- {availableProviders.map((provider) => ( - - ))} -
- - {(showApplePay || showGooglePay) && ( -
- Also supports: -
- {showApplePay && ( -
- Apple Pay -
- )} - {showGooglePay && ( -
- Google Pay -
- )} -
-
- )} -
- ); -}; - -export default PaymentProviders; diff --git a/components/pricing/PriceToggle.tsx b/components/pricing/PriceToggle.tsx deleted file mode 100644 index f48b3c6d..00000000 --- a/components/pricing/PriceToggle.tsx +++ /dev/null @@ -1,43 +0,0 @@ - -import { useState } from "react"; - -interface PriceToggleProps { - onToggle: (isAnnual: boolean) => void; - isAnnual: boolean; -} - -const PriceToggle = ({ onToggle, isAnnual }: PriceToggleProps) => { - return ( -
-
- - Monthly - - -
- - Annual - -
- Save 20% -
-
-
-
- ); -}; - -export default PriceToggle; diff --git a/components/pricing/PricingCard.tsx b/components/pricing/PricingCard.tsx deleted file mode 100644 index 6338683a..00000000 --- a/components/pricing/PricingCard.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Check, X } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -export interface Feature { - name: string; - available: boolean; -} - -export interface PricingPlan { - name: string; - price: { - monthly: number; - annual: number; - }; - description: string; - features: Feature[]; - cta: string; - badge?: "popular" | "best-value"; - priceId: { - monthly: string | null; - annual: string | null; - }; - dynamicPrice?: { - basePrice: number; - currentPrice: number; - percentIncrease: number; - }; - isTrial?: boolean; - trialDays?: number; -} - -interface PricingCardProps { - plan: PricingPlan; - isAnnual: boolean; - onSelectPlan: (plan: PricingPlan) => void; -} - -const PricingCard = ({ plan, isAnnual, onSelectPlan }: PricingCardProps) => { - const price = isAnnual ? plan.price.annual : plan.price.monthly; - const originalPrice = plan.dynamicPrice ? plan.dynamicPrice.basePrice : null; - const hasDynamicPrice = plan.dynamicPrice && plan.dynamicPrice.percentIncrease > 0; - - return ( -
- {plan.badge && ( -
- {plan.badge === "popular" ? "Most Popular" : "Best Value"} -
- )} - -

{plan.name}

-

{plan.description}

- -
-
- ${price} - /{isAnnual ? "year" : "month"} -
- {hasDynamicPrice && ( -
- ${originalPrice} -
-
- In demand -
- Current demand has increased pricing by {plan.dynamicPrice?.percentIncrease}%. We adjust prices based on demand to ensure availability. -
-
-
-
- )} - {plan.trialDays && ( -
- Free {plan.trialDays}-day trial -
- )} - {plan.isTrial && !plan.trialDays && ( -
- Free 7-day trial -
- )} -
- -
-
- {plan.features.map((feature, index) => ( -
- {feature.available ? ( - - ) : ( - - )} - - {feature.name} - -
- ))} -
-
- - -
- ); -}; - -export default PricingCard; diff --git a/components/pricing/PricingPage.tsx b/components/pricing/PricingPage.tsx deleted file mode 100644 index fe532178..00000000 --- a/components/pricing/PricingPage.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useState } from "react"; -import { loadStripe } from "@stripe/stripe-js"; -import { useToast } from "@/hooks/use-toast"; -import stripeConfig from "../../lib/stripeConfig"; -import PricingCard, { PricingPlan } from "@/components/pricing/PricingCard"; -import PriceToggle from "@/components/pricing/PriceToggle"; -import PaymentProviders, { PaymentProvider } from "@/components/pricing/PaymentProviders"; - -const stripePromise = stripeConfig.publishableKey - ? loadStripe(stripeConfig.publishableKey) - : Promise.reject(new Error("Stripe publishable key is missing")); - -const pricingPlans: PricingPlan[] = [ - { - name: "Free", - price: { monthly: 0, annual: 0 }, - description: "Manual calculations only, ideal for light or trial use", - features: [ - { name: "3 AI scans/month", available: true }, - { name: "10 saved doses", available: true }, - { name: "Manual calculations", available: true }, - { name: "Basic support", available: true }, - { name: "Faster scans & no mid-session limits", available: false }, - { name: "Priority scan queue", available: false }, - ], - cta: "Start Free", - priceId: { monthly: null, annual: null }, - }, - { - name: "Starter", - price: { monthly: 4.99, annual: 44.99 }, - description: "For occasional dosing with basic AI assistance", - features: [ - { name: "10 AI scans/month", available: true }, - { name: "20 saved doses", available: true }, - { name: "Manual calculations", available: true }, - { name: "Basic support", available: true }, - { name: "Priority processing", available: false }, - { name: "Priority scan queue", available: false }, - ], - cta: "Upgrade to Starter", - priceId: { - monthly: "price_1RYgx7AY2p4W374YR9UxS0vr", - annual: "price_1RYgx7AY2p4W374Yy23EtyIm" - }, - }, - { - name: "Basic Pro", - price: { monthly: 9.99, annual: 89.99 }, - description: "For consistent logging with AI scan assistance", - features: [ - { name: "20 AI scans/month", available: true }, - { name: "Unlimited logs", available: true }, - { name: "Manual calculations", available: true }, - { name: "Priority support", available: true }, - { name: "Priority processing", available: false }, - { name: "Priority scan queue", available: false }, - ], - cta: "Upgrade to Basic Pro", - badge: "popular", - priceId: { - monthly: "price_1RYgyPAY2p4W374YNbpBpbqv", - annual: "price_1RYgyPAY2p4W374YJOhwDafY" - }, - }, - { - name: "Full Pro", - price: { monthly: 20, annual: 179.99 }, - description: "Complete solution with unlimited AI scans and logs", - features: [ - { name: "Unlimited AI scans", available: true }, - { name: "Unlimited logs", available: true }, - { name: "Manual calculations", available: true }, - { name: "Priority processing", available: true }, - { name: "Priority scan queue", available: true }, - { name: "Premium support", available: true }, - ], - cta: "Start Free Trial", - badge: "best-value", - trialDays: 7, - priceId: { - monthly: "price_1RUHgxAY2p4W374Yb5EWEtZ0", - annual: "price_1RYgzUAY2p4W374YHiBBHvuX" - }, - }, -]; - -export default function PricingPage() { - const { toast } = useToast(); - const [isAnnual, setIsAnnual] = useState(false); - const [selectedPaymentProvider, setSelectedPaymentProvider] = - useState("stripe"); - - const initiateStripeCheckout = async (plan: PricingPlan) => { - try { - const stripe = await stripePromise; - if (!stripe) { - toast({ - title: "Error", - description: "Stripe is not initialized. Please try again later.", - variant: "destructive", - }); - return; - } - - const priceId = isAnnual ? plan.priceId.annual : plan.priceId.monthly; - if (!priceId) { - toast({ - title: "Free Plan Selected", - description: "No checkout needed for Free plan.", - }); - return; - } - - const res = await fetch("/api/create-checkout-session", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - priceId, - successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`, - cancelUrl: `${window.location.origin}/pricing`, - hasTrial: plan.trialDays ? true : false, - }), - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => ({ error: "Unknown error" })); - - // Provide specific error messages for common configuration issues - if (errorData.error && errorData.error.includes("publishable API key")) { - throw new Error("Payment system configuration error. Please contact support."); - } else if (errorData.error && errorData.error.includes("secret key")) { - throw new Error("Payment system temporarily unavailable. Please try again later."); - } else { - throw new Error(errorData.error || "Failed to create checkout session"); - } - } - - const { sessionId } = await res.json(); - await stripe.redirectToCheckout({ sessionId }); - } catch (error) { - console.error("Checkout error:", error); - - // Provide user-friendly error messages - let description = "Unable to initiate checkout. Please try again."; - if (error.message.includes("configuration error")) { - description = "Payment system configuration error. Please contact support - this issue has been logged."; - } else if (error.message.includes("temporarily unavailable")) { - description = "Payment system temporarily unavailable. Please try again in a few moments."; - } - - toast({ - title: "Checkout Error", - description, - variant: "destructive", - }); - } - }; - - const handleCheckout = (plan: PricingPlan) => { - switch (selectedPaymentProvider) { - case "stripe": - initiateStripeCheckout(plan); - break; - case "lemonsqueezy": - console.log("LemonSqueezy checkout:", plan.name); - break; - case "revenuecat": - console.log("RevenueCat checkout:", plan.name); - break; - case "paddle": - console.log("Paddle checkout:", plan.name); - break; - default: - console.error("Unknown payment provider"); - } - }; - - return ( -
-

SafeDose Pricing

-
- -
- -
- {pricingPlans.map((plan, idx) => ( - - ))} -
-
- ); -} \ No newline at end of file diff --git a/lib/analytics.ts b/lib/analytics.ts index 30ccc27b..2068360c 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -9,12 +9,6 @@ export const ANALYTICS_EVENTS = { SIGN_IN_FAILURE: 'sign_in_failure', SIGN_UP_SUCCESS: 'sign_up_success', LOGOUT: 'logout', - VIEW_PRICING_PAGE: 'view_pricing_page', - INITIATE_UPGRADE: 'initiate_upgrade', - UPGRADE_SUCCESS: 'upgrade_success', - UPGRADE_FAILURE: 'upgrade_failure', - CANCEL_SUBSCRIPTION: 'cancel_subscription', - DOWNGRADE_PLAN: 'downgrade_plan', SCAN_ATTEMPT: 'scan_attempt', SCAN_SUCCESS: 'scan_success', SCAN_FAILURE: 'scan_failure', @@ -76,7 +70,6 @@ export const ANALYTICS_EVENTS = { // User property names export const USER_PROPERTIES = { - PLAN_TYPE: 'plan_type', IS_ANONYMOUS: 'is_anonymous', IS_LICENSED_PROFESSIONAL: 'is_licensed_professional', IS_PROFESSIONAL_ATHLETE: 'is_professional_athlete', diff --git a/lib/hooks/usePowerUserPromotion.ts b/lib/hooks/usePowerUserPromotion.ts index 344cc8d2..f5c59355 100644 --- a/lib/hooks/usePowerUserPromotion.ts +++ b/lib/hooks/usePowerUserPromotion.ts @@ -63,20 +63,7 @@ export function usePowerUserPromotion() { data = { ...data, ...JSON.parse(stored) }; } - // For authenticated users, also check Firestore for subscription status - if (user && !user.isAnonymous) { - try { - const userDocRef = doc(db, 'users', user.uid); - const userDoc = await getDoc(userDocRef); - if (userDoc.exists()) { - const userData = userDoc.data(); - data.plan = userData.plan || 'free'; - data.hasActiveSubscription = userData.plan === 'plus' || userData.plan === 'pro'; - } - } catch (error) { - console.error('[PowerUserPromotion] Error loading user subscription status:', error); - } - } + // SafeDose is now free and open source - no subscription status needed setPromotionData(data); console.log(`[PowerUserPromotion] Loaded promotion data for ${userId}:`, data); @@ -116,44 +103,11 @@ export function usePowerUserPromotion() { }, [promotionData, savePromotionData]); // Check if promotion should be shown + // Note: Power user promotions are disabled as SafeDose is now fully free and open source const shouldShowPromotion = useCallback(() => { - console.log('[PowerUserPromotion] === CHECKING IF PROMOTION SHOULD SHOW ==='); - console.log('[PowerUserPromotion] Current promotion data:', promotionData); - console.log('[PowerUserPromotion] Is loading:', isLoading); - console.log('[PowerUserPromotion] Min doses required:', MIN_DOSES_FOR_PROMOTION); - - // Don't show if loading - if (isLoading) { - console.log('[PowerUserPromotion] ❌ Not showing - still loading'); - return false; - } - - // Don't show if user has active subscription - if (promotionData.hasActiveSubscription) { - console.log('[PowerUserPromotion] ❌ Not showing - user has active subscription:', promotionData.plan); - return false; - } - - // Don't show if user hasn't reached minimum dose count - if (promotionData.doseCount < MIN_DOSES_FOR_PROMOTION) { - console.log('[PowerUserPromotion] ❌ Not showing - dose count too low:', promotionData.doseCount, 'minimum:', MIN_DOSES_FOR_PROMOTION); - return false; - } - - // Check if enough time has passed since last shown - if (promotionData.lastShownDate) { - const lastShown = new Date(promotionData.lastShownDate); - const daysSinceLastShown = Math.floor((Date.now() - lastShown.getTime()) / (1000 * 60 * 60 * 24)); - - if (daysSinceLastShown < DAYS_BETWEEN_PROMOTIONS) { - console.log('[PowerUserPromotion] ❌ Not showing - shown too recently:', daysSinceLastShown, 'days ago, minimum:', DAYS_BETWEEN_PROMOTIONS); - return false; - } - } - - console.log('[PowerUserPromotion] ✅ Should show promotion - dose count:', promotionData.doseCount, 'last shown:', promotionData.lastShownDate); - return true; - }, [isLoading, promotionData]); + console.log('[PowerUserPromotion] Power user promotions are disabled - SafeDose is free'); + return false; + }, []); // Mark promotion as shown const markPromotionShown = useCallback(async () => { diff --git a/lib/hooks/useUsageTracking.ts b/lib/hooks/useUsageTracking.ts index 7362459c..70655677 100644 --- a/lib/hooks/useUsageTracking.ts +++ b/lib/hooks/useUsageTracking.ts @@ -9,14 +9,11 @@ import { setAnalyticsUserProperties, USER_PROPERTIES } from '../analytics'; const MAX_RETRIES = 3; const INITIAL_BACKOFF = 1000; // 1 second -// Helper function to determine scan limits based on user plan and authentication status -// Business logic: Anonymous users get minimal scans to encourage sign-up, -// while authenticated users get progressively more scans based on their subscription tier -const getLimitForPlan = (plan: string, isAnonymous: boolean) => { - if (isAnonymous) return 3; // Anonymous users - if (plan === 'plus') return 50; // Plus plan - if (plan === 'pro') return 500; // Pro plan - return 10; // Signed-in free users +// Helper function to determine scan limits based on authentication status +// SafeDose is free and open source - limits exist only to prevent abuse +const getLimitForUser = (isAnonymous: boolean) => { + if (isAnonymous) return 3; // Anonymous users - minimal to encourage account creation + return Infinity; // Signed-in users have unlimited scans }; export function useUsageTracking() { @@ -95,7 +92,7 @@ export function useUsageTracking() { if (!userDoc.exists()) { // Create new user document if it doesn't exist const currentMonthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(); - data = { scansUsed: 0, plan: 'free', lastReset: currentMonthStart }; + data = { scansUsed: 0, lastReset: currentMonthStart }; await setDocWithEnv(userDocRef, data); console.log('Created new user document:', data); } else { @@ -122,14 +119,13 @@ export function useUsageTracking() { } } } - const limit = getLimitForPlan(data.plan || 'free', user.isAnonymous); - const newUsageData = { scansUsed: data.scansUsed || 0, plan: data.plan || 'free', limit, lastReset: data.lastReset }; + const limit = getLimitForUser(user.isAnonymous); + const newUsageData = { scansUsed: data.scansUsed || 0, plan: 'free', limit, lastReset: data.lastReset }; setUsageData(newUsageData); await saveCachedUsage(newUsageData); - // Set user properties for analytics when plan data is available + // Set user properties for analytics setAnalyticsUserProperties({ - [USER_PROPERTIES.PLAN_TYPE]: data.plan || 'free', [USER_PROPERTIES.IS_ANONYMOUS]: user.isAnonymous, }); @@ -176,7 +172,7 @@ export function useUsageTracking() { if (!userDoc.exists()) { // Create new user document if it doesn't exist const currentMonthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(); - data = { scansUsed: 0, plan: 'free', lastReset: currentMonthStart }; + data = { scansUsed: 0, lastReset: currentMonthStart }; await setDocWithEnv(userDocRef, data); console.log('Created new user document:', data); } else { @@ -192,7 +188,6 @@ export function useUsageTracking() { } else { // Monthly reset logic // Automatically reset usage counters at the beginning of each calendar month - // This ensures users get their full monthly allowance regardless of when they signed up const now = new Date(); const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); const lastReset = new Date(data.lastReset).toISOString(); @@ -205,8 +200,8 @@ export function useUsageTracking() { } } } - const limit = getLimitForPlan(data.plan || 'free', user.isAnonymous); - const newUsageData = { scansUsed: data.scansUsed || 0, plan: data.plan || 'free', limit, lastReset: data.lastReset }; + const limit = getLimitForUser(user.isAnonymous); + const newUsageData = { scansUsed: data.scansUsed || 0, plan: 'free', limit, lastReset: data.lastReset }; setUsageData(newUsageData); await saveCachedUsage(newUsageData); return newUsageData.scansUsed < newUsageData.limit; @@ -241,8 +236,8 @@ export function useUsageTracking() { if (!userDoc.exists()) { console.log('[useUsageTracking] User document does not exist, creating new one'); const currentMonthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString(); - await setDocWithEnv(userDocRef, { scansUsed: 0, plan: 'free', lastReset: currentMonthStart }); - console.log('[useUsageTracking] Created new user document for increment:', { scansUsed: 0, plan: 'free', lastReset: currentMonthStart }); + await setDocWithEnv(userDocRef, { scansUsed: 0, lastReset: currentMonthStart }); + console.log('[useUsageTracking] Created new user document for increment:', { scansUsed: 0, lastReset: currentMonthStart }); } console.log('[useUsageTracking] Updating Firestore document with increment'); diff --git a/lib/stripeConfig.js b/lib/stripeConfig.js deleted file mode 100644 index 3b560f19..00000000 --- a/lib/stripeConfig.js +++ /dev/null @@ -1,62 +0,0 @@ -// Determine if we're in a server-side environment (Node.js) or client-side environment (Expo) -const isServerEnvironment = typeof process !== 'undefined' && process.env && typeof window === 'undefined'; - -// Ensure environment variables are loaded only in server environments -if (isServerEnvironment) { - require('dotenv').config(); -} - -// Get environment variables from the appropriate source based on environment -const getEnvVar = (key, defaultValue = undefined) => { - // For server-side (Vercel serverless functions), use process.env - if (isServerEnvironment) { - return process.env[key] || defaultValue; - } - // For client-side (Expo app), use expo-constants - try { - const Constants = require('expo-constants').default; - return Constants.expoConfig?.extra?.[key] || defaultValue; - } catch (error) { - console.warn('expo-constants not available, falling back to default', key); - return defaultValue; - } -}; - -let stripeMode = getEnvVar('STRIPE_MODE', 'test'); - -// Validate mode -if (stripeMode !== 'test' && stripeMode !== 'live') { - console.error(`Invalid STRIPE_MODE: ${stripeMode}. Must be 'test' or 'live'. Defaulting to 'test'.`); - stripeMode = 'test'; -} - -const effectiveMode = stripeMode === 'live' ? 'live' : 'test'; - -const stripeConfig = { - publishableKey: effectiveMode === 'live' - ? getEnvVar('STRIPE_LIVE_PUBLISHABLE_KEY') - : getEnvVar('STRIPE_TEST_PUBLISHABLE_KEY'), - secretKey: effectiveMode === 'live' - ? getEnvVar('STRIPE_LIVE_SECRET_KEY') - : getEnvVar('STRIPE_TEST_SECRET_KEY'), - priceId: effectiveMode === 'live' - ? getEnvVar('STRIPE_LIVE_PRICE_ID', 'price_1RUHgxAY2p4W374Yb5EWEtZ0') // Live price ID - : getEnvVar('STRIPE_TEST_PRICE_ID', 'price_1REyzMPE5x6FmwJPyJVJIEXe'), // Existing test price ID - mode: effectiveMode, -}; - -// Debug logging -console.log(`Stripe Configuration - Mode: ${stripeConfig.mode}`); -console.log(`Stripe Publishable Key: ${stripeConfig.publishableKey ? stripeConfig.publishableKey.substring(0, 12) + '...' : 'NOT SET'}`); -console.log(`Stripe Secret Key: ${stripeConfig.secretKey ? stripeConfig.secretKey.substring(0, 12) + '...' : 'NOT SET'}`); - -// Validation warnings -if (!stripeConfig.publishableKey) { - console.warn(`Stripe publishable key not set for ${effectiveMode} mode`); -} -if (!stripeConfig.secretKey) { - console.warn(`Stripe secret key not set for ${effectiveMode} mode`); -} - -// Always provide CommonJS export for compatibility -module.exports = stripeConfig; \ No newline at end of file diff --git a/lib/stripeConfig.server.js b/lib/stripeConfig.server.js deleted file mode 100644 index 009c3db4..00000000 --- a/lib/stripeConfig.server.js +++ /dev/null @@ -1,74 +0,0 @@ -require('dotenv').config(); - -const stripeMode = process.env.STRIPE_MODE || 'test'; - -// Validate mode -if (stripeMode !== 'test' && stripeMode !== 'live') { - console.error(`Invalid STRIPE_MODE: ${stripeMode}. Must be 'test' or 'live'. Defaulting to 'test'.`); - // Fall back to test mode -} - -const effectiveMode = (stripeMode === 'live') ? 'live' : 'test'; - -const stripeConfig = { - publishableKey: effectiveMode === 'live' - ? process.env.STRIPE_LIVE_PUBLISHABLE_KEY - : (process.env.STRIPE_TEST_PUBLISHABLE_KEY || process.env.STRIPE_PUBLISHABLE_KEY), - secretKey: effectiveMode === 'live' - ? process.env.STRIPE_LIVE_SECRET_KEY - : (process.env.STRIPE_TEST_SECRET_KEY || process.env.STRIPE_SECRET_KEY), - priceId: effectiveMode === 'live' - ? (process.env.STRIPE_LIVE_PRICE_ID || 'price_1RUHgxAY2p4W374Yb5EWEtZ0') - : (process.env.STRIPE_TEST_PRICE_ID || 'price_1REyzMPE5x6FmwJPyJVJIEXe'), - mode: effectiveMode, -}; - -// Validate that keys are not swapped -if (stripeConfig.publishableKey && !stripeConfig.publishableKey.startsWith('pk_')) { - console.error(`CRITICAL ERROR: Publishable key has invalid format. Expected 'pk_*', got: ${stripeConfig.publishableKey.substring(0, 7)}...`); - stripeConfig.publishableKey = null; // Reset to prevent usage -} - -if (stripeConfig.secretKey && stripeConfig.publishableKey && stripeConfig.secretKey === stripeConfig.publishableKey) { - console.error(`CRITICAL ERROR: Secret key and publishable key are the same! This indicates a configuration error.`); - stripeConfig.secretKey = null; // Reset to prevent usage -} - -// Debug logging -console.log(`Stripe Configuration - Mode: ${stripeConfig.mode}`); -console.log(`Stripe Secret Key: ${stripeConfig.secretKey ? stripeConfig.secretKey.substring(0, 12) + '...' : 'NOT SET'}`); - -// Additional debug logging for environment variables -console.log(`Environment check - STRIPE_MODE: ${process.env.STRIPE_MODE}`); -console.log(`Environment check - effectiveMode: ${effectiveMode}`); -if (effectiveMode === 'live') { - console.log(`Live mode - STRIPE_LIVE_SECRET_KEY: ${process.env.STRIPE_LIVE_SECRET_KEY ? process.env.STRIPE_LIVE_SECRET_KEY.substring(0, 12) + '...' : 'NOT SET'}`); - console.log(`Live mode - STRIPE_LIVE_PUBLISHABLE_KEY: ${process.env.STRIPE_LIVE_PUBLISHABLE_KEY ? process.env.STRIPE_LIVE_PUBLISHABLE_KEY.substring(0, 12) + '...' : 'NOT SET'}`); -} else { - console.log(`Test mode - STRIPE_TEST_SECRET_KEY: ${process.env.STRIPE_TEST_SECRET_KEY ? process.env.STRIPE_TEST_SECRET_KEY.substring(0, 12) + '...' : 'NOT SET'}`); - console.log(`Test mode - STRIPE_SECRET_KEY (fallback): ${process.env.STRIPE_SECRET_KEY ? process.env.STRIPE_SECRET_KEY.substring(0, 12) + '...' : 'NOT SET'}`); - console.log(`Test mode - STRIPE_TEST_PUBLISHABLE_KEY: ${process.env.STRIPE_TEST_PUBLISHABLE_KEY ? process.env.STRIPE_TEST_PUBLISHABLE_KEY.substring(0, 12) + '...' : 'NOT SET'}`); - console.log(`Test mode - STRIPE_PUBLISHABLE_KEY (fallback): ${process.env.STRIPE_PUBLISHABLE_KEY ? process.env.STRIPE_PUBLISHABLE_KEY.substring(0, 12) + '...' : 'NOT SET'}`); -} - -// Validation warnings -if (!stripeConfig.secretKey) { - console.warn(`Stripe secret key not set for ${effectiveMode} mode`); -} - -// Key type validation -if (stripeConfig.secretKey) { - if (stripeConfig.secretKey.startsWith('pk_')) { - console.error(`CRITICAL ERROR: Secret key appears to be a publishable key! This will cause API failures.`); - console.error(`Secret key value: ${stripeConfig.secretKey.substring(0, 12)}...`); - stripeConfig.secretKey = null; // Reset to prevent usage - } else if (!stripeConfig.secretKey.startsWith('sk_')) { - console.error(`WARNING: Secret key does not start with 'sk_' - format may be incorrect.`); - console.error(`Secret key value: ${stripeConfig.secretKey.substring(0, 12)}...`); - stripeConfig.secretKey = null; // Reset to prevent usage - } else { - console.log(`✓ Secret key format validation passed`); - } -} - -module.exports = stripeConfig; \ No newline at end of file diff --git a/other_key_screens.md b/other_key_screens.md index 4fec6ca6..15a6c19c 100644 --- a/other_key_screens.md +++ b/other_key_screens.md @@ -2,29 +2,10 @@ This section details other notable screens within the application that play specific roles in the user journey but are not part of the main tab navigation or primary onboarding sequence. -### Success Screen (`app/success.tsx`) +### Success Screen (`app/success.tsx`) - DEPRECATED -* **Screen Name:** Success -* **File Path:** `app/success.tsx` - -**Purpose:** -The primary purpose of the `app/success.tsx` screen is to provide clear and positive visual confirmation to the user immediately after they have successfully completed a significant action or transaction within the app. This enhances user experience by acknowledging their achievement and providing a sense of completion. - -**Potential Triggers / Entry Points:** -While the screen can be used for various success scenarios, a key anticipated trigger is: - -* **After Successful Subscription Purchase:** Following a successful payment and subscription activation initiated from `app/pricing.tsx`, the user would likely be redirected to this screen to confirm their new plan is active. -* **Other Major Actions:** Although less common for this specific application's current scope, it could theoretically be used after other significant, multi-step processes if they existed (e.g., a complex initial setup different from the current onboarding). - * *Note:* It is not used for post-login success, as `app/login.tsx` directly navigates to `/(tabs)/new-dose`. - -**Expected Content and Functionality:** -The `app/success.tsx` screen is expected to feature: - -* **Clear Success Message:** A prominent message conveying success, such as "Payment Successful!", "Subscription Activated!", "You're All Set!", or similar. -* **Visual Cue:** An icon (e.g., a checkmark) or a relevant graphic that visually reinforces the successful outcome. -* **Confirmation Details (Optional):** Depending on the context, it might briefly reiterate what was achieved (e.g., "Your Premium Plan is now active."). -* **Navigation Button:** A primary call-to-action button to navigate the user away from the success screen. This button might be labeled "Continue to App," "Go to Dashboard," "Start Using Premium Features," or similar, and would typically lead to the main application area (e.g., `/(tabs)/new-dose`) or a relevant feature page. -* **Automatic Redirect (Optional):** The screen might be configured to automatically redirect the user to another screen after a short delay (e.g., 3-5 seconds) if no action is taken on the button. +* **Status:** This screen has been removed as part of SafeDose's transition to a fully free and open-source model. +* **Historical Context:** Previously used for subscription confirmation flows, which are no longer part of the application. ### Reconstitution Screen (`app/reconstitution.tsx`) diff --git a/pricing_subscription_flow.md b/pricing_subscription_flow.md deleted file mode 100644 index 846434a0..00000000 --- a/pricing_subscription_flow.md +++ /dev/null @@ -1,70 +0,0 @@ -## Pricing and Subscription Flow - -The application incorporates a subscription model, offering different tiers of access and features to users. This is indicated by several elements within the codebase, including usage limit modals, subscription management options in settings, and dedicated screens for pricing and payment processing. - -### Indicators of a Subscription Model: - -* **Usage Limit Modals (`LimitModal`, `LogLimitModal`):** These modals appear when users encounter usage restrictions (e.g., on the number of AI scans or logged doses). They often check conditions like `usageData.plan !== 'free'` or `isPremium`, implying that paid plans ("Premium") bypass these free-tier limitations. -* **Settings Screen (`app/settings.tsx`):** This screen includes a "Cancel Subscription" button, directly pointing to a subscription-based service. -* **Dedicated Pricing Screen (`app/pricing.tsx`):** The existence of this file signifies a specific location where users can view and choose subscription plans. -* **Backend Payment Processing (Stripe):** Files like `STRIPE_SETUP.md` and `api/stripe-webhook.js` strongly suggest the use of Stripe for handling payments and subscription management on the backend. - -### Entry Points to the Pricing Screen (`app/pricing.tsx`) - -Users are typically directed to the pricing screen from various points within the app when they either hit a usage limit or proactively seek to upgrade: - -1. **From Usage Limit Modals (`LimitModal`, `LogLimitModal`):** - * When a user on a free plan exhausts their allowed usage (e.g., maximum free scans or log entries), the respective modal (`LimitModal` or `LogLimitModal`) is displayed. - * These modals are expected to contain an "Upgrade," "View Plans," or similar call-to-action button that navigates the user directly to `app/pricing.tsx`. -2. **From the Settings Screen (`app/settings.tsx`):** - * The settings area likely includes options such as "Manage Subscription," "View Subscription Plans," or "Upgrade Account." - * Selecting one of these options would navigate the user to `app/pricing.tsx` to see available subscription tiers. -3. **Promotional Elements:** - * The application might feature other promotional banners, cards, or messages within its UI (e.g., on the main dashboard or specific feature screens) that highlight the benefits of premium plans and link to `app/pricing.tsx`. - -### Pricing Screen (`app/pricing.tsx`) - Assumed Content & Functionality - -The `app/pricing.tsx` screen is the central place for users to understand and select a subscription plan. - -* **Purpose:** To clearly present the available subscription plans, detailing the features, benefits, and costs associated with each. -* **Expected Content:** - * **Plan Comparison:** A side-by-side comparison of different available tiers (e.g., "Free," "Premium," "Pro"). - * **Feature List:** A breakdown of features included in each plan, such as: - * Number of AI scans allowed (e.g., per day/month, or unlimited). - * Number of dose logs that can be stored. - * Access to exclusive or advanced features. - * Customer support levels. - * **Pricing Details:** Clear indication of the cost for each plan, often with options for monthly or annual billing cycles (potentially with a discount for annual commitments). - * **Call-to-Action Buttons:** Prominent buttons like "Choose Plan," "Subscribe," or "Upgrade" for each plan, allowing users to select their desired option. -* **Payment Integration:** - * Upon selecting a plan, the user would proceed to a payment step. - * This step is expected to integrate with a third-party payment provider, heavily implied to be **Stripe** based on backend file names. - * Users would securely enter their payment details (credit card information, etc.) through an interface likely provided or heavily influenced by the Stripe integration (e.g., Stripe Elements or a redirect to a Stripe Checkout page). - -### Post-Subscription Actions - -Once a user successfully subscribes to a paid plan: - -1. **Account Update:** - * The user's profile or account status within the application's backend and potentially in the local `userProfile` (e.g., `userProfile.plan`) is updated to reflect the new subscription tier. - * The `isPremium` flag or equivalent would be set to true. -2. **Feature Unlocking:** - * The application immediately unlocks features and lifts restrictions according to the benefits of the newly subscribed plan. For example, scan limits might be increased or removed. -3. **Navigation/Confirmation:** - * The user might be navigated to a dedicated success screen (the `app/success.tsx` screen could serve this purpose) confirming their successful subscription. - * Alternatively, they might be redirected back to the screen from which they initiated the upgrade process, now with enhanced access. - * An email confirmation of the subscription is also typically sent. - -### Subscription Cancellation - -* **Entry Point:** The "Cancel Subscription" button located on the `app/settings.tsx` screen is the user-facing starting point for this process. -* **Process:** - * As noted by the `// TODO: Add real cancellation or downgrade logic` comment in `app/settings.tsx`, the full implementation is pending. - * A complete cancellation flow would involve: - * User confirmation of their intent to cancel. - * Backend communication with the payment provider (Stripe) to request the cancellation of the subscription. This would typically stop future recurring billing. - * The user's plan might remain active until the end of the current paid billing cycle, after which it would revert to a free tier or a restricted state. - * Updating the user's plan status in the application's database. - * Providing confirmation to the user that their subscription has been cancelled or is scheduled for cancellation. - -This flow ensures users can understand plan benefits, make informed purchase decisions, and manage their subscription status within the application. The integration with a robust payment provider like Stripe is crucial for handling the financial transactions securely and reliably. diff --git a/types/env.d.ts b/types/env.d.ts index 08f6a91b..25691afa 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -4,14 +4,6 @@ declare module 'expo-constants' { expoConfig: { extra: { OPENAI_API_KEY: string; - STRIPE_PUBLISHABLE_KEY: string; - STRIPE_MODE: string; - STRIPE_TEST_PUBLISHABLE_KEY: string; - STRIPE_TEST_SECRET_KEY: string; - STRIPE_LIVE_PUBLISHABLE_KEY: string; - STRIPE_LIVE_SECRET_KEY: string; - STRIPE_TEST_PRICE_ID: string; - STRIPE_LIVE_PRICE_ID: string; }; }; }