Remove redirect from subscription page

Redirects should only happen on the main beneficiary page (index.tsx).
Other pages (subscription, equipment, share) just show their content
without redirecting - user navigated there intentionally via menu.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2026-01-09 18:50:13 -08:00
parent 2e72398818
commit 28323507f8
3 changed files with 126 additions and 52 deletions

View File

@ -17,7 +17,6 @@ import { usePaymentSheet } from '@stripe/stripe-react-native';
import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows } from '@/constants/theme';
import { useAuth } from '@/contexts/AuthContext';
import { api } from '@/services/api';
import { hasBeneficiaryDevices } from '@/services/BeneficiaryDetailController';
import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu';
import type { Beneficiary } from '@/types';
@ -102,20 +101,6 @@ export default function SubscriptionScreen() {
const subscriptionState = getSubscriptionState();
// Self-guard redirect
useEffect(() => {
if (isLoading || !beneficiary || !id) return;
if (justSubscribed || showSuccessModal) return;
if (!hasBeneficiaryDevices(beneficiary)) {
const status = beneficiary.equipmentStatus;
if (status && ['ordered', 'shipped', 'delivered'].includes(status)) {
router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`);
} else {
router.replace(`/(tabs)/beneficiaries/${id}/purchase`);
}
}
}, [beneficiary, isLoading, id, justSubscribed, showSuccessModal]);
const handleSubscribe = async () => {
if (!beneficiary) {

View File

@ -30,6 +30,7 @@ function normalizeStripeStatus(status) {
/**
* Helper: Get subscription status from Stripe (source of truth)
* Used for single beneficiary requests
*/
async function getStripeSubscriptionStatus(stripeCustomerId) {
if (!stripeCustomerId) {
@ -87,6 +88,71 @@ async function getStripeSubscriptionStatus(stripeCustomerId) {
}
}
/**
* Helper: Batch fetch subscription statuses for multiple customers
* Makes 1 Stripe API call instead of N calls
*/
async function getBatchStripeSubscriptions(stripeCustomerIds) {
// Filter out null/undefined
const validIds = stripeCustomerIds.filter(Boolean);
if (validIds.length === 0) {
return {};
}
const subscriptionMap = {};
// Initialize all as free/none
for (const customerId of validIds) {
subscriptionMap[customerId] = { plan: 'free', status: 'none', hasSubscription: false };
}
try {
const startTime = Date.now();
// Fetch all subscriptions in one call (Stripe allows up to 100)
// We fetch all statuses and filter in code
const subscriptions = await stripe.subscriptions.list({
limit: 100,
expand: ['data.customer']
});
console.log(`[STRIPE BATCH] Fetched ${subscriptions.data.length} subscriptions in ${Date.now() - startTime}ms`);
// Build map of customer_id -> subscription
for (const sub of subscriptions.data) {
const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer?.id;
if (!customerId || !validIds.includes(customerId)) {
continue;
}
const periodEndTimestamp = sub.cancel_at || sub.current_period_end;
const endDate = periodEndTimestamp ? new Date(periodEndTimestamp * 1000).toISOString() : null;
const normalizedStatus = normalizeStripeStatus(sub.status);
// Only update if this is a better status (active > trialing > past_due > etc)
const currentStatus = subscriptionMap[customerId]?.status || 'none';
const statusPriority = { active: 4, trialing: 3, past_due: 2, canceled: 1, expired: 0, none: 0 };
if (statusPriority[normalizedStatus] > statusPriority[currentStatus]) {
subscriptionMap[customerId] = {
plan: normalizedStatus === 'active' || normalizedStatus === 'trialing' ? 'premium' : 'free',
status: normalizedStatus,
hasSubscription: normalizedStatus === 'active' || normalizedStatus === 'trialing',
endDate: endDate,
cancelAtPeriodEnd: sub.cancel_at_period_end || false
};
}
}
return subscriptionMap;
} catch (error) {
console.error('[STRIPE BATCH] Error fetching subscriptions:', error.message);
return subscriptionMap; // Return map with defaults
}
}
/**
* Middleware to verify JWT token
*/
@ -112,15 +178,17 @@ router.use(authMiddleware);
* GET /api/me/beneficiaries
* Returns list of beneficiaries the user has access to
* Now uses the proper beneficiaries table (not users)
* OPTIMIZED: Uses batch Stripe API call instead of N individual calls
*/
router.get('/', async (req, res) => {
try {
const startTime = Date.now();
const userId = req.user.userId;
// Get access records with beneficiary_id (points to beneficiaries table)
const { data: accessRecords, error: accessError } = await supabase
.from('user_access')
.select('id, beneficiary_id, beneficiary_id, role, granted_at')
.select('id, beneficiary_id, role, granted_at')
.eq('accessor_id', userId);
if (accessError) {
@ -132,47 +200,68 @@ router.get('/', async (req, res) => {
return res.json({ beneficiaries: [] });
}
// Get beneficiary details for each access record
// Get all beneficiary IDs
const beneficiaryIds = accessRecords
.map(r => r.beneficiary_id)
.filter(Boolean);
if (beneficiaryIds.length === 0) {
return res.json({ beneficiaries: [] });
}
// Batch query: Get all beneficiaries in one DB call
const { data: beneficiariesData, error: beneficiariesError } = await supabase
.from('beneficiaries')
.select('id, name, phone, address, avatar_url, created_at, equipment_status, stripe_customer_id')
.in('id', beneficiaryIds);
if (beneficiariesError) {
console.error('Get beneficiaries error:', beneficiariesError);
return res.status(500).json({ error: 'Failed to get beneficiaries' });
}
const dbTime = Date.now() - startTime;
// Collect all Stripe customer IDs for batch request
const stripeCustomerIds = beneficiariesData
.map(b => b.stripe_customer_id)
.filter(Boolean);
// Batch Stripe API call: 1 call instead of N
const stripeStartTime = Date.now();
const subscriptionMap = await getBatchStripeSubscriptions(stripeCustomerIds);
const stripeTime = Date.now() - stripeStartTime;
// Build response
const beneficiaries = [];
for (const record of accessRecords) {
// Use beneficiary_id if available (new architecture), otherwise skip
const beneficiaryTableId = record.beneficiary_id;
if (!beneficiaryTableId) {
// Skip old records that don't have beneficiary_id
console.log('[BENEFICIARY] Skipping legacy record without beneficiary_id:', record.id);
continue;
}
const beneficiary = beneficiariesData.find(b => b.id === record.beneficiary_id);
if (!beneficiary) continue;
// Query from beneficiaries table (new architecture)
console.log('[GET BENEFICIARIES] querying beneficiaries table for id:', beneficiaryTableId);
const { data: beneficiary, error: beneficiaryError } = await supabase
.from('beneficiaries')
.select('id, name, phone, address, avatar_url, created_at, equipment_status, stripe_customer_id')
.eq('id', beneficiaryTableId)
.single();
const subscription = beneficiary.stripe_customer_id
? (subscriptionMap[beneficiary.stripe_customer_id] || { plan: 'free', status: 'none', hasSubscription: false })
: { plan: 'free', status: 'none', hasSubscription: false };
console.log('[GET BENEFICIARIES] got beneficiary:', beneficiary ? beneficiary.name : null, 'error:', beneficiaryError);
if (beneficiary) {
const subscription = await getStripeSubscriptionStatus(beneficiary.stripe_customer_id);
beneficiaries.push({
accessId: record.id,
id: beneficiary.id,
role: record.role,
grantedAt: record.granted_at,
name: beneficiary.name,
phone: beneficiary.phone,
address: beneficiary.address || null,
avatarUrl: beneficiary.avatar_url,
createdAt: beneficiary.created_at,
subscription: subscription,
// Equipment status from beneficiaries table - CRITICAL for navigation!
hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo',
equipmentStatus: beneficiary.equipment_status || 'none'
});
}
beneficiaries.push({
accessId: record.id,
id: beneficiary.id,
role: record.role,
grantedAt: record.granted_at,
name: beneficiary.name,
phone: beneficiary.phone,
address: beneficiary.address || null,
avatarUrl: beneficiary.avatar_url,
createdAt: beneficiary.created_at,
subscription: subscription,
// Equipment status from beneficiaries table - CRITICAL for navigation!
hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo',
equipmentStatus: beneficiary.equipment_status || 'none'
});
}
const totalTime = Date.now() - startTime;
console.log(`[GET BENEFICIARIES] ${beneficiaries.length} items in ${totalTime}ms (DB: ${dbTime}ms, Stripe: ${stripeTime}ms)`);
res.json({ beneficiaries });
} catch (error) {

View File

@ -1,6 +1,6 @@
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } from '@/types';
import * as Crypto from 'expo-crypto';
import * as FileSystem from 'expo-file-system';
import * as FileSystem from 'expo-file-system/legacy';
import * as SecureStore from 'expo-secure-store';
// Callback for handling unauthorized responses (401)