<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>