diff --git a/package-lock.json b/package-lock.json index 6add1e9..2802995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@google/generative-ai": "^0.21.0", "@sveltejs/adapter-auto": "^2.1.1", "@sveltejs/adapter-node": "^1.3.1", - "dotenv": "^16.3.1", + "dotenv": "^16.4.7", "node-fetch": "^3.3.2", "node-telegram-bot-api": "^0.61.0" }, @@ -22,6 +22,7 @@ "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.15", + "dotenv-cli": "^8.0.0", "eslint": "^8.28.0", "eslint-plugin-svelte": "^2.30.0", "mongodb": "^5.7.0", @@ -1726,10 +1727,11 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1895,14 +1897,41 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-8.0.0.tgz", + "integrity": "sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^16.3.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" } }, "node_modules/dunder-proto": { diff --git a/package.json b/package.json index 0a51582..c0be224 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint ." + "lint": "eslint .", + "setup-stripe": "dotenv -e .env -- node scripts/setup-stripe.js", + "check-plans": "dotenv -e .env -- node scripts/check-active-plans.js", + "update-plans": "dotenv -e .env -- node scripts/update-plan-durations.js" }, "devDependencies": { "@iconify/svelte": "^3.1.4", @@ -17,6 +20,7 @@ "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.15", + "dotenv-cli": "^8.0.0", "eslint": "^8.28.0", "eslint-plugin-svelte": "^2.30.0", "mongodb": "^5.7.0", @@ -34,7 +38,7 @@ "@google/generative-ai": "^0.21.0", "@sveltejs/adapter-auto": "^2.1.1", "@sveltejs/adapter-node": "^1.3.1", - "dotenv": "^16.3.1", + "dotenv": "^16.4.7", "node-fetch": "^3.3.2", "node-telegram-bot-api": "^0.61.0" } diff --git a/scripts/update-plan-durations.js b/scripts/update-plan-durations.js new file mode 100644 index 0000000..1fbdf27 --- /dev/null +++ b/scripts/update-plan-durations.js @@ -0,0 +1,80 @@ +import { MongoClient } from 'mongodb'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .env +dotenv.config({ path: resolve(__dirname, '../.env') }); + +if (!process.env.MONGO_URI) { + console.error('MONGO_URI is not defined in .env'); + process.exit(1); +} + +const uri = process.env.MONGO_URI; +const client = new MongoClient(uri); + +const PLAN_LIMITS = { + weekend: { + maxDays: 7 + }, + vacation: { + maxDays: 14 + }, + extended: { + maxDays: 30 + } +}; + +async function updatePlanDurations() { + try { + await client.connect(); + console.log('Connected to MongoDB'); + + const db = client.db('splitgram'); + const now = new Date(); + + // Find all users with active plans + const usersWithPlans = await db.collection('users').find({ + 'plan.expires_at': { $gt: now } + }).toArray(); + + console.log('\nActive Plans:', usersWithPlans.length); + + for (const user of usersWithPlans) { + const planType = user.plan.type; + const startDate = new Date(user.plan.started_at); + + // Calculate new expiry based on new durations + const newExpiryDate = new Date(startDate); + newExpiryDate.setDate(startDate.getDate() + PLAN_LIMITS[planType].maxDays); + + console.log('\nUpdating plan for user:', user.id); + console.log('Plan Type:', planType); + console.log('Started:', startDate.toLocaleString()); + console.log('Old Expiry:', new Date(user.plan.expires_at).toLocaleString()); + console.log('New Expiry:', newExpiryDate.toLocaleString()); + + // Update the expiration date + const result = await db.collection('users').updateOne( + { id: user.id }, + { + $set: { + 'plan.expires_at': newExpiryDate + } + } + ); + + console.log('Update result:', result.modifiedCount === 1 ? 'Success' : 'Failed'); + console.log('---'); + } + + } finally { + await client.close(); + } +} + +updatePlanDurations().catch(console.error); \ No newline at end of file diff --git a/src/lib/bot/bot.ts b/src/lib/bot/bot.ts index cb62538..ec98976 100644 --- a/src/lib/bot/bot.ts +++ b/src/lib/bot/bot.ts @@ -13,7 +13,8 @@ import { PlanType, checkPremiumAccess, updateUsageStats, - sendExpirationMessage + sendExpirationMessage, + addChildrenToPlan } from "../db/interface"; import { formatUser, memberToList, pmd2 } from "./utils"; import { GeminiImageProcessor } from "../../../services/gemini-processor"; @@ -237,153 +238,99 @@ bot.onText(/\/start|\/setup|\/app/, async (message) => { bot.on("callback_query", async (callbackQuery) => { const message = callbackQuery.message; if (!message) return; - + + const languageCode = callbackQuery.from.language_code; const data = callbackQuery.data; - const user = callbackQuery.from; - const languageCode = user.language_code; try { - // Allow these actions without premium - if (data === "plan_management") { - console.log("Checking plan details for user:", user.id); - const { hasPlan, plan } = await checkUserPlan(user.id); - console.log("Plan check result:", { hasPlan, plan }); - - let messageText; - if (hasPlan && plan) { - const { type, started_at, expires_at } = plan; - const limits = await getUserPlanLimits(user.id); - console.log("Plan limits:", limits); - - messageText = translate(languageCode, "bot.plan_details", { - planType: translate(languageCode, `bot.plan_type.${type}`), - startDate: started_at.toLocaleDateString(), - expiryDate: expires_at.toLocaleDateString(), - remainingTravelers: String(limits?.travelersRemaining || 0), - remainingScans: String(limits?.scansRemaining || 0), - remainingDays: String(limits?.daysRemaining || 0), - remainingTrips: String(limits?.tripsRemaining || 0) - }); - } else { - console.log("No active plan found"); - messageText = translate(languageCode, "bot.no_active_plan"); - } - - await bot.sendMessage(message.chat.id, messageText, { - parse_mode: "MarkdownV2", - reply_markup: { - inline_keyboard: [ - [{ - text: translate(languageCode, "bot.upgrade_plan"), - url: `https://chopchopsplit.com/#pricing?user_id=${user.id}` - }] - ] - } - }); - } else if (data === "adduser") { - await registerUser(user, message); - } else { - // Require premium for all other actions - if (!await checkPremiumAndNotify(user.id, message.chat.id, languageCode)) { - await bot.answerCallbackQuery(callbackQuery.id); - return; - } - - if (data === "openbot") { - await sendPrivateMessage(user.id, user.language_code); - } else if (data === "split") { - await sendSplitExpenses(user, message); - } else if (data === "receipt") { - // Handle receipt button click - console.log(`Adding user ${user.id} to awaiting receipt`); - state.awaitingReceipt.add(user.id); + if (data) { + if (data === 'plan_management') { + console.log("Checking plan details for user:", callbackQuery.from.id); + const { hasPlan, plan } = await checkUserPlan(callbackQuery.from.id); - await bot.sendMessage( - message.chat.id, - translate(languageCode, "bot.send_receipt_photo"), - { parse_mode: "MarkdownV2" } - ); - } else if (data?.startsWith('split_receipt:')) { - try { - console.log("Processing split receipt callback"); - const messageText = message.text; - console.log("Message text:", messageText); - - if (!messageText) { - console.log("No message text found"); - return; - } - - // Parse total amount from the message - const totalMatch = messageText.match(/Total: ([\d.]+)/); - if (!totalMatch) throw new Error("Could not find total amount"); - - const amount = parseFloat(totalMatch[1]); - - // Get store name and date - const storeMatch = messageText.match(/Store: (.+)\n/); - const dateMatch = messageText.match(/Date: (.+)/); - - const description = `Receipt from ${storeMatch?.[1] || 'store'} on ${dateMatch?.[1] || 'unknown date'}`; - - // Parse items from the message text - const items = messageText - .split('\n') - .filter(line => line.includes(': ') && line.includes(' = ')) - .map(line => { - const [name, details] = line.split(': '); - const [quantity, unitPrice, , totalPrice] = details.split(' ').map(n => parseFloat(n) || 0); - return { - name, - quantity, - unitPrice, - totalPrice, - assignedTo: [] - }; - }); - - console.log("Message Text:", messageText); - - // Update regex patterns to match the exact format - const serviceChargeMatch = messageText.match(/Service Charge: ([\d.]+)/); - const serviceTaxMatch = messageText.match(/Service Tax: ([\d.]+)/); - - console.log("Service Charge Match:", serviceChargeMatch); - console.log("Service Tax Match:", serviceTaxMatch); - - const serviceCharge = parseFloat(serviceChargeMatch?.[1] || '0'); - const serviceTax = parseFloat(serviceTaxMatch?.[1] || '0'); - - // Add this to parse taxesIncluded from message - const total = parseFloat(totalMatch[1]); - const subtotalMatch = messageText.match(/Subtotal: ([\d.]+)/); - const subtotal = parseFloat(subtotalMatch?.[1] || '0'); - const taxesIncluded = Math.abs(total - subtotal) < 0.01; - - // Create URL with processed items and tax information - const webAppUrl = `${getBaseHost()}/webapp/add-split?amount=${amount}&description=${encodeURIComponent(description)}&receiptItems=${encodeURIComponent(JSON.stringify(items))}&serviceCharge=${serviceCharge}&serviceTax=${serviceTax}&taxesIncluded=${taxesIncluded}`; - - console.log("Web app URL:", webAppUrl); - - // Send web app button in private chat + if (!hasPlan || !plan) { await bot.sendMessage( - user.id, - `Receipt Details\\:\nAmount: RM${pmd2(amount.toFixed(2))} \n${pmd2(description)}`, + message.chat.id, + translate(languageCode, "bot.no_active_plan"), { parse_mode: "MarkdownV2", reply_markup: { inline_keyboard: [[{ - text: translate(languageCode, "bot.add_split"), - web_app: { url: webAppUrl } + text: translate(languageCode, "bot.get_premium"), + url: `https://chopchopsplit.com/#pricing?user_id=${callbackQuery.from.id}` }]] } } ); + return; + } + + console.log("Getting plan limits for user:", callbackQuery.from.id); + const limits = await getUserPlanLimits(callbackQuery.from.id); + + // Helper function to escape special characters for MarkdownV2 + const escapeMarkdown = (text: string) => { + return text + // Escape all special characters except asterisks used for bold + .replace(/([_[\]()~`>#+=|{}.!-])/g, '\\$1'); + }; + + // Format dates + const startDate = plan.started_at.toLocaleDateString(); + const endDate = plan.expires_at.toLocaleDateString(); + const planType = plan.type.charAt(0).toUpperCase() + plan.type.slice(1); + + // Create message with actual values instead of templates + const planMessage = [ + `Your current plan: *${planType} Plan*`, + "", + `Started: ${startDate}`, + `Expires: ${endDate}`, + "", + "Remaining limits:", + `- Travelers: ${Math.max(0, limits?.travelersRemaining || 0)}`, + `- Receipt scans: ${Math.max(0, limits?.scansRemaining || 0)}`, + `- Days: ${Math.max(0, limits?.daysRemaining || 0)}`, + `- Trips: ${Math.max(0, limits?.tripsRemaining || 0)}` + ].join("\n"); + + await bot.sendMessage( + message.chat.id, + escapeMarkdown(planMessage), + { + parse_mode: "MarkdownV2", + reply_markup: { + inline_keyboard: [[{ + text: translate(languageCode, "bot.get_premium"), + url: `https://chopchopsplit.com/#pricing?user_id=${callbackQuery.from.id}` + }]] + } + } + ); + } else if (data.startsWith('share_plan:')) { + const childId = parseInt(data.split(':')[1]); + const parentId = callbackQuery.from.id; + + try { + const success = await addChildrenToPlan(parentId, [childId]); + if (success) { + await bot.sendMessage( + message.chat.id, + translate(languageCode, "bot.plan_shared_success", { + user: (await bot.getChatMember(message.chat.id, childId)).user.first_name + }), + { parse_mode: "MarkdownV2" } + ); + } else { + await bot.sendMessage( + message.chat.id, + translate(languageCode, "bot.plan_shared_error"), + { parse_mode: "MarkdownV2" } + ); + } } catch (error) { - console.error("Error processing split receipt:", error); + console.error("Error sharing plan:", error); sendError(message.chat.id, languageCode, error); - await bot.answerCallbackQuery(callbackQuery.id, { text: "Error processing receipt" }); - return; } } } @@ -887,3 +834,64 @@ function initializePing() { setInterval(pingServer, 840000); console.log("Self-ping initialized"); } + +// Add new command handlers for managing plan access +bot.onText(/\/share/, async (message) => { + const userId = message.from?.id; + const languageCode = message.from?.language_code; + + if (!userId) return; + + // Check if user has their own plan (not a child plan) + const { hasPlan, plan, isChild } = await checkUserPlan(userId); + if (!hasPlan || !plan || isChild) { + await bot.sendMessage( + message.chat.id, + translate(languageCode, "bot.premium_required"), + { + parse_mode: "MarkdownV2", + reply_markup: { + inline_keyboard: [ + [{ + text: translate(languageCode, "bot.get_premium"), + url: `https://chopchopsplit.com/#pricing?user_id=${userId}` + }] + ] + } + } + ); + return; + } + + // Get group members + const group = await getGroupById(message.chat.id); + if (!group) return; + + // Create keyboard with all group members except the plan owner + const keyboard = group.members + .filter(member => member.id !== userId && !plan.children?.includes(member.id)) + .map(member => [{ + text: `${member.first_name} ${member.last_name || ''}`, + callback_data: `share_plan:${member.id}` + }]); + + if (keyboard.length === 0) { + await bot.sendMessage( + message.chat.id, + translate(languageCode, "bot.no_members_to_share"), + { parse_mode: "MarkdownV2" } + ); + return; + } + + await bot.sendMessage( + message.chat.id, + translate(languageCode, "bot.select_members_to_share"), + { + parse_mode: "MarkdownV2", + reply_markup: { + inline_keyboard: keyboard + } + } + ); +}); diff --git a/src/lib/db/interface.ts b/src/lib/db/interface.ts index d407fed..b638e50 100644 --- a/src/lib/db/interface.ts +++ b/src/lib/db/interface.ts @@ -6,6 +6,7 @@ import { translate } from "../i18n/i18n"; interface CachedPlan { hasPlan: boolean; plan?: Plan; + isChild?: boolean; cachedAt: number; } @@ -458,6 +459,9 @@ export interface Plan { type: PlanType; started_at: Date; expires_at: Date; + parent_id?: number; // If set, this user is a child of another premium user + children?: number[]; // List of user IDs that this premium user has granted access to + group_id: number; // The group this plan is associated with } // Plan limits @@ -465,67 +469,89 @@ export const PLAN_LIMITS = { weekend: { maxTravelers: 4, maxScans: 50, - maxDays: 3, + maxDays: 7, maxTrips: 1 }, vacation: { maxTravelers: 8, maxScans: 200, - maxDays: 7, + maxDays: 14, maxTrips: 2 }, extended: { maxTravelers: 15, maxScans: Infinity, - maxDays: 14, + maxDays: 30, maxTrips: 5 } } as const; // Check user's plan status with caching -export const checkUserPlan = async (userId: number): Promise<{ hasPlan: boolean; plan?: Plan }> => { +export const checkUserPlan = async (userId: number): Promise<{ hasPlan: boolean; plan?: Plan; isChild?: boolean }> => { const now = new Date(); const cached = planCache.get(userId); // Check if we have a valid cached result if (cached && (now.getTime() - cached.cachedAt) < CACHE_TTL) { console.log("Using cached plan data for user:", userId); - return { hasPlan: cached.hasPlan, plan: cached.plan }; + return { hasPlan: cached.hasPlan, plan: cached.plan, isChild: cached.isChild }; } console.log("Checking plan in database for user:", userId); - // Query database for fresh data + // First check if user has their own plan const user = await db.collection("users").findOne({ id: userId, 'plan.expires_at': { $gt: now } }); - - console.log("User data from MongoDB:", JSON.stringify(user, null, 2)); - - let result: { hasPlan: boolean; plan?: Plan }; - + + // If no direct plan, check if user is a child of a premium user if (!user?.plan) { - console.log("No active plan found for user"); - result = { hasPlan: false }; - } else { - // Parse the plan data - const plan = { - type: user.plan.type as PlanType, - started_at: new Date(user.plan.started_at), - expires_at: new Date(user.plan.expires_at) - }; + const parentUser = await db.collection("users").findOne({ + 'plan.children': userId, + 'plan.expires_at': { $gt: now } + }); + + if (parentUser?.plan) { + console.log("Found parent plan:", parentUser.id); + const result = { + hasPlan: true, + plan: parentUser.plan, + isChild: true + }; + planCache.set(userId, { + ...result, + cachedAt: now.getTime() + }); + return result; + } - console.log("Active plan found:", { - type: plan.type, - started_at: plan.started_at.toISOString(), - expires_at: plan.expires_at.toISOString() + console.log("No active plan found for user"); + const result = { hasPlan: false }; + planCache.set(userId, { + ...result, + cachedAt: now.getTime() }); - - result = { hasPlan: true, plan }; + return result; } - // Cache the result + // User has their own plan + const plan = { + type: user.plan.type as PlanType, + started_at: new Date(user.plan.started_at), + expires_at: new Date(user.plan.expires_at), + children: user.plan.children || [], + group_id: user.plan.group_id + }; + + console.log("Active plan found:", { + type: plan.type, + started_at: plan.started_at.toISOString(), + expires_at: plan.expires_at.toISOString(), + children: plan.children + }); + + const result = { hasPlan: true, plan, isChild: false }; planCache.set(userId, { ...result, cachedAt: now.getTime() @@ -534,6 +560,65 @@ export const checkUserPlan = async (userId: number): Promise<{ hasPlan: boolean; return result; }; +// Add child users to a parent's plan +export const addChildrenToPlan = async (parentId: number, childIds: number[]): Promise => { + try { + const { hasPlan, plan } = await checkUserPlan(parentId); + if (!hasPlan || !plan) { + console.log("Parent user doesn't have an active plan"); + return false; + } + + // Add children to the plan + const result = await db.collection("users").updateOne( + { id: parentId }, + { + $addToSet: { + 'plan.children': { $each: childIds } + } + } + ); + + if (result.modifiedCount === 1) { + // Invalidate cache for parent and all children + invalidatePlanCache(parentId); + childIds.forEach(childId => invalidatePlanCache(childId)); + return true; + } + + return false; + } catch (error) { + console.error("Error adding children to plan:", error); + return false; + } +}; + +// Remove child users from a parent's plan +export const removeChildFromPlan = async (parentId: number, childId: number): Promise => { + try { + const result = await db.collection("users").updateOne( + { id: parentId }, + { + $pull: { + 'plan.children': childId + } + } + ); + + if (result.modifiedCount === 1) { + // Invalidate cache for parent and child + invalidatePlanCache(parentId); + invalidatePlanCache(childId); + return true; + } + + return false; + } catch (error) { + console.error("Error removing child from plan:", error); + return false; + } +}; + // Add function to invalidate cache when plan changes export const invalidatePlanCache = (userId: number) => { console.log("Invalidating plan cache for user:", userId); @@ -709,3 +794,52 @@ export const sendExpirationMessage = async (bot: any, userId: number, planType: } }); }; + +// Add function to update existing plan durations +export const updateExistingPlanDurations = async () => { + console.log("Updating existing plan durations"); + const now = new Date(); + + try { + // Find all users with active plans + const usersWithPlans = await db.collection("users").find({ + 'plan.expires_at': { $gt: now } + }).toArray(); + + console.log(`Found ${usersWithPlans.length} active plans to update`); + + for (const user of usersWithPlans) { + const planType = user.plan.type as PlanType; + const startDate = new Date(user.plan.started_at); + + // Calculate new expiry based on new durations + const newExpiryDate = new Date(startDate); + newExpiryDate.setDate(startDate.getDate() + PLAN_LIMITS[planType].maxDays); + + console.log(`Updating plan for user ${user.id}:`, { + type: planType, + oldExpiry: user.plan.expires_at, + newExpiry: newExpiryDate + }); + + // Update the expiration date + await db.collection("users").updateOne( + { id: user.id }, + { + $set: { + 'plan.expires_at': newExpiryDate + } + } + ); + + // Invalidate cache for this user + invalidatePlanCache(user.id); + } + + console.log("Successfully updated all active plan durations"); + return { success: true, updatedCount: usersWithPlans.length }; + } catch (error: any) { + console.error("Error updating plan durations:", error); + return { success: false, error: error?.message || 'Unknown error' }; + } +}; diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 810f7eb..2dccff8 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -79,11 +79,11 @@ export const dictionary = { "subscription.success": "โœจ Thank you for subscribing to ChopChopSplit Premium! Your subscription is now active.", "subscription.error": "โŒ Subscription failed: {{message}}", "subscription.command": "*๐ŸŒŸ ChopChopSplit Premium*\n\nUnlock all features:\n\\- Unlimited groups\n\\- Receipt scanning\n\\- Advanced analytics\n\\- Priority support\n\nChoose your plan:", - "bot.help_message": "*Available Commands:*\n\n๐Ÿ“‹ /chop \\- View all debts in the group\n๐Ÿงพ /receipt \\- Process a receipt photo\n\\- Send the command\n\\- Send the receipt photo\n\\- Bot will process and let you chop chop the bill\nโ“ /help \\- Show this help message", + "bot.help_message": "*Available Commands:*\n\n๐Ÿ“‹ /chop \\- View all debts in the group\n๐Ÿงพ /receipt \\- Process a receipt photo\n\\- Send the command\n\\- Send the receipt photo\n\\- Bot will process and let you chop chop the bill\n๐ŸŽ /share \\- Share your premium access with friends\nโ“ /help \\- Show this help message", "bot.add_receipt": "๐Ÿ“ธ Add receipt", "bot.send_receipt_photo": "Please send a photo of your receipt\\.", "bot.plan_management": "โญ๏ธ Plan management", - "bot.plan_details": "Your current plan: *{{planType}}*\n\nStarted: {{startDate}}\nExpires: {{expiryDate}}\n\nRemaining limits:\n\\- Travelers: {{remainingTravelers}}\n\\- Receipt scans: {{remainingScans}}\n\\- Days: {{remainingDays}}\n\\- Trips: {{remainingTrips}}", + "bot.plan_details": "Your current plan: *{{planType}}*\n\nStarted: {{startDate}}\nExpires: {{endDate}}\n\nRemaining limits:\n\\- Travelers: {{travelers}}\n\\- Receipt scans: {{scans}}\n\\- Days: {{days}}\n\\- Trips: {{trips}}", "bot.no_active_plan": "You don't have an active plan\\. Click the button below to upgrade and unlock premium features\\!", "bot.upgrade_plan": "โญ๏ธ Upgrade plan", "bot.plan_type.weekend": "Weekend Plan", @@ -102,5 +102,9 @@ export const dictionary = { "bot.group.needs_admin": "*๐Ÿ”‘ Please make me an admin first\\!*\n\nClick the button below to add me with admin permissions\\.\nAfter that, use /start to continue setup\\!", "bot.group.make_admin": "๐Ÿ”‘ Add Bot with Admin Rights", "bot.admin_error": "โŒ Failed to make me an admin\\. Please try manually:\n\n1\\. Open group settings\n2\\. Tap \"Administrators\"\n3\\. Add \"ChopChop\" as admin\n4\\. Use /start when done", + "bot.select_members_to_share": "*Share Your Premium Access*\n\nSelect friends to share your premium access with\\. They will be able to use all premium features\\!", + "bot.no_members_to_share": "No friends available to share with\\. Everyone either already has access or you're the only one in the group\\!", + "bot.plan_shared_success": "โœจ Successfully shared premium access with {{user}}\\!", + "bot.plan_shared_error": "โŒ Failed to share premium access\\. Please try again\\.", } } as Dictionary;