diff --git a/.env.sample b/.env.sample index 55c5e57..cd50b52 100644 --- a/.env.sample +++ b/.env.sample @@ -11,6 +11,3 @@ POSTGRESQL_CAFILE="" POSTMARK_SERVER_API_TOKEN="fake" STRIPE_API_KEY="fake" - -PAYPAL_CLIENT_ID="fake" -PAYPAL_CLIENT_SECRET="fake" diff --git a/.github/workflows/cron-check-subscriptions.yml b/.github/workflows/cron-check-subscriptions.yml index 75a6439..0dea729 100644 --- a/.github/workflows/cron-check-subscriptions.yml +++ b/.github/workflows/cron-check-subscriptions.yml @@ -22,7 +22,5 @@ jobs: POSTGRESQL_PORT: ${{ secrets.POSTGRESQL_PORT }} POSTGRESQL_CAFILE: ${{ secrets.POSTGRESQL_CAFILE }} STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }} - PAYPAL_CLIENT_ID: ${{ secrets.PAYPAL_CLIENT_ID }} - PAYPAL_CLIENT_SECRET: ${{ secrets.PAYPAL_CLIENT_SECRET }} run: | make crons/check-subscriptions diff --git a/README.md b/README.md index 9609dc1..8d0832b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It's not compatible with Budget Zen v2 ([end-to-end encrypted via Userbase](http Or check the [Development section below](#development). -> **NOTE:** You don't need to have emails (Postmark) and subscriptions (Stripe/PayPal) setup to have the app work. Those are only used for allowing others to automatically manage their account. You can simply make any `user.status = 'active'` and `user.subscription.expires_at = new Date('2100-01-01')` to "never" expire, in the database, directly. +> **NOTE:** You don't need to have emails (Postmark) and subscriptions (Stripe) setup to have the app work. Those are only used for allowing others to automatically manage their account. You can simply make any `user.status = 'active'` and `user.subscription.expires_at = new Date('2100-01-01')` to "never" expire, in the database, directly. ## Framework-less diff --git a/crons/check-subscriptions.ts b/crons/check-subscriptions.ts index 706dba8..adeb747 100644 --- a/crons/check-subscriptions.ts +++ b/crons/check-subscriptions.ts @@ -1,6 +1,5 @@ import Database, { sql } from '/lib/interfaces/database.ts'; import { getSubscriptions as getStripeSubscriptions } from '/lib/providers/stripe.ts'; -import { getPayments as getPaypalPayments } from '/lib/providers/paypal.ts'; import { sendSubscriptionExpiredEmail } from '/lib/providers/postmark.ts'; import { updateUser } from '/lib/data-utils.ts'; import { User } from '/lib/types.ts'; @@ -15,8 +14,6 @@ async function checkSubscriptions() { let updatedUsers = 0; - const now = new Date(); - const stripeSubscriptions = await getStripeSubscriptions(); for (const subscription of stripeSubscriptions) { @@ -57,46 +54,6 @@ async function checkSubscriptions() { } } - const paypalPayments = await getPaypalPayments(); - - for (const payment of paypalPayments) { - const matchingUser = users.find((user) => - user.email === payment.payer.payer_info.email && - // Skip payments that aren't related to Budget Zen - payment.transactions.find((transaction) => - transaction.soft_descriptor.toLocaleLowerCase().includes('budget zen') - ) - ); - - if (matchingUser) { - if (!matchingUser.subscription.external.paypal) { - matchingUser.subscription.external.paypal = { - user_id: payment.payer.payer_info.payer_id, - subscription_id: payment.id, - }; - } - - matchingUser.subscription.expires_at = new Date( - new Date(payment.update_time).setUTCMonth(new Date(payment.update_time).getUTCMonth() + 1), - ).toISOString(); - matchingUser.subscription.updated_at = new Date().toISOString(); - - if (new Date(matchingUser.subscription.expires_at) > now) { - matchingUser.status = 'active'; - } else { - if (matchingUser.status === 'active') { - await sendSubscriptionExpiredEmail(matchingUser.email); - } - - matchingUser.status = 'inactive'; - } - - await updateUser(matchingUser); - - ++updatedUsers; - } - } - console.log('Updated', updatedUsers, 'users'); } catch (error) { console.log(error); diff --git a/lib/providers/paypal.ts b/lib/providers/paypal.ts deleted file mode 100644 index 013f263..0000000 --- a/lib/providers/paypal.ts +++ /dev/null @@ -1,101 +0,0 @@ -import 'std/dotenv/load.ts'; - -const PAYPAL_CLIENT_ID = Deno.env.get('PAYPAL_CLIENT_ID') || ''; -const PAYPAL_CLIENT_SECRET = Deno.env.get('PAYPAL_CLIENT_SECRET') || ''; - -interface PaypalPayment { - id: string; - intent: 'sale' | 'authorize' | 'order'; - transactions: PaypalTransaction[]; - state: 'created' | 'approved' | 'failed'; - failure_reason: string; - create_time: string; - update_time: string; - payer: { - payer_info: { - email: string; - payer_id: string; - }; - }; -} - -interface PaypalTransaction { - invoice_number: string; - soft_descriptor: string; - item_list: { - items: PaypalItemListItem[]; - }; - amount: { - currency: string; - total: string; - }; -} - -interface PaypalItemListItem { - sku: string; - name: string; - description: string; - quantity: string; - price: string; - currency: string; -} - -interface PaypalResponse { - count: number; - next_id: string; - payments: PaypalPayment[]; -} - -let paypalAccessToken = ''; - -async function getApiRequestHeaders() { - if (!paypalAccessToken) { - paypalAccessToken = await getAccessToken(); - } - - return { - 'Authorization': `Bearer ${paypalAccessToken}`, - 'Accept': 'application/json; charset=utf-8', - 'Content-Type': 'application/json; charset=utf-8', - }; -} - -async function getAccessToken() { - const body = { grant_type: 'client_credentials' }; - - const response = await fetch('https://api-m.paypal.com/v1/oauth2/token', { - method: 'POST', - headers: { - 'Authorization': `Basic ${btoa(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`)}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(Object.entries(body)).toString(), - }); - - const result = (await response.json()) as { access_token: string }; - - return result.access_token; -} - -export async function getPayments() { - const searchParams = new URLSearchParams(); - - searchParams.set('count', '100'); - - // NOTE: This is deprecated, but there's no good alternative yet - const response = await fetch(`https://api-m.paypal.com/v1/payments/payment?${searchParams.toString()}`, { - method: 'GET', - headers: await getApiRequestHeaders(), - }); - - const result = (await response.json()) as PaypalResponse; - - const payments = result.payments; - - if (!payments) { - console.log(JSON.stringify({ payments }, null, 2)); - throw new Error(`Failed to make API request: "${result}"`); - } - - return payments; -} diff --git a/lib/utils.ts b/lib/utils.ts index 2fb6e76..2c9b625 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -19,14 +19,6 @@ export const PORT = Deno.env.get('PORT') || 8000; export const STRIPE_MONTHLY_URL = 'https://buy.stripe.com/eVa01H57C3MB6CQ14s'; export const STRIPE_YEARLY_URL = 'https://buy.stripe.com/28o5m1dE896V0es8wV'; export const STRIPE_CUSTOMER_URL = 'https://billing.stripe.com/p/login/4gw15w3G9bDyfWU6oo'; -export const PAYPAL_MONTHLY_URL = - `https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-41N48210MJ2770038MQDVBLI&return_url=${ - encodeURI(`${baseUrl}/pricing?paypalCheckoutId=true`) - }`; -export const PAYPAL_YEARLY_URL = - `https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-20P504881F952811BMQDVA4Q&return_url=${ - encodeURI(`${baseUrl}/pricing?paypalCheckoutId=true`) - }`; export const PAYPAL_CUSTOMER_URL = 'https://www.paypal.com'; export interface PageContentResult { @@ -81,8 +73,6 @@ function basicLayout(htmlContent: string, { currentPath, titlePrefix, descriptio STRIPE_MONTHLY_URL: '${STRIPE_MONTHLY_URL}', STRIPE_YEARLY_URL: '${STRIPE_YEARLY_URL}', STRIPE_CUSTOMER_URL: '${STRIPE_CUSTOMER_URL}', - PAYPAL_MONTHLY_URL: '${PAYPAL_MONTHLY_URL}', - PAYPAL_YEARLY_URL: '${PAYPAL_YEARLY_URL}', PAYPAL_CUSTOMER_URL: '${PAYPAL_CUSTOMER_URL}', }; diff --git a/pages/api/subscription.ts b/pages/api/subscription.ts index 6a77e8a..7a65f85 100644 --- a/pages/api/subscription.ts +++ b/pages/api/subscription.ts @@ -1,14 +1,13 @@ import { updateUser, validateUserAndSession } from '/lib/data-utils.ts'; import { getSubscriptions as getStripeSubscriptions } from '/lib/providers/stripe.ts'; -import { getPayments as getPaypalPayments } from '/lib/providers/paypal.ts'; export async function pageAction(request: Request) { if (request.method !== 'POST') { return new Response('Not Implemented', { status: 501 }); } - const { session_id, user_id, provider }: { session_id: string; user_id: string; provider: 'paypal' | 'stripe' } = - await request.json(); + const { session_id, user_id, provider }: { session_id: string; user_id: string; provider: 'stripe' } = await request + .json(); if (!session_id || !user_id) { return new Response('Bad Request', { status: 400 }); @@ -34,28 +33,6 @@ export async function pageAction(request: Request) { }; user.status = 'active'; - await updateUser(user); - } - } else if (provider === 'paypal') { - const payments = await getPaypalPayments(); - - const payment = payments.find((payment) => - payment.payer.payer_info.email === user.email && - payment.transactions.find((transaction) => transaction.soft_descriptor.toLocaleLowerCase().includes('budget zen')) - ); - - if (payment) { - user.subscription.isMonthly = parseInt(payment.transactions[0].amount.total, 10) < 10; - user.subscription.updated_at = new Date().toISOString(); - user.subscription.expires_at = new Date( - new Date(payment.update_time).setUTCMonth(new Date(payment.update_time).getUTCMonth() + 1), - ).toISOString(); - user.subscription.external.paypal = { - user_id: payment.payer.payer_info.payer_id, - subscription_id: payment.id, - }; - user.status = 'active'; - await updateUser(user); } } diff --git a/public/ts/pricing.ts b/public/ts/pricing.ts index ae156a2..dca40ea 100644 --- a/public/ts/pricing.ts +++ b/public/ts/pricing.ts @@ -15,9 +15,9 @@ document.addEventListener('app-loaded', async () => { const headers = commonRequestHeaders; - const provider = urlSearchParams.get('paypalCheckoutId') ? 'paypal' : 'stripe'; + const provider = 'stripe'; - const body: { user_id: string; session_id: string; provider: 'stripe' | 'paypal' } = { + const body: { user_id: string; session_id: string; provider: 'stripe' } = { user_id: session.userId, session_id: session.sessionId, provider, @@ -45,30 +45,7 @@ document.addEventListener('app-loaded', async () => { return; } - const { Swal } = window; - - const stripeOrPayPalDialogResult = await Swal.fire({ - icon: 'question', - title: 'Stripe or PayPal?', - text: 'Do you prefer paying via Stripe or PayPal?', - focusConfirm: false, - showCancelButton: true, - showDenyButton: true, - confirmButtonText: 'Stripe', - denyButtonText: 'PayPal', - cancelButtonText: 'Wait, cancel.', - }); - - if ( - stripeOrPayPalDialogResult.isConfirmed || - stripeOrPayPalDialogResult.isDenied - ) { - if (stripeOrPayPalDialogResult.isDenied) { - window.location.href = window.app.PAYPAL_MONTHLY_URL; - } else { - window.location.href = window.app.STRIPE_MONTHLY_URL; - } - } + window.location.href = window.app.STRIPE_MONTHLY_URL; } async function subscribeYearly(event: Event) { @@ -82,30 +59,7 @@ document.addEventListener('app-loaded', async () => { return; } - const { Swal } = window; - - const stripeOrPayPalDialogResult = await Swal.fire({ - icon: 'question', - title: 'Stripe or PayPal?', - text: 'Do you prefer paying via Stripe or PayPal?', - focusConfirm: false, - showCancelButton: true, - showDenyButton: true, - confirmButtonText: 'Stripe', - denyButtonText: 'PayPal', - cancelButtonText: 'Wait, cancel.', - }); - - if ( - stripeOrPayPalDialogResult.isConfirmed || - stripeOrPayPalDialogResult.isDenied - ) { - if (stripeOrPayPalDialogResult.isDenied) { - window.location.href = window.app.PAYPAL_YEARLY_URL; - } else { - window.location.href = window.app.STRIPE_YEARLY_URL; - } - } + window.location.href = window.app.STRIPE_YEARLY_URL; } function getValidSubscriptionHtmlElement() { @@ -136,7 +90,7 @@ document.addEventListener('app-loaded', async () => { } async function updateUI() { - if (urlSearchParams.get('stripeCheckoutId') || urlSearchParams.get('paypalCheckoutId')) { + if (urlSearchParams.get('stripeCheckoutId')) { await subscriptionSuccessfull(); } diff --git a/public/ts/utils.ts b/public/ts/utils.ts index c662231..215ef11 100644 --- a/public/ts/utils.ts +++ b/public/ts/utils.ts @@ -13,8 +13,6 @@ export interface App { STRIPE_MONTHLY_URL: string; STRIPE_YEARLY_URL: string; STRIPE_CUSTOMER_URL: string; - PAYPAL_MONTHLY_URL: string; - PAYPAL_YEARLY_URL: string; PAYPAL_CUSTOMER_URL: string; isLoggedIn: boolean; showLoading: () => void;