This is the code that runs https://bandolier.learnjsthehardway.com/ for you to review. It uses the https://git.learnjsthehardway.com/learn-javascript-the-hard-way/bandolier-template to create the documentation for the project.
https://bandolier.learnjsthehardway.com/
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.
412 lines
12 KiB
412 lines
12 KiB
2 years ago
|
<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>
|