Split checkout widget into separate components
This commit is contained in:
@@ -12,6 +12,7 @@ type Invoice = {
|
||||
vat: number
|
||||
processing_fee: number
|
||||
country: string
|
||||
payment_gateway: string
|
||||
payment_method: string
|
||||
status: string
|
||||
}
|
||||
@@ -197,6 +198,7 @@ onMount(() => {
|
||||
<td>VAT</td>
|
||||
<td>Fee</td>
|
||||
<td>Country</td>
|
||||
<td>Gateway</td>
|
||||
<td>Method</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
@@ -210,6 +212,7 @@ onMount(() => {
|
||||
<td><Euro amount={row.vat}/></td>
|
||||
<td><Euro amount={row.processing_fee}/></td>
|
||||
<td>{row.country}</td>
|
||||
<td>{row.payment_gateway}</td>
|
||||
<td>{row.payment_method}</td>
|
||||
<td>{row.status}</td>
|
||||
</tr>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import CreditDeposit from "layout/CreditDeposit.svelte";
|
||||
import Checkout from "layout/checkout/Checkout.svelte";
|
||||
import LoginRegister from "login/LoginRegister.svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
</script>
|
||||
@@ -19,13 +19,13 @@ import Euro from "util/Euro.svelte";
|
||||
balance is <Euro amount={window.user.balance_micro_eur}/>. Use
|
||||
the form below to top up your balance.
|
||||
</p>
|
||||
<CreditDeposit/>
|
||||
<Checkout/>
|
||||
{:else}
|
||||
<p>
|
||||
You are currently logged in as '{window.user.username}'. Use the
|
||||
form below to activate prepaid on this account.
|
||||
</p>
|
||||
<CreditDeposit/>
|
||||
<Checkout/>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
@@ -1,334 +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";
|
||||
import CreditDepositNav from "./CreditDepositNav.svelte";
|
||||
|
||||
let loading = false
|
||||
let amount = 20
|
||||
let country: typeof countries.all[0] = null
|
||||
let country_input = ""
|
||||
let provider: PaymentProvider = null
|
||||
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 checkout_provider = window.localStorage.getItem("checkout_provider")
|
||||
for (const p of providers) {
|
||||
if (p.name === checkout_provider) {
|
||||
set_provider(p)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
type PaymentProvider = {
|
||||
icon: string,
|
||||
name: string,
|
||||
label: string,
|
||||
crypto?: boolean,
|
||||
};
|
||||
const providers: PaymentProvider[] = [
|
||||
{icon: "paypal_full", name: "paypal", label: "PayPal"},
|
||||
{icon: "mollie", name: "mollie", label: "Mollie"},
|
||||
{icon: "bitcoin", name: "btc", label: "Bitcoin", crypto: true},
|
||||
{icon: "dogecoin", name: "doge", label: "Dogecoin", crypto: true},
|
||||
{icon: "monero", name: "xmr", label: "Monero", crypto: true},
|
||||
]
|
||||
|
||||
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 set_provider = (p: PaymentProvider) => {
|
||||
provider = p
|
||||
|
||||
// Cache the value for next checkout
|
||||
window.localStorage.setItem("checkout_provider", p.name)
|
||||
}
|
||||
|
||||
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", provider.name)
|
||||
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 if provider === null}
|
||||
<CreditDepositNav bind:country={country} bind:provider={provider} bind:vat={vat}/>
|
||||
|
||||
<h2>Please select a payment provider</h2>
|
||||
|
||||
<div class="providers">
|
||||
{#each providers as p (p.name)}
|
||||
<button on:click={() => set_provider(p)}>
|
||||
<img src="/res/img/payment_providers/{p.icon}.svg" alt={p.label} title={p.label}/>
|
||||
{p.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<CreditDepositNav bind:country={country} bind:provider={provider} bind:vat={vat}/>
|
||||
|
||||
{#if provider.crypto === true}
|
||||
<p style="text-align: initial;" class="highlight_blue">
|
||||
When paying with cryptocurrencies it is important that you pay the
|
||||
<b>exact amount</b> stated on the order. If you pay too little, the
|
||||
order fails. If you pay too much then the remaining credit will not
|
||||
be added to your account. Pay close attention when sending a payment
|
||||
from an online exchange, sometimes they will subtract the fees from
|
||||
the amount sent which will cause the payment to fail.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
.providers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
|
||||
}
|
||||
.providers > button {
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.providers > button > img {
|
||||
max-width: 3em;
|
||||
max-height: 3em;
|
||||
}
|
||||
|
||||
.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>
|
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let country: {name?: string, emoji?: string} = null
|
||||
export let provider: {label: string} = null
|
||||
export let vat = 0
|
||||
</script>
|
||||
|
||||
<div style="display: flex;">
|
||||
{#if provider !== null}
|
||||
<button on:click={() => provider = null} style="flex: 0 0 auto;">
|
||||
<i class="icon">chevron_left</i>
|
||||
Change provider
|
||||
</button>
|
||||
{:else if country !== null}
|
||||
<button on:click={() => country = null} style="flex: 0 0 auto;">
|
||||
<i class="icon">chevron_left</i>
|
||||
Change country
|
||||
</button>
|
||||
{/if}
|
||||
<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)
|
||||
{#if provider !== null}with {provider.label}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
100
svelte/src/layout/checkout/Checkout.svelte
Normal file
100
svelte/src/layout/checkout/Checkout.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import LoadingIndicator from "util/LoadingIndicator.svelte";
|
||||
import PickAmount from "./PickAmount.svelte";
|
||||
import PickCountry from "./PickCountry.svelte";
|
||||
import { payment_providers, type CheckoutState } from "./CheckoutLib";
|
||||
import PickName from "./PickName.svelte";
|
||||
import { get_misc_vat_rate, get_user } from "lib/PixeldrainAPI";
|
||||
import { countries } from "country-data-list";
|
||||
import PickProvider from "./PickProvider.svelte";
|
||||
|
||||
let loading = false
|
||||
let state: CheckoutState = {country: null, provider: null, amount: 0, vat: 0, name: ""}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const user = await get_user()
|
||||
|
||||
// Get the country from the user profile
|
||||
if (user.checkout_country !== "" && countries[user.checkout_country] !== undefined) {
|
||||
const vat_rate = await get_misc_vat_rate(user.checkout_country)
|
||||
state.vat = vat_rate.vat*100
|
||||
state.country = countries[user.checkout_country]
|
||||
}
|
||||
|
||||
// Get the provider from the user profile
|
||||
for (const p of payment_providers) {
|
||||
if (p.name === user.checkout_provider) {
|
||||
state.provider = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get the checkout name from the user profile
|
||||
if (user.checkout_name !== "") {
|
||||
state.name = user.checkout_name
|
||||
}
|
||||
}catch (err) {
|
||||
alert("Failed to get user:"+err)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="highlight_border">
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
{#if state.country !== null}
|
||||
<div class="navbar">
|
||||
{#if state.country !== null}
|
||||
<button on:click={() => state.country = null}>
|
||||
Country:
|
||||
<span style="font-size: 1.5em; line-height: 1em;">{state.country.emoji}</span>
|
||||
{state.country.name}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if state.provider !== null}
|
||||
<button on:click={() => state.provider = null}>
|
||||
Provider:
|
||||
<img
|
||||
class="provider_icon"
|
||||
src="/res/img/payment_providers/{state.provider.icon}.svg"
|
||||
alt={state.provider.label}
|
||||
title={state.provider.label}/>
|
||||
{state.provider.label}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if state.name !== ""}
|
||||
<button on:click={() => state.name = ""}>
|
||||
Name: {state.name}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<hr/>
|
||||
{/if}
|
||||
|
||||
{#if state.country === null}
|
||||
<PickCountry bind:state bind:loading={loading}/>
|
||||
{:else if state.provider === null}
|
||||
<PickProvider bind:state/>
|
||||
{:else if state.provider.need_name === true && state.name === ""}
|
||||
<PickName bind:state/>
|
||||
{:else}
|
||||
<PickAmount bind:state bind:loading/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.provider_icon {
|
||||
height: 1.5em;
|
||||
}
|
||||
</style>
|
106
svelte/src/layout/checkout/CheckoutLib.ts
Normal file
106
svelte/src/layout/checkout/CheckoutLib.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { countries } from "country-data-list";
|
||||
import { get_endpoint } from "lib/PixeldrainAPI";
|
||||
|
||||
export type CheckoutState = {
|
||||
country: typeof countries.all[0]
|
||||
provider: PaymentProvider
|
||||
amount: number
|
||||
vat: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export type PaymentProvider = {
|
||||
icon: string
|
||||
name: string
|
||||
label: string
|
||||
crypto?: boolean
|
||||
need_name?: boolean
|
||||
country_filter?: string[]
|
||||
};
|
||||
|
||||
export const payment_providers: PaymentProvider[] = [
|
||||
{
|
||||
icon: "paypal",
|
||||
name: "paypal",
|
||||
label: "PayPal",
|
||||
}, {
|
||||
icon: "bancontact",
|
||||
name: "bancontact",
|
||||
label: "Bancontact",
|
||||
need_name: true,
|
||||
country_filter: ["BE"],
|
||||
}, {
|
||||
icon: "eps",
|
||||
name: "eps",
|
||||
label: "EPS",
|
||||
need_name: true,
|
||||
country_filter: ["AT"],
|
||||
}, {
|
||||
icon: "ideal",
|
||||
name: "ideal",
|
||||
label: "iDEAL",
|
||||
need_name: true,
|
||||
country_filter: ["NL"],
|
||||
}, {
|
||||
icon: "p24",
|
||||
name: "p24",
|
||||
label: "Przelewy24",
|
||||
need_name: true,
|
||||
country_filter: ["PL"],
|
||||
}, {
|
||||
// icon: "trustly",
|
||||
// name: "trustly",
|
||||
// label: "Trustly",
|
||||
// need_name: true,
|
||||
// country_filter: ["AT", "DE", "DK", "EE", "ES", "FI", "GB", "LT", "LV", "NL", "NO", "SE"]
|
||||
// }, {
|
||||
icon: "bitcoin",
|
||||
name: "btc",
|
||||
label: "Bitcoin",
|
||||
crypto: true,
|
||||
}, {
|
||||
icon: "dogecoin",
|
||||
name: "doge",
|
||||
label: "Dogecoin",
|
||||
crypto: true,
|
||||
}, {
|
||||
icon: "monero",
|
||||
name: "xmr",
|
||||
label: "Monero",
|
||||
crypto: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const checkout = async (state: CheckoutState) => {
|
||||
if (state.amount < 10) {
|
||||
alert("Amount needs to be at least €10")
|
||||
return
|
||||
} else if (state.provider.need_name && !state.name) {
|
||||
alert("Name is required for this provider")
|
||||
return
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.set("amount", String(state.amount * 1e6))
|
||||
form.set("network", state.provider.name)
|
||||
form.set("country", state.country.alpha2)
|
||||
|
||||
if (state.provider.need_name) {
|
||||
form.set("name", state.name)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
97
svelte/src/layout/checkout/PickAmount.svelte
Normal file
97
svelte/src/layout/checkout/PickAmount.svelte
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { checkout, type CheckoutState } from "./CheckoutLib";
|
||||
|
||||
export let state: CheckoutState
|
||||
export let loading: boolean
|
||||
|
||||
const amounts = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]
|
||||
let amount = 20
|
||||
|
||||
const submit = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
loading = true
|
||||
state.amount = amount
|
||||
await checkout(state)
|
||||
loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if state.provider.crypto === true}
|
||||
<p style="text-align: initial;" class="highlight_blue">
|
||||
When paying with cryptocurrencies it is important that you pay
|
||||
the <b>exact amount</b> stated on the order. If you pay too
|
||||
little, the order fails. If you pay too much then the remaining
|
||||
credit will not be added to your account. Pay close attention
|
||||
when sending a payment from an online exchange, sometimes they
|
||||
will subtract the fees from the amount sent which will cause the
|
||||
payment to fail.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<form class="amount_grid" on:submit={submit}>
|
||||
<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 custom_amount">
|
||||
<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)*(state.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>
|
||||
|
||||
<style>
|
||||
.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; }
|
||||
|
||||
.custom_amount {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.custom_amount > div {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
.custom_amount > input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
122
svelte/src/layout/checkout/PickCountry.svelte
Normal file
122
svelte/src/layout/checkout/PickCountry.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { countries } from "country-data-list";
|
||||
import { get_misc_vat_rate, put_user } from "lib/PixeldrainAPI";
|
||||
import { payment_providers, type CheckoutState } from "./CheckoutLib";
|
||||
|
||||
export let state: CheckoutState
|
||||
export let loading: boolean
|
||||
let country_input = ""
|
||||
|
||||
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]
|
||||
|
||||
// We always clear the provider field after picking a country, as providers
|
||||
// vary per country. If we did not clear the provider then we may end up
|
||||
// with an invalid combination
|
||||
state.provider = null
|
||||
|
||||
try {
|
||||
// Save the user's country for later use
|
||||
await put_user({checkout_country: c.alpha3})
|
||||
|
||||
const vat_rate = await get_misc_vat_rate(c.alpha3)
|
||||
state.vat = vat_rate.vat*100
|
||||
state.country = c
|
||||
} 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>
|
||||
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.label} title={p.label}/>
|
||||
{p.label}
|
||||
</div>
|
||||
{/each}
|
||||
</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;
|
||||
}
|
||||
.bankicon {
|
||||
width: 2em;
|
||||
}
|
||||
</style>
|
50
svelte/src/layout/checkout/PickName.svelte
Normal file
50
svelte/src/layout/checkout/PickName.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { put_user } from "lib/PixeldrainAPI";
|
||||
import { type CheckoutState } from "./CheckoutLib";
|
||||
|
||||
export let state: CheckoutState
|
||||
let name: string
|
||||
|
||||
const submit = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
await put_user({checkout_name: name})
|
||||
} catch(err) {
|
||||
alert("Failed to update user:"+ err)
|
||||
}
|
||||
|
||||
state.name = name
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="name_form" on:submit={submit}>
|
||||
<div>
|
||||
This payment provider requires a name for checkout. Please enter your
|
||||
full name below
|
||||
</div>
|
||||
<input bind:value={name} type="text" autocomplete="name"/>
|
||||
|
||||
<button type="submit" class="button_highlight" style="align-self: end;">
|
||||
Continue
|
||||
<i class="icon">chevron_right</i>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
.name_form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
width: 500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
44
svelte/src/layout/checkout/PickProvider.svelte
Normal file
44
svelte/src/layout/checkout/PickProvider.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { put_user } from "lib/PixeldrainAPI";
|
||||
import { payment_providers, type CheckoutState, type PaymentProvider } from "./CheckoutLib";
|
||||
|
||||
export let state: CheckoutState
|
||||
|
||||
const set_provider = async (p: PaymentProvider) => {
|
||||
try {
|
||||
await put_user({checkout_provider: p.name})
|
||||
} catch(err) {
|
||||
alert("Failed to update user:"+ err)
|
||||
}
|
||||
|
||||
state.provider = p
|
||||
}
|
||||
</script>
|
||||
|
||||
<span>Please select a payment provider</span>
|
||||
|
||||
<div class="providers">
|
||||
{#each payment_providers as p (p.name)}
|
||||
{#if p.country_filter === undefined || p.country_filter.includes(state.country.alpha2)}
|
||||
<button on:click={() => set_provider(p)}>
|
||||
<img src="/res/img/payment_providers/{p.icon}.svg" alt={p.label} title={p.label}/>
|
||||
{p.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.providers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));
|
||||
}
|
||||
.providers > button {
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.providers > button > img {
|
||||
max-width: 3em;
|
||||
max-height: 3em;
|
||||
}
|
||||
</style>
|
@@ -24,6 +24,9 @@ export type User = {
|
||||
file_embed_domains: string,
|
||||
skip_file_viewer: boolean,
|
||||
affiliate_user_name: string,
|
||||
checkout_country: string,
|
||||
checkout_name: string,
|
||||
checkout_provider: string,
|
||||
}
|
||||
|
||||
export type Subscription = {
|
||||
|
@@ -4,6 +4,7 @@ import CopyButton from "layout/CopyButton.svelte";
|
||||
import Form from "util/Form.svelte";
|
||||
import Button from "layout/Button.svelte";
|
||||
import OtpSetup from "./OTPSetup.svelte";
|
||||
import { put_user } from "lib/PixeldrainAPI";
|
||||
|
||||
let affiliate_link = window.location.protocol+"//"+window.location.host + "?ref=" + encodeURIComponent(window.user.username)
|
||||
let affiliate_deny = false
|
||||
@@ -52,6 +53,22 @@ let account_settings = {
|
||||
log in. If you forget your username you can still log in using
|
||||
your e-mail address if you have one configured`,
|
||||
separator: true,
|
||||
}, {
|
||||
name: "checkout_country",
|
||||
label: "Country code used at checkout",
|
||||
type: "text",
|
||||
default_value: window.user.checkout_country,
|
||||
}, {
|
||||
name: "checkout_provider",
|
||||
label: "Default payment provider used at checkout",
|
||||
type: "text",
|
||||
default_value: window.user.checkout_provider,
|
||||
}, {
|
||||
name: "checkout_name",
|
||||
label: "Full name used at checkout",
|
||||
type: "text",
|
||||
default_value: window.user.checkout_name,
|
||||
separator: true,
|
||||
},
|
||||
],
|
||||
submit_label: `<i class="icon">save</i> Save`,
|
||||
@@ -67,11 +84,23 @@ let account_settings = {
|
||||
form.append("email", fields.email)
|
||||
form.append("password_new", fields.password_new1)
|
||||
form.append("username", fields.username)
|
||||
form.append("checkout_country", fields.checkout_country)
|
||||
form.append("checkout_provider", fields.checkout_provider)
|
||||
form.append("checkout_name", fields.checkout_name)
|
||||
|
||||
const resp = await fetch(window.api_endpoint+"/user", { method: "PUT", body: form });
|
||||
if(resp.status >= 400) {
|
||||
return {error_json: await resp.json()}
|
||||
try {
|
||||
await put_user({
|
||||
email: fields.email,
|
||||
password_new: fields.password_new1,
|
||||
username: fields.username,
|
||||
checkout_country: fields.checkout_country,
|
||||
checkout_provider: fields.checkout_provider,
|
||||
checkout_name: fields.checkout_name,
|
||||
})
|
||||
} catch (err) {
|
||||
return {error_json: err}
|
||||
}
|
||||
|
||||
return {success: true, message: "Success! Your changes have been saved"}
|
||||
},
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import CreditDeposit from "layout/CreditDeposit.svelte";
|
||||
import Checkout from "layout/checkout/Checkout.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import Euro from "util/Euro.svelte";
|
||||
import { formatDate } from "util/Formatting";
|
||||
@@ -99,7 +99,7 @@ onMount(() => {
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<CreditDeposit/>
|
||||
<Checkout/>
|
||||
{/if}
|
||||
|
||||
<h3 id="invoices">Past invoices</h3>
|
||||
|
Reference in New Issue
Block a user