Add paypal checkout

This commit is contained in:
2025-04-16 17:12:33 +02:00
parent bc112b13c9
commit 392c61e827
13 changed files with 339 additions and 84 deletions

View File

@@ -1,13 +1,12 @@
<script>
import CreditDeposit from "layout/CreditDeposit.svelte";
import { onMount } from "svelte";
import Euro from "util/Euro.svelte";
import { formatDate } from "util/Formatting";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import MollieDeposit from "./MollieDeposit.svelte";
let loading = false
let credit_amount = 10
let tab = "mollie"
const checkout = async (network = "", amount = 0, country = "") => {
loading = true
@@ -55,7 +54,7 @@ const load_invoices = async () => {
invoices_tmp.forEach(row => {
row.time = new Date(row.time)
if (row.payment_method === "mollie" && row.status === "open") {
if (row.status === "open") {
unpaid_invoice = true
}
})
@@ -89,61 +88,18 @@ onMount(() => {
amount={window.user.balance_micro_eur}/>
</p>
<div class="tab_bar">
<button on:click={() => tab = "mollie"} class:button_highlight={tab === "mollie"}>
<i class="icon">euro</i>
Mollie
</button>
<button on:click={() => tab = "btcpay"} class:button_highlight={tab === "btcpay"}>
<i class="icon">currency_bitcoin</i>
Crypto
</button>
</div>
{#if tab === "mollie"}
{#if unpaid_invoice}
<div class="highlight_yellow">
<p>
You still have an unpaid invoice open. Please pay that one
before requesting a new invoice. You can find unpaid
invoices at the bottom of this page. You can cancel an
invoice by clicking Pay, and then clicking the Back link at
the bottom of the page.
</p>
</div>
{:else}
<MollieDeposit/>
{/if}
{:else if tab === "btcpay"}
<div class="highlight_border">
<p style="text-align: initial">
Alternatively you can use Bitcoin, Lightning network (<a
href="https://btcpay.pixeldrain.com/embed/uS2mbWjXUuaAqMh8XLjkjwi8oehFuxeBZxekMxv68LN/BTC/ln"
target="_blank" rel="noreferrer">node info</a>) and Dogecoin to deposit
credits on your pixeldrain account. You must pay the full amount as
stated on the invoice, else your payment will fail.
{#if unpaid_invoice}
<div class="highlight_yellow">
<p>
You still have an unpaid invoice open. Please pay that one
before requesting a new invoice. You can find unpaid
invoices at the bottom of this page. You can cancel an
invoice by clicking Pay, and then clicking the Back link at
the bottom of the page.
</p>
<p style="text-align: initial">
Do note that it is not possible to withdraw coins from your
pixeldrain account. It's not a wallet. Any amount of money you
deposit has to be used up.
</p>
Deposit amount €
<input type="number" bind:value={credit_amount} min="10"/>
<br/>
Choose payment method:<br/>
<button on:click={() => {checkout("btc", credit_amount)}}>
<i class="icon">currency_bitcoin</i> Bitcoin
</button>
<button on:click={() => {checkout("btc_lightning", credit_amount)}}>
<i class="icon">bolt</i> Lightning network
</button>
<button on:click={() => {checkout("doge", credit_amount)}}>
<span class="icon_unicode">Ð</span> Dogecoin
</button>
<button on:click={() => {checkout("xmr", credit_amount)}}>
<span class="icon_unicode">M</span> Monero
</button>
</div>
{:else}
<CreditDeposit/>
{/if}
<h3 id="invoices">Past invoices</h3>
@@ -174,7 +130,7 @@ onMount(() => {
<td>{row.country}</td>
<td>{row.payment_method}</td>
<td>
{#if row.status === "InvoiceCreated" || row.status === "open"}
{#if row.status === "InvoiceCreated" || row.status === "open" || row.status === "CREATED" || row.status === "PAYER_ACTION_REQUIRED"}
Waiting for payment
{:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations
@@ -189,7 +145,12 @@ onMount(() => {
{/if}
</td>
<td>
{#if row.status === "New" || row.status === "InvoiceCreated" || row.status === "open"}
{#if row.status === "New" ||
row.status === "InvoiceCreated" ||
row.status === "open" ||
row.status === "CREATED" ||
row.status === "PAYER_ACTION_REQUIRED"
}
<a href="/api/user/pay_invoice/{row.id}" class="button button_highlight">
<i class="icon">paid</i> Pay
</a>

View File

@@ -1,277 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import Euro from "util/Euro.svelte";
import LoadingIndicator from "util/LoadingIndicator.svelte";
import { countries } from "country-data-list";
import { get_endpoint, get_misc_vat_rate } from "lib/PixeldrainAPI";
let loading = false
let amount = 20
let country: typeof countries.all[0] = null
let country_input = ""
let vat = 0
const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
onMount(() => {
const checkout_country = window.localStorage.getItem("checkout_country")
if (countries[checkout_country] !== undefined) {
country_input = checkout_country
set_country()
}
})
const payment_providers = [
{icon: "paypal", name: "PayPal"},
{icon: "creditcard", name: "Credit/debit"},
{icon: "apple_pay", name: "Apple Pay"},
{icon: "google_pay", name: "Google Pay"},
{icon: "bancomat", name: "Bancomat"},
{icon: "bancontact", name: "Bancontact"},
{icon: "belfius", name: "Belfius"},
{icon: "blik", name: "Blik"},
{icon: "eps", name: "EPS"},
{icon: "ideal", name: "iDEAL"},
{icon: "ideal_in3", name: "iDeal in3"},
{icon: "kbc", name: "KBC"},
{icon: "mb_way", name: "MB Way"},
{icon: "multibanco", name: "Multibanco"},
{icon: "p24", name: "Przelewy24"},
{icon: "riverty", name: "Riverty"},
{icon: "satispay", name: "Satispay"},
{icon: "sepa", name: "SEPA Transfer"},
{icon: "twint", name: "Twint"},
]
const set_country = async (e?: Event) => {
loading = true
if (e !== undefined) {
e.preventDefault()
}
if (countries[country_input] === undefined) {
alert("Please enter a valid country code")
return
}
const c = countries[country_input]
// Cache the value for next checkout
window.localStorage.setItem("checkout_country", c.alpha3)
try {
const vat_rate = await get_misc_vat_rate(c.alpha3)
vat = vat_rate.vat*100
country = c
} catch (err) {
alert(err)
} finally {
loading = false
}
}
const checkout = async () => {
loading = true
if (amount < 10) {
alert("Amount needs to be at least €10")
return
}
const form = new FormData()
form.set("amount", String(amount*1e6))
form.set("network", "mollie")
form.set("country", country.alpha2)
try {
const resp = await fetch(
get_endpoint()+"/user/invoice",
{method: "POST", body: form},
)
if(resp.status >= 400) {
let json = await resp.json()
throw json.message
}
window.location = (await resp.json()).checkout_url
} catch (err) {
alert(err)
} finally {
loading = false
}
}
const format_country = (c: typeof countries.all[0]) => {
let str = ""
if (c.emoji !== undefined) {
str += c.emoji + " "
} else {
str += "🌐 "
}
str += c.name+" "
str += "("+c.alpha2+", "
str += c.alpha3+")"
return str
}
</script>
<div class="highlight_border">
<LoadingIndicator loading={loading}/>
{#if country === null}
<div>
Please pick your country of residence
</div>
<div>
<form on:submit|preventDefault={set_country} class="country_form">
<div class="country_search">
<datalist id="country_picker">
{#each countries.all.filter(c => c.status === "assigned") as c}
<option value={c.alpha2}>{format_country(c)}</option>
{/each}
</datalist>
<input
bind:value={country_input}
type="text"
list="country_picker"
placeholder="Search for country"
style="flex: 1 1 auto;">
<button type="submit" class="button_highlight" style="flex: 0 0 auto;">
<i class="icon">send</i>
Continue
</button>
</div>
<select bind:value={country_input} on:dblclick={set_country} style="padding: 0;" size="10">
{#each countries.all.filter(c => c.status === "assigned") as c}
<option value={c.alpha2}>{format_country(c)}</option>
{/each}
</select>
</form>
</div>
<hr/>
<div>
We support the following payment processors
</div>
<div class="processors">
{#each payment_providers as p (p.name)}
<div>
<img class="bankicon" src="/res/img/payment_providers/{p.icon}.svg" alt={p.name} title={p.name}/>
{p.name}
</div>
{/each}
</div>
{:else}
<div style="display: flex;">
<button on:click={() => country = null} style="flex: 0 0 auto;">
<i class="icon">chevron_left</i>
Change country
</button>
<div style="flex: 1 1 auto;"></div>
<div style="flex: 0 0 auto; display: flex; gap: 0.25em; align-items: center;">
<span>Paying from</span>
<span style="font-size: 1.5em; line-height: 1em;">{country.emoji}</span>
<span>{country.name} ({vat}% VAT)</span>
</div>
</div>
<form class="amount_grid" on:submit|preventDefault={checkout}>
<div class="span3">Please choose an amount</div>
{#each amounts as a}
<button
on:click|preventDefault={() => amount = a}
class="amount_button"
class:button_highlight={amount === a}
>
{a}
</button>
{/each}
<div class="span3 mollie_checkout">
<div>Custom amount €</div>
<input type="number" bind:value={amount} min="10"/>
</div>
<div class="span2" style="text-align: initial;">
Total including VAT:
<Euro amount={(amount*1e6) + (amount*1e6)*(vat/100)}/>
</div>
<button type="submit" class="button_highlight">
<i class="icon">paid</i> Checkout
</button>
</form>
<hr/>
<p style="text-align: initial;">
This Pixeldrain premium plan costs €1 per month, but due to
processing fees we can't accept payments less than €10. So your
deposit will give roughly 10 months of premium service depending on
usage. You can track your spending on the <a
href="/user/prepaid/transactions">transactions page</a>.
</p>
{/if}
</div>
<style>
.country_form {
display: flex;
flex-direction: column;
margin: auto;
width: 600px;
max-width: 100%;
}
.country_search {
display: flex;
flex-direction: row;
}
.processors {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.processors > div {
display: flex;
align-items: center;
gap: 5px;
margin: 3px;
}
.amount_grid {
max-width: 500px;
gap: 4px;
display: inline-grid;
align-items: center;
grid-template-columns: 1fr 1fr 1fr;
}
.amount_button {
font-size: 1.1em;
line-height: 1.6em;
justify-content: center;
}
.span2 {
grid-column: span 2;
}
.span3 {
grid-column: span 3;
}
.mollie_checkout {
display: flex;
flex-direction: row;
}
.mollie_checkout > div {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.mollie_checkout > input[type="number"] {
flex: 1 1 auto;
}
.bankicon {
width: 40px;
height: 30px;
}
</style>

View File

@@ -1,5 +1,4 @@
<script>
import { onMount } from "svelte";
import Euro from "util/Euro.svelte"
import LoadingIndicator from "util/LoadingIndicator.svelte";
import SuccessMessage from "util/SuccessMessage.svelte";
@@ -47,20 +46,12 @@ const update = async (plan) => {
loading = false
}
}
let checkout_success = false
onMount(() => {
if (window.location.hash === "#checkout_complete") {
checkout_success = true
}
})
</script>
<LoadingIndicator loading={loading}/>
<section>
{#if checkout_success}
{#if window.location.hash === "#checkout_complete"}
<div class="highlight_green">
<h2>Payment successful!</h2>
<p>
@@ -78,6 +69,24 @@ onMount(() => {
support@pixeldrain.com.
</p>
</div>
{:else if window.location.hash === "#order_expired"}
<div class="highlight_yellow">
<h2>Order expired</h2>
<p>
This order has expired. Please create a new order on the <a
href="/user/prepaid/deposit">credit deposit page</a>.
</p>
</div>
{:else if window.location.hash === "#order_canceled"}
<div class="highlight_yellow">
<h2>Order canceled</h2>
<p>
You have chosen to cancel the order. If you still want to
proceed with the order you can initiate payment again from the
<a href="/user/prepaid/deposit">deposit page</a>. If not then
you can let the invoice expire.
</p>
</div>
{/if}
<PatreonActivationResult/>