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.
bandolier-website/client/pages/Payment.svelte

412 lines
12 KiB

<script>
import Layout from "$/client/Layout.svelte";
import { push } from 'svelte-spa-router';
import Paypal from "$/client/components/Paypal.svelte";
import Stripe from "$/client/components/Stripe.svelte";
import FakePayment from "$/client/components/FakePayment.svelte";
import Spinner from "$/client/components/Spinner.svelte";
import Icon from "$/client/components/Icon.svelte";
import * as config from "$/client/config.js";
import { onMount } from "svelte";
import { defer } from "$/client/helpers.js";
import api from "$/client/api.js";
export let params = {status: false};
let product = {};
let loading = false;
let load_defer = defer();
let payment_clicked = false;
let paypal_active = true;
let stripe_active = false;
let discount = 0;
let payment;
let form = {
paid: false,
tos_agree: false,
_valid: false,
_errors: {}
}
const payment_finished = (system, data) => {
form.payment_system = system;
form.paid = true;
form.payment_id = data.internal_id;
form = form;
loading = false;
}
const payment_error = (system, data) => {
loading = false;
// when data has a payment it's from an invalidated payment
if(data.payment) {
if(data.payment.system === "stripe") {
payment = data.payment;
} else {
form._errors.main = "Your last payment failed.";
form._errors.failed_payment = data.payment;
}
} else if(data.instrument_declined) {
form._errors.main = "Instrument declined. Verify all fields are correct and contact your bank.";
form._errors.instrument_declined = true;
} else {
form._errors.main = "Payment error. Please try again.";
form._errors.payment = true;
}
form = form;
}
const payment_canceled = (system, data) => {
form._errors.payment_canceled = true;
form._errors.main = "Your payment was canceled.";
loading = false;
}
const load_payment = async () => {
const [status, data] = await api.get("/api/product", { product_id: config.product.id });
console.log("DATA", data, "STATUS", status);
if(status === 200) {
if(params.status === "finished") {
// this is a payment like Stripe that does a redirect
product = data;
payment_finished("stripe", {});
load_defer.resolve();
} else {
// this is most likely because they paid but came back,
// bounce to the root and hope it doesn't make a loop
push("/");
}
} else if(status === 402) {
// we EXPECT a 402 here, since it's the payment page
product = data.course;
if(params.status === "finished") {
// shouldn't get finished and also have a failed
// payment so reset here
await push("/payment/");
} else if(data.payment) {
// this means they paid but it was invalidated, or
// or this is a stripe payment and it needs to be
// redone again
payment_error(data.payment.system, data);
}
load_defer.resolve();
} else {
console.error(status, data);
}
}
const reset_payment = async () => {
// stupid but kind of the only way to pull this off
product = {};
loading = false;
payment_clicked = false;
discount = 0;
form = {
paid: false,
tos_agree: false,
_valid: false,
_errors: {}
}
load_defer = defer();
await load_payment();
}
const make_choice = (stripe, paypal) => {
payment_clicked = true;
stripe_active = stripe;
paypal_active = paypal;
}
onMount(load_payment);
</script>
<style>
card#paypal-warning {
max-width: var(--width-card);
min-width: var(--width-card);
}
card#paypal-warning middle {
height: 100%;
}
card#paypal-warning top {
text-align: center;
}
invoice-totals > table {
border-radius: 0;
border: 0px;
border-top: 1px solid var(--value7);
width: 100%;
}
invoice-totals > table tr {
background-color: unset;
}
invoice-totals > table tr th {
text-align: left;
font-weight: normal;
}
invoice-totals > table tr td {
text-align: right;
}
invoice-totals > table tr#total {
border-top: 1px solid var(--value7);
}
invoice-totals > table tr#total th {
font-weight: bold;
font-size: 1.1em;
}
card.error top {
background-color: var(--color-info);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
}
card.error middle {
font-size: 1.5em;
padding-top: 1rem;
padding-bottom: 1rem;
}
invoice-item {
padding-top: 1.3rem;
}
invoice-discount {
display: flex;
}
payment {
display: flex;
flex-direction: row;
justify-content: flex-start;
min-height: calc(100vh - var(--fixed-header-height) - var(--fixed-footer-height));
width: 100%;
}
payment > left,
payment > right {
width: 50%;
display: flex;
flex-direction: column;
padding: 1rem;
padding-top: 1.3rem;
min-height: min-content;
}
payment > right {
background-color: var(--color-bg-tertiary);
border-left: 1px solid var(--color-bg-secondary);
}
payment > left {
justify-content: flex-start;
flex-direction: row-reverse;
}
payment > right invoice {
max-width: calc(var(--width-content) / 2);
}
payment > left info {
display: flex;
flex-direction: column;
max-width: calc(var(--width-content) / 2);
}
payment > left info product-info {
display: block;
}
.pay-button {
width: 100%;
min-width: 100%;
max-width: 100%;
}
@media only screen and (max-width: 800px) {
payment {
width: 100%;
flex-direction: column-reverse;
justify-content: flex-end;
}
payment > right {
width: 100%;
align-content: center;
justify-content: center;
}
payment > right invoice {
align-self: center;
}
payment > left info {
display: flex;
align-self: center;
flex-direction: column-reverse;
}
payment > left {
width: 100%;
max-width: unset;
min-width: unset;
box-shadow: unset;
align-content: center;
justify-content: center;
}
}
</style>
<Layout horizontal={true} authenticated={true} fullwidth={ true }>
<payment>
<left>
<info>
{#if !payment_clicked && !form.paid}
<product-info>
<h1>Product Info Here</h1>
</product-info>
{/if}
{#await load_defer}
<Spinner />
{:then}
{#if form.paid}
<div>
<h1>Payment Accepted!</h1>
<p>Thank you for your purchase, and welcome to my
course! Be sure to join the Discord server when
you get a chance. Details are available once
you <b>click this button</b>:
</p>
<button type="button" data-testid="get-started"
on:click|preventDefault={ () => push("/") }>Get Started</button>
</div>
{:else if form._errors.main}
<div>
<card class="error">
<top data-testid="payment-error"><h1>Payment Error</h1></top>
<middle>
<b>{ form._errors.main }</b>
<br>
<span>
{#if form._errors.failed_payment}
There was an error validating your payment.
Please contact support to investigate why it failed and check your credit card or paypal to confirm the amount was correct and the payment actually finished.
{:else if form._errors.instrument_declined}
Instrument declined. Check that <b>all fields
are filled out correctly.</b> Paypal also
adds fields without labeling them so look
for empty fields like <b>Street Address</b>.
{:else if form._errors.payment}
Please try again and confirm that
the payment didn't actually process on your
side.
{:else if form._errors.payment_canceled}
Payment was canceled, either by you or by
the payment processor. Plase try again.
{/if}
You can also email <a href="mailto:help@learnjsthehardway.com">help@learnjsthehardway.com</a> to ask for help.
</span>
</middle>
<bottom>
<button-group>
<button data-testid="try-again" type="button" on:click|preventDefault={ reset_payment }>Try Again</button>
</button-group>
</bottom>
<card>
</div>
{:else}
<div>
{#if config.fake_payments}
<h3 class="center">Fake Payment Buttons</h3>
<FakePayment
on:finished={ (ev) => payment_finished("fake", ev.detail) }
on:error={ (ev) => payment_error("fake", ev.detail) }
on:canceled={ (ev) => payment_canceled("fake", ev.detail) }
/>
{:else}
{#if paypal_active}
<h3 class="center">Select your Payment Method</h3>
<Paypal
product={ product }
credit_card={ true }
on:click={ () => make_choice(false, true) }
on:loading={ () => loading = true }
on:finished={ (ev) => payment_finished("paypal", ev.detail) }
on:error={ (ev) => payment_error("paypal", ev.detail) }
on:canceled={ (ev) => payment_canceled("paypal", ev.detail) }
/>
{/if}
{#if stripe_active}
<Stripe
on:click={ () => make_choice(true, false) }
on:loading={ () => loading = true }
on:finished={ (ev) => payment_finished("stripe", ev.detail) }
on:error={ (ev) => payment_error("stripe", ev.detail) }
on:canceled={ (ev) => payment_canceled("stripe", ev.detail) }
/>
<br>
<a on:click|preventDefault={() => make_choice(false, true)}>
<button class="pay-button" type="button">
Use Paypal Instead.
</button>
</a>
{:else}
<br>
<a on:click|preventDefault={() => make_choice(true, false)}>
<button class="pay-button" type="button">Hate Paypal? Use Stripe Instead!</button>
</a>
{/if}
{/if}
</div>
{/if}
{/await}
</info>
</left>
<right>
<invoice>
<invoice-item class="horizontal">
<Icon name="book" /> <b>{ product.title }</b>
</invoice-item>
<invoice-totals>
<table>
<tr><th>{ product.description }</th><td>{product.currency_symbol}{ product.price }</td></tr>
{#if discount > 0}
<tr><th>Discount</th><td>{ discount }</td></tr>
{/if}
<tr><th>Subtotal</th><td>{product.currency_symbol}{ product.price }</td></tr>
<tr id="total"><th>Total ({product.currency})</th>
<td>{product.currency_symbol}{ product.price }</td>
</tr>
</table>
</invoice-totals>
<br><hr>
<p>Thank you for purchasing <b>Learn JavaScript the Hard Way.</b> If you have problems with the purchase please email me at <a href="mailto:payments@learnjsthehardway.com">payments@learnjsthehardway.com</a> and tell what went wrong.</p>
</invoice>
</right>
</payment>
</Layout>