fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
1. replaceAll (Find & Replace) — une seule transaction ProseMirror au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés. 2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs qui laissaient un nœud fantôme invisible dans le document. 3. Conversion Markdown → richtext — breaks: true dans marked.parse() Les simple newlines sont maintenant convertis en <br>. + préserve les blocs custom (toggle, callout, math, columns, outline, link-preview) en commentaires HTML lors de l'export MD. 4. emitNoteChange exercices — shape corrigée (type:'created' attend un objet Note, pas noteId/notebookId séparés). 5. Raccourcis clavier sans conflit : Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier) Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets) Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { SubscriptionTier } from '@/lib/entitlements';
|
||||
import type { SubscriptionTier } from '@/lib/plan-entitlements';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { getConfigValue } from '@/lib/config';
|
||||
|
||||
export type BillingTier = 'PRO' | 'BUSINESS';
|
||||
export type BillingInterval = 'month' | 'year';
|
||||
@@ -21,6 +22,14 @@ export const DEFAULT_PRICES: Record<BillingTier, Record<BillingInterval, Dynamic
|
||||
},
|
||||
};
|
||||
|
||||
export async function isBillingEnabled(): Promise<boolean> {
|
||||
const flag = await getConfigValue('BILLING_ENABLED', '');
|
||||
if (flag === 'true') return true;
|
||||
if (flag === 'false') return false;
|
||||
return process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true'
|
||||
|| process.env.NODE_ENV === 'development';
|
||||
}
|
||||
|
||||
export async function getDynamicPrices(): Promise<Record<BillingTier, Record<BillingInterval, DynamicPrice>>> {
|
||||
const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder';
|
||||
if (isMock) {
|
||||
@@ -40,13 +49,12 @@ export async function getDynamicPrices(): Promise<Record<BillingTier, Record<Bil
|
||||
|
||||
const retrieveAndFormatPrice = async (tier: BillingTier, interval: BillingInterval) => {
|
||||
try {
|
||||
const priceId = resolvePriceId(tier, interval);
|
||||
const priceId = await resolvePriceId(tier, interval);
|
||||
const price = await stripe.prices.retrieve(priceId);
|
||||
if (price.unit_amount !== null && price.unit_amount !== undefined) {
|
||||
const amount = price.unit_amount / 100;
|
||||
const currency = price.currency.toUpperCase();
|
||||
|
||||
// Format the price nicely
|
||||
|
||||
let display = '';
|
||||
if (currency === 'EUR') {
|
||||
display = `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 })} €`;
|
||||
@@ -57,12 +65,11 @@ export async function getDynamicPrices(): Promise<Record<BillingTier, Record<Bil
|
||||
} else {
|
||||
display = `${amount} ${currency}`;
|
||||
}
|
||||
|
||||
|
||||
result[tier][interval] = { display, amount, currency };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[stripe-prices] Failed to retrieve price for ${tier}/${interval}:`, err);
|
||||
// Fallback to default in case of error
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,40 +83,42 @@ export async function getDynamicPrices(): Promise<Record<BillingTier, Record<Bil
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolvePriceId(tier: BillingTier, interval: BillingInterval): string {
|
||||
const map: Record<BillingTier, Record<BillingInterval, string>> = {
|
||||
PRO: {
|
||||
month: process.env.STRIPE_PRICE_PRO_MONTHLY!,
|
||||
year: process.env.STRIPE_PRICE_PRO_ANNUAL!,
|
||||
},
|
||||
BUSINESS: {
|
||||
month: process.env.STRIPE_PRICE_BUSINESS_MONTHLY!,
|
||||
year: process.env.STRIPE_PRICE_BUSINESS_ANNUAL!,
|
||||
},
|
||||
};
|
||||
const priceId = map[tier][interval];
|
||||
if (!priceId) {
|
||||
const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder';
|
||||
if (isMock && process.env.NODE_ENV !== 'test') {
|
||||
return `price_mock_${tier.toLowerCase()}_${interval}`;
|
||||
}
|
||||
throw new Error(`No Stripe price ID configured for ${tier}/${interval}`);
|
||||
const PRICE_ENV_KEYS: Record<BillingTier, Record<BillingInterval, string>> = {
|
||||
PRO: {
|
||||
month: 'STRIPE_PRICE_PRO_MONTHLY',
|
||||
year: 'STRIPE_PRICE_PRO_ANNUAL',
|
||||
},
|
||||
BUSINESS: {
|
||||
month: 'STRIPE_PRICE_BUSINESS_MONTHLY',
|
||||
year: 'STRIPE_PRICE_BUSINESS_ANNUAL',
|
||||
},
|
||||
};
|
||||
|
||||
export async function resolvePriceId(tier: BillingTier, interval: BillingInterval): Promise<string> {
|
||||
const configKey = PRICE_ENV_KEYS[tier][interval];
|
||||
const fromDb = await getConfigValue(configKey, '');
|
||||
const priceId = fromDb || process.env[configKey] || '';
|
||||
if (priceId) return priceId;
|
||||
|
||||
const isMock = !process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY === 'sk_test_placeholder';
|
||||
if (isMock && process.env.NODE_ENV !== 'test') {
|
||||
return `price_mock_${tier.toLowerCase()}_${interval}`;
|
||||
}
|
||||
return priceId;
|
||||
throw new Error(`No Stripe price ID configured for ${tier}/${interval}`);
|
||||
}
|
||||
|
||||
export function priceIdToTier(priceId: string): SubscriptionTier | null {
|
||||
export async function priceIdToTier(priceId: string): Promise<SubscriptionTier | null> {
|
||||
if (priceId && priceId.startsWith('price_mock_')) {
|
||||
if (priceId.includes('pro')) return 'PRO';
|
||||
if (priceId.includes('business')) return 'BUSINESS';
|
||||
return 'BASIC';
|
||||
}
|
||||
|
||||
const entries: Array<[string | undefined, SubscriptionTier]> = [
|
||||
[process.env.STRIPE_PRICE_PRO_MONTHLY, 'PRO'],
|
||||
[process.env.STRIPE_PRICE_PRO_ANNUAL, 'PRO'],
|
||||
[process.env.STRIPE_PRICE_BUSINESS_MONTHLY, 'BUSINESS'],
|
||||
[process.env.STRIPE_PRICE_BUSINESS_ANNUAL, 'BUSINESS'],
|
||||
const entries: Array<[string, SubscriptionTier]> = [
|
||||
[(await getConfigValue('STRIPE_PRICE_PRO_MONTHLY', '')) || process.env.STRIPE_PRICE_PRO_MONTHLY || '', 'PRO'],
|
||||
[(await getConfigValue('STRIPE_PRICE_PRO_ANNUAL', '')) || process.env.STRIPE_PRICE_PRO_ANNUAL || '', 'PRO'],
|
||||
[(await getConfigValue('STRIPE_PRICE_BUSINESS_MONTHLY', '')) || process.env.STRIPE_PRICE_BUSINESS_MONTHLY || '', 'BUSINESS'],
|
||||
[(await getConfigValue('STRIPE_PRICE_BUSINESS_ANNUAL', '')) || process.env.STRIPE_PRICE_BUSINESS_ANNUAL || '', 'BUSINESS'],
|
||||
];
|
||||
for (const [envPriceId, tier] of entries) {
|
||||
if (envPriceId && envPriceId === priceId) return tier;
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function syncSubscriptionFromStripe(
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const priceId = subscription.items.data[0]?.price?.id ?? null;
|
||||
const resolvedTier = priceId ? priceIdToTier(priceId) : null;
|
||||
const resolvedTier = priceId ? await priceIdToTier(priceId) : null;
|
||||
|
||||
const tierFromMetadata =
|
||||
(subscription.metadata?.tier as string | undefined) ??
|
||||
|
||||
Reference in New Issue
Block a user