Skip to content

Instantly share code, notes, and snippets.

@wilsonowilson
Created June 29, 2022 17:26
Show Gist options
  • Save wilsonowilson/16ffa7a238df1fb5e3827038530e972e to your computer and use it in GitHub Desktop.
Save wilsonowilson/16ffa7a238df1fb5e3827038530e972e to your computer and use it in GitHub Desktop.
Paddle Stripe-Style Checkout
<script context="module" lang="ts">
import { getSubscriptionPlan } from '$lib/billing/api';
import { Account, getInitialUser, SubscriptionPlan } from '$lib/core/api';
export const router = false;
import type { Load } from '@sveltejs/kit';
import { onMount } from 'svelte';
export const load: Load = async ({ params }) => {
const id = params.planId;
const result = await getSubscriptionPlan(id);
if (result.isErr()) {
return {
status: 500,
error: result.error.message
};
}
if (!result.value) {
return { status: 404 };
}
return {
props: {
plan: result.value
}
};
};
</script>
<script lang="ts">
import Icon from '$lib/core/components/Icon.svelte';
import Logo from '$lib/core/components/Logo.svelte';
import { getUserAccount } from '$lib/onboarding/api';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { account as accountStore } from '$lib/core/stores/accountStore';
import { fade } from 'svelte/transition';
import LoadingIcon from '$lib/core/components/LoadingIcon.svelte';
export let plan: SubscriptionPlan;
let currency = 'USD';
$: currencySymbol = currency == 'USD' ? '$' : '';
let subtotal = '';
let tax = '';
let total = '';
let taxPercent = '';
let loading = true;
onMount(async () => {
const user = await getInitialUser();
if (!user) {
goto('/signin');
return;
}
let account: Account = $accountStore;
if (!account) {
const result = await getUserAccount(user.uid);
if (result.isErr()) {
return;
} else {
account = result.value;
}
}
// @ts-ignore
await Paddle.Setup({
vendor: 140287,
eventCallback: function (eventData) {
updatePrices(eventData);
}
});
// @ts-ignore
await Paddle.Checkout.open({
method: 'inline',
product: plan.paddle_id,
allowQuantity: false,
successCallback: () => {
window.location.href = `/checkout/${$page.params.planId}/success`;
},
frameTarget: 'checkout-container',
email: user.email,
passthrough: JSON.stringify({
account_id: account.id
}),
frameStyle: 'width:100%; min-width:280px; background-color: transparent; border: none;'
});
});
function updatePrices(data) {
var _subtotal =
data.eventData.checkout.prices.customer.total -
data.eventData.checkout.prices.customer.total_tax;
subtotal = _subtotal.toFixed(2);
currency = data.eventData.checkout.prices.customer.currency;
tax = data.eventData.checkout.prices.customer.total_tax;
total = data.eventData.checkout.prices.customer.total;
taxPercent = ((parseFloat(tax) / parseFloat(subtotal)) * 100).toFixed(0).toString();
loading = false;
}
</script>
<div class="flex flex-col h-screen">
<div class="duration-100 flex-grow overflow-y-auto">
<div class=" lg:grid lg:grid-cols-2 min-h-screen">
<div
class=" {loading
? 'bg-zinc-200 text-black'
: 'bg-primary-base text-white'} duration-100 flex justify-center lg:justify-end py-12 lg:py-20 pr-20 w-full"
>
<div class="max-w-sm lg:max-w-[26rem] mx-auto lg:mx-0 w-full lg:float-right">
<div class="w-full px-8 lg:px-12">
<div
class="flex items-center gap-2 md:translate-x-0 -translate-x-6 text-zinc-300 mb-8 lg:-ml-8"
>
<a href="/pricing" class="hover:bg-white/20 duration-75 p-1 rounded-full">
<Icon name="arrow-right" class="rotate-180" size="20px" />
</a>
<div
class="p-1.5 {loading
? 'opacity-30'
: 'opacity-100'} duration-100 bg-white shadow-md rounded-full"
>
<Logo size="20px" />
</div>
</div>
{#if loading}
<div class="w-44 h-6 plc" />
{:else}
<div class="sm:text-lg whitespace-nowrap">
Subscribe to {plan.name}
</div>
{/if}
<div class="flex items-end mt-2">
{#if loading}
<div class="w-40 sm:w-64 h-8 sm:h-12 plc" />
{:else}
<div class="font-bold text-2xl sm:text-4xl">
{currencySymbol}{subtotal}
</div>
<div class="mb-1 ml-1 flex flex-none">
<div>
/
{#if plan.cycle == 'monthly'}
month
{:else if plan.cycle == 'yearly'}
year
{/if}
</div>
</div>
{/if}
</div>
{#if loading}
<div class="w-40 h-6 mt-2 plc lg:hidden" />
{:else}
<p class="text-white/70 mt-2 lg:hidden">Includes {currencySymbol}{tax} VAT</p>
{/if}
<div class="hidden lg:block">
<div class="flex justify-between mt-8">
<div>
{#if loading}
<div class="w-24 h-9 plc" />
{:else}
<div>{plan.name}</div>
<div class="text-sm text-opacity-70 text-white">
{#if plan.cycle == 'monthly'}
Billed Monthly
{:else if plan.cycle == 'yearly'}
Billed Yearly
{:else if plan.cycle == 'lifetime'}
Lifetime
{/if}
</div>
{/if}
</div>
{#if loading}
<div class="w-16 h-5 plc" />
{:else}
<div class="">{currencySymbol}{subtotal}</div>
{/if}
</div>
<div class="flex justify-between mt-8">
<div>
{#if loading}
<div class="w-20 h-6 plc" />
{:else}
<div>VAT ({taxPercent}%)</div>
{/if}
</div>
{#if loading}
<div class="w-16 h-5 plc" />
{:else}
<div class="">{currencySymbol}{tax}</div>
{/if}
</div>
<hr class="border-white/20 mt-8" />
<div class="flex justify-between mt-8">
<div>
{#if loading}
<div class="w-28 h-6 plc" />
{:else}
<div>Total due today</div>
{/if}
</div>
{#if loading}
<div class="w-16 h-5 plc" />
{:else}
<div class="">{currencySymbol}{total}</div>
{/if}
</div>
{#if loading}
<div class="w-52 mt-28 h-5 plc" />
{:else}
<div in:fade={{ duration: 200 }} class="mt-28 flex text-sm items-center gap-1">
Powered by
<img
src="https://theme.zdassets.com/theme_assets/1561536/c0497b032cd66c6f8aa15ad713cf7bba60ad83ad.png"
alt=""
class="h-4"
/>
<div class="w-[1px] mx-2 bg-white/30 h-4" />
<div class="flex items-center gap-2 text-sm text-white/80">
<a href="https://paddle.com/legal/checkout-buyer-terms/" target="_blank"
>Terms</a
>
<a href="https://paddle.com/legal/privacy/" target="_blank">Privacy</a>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<div class="lg:shadow-xl bg-white px-0 sm:px-8 py-12 lg:py-20 lg:pl-20 lg:pt-20 w-full">
<div class="px-8 mx-auto lg:mx-0 max-w-md">
{#if loading}
<div class="w-44 h-8 plc" />
{:else}
<h3 class="text-xl font-bold">Payment Details</h3>
{/if}
{#if loading}
<div class="w-full h-14 mt-2 plc" />
{:else}
<p class="text-sm text-gray-600 mt-2">
Complete your purchase by providing your payment information.
</p>
{/if}
</div>
<div class="max-w-md px-4 w-full mt-1 mx-auto lg:mx-0">
{#if loading}
<div class="mt-8 flex justify-center">
<LoadingIcon size="28px" scale={1.4} />
</div>
{/if}
<div class="checkout-container {loading ? 'opacity-0' : 'opacity-100'}" />
</div>
</div>
</div>
</div>
</div>
<style>
.plc {
@apply bg-gradient-to-r from-black/5 to-black/5 rounded-md animate-pulse;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment