Add Mollie payment terminal

This commit is contained in:
2023-09-14 01:35:27 +02:00
parent 06018aa4ed
commit 1a332f6d57
4 changed files with 264 additions and 55 deletions

View File

@@ -3,20 +3,28 @@ import { onMount } from "svelte";
import Euro from "../util/Euro.svelte"; import Euro from "../util/Euro.svelte";
import { formatDate } from "../util/Formatting.svelte"; import { formatDate } from "../util/Formatting.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import MollieDeposit from "./MollieDeposit.svelte";
let loading = false let loading = false
let credit_amount = 10 let credit_amount = 10
const checkout = async (network) => { const checkout = async (network = "", amount = 0, country = "") => {
loading = true loading = true
if (amount < 10) {
alert("Amount needs to be at least €10")
return
}
const form = new FormData() const form = new FormData()
form.set("amount", credit_amount*1e6) form.set("amount", amount*1e6)
form.set("network", network) form.set("network", network)
form.set("country", country)
try { try {
const resp = await fetch( const resp = await fetch(
window.api_endpoint+"/btcpay/deposit", window.api_endpoint+"/user/invoice",
{ method: "POST", body: form }, {method: "POST", body: form},
) )
if(resp.status >= 400) { if(resp.status >= 400) {
let json = await resp.json() let json = await resp.json()
@@ -35,7 +43,7 @@ let invoices = []
const load_invoices = async () => { const load_invoices = async () => {
loading = true loading = true
try { try {
const resp = await fetch(window.api_endpoint+"/btcpay/invoice") const resp = await fetch(window.api_endpoint+"/user/invoice")
if(resp.status >= 400) { if(resp.status >= 400) {
throw new Error((await resp.json()).message) throw new Error((await resp.json()).message)
} }
@@ -66,19 +74,19 @@ onMount(() => {
<section> <section>
<h2>Deposit credits</h2> <h2>Deposit credits</h2>
<p> <p>
To deposit funds on your pixeldrain account please contact Pixeldrain credits can be used for our prepaid plans, which are
<a href="mailto:sales@pixeldrain.com">sales@pixeldrain.com</a>. We will different from the Patreon plans. With prepaid you only pay for what you
prepare an invoice for you. We accept wire transfers and PayPal. As we use, at a rate of €4 per TB per month for storage and €2 per TB for data
are located in the Netherlands SEPA payments should be free and instant. transfer.
</p> </p>
<p> <p>
Pixeldrain credits can be used for our prepaid plans, which are Use the form below to deposit credit on your pixeldrain account using
different from the Patreon plans. Because preparing invoices requires Mollie. The minimum deposit is €10. <b>Mollie payments are currently
some manual labour we only accept deposits of €100 or more. The prepaid only available in Europe</b>.
plans are meant for people who require a lot of bandwidth or storage.
Paying only for what you use means you can save money compared to a
fixed monthly price.
</p> </p>
<MollieDeposit on:checkout={e => checkout("mollie", e.detail.amount, e.detail.country)}/>
<h3>Crypto payments</h3> <h3>Crypto payments</h3>
<p> <p>
Alternatively you can use Bitcoin, Lightning network (<a Alternatively you can use Bitcoin, Lightning network (<a
@@ -94,16 +102,16 @@ onMount(() => {
</p> </p>
<div class="highlight_border"> <div class="highlight_border">
Deposit amount € Deposit amount €
<input type="number" bind:value={credit_amount} min="1"/> <input type="number" bind:value={credit_amount} min="10"/>
<br/> <br/>
Choose payment method:<br/> Choose payment method:<br/>
<button on:click={() => {checkout("btc")}}> <button on:click={() => {checkout("btc", credit_amount)}}>
<span class="icon_unicode"></span> Bitcoin <span class="icon_unicode"></span> Bitcoin
</button> </button>
<button on:click={() => {checkout("btc_lightning")}}> <button on:click={() => {checkout("btc_lightning", credit_amount)}}>
<i class="icon">bolt</i> Lightning network <i class="icon">bolt</i> Lightning network
</button> </button>
<button on:click={() => {checkout("doge")}}> <button on:click={() => {checkout("doge", credit_amount)}}>
<span class="icon_unicode">Ð</span> Dogecoin <span class="icon_unicode">Ð</span> Dogecoin
</button> </button>
</div> </div>
@@ -118,6 +126,9 @@ onMount(() => {
<tr> <tr>
<td>Created</td> <td>Created</td>
<td>Amount</td> <td>Amount</td>
<td>VAT</td>
<td>Country</td>
<td>Method</td>
<td>Status</td> <td>Status</td>
<td></td> <td></td>
</tr> </tr>
@@ -126,23 +137,28 @@ onMount(() => {
{#each invoices as row (row.id)} {#each invoices as row (row.id)}
<tr> <tr>
<td>{formatDate(row.time, true, true, false)}</td> <td>{formatDate(row.time, true, true, false)}</td>
<td><Euro amount={row.amount}></Euro></td> <td><Euro amount={row.amount}/></td>
<td><Euro amount={row.vat}/></td>
<td>{row.country}</td>
<td>{row.payment_method}</td>
<td> <td>
{#if row.status === "InvoiceCreated"} {#if row.status === "InvoiceCreated" || row.status === "open"}
New (waiting for payment) Waiting for payment
{:else if row.status === "InvoiceProcessing"} {:else if row.status === "InvoiceProcessing"}
Payment received, waiting for confirmations Payment received, waiting for confirmations
{:else if row.status === "InvoiceSettled"} {:else if row.status === "InvoiceSettled" || row.status === "paid"}
Paid Paid
{:else if row.status === "InvoiceExpired"} {:else if row.status === "InvoiceExpired"}
Expired Expired
{:else if row.status === "canceled"}
Canceled
{:else} {:else}
{row.status} {row.status}
{/if} {/if}
</td> </td>
<td> <td>
{#if row.status === "New" || row.status === "InvoiceCreated"} {#if row.status === "New" || row.status === "InvoiceCreated" || row.status === "open"}
<a href={row.checkout_url} class="button button_highlight"> <a href="/api/user/pay_invoice/{row.id}" class="button button_highlight">
<i class="icon">paid</i> Pay <i class="icon">paid</i> Pay
</a> </a>
{/if} {/if}

View File

@@ -0,0 +1,148 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
At, Be, Bg, Hr, Cy, Cz, Dk, Ee, Fi, Fr, De, Gr, Hu, Ie, It, Lv, Lt, Lu, Mt,
Nl, Pl, Pt, Ro, Sk, Si, Es, Se,
} from 'svelte-flag-icons';
import Euro from '../util/Euro.svelte';
let dispatch = createEventDispatcher()
let credit_amount = 10
$: credit_micro = credit_amount*1e6
$: vat_micro = country === null ? 0 : (credit_amount*1e6)*(country.vat/100)
let country = null
let countries = [
{name: "Austria", code: At, vat: 20},
{name: "Belgium", code: Be, vat: 21},
{name: "Bulgaria", code: Bg, vat: 20},
{name: "Croatia", code: Hr, vat: 25},
{name: "Cyprus", code: Cy, vat: 19},
{name: "Czechia", code: Cz, vat: 21},
{name: "Denmark", code: Dk, vat: 25},
{name: "Estonia", code: Ee, vat: 20},
{name: "Finland", code: Fi, vat: 24},
{name: "France", code: Fr, vat: 20},
{name: "Germany", code: De, vat: 19},
{name: "Greece", code: Gr, vat: 24},
{name: "Hungary", code: Hu, vat: 27},
{name: "Ireland", code: Ie, vat: 23},
{name: "Italy", code: It, vat: 22},
{name: "Latvia", code: Lv, vat: 21},
{name: "Lithuania", code: Lt, vat: 21},
{name: "Luxembourg", code: Lu, vat: 16},
{name: "Malta", code: Mt, vat: 18},
{name: "Netherlands", code: Nl, vat: 21},
{name: "Poland", code: Pl, vat: 23},
{name: "Portugal", code: Pt, vat: 23},
{name: "Romania", code: Ro, vat: 19},
{name: "Slovakia", code: Sk, vat: 20},
{name: "Slovenia", code: Si, vat: 22},
{name: "Spain", code: Es, vat: 21},
{name: "Sweden", code: Se, vat: 25},
]
let amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
const checkout = () => {
dispatch("checkout", {country: country.name, amount: credit_amount})
}
</script>
<div class="highlight_border">
{#if country === null}
<div>Please pick your country of residence</div>
<div class="countries">
{#each countries as c}
<button on:click={() => country = c}>
<svelte:component this={c.code} size="1.5em" />
<span>{c.name}</span>
</button>
{/each}
</div>
{:else}
<div style="display: flex;">
<button on:click={() => country = null} style="flex: 0 0 auto;">
<i class="icon">chevron_left</i>
Back
</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>
<svelte:component this={country.code} size="1.5em" />
<span>{country.name} ({country.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={() => credit_amount = a} style="font-size: 1.1em;" class:button_highlight={credit_amount === a}>
<div style="padding: 6px;">{a}</div>
</button>
{/each}
<div class="span3 mollie_checkout">
<div>Custom amount €</div>
<input type="number" bind:value={credit_amount} min="10"/>
</div>
<div class="span2" style="text-align: initial;">
Total including VAT:
<Euro amount={credit_micro + vat_micro}/>
</div>
<button type="submit" class="button_highlight">
<i class="icon">paid</i> Checkout
</button>
</form>
{/if}
</div>
<style>
.countries {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.countries > button {
display: flex;
flex-direction: row;
text-align: left;
}
.countries > button > span {
margin-left: 0.5em;
}
.amount_grid {
max-width: 500px;
gap: 4px;
display: inline-grid;
align-items: center;
grid-template-columns: 1fr 1fr 1fr;
}
.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;
}
</style>

View File

@@ -1,4 +1,5 @@
<script> <script>
import { onMount } from "svelte";
import Euro from "../util/Euro.svelte" import Euro from "../util/Euro.svelte"
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import SuccessMessage from "../util/SuccessMessage.svelte"; import SuccessMessage from "../util/SuccessMessage.svelte";
@@ -30,21 +31,38 @@ const update = async () => {
loading = false loading = false
} }
} }
let checkout_success = false
onMount(() => {
if (window.location.hash === "#checkout_complete") {
checkout_success = true
window.location.hash = ""
}
})
</script> </script>
<LoadingIndicator loading={loading}/> <LoadingIndicator loading={loading}/>
<section> <section>
{#if checkout_success}
<div class="highlight_green">
<h2>Payment successful!</h2>
<p>
The credit has been added to your account balance. Activate a
subscription plan below to finish upgrading your account.
</p>
</div>
{/if}
<h2>Manage subscription</h2> <h2>Manage subscription</h2>
<p> <p>
Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro> Current account balance: <Euro amount={window.user.balance_micro_eur}></Euro>
</p> </p>
<p> <p>
When your prepaid subscription is active you will be charged daily based Prepaid subscriptions are charged daily based on usage. When you reach
on usage. The prepaid subscription will stay active for as long as you negative balance the subscription will automatically end. You will need
have credit on your account. When you reach negative balance the a positive balance to activate the subscription again.
subscription will automatically end. You will need a positive balance to
activate the subscription again.
</p> </p>
<h3>Prepaid plans</h3> <h3>Prepaid plans</h3>
@@ -65,16 +83,28 @@ const update = async () => {
</div> </div>
<div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}> <div class="feat_normal round_tr" class:feat_highlight={subscription === "prepaid"}>
<ul> <ul>
<li>Base price of €1 per month</li> <li>Base price of €2 per month</li>
<li>€4 per TB per month for storage</li> <li>€4 per TB per month for storage</li>
<li>€2 per TB for data transfer</li> <li>
€2 per TB for data transfer (with <a
href="user/sharing/bandwidth">bandwidth sharing</a>
enabled)
</li>
<li>Files never expire as long as subscription is active</li> <li>Files never expire as long as subscription is active</li>
<li>
Download page <a href="/user/sharing/branding">branding
options</a>
</li>
<li>
File <a href="/user/sharing/embedding">embedding
control</a> options
</li>
</ul> </ul>
</div> </div>
</div> </div>
<div> <div>
<div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}> <div class="feat_label" class:feat_highlight={subscription === "prepaid_temp_storage_120d"}>
120 days storage<br/> 240 days storage<br/>
{#if subscription === "prepaid_temp_storage_120d"} {#if subscription === "prepaid_temp_storage_120d"}
Currently active Currently active
{:else} {:else}
@@ -88,8 +118,20 @@ const update = async () => {
<ul> <ul>
<li>Base price of €1 per month</li> <li>Base price of €1 per month</li>
<li>€0.50 per TB per month for storage</li> <li>€0.50 per TB per month for storage</li>
<li>€2 per TB for data transfer</li> <li>
<li>Files expire 120 days after the last time they're viewed</li> €2 per TB for data transfer (with <a
href="user/sharing/bandwidth">bandwidth sharing</a>
enabled)
</li>
<li>Files never expire as long as subscription is active</li>
<li>
Download page <a href="/user/sharing/branding">branding
options</a>
</li>
<li>
File <a href="/user/sharing/embedding">embedding
control</a> options
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -108,6 +150,7 @@ const update = async () => {
<div class="feat_normal round_br" class:feat_highlight={subscription === ""}> <div class="feat_normal round_br" class:feat_highlight={subscription === ""}>
<ul> <ul>
<li>Standard free plan, files expire after 60 days.</li> <li>Standard free plan, files expire after 60 days.</li>
<li>Download limit of 10 GB per day</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -147,32 +147,34 @@ onMount(() => {
<td>Time</td> <td>Time</td>
<td>Balance</td> <td>Balance</td>
<td>Subscription</td> <td>Subscription</td>
<td colspan="2">Storage</td> <td>Storage</td>
<td colspan="2">Bandwidth</td> <td>Bandwidth</td>
<td>Deposit</td> <td>Deposit</td>
</tr> </tr>
<tr>
<td></td>
<td></td>
<td>Charge</td>
<td>Charge</td>
<td>Usage</td>
<td>Charge</td>
<td>Usage</td>
<td></td>
</tr>
</thead> </thead>
<tbody> <tbody>
{#each transactions.rows as row} {#each transactions.rows as row}
<tr> <tr>
<td>{formatDate(row.time, true, true, false)}</td> <td>
<td><Euro amount={row.new_balance}></Euro></td> {formatDate(row.time, true, true, false)}
<td><Euro amount={row.subscription_charge} precision="6"></Euro></td> </td>
<td><Euro amount={row.storage_charge} precision="6"></Euro></td> <td>
<td>{formatDataVolume(row.storage_used, 3)}</td> <Euro amount={row.new_balance}></Euro>
<td><Euro amount={row.bandwidth_charge} precision="6"></Euro></td> </td>
<td>{formatDataVolume(row.bandwidth_used, 3)}</td> <td>
<td><Euro amount={row.deposit_amount}></Euro></td> <Euro amount={row.subscription_charge} precision="6"/>
</td>
<td>
<Euro amount={row.storage_charge} precision="6"/>
({formatDataVolume(row.storage_used, 3)})
</td>
<td>
<Euro amount={row.bandwidth_charge} precision="6"/>
({formatDataVolume(row.bandwidth_used, 3)})
</td>
<td>
<Euro amount={row.deposit_amount}/>
</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>