You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
7.3 KiB

<script>
import { link } from 'svelte-spa-router';
import { fade } from 'svelte/transition';
import { onMount } from "svelte";
import api from "$/client/api.js";
import FormField from "$/client/components/FormField.svelte";
import Icon from "$/client/components/Icon.svelte";
import IconImage from "$/client/components/IconImage.svelte";
import Paypal from "$/client/components/Paypal.svelte";
import BTCPay from "$/client/components/BTCPay.svelte";
import Layout from "$/client/Layout.svelte";
import { fake_payments } from "$/client/config.js";
import { log } from "$/client/logging.js";
const quips = {
"0": "Awww, really? Alright then.",
"10": "That's a good start.",
"20": "Even more fair.",
"30": "Sweet!",
"40": "Yes! Love it!",
"50": "Booyah!",
"60": "Really?! Thank you!",
"70": "Whoa! No way!",
"80": "Fantastic! THANK YOU!",
"90": "NO WAY! YES!",
"100": "I love you!"
}
const form = {
amount: 10,
_errors: {},
_valid: true
}
// BUG: we have to duplicate this logic here because of how paypal works
$: form._valid = form.amount >= 0 && form.amount <= 100;
const amount_by = 10;
let paid_in_full = false;
let payment_failed = false;
const change_amount = () => {
form.amount = form.amount < 100 ? form.amount + amount_by : 0;
}
const payment_finished = (event) => {
const { system, status } = event.detail;
log.assert(status === "complete", "Payment status should be complete with finished event", system, event.detail);
paid_in_full = true;
}
const payment_canceled = (event) => {
log.debug("CANCELED", event);
}
const payment_error = () => {
payment_failed = true;
}
const payment_loading = (event) => {
log.debug("LOADING", event);
}
const pay_nothing = async () => {
// posting to /api/user/payments is how you do a free purchase
let [status, data] = await api.post("/api/user/payments", { amount: form.amount }, api.logout_user);
if(status === 200) {
paid_in_full = true;
} else {
log.error("status", status, "data", data);
payment_failed = true;
form._errors.main = data.message || data.error || "Payment Error.";
}
}
// this is a demo of how to check a user's purchase quickly
const already_paid = async () => {
let [status, data] = await api.get("/api/user/payments");
if(status === 200 && data.paid === true) {
paid_in_full = true;
} else {
log.debug("GET to /api/user/payments returned", status, data);
}
}
onMount(async () => {
await already_paid();
});
</script>
<style>
form#purchase {
width: min-content;
}
label.slider {
cursor: pointer;
display: flex;
height: 4rem;
width: 100%;
background-color: var(--color-bg-secondary);
user-select: none;
-webkit-user-select: none;
}
label.slider::after {
display: flex;
justify-content: center;
align-items: center;
width: calc(1% * var(--amount));
min-width: 2ch;
background-color: var(--color-bg-tertiary);
height: 4rem;
counter-reset: amount var(--amount);
content: '$' counter(amount);
transition: 0.5s;
}
input#amount {
display: none;
}
card {
width: 400px;
border-radius: var(--border-radius) var(--border-radius) 0px 0px;
}
card#failed top {
background-color: var(--color-bg-secondary);
text-align: center;
padding: 1rem;
}
payments {
display: flex;
flex-direction: column;
}
payments.disabled {
filter: blur(5px);
}
form card bottom {
padding: 0.5rem;
}
callout {
border-radius: 0px 0px var(--border-radius) var(--border-radius);
}
</style>
<Layout centered={ true } authenticated={ true }>
{#if paid_in_full}
<card in:fade|local id="paid">
<top>
<IconImage name="dollar-sign" />
</top>
<middle>
<h1>Welcome!</h1>
<p>Thank you for your purchase. You can now enjoy the entire
site.
</p>
</middle>
<bottom>
<button-group>
<button data-testid="payment-done-button"><a href="/" use:link>Start Browsing</a></button>
</button-group>
</bottom>
</card>
{:else}
{#if payment_failed}
<card in:fade|local id="failed">
<top><h1 data-testid="payment-error">Payment Error!</h1></top>
<middle>
<p style="font-size: 1.5em;">There was an error processing your payment. Please try again later.</p>
<p>You can email <a href="mailto:help@xor.academy">help@xor.academy</a> to get help with this purchase.</p>
</middle>
<bottom>
<button-group>
<button type="button"><a href="/" use:link><Icon name="arrow-left-circle" size="36" /> Cancel</a></button>
<button data-testid="payment-tryagain" on:click={ () => payment_failed = false }><Icon name="credit-card" size="36" light={ true } /> Try Again</button>
</button-group>
</bottom>
</card>
{:else}
<form method="POST" id="purchase">
<card>
<top>
{#if payment_failed }
<h1>Payment Error!</h1>
{:else}
<h1>What's a Fair Price?</h1>
{/if}
</top>
<middle>
<FormField form={ form } field="amount" label="{ quips[form.amount] || `Bad Value! ${form.amount}` }">
<label data-testid="payment-amount" class="slider"
for="amount" style="--amount: { form.amount };"
on:click={ () => change_amount() }>
</label>
<input type="numeric" id="amount" bind:value={ form.amount } />
</FormField>
{#if form._valid}
<p>Click on the slider to change <b>how much you think this course is worth</b>, then
<b>select your payment method</b>.</p>
{:else}
<p><b>You've caused an error in this form. Please email help@xor.academy and tell them how you did this.</b></p>
{/if}
</middle>
<bottom>
<payments class:disabled={ !form._valid }>
{#if form.amount > 0}
<Paypal credit_card={ true } amount={ form.amount }
on:error={ payment_error}
on:finished={ payment_finished }
on:canceled={ payment_canceled }
on:loading={ payment_loading }
disabled = { !form._valid }/>
<BTCPay amount={ form.amount }
on:error={ payment_error}
on:finished={ payment_finished }
on:canceled={ payment_canceled }
on:loading={ payment_loading }
disabled = { !form._valid } />
{:else}
<button type="button" data-testid="button-paynothing" on:click|preventDefault={ pay_nothing }>Pay Nothing</button>
{/if}
</payments>
</bottom>
</card>
{#if fake_payments}
<callout class="warning">
Payments are fake right now. Use a fake CC from Paypal,
and use testnet coins for BTC/LTC.
</callout>
{/if}
</form>
{/if}
{/if}
</Layout>