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:
parent
2e72398818
commit
28323507f8
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user