diff --git a/src/lib/components/ErrorStack.svelte b/src/lib/components/ErrorStack.svelte new file mode 100644 index 0000000..adb9b5b --- /dev/null +++ b/src/lib/components/ErrorStack.svelte @@ -0,0 +1,105 @@ + + +{#if errors && errors?.length > 0} +
+ {#each errors as error, index (`${index}-${error}`)} +

+ + + {error} + Prøv igjen eller gi en lyd på kontakt@planetposen.no så tar vi kontakt med deg. +

+ {/each} +
+{/if} + + diff --git a/src/lib/components/StripeCard.svelte b/src/lib/components/StripeCard.svelte index 5290be3..5edf616 100644 --- a/src/lib/components/StripeCard.svelte +++ b/src/lib/components/StripeCard.svelte @@ -1,11 +1,15 @@ @@ -96,7 +48,6 @@ .card { // padding: 1rem; - margin: 0 0.5rem; border: 2px solid black; @include desktop { diff --git a/src/lib/interfaces/IOrderValidationError.ts b/src/lib/interfaces/IOrderValidationError.ts new file mode 100644 index 0000000..9cd620e --- /dev/null +++ b/src/lib/interfaces/IOrderValidationError.ts @@ -0,0 +1,5 @@ +export default interface IOrderValidationError { + type: string; + field: number | string; + message: string; +} diff --git a/src/lib/stripe/index.ts b/src/lib/stripe/index.ts new file mode 100644 index 0000000..77d4942 --- /dev/null +++ b/src/lib/stripe/index.ts @@ -0,0 +1,100 @@ +import { buildApiUrl } from '$lib/utils/apiUrl'; +import { loadStripe } from '@stripe/stripe-js/pure'; +import type { + ConfirmCardPaymentData, + PaymentIntentResult, + Stripe, + StripeError +} from '@stripe/stripe-js'; + +let stripeInstance: Stripe; + +class StripeUnableToAccepPaymentsError extends Error { + constructor() { + const message = 'Card failed to load or never got a client secret. Unable to accept payments'; + super(message); + } +} + +// class StripePaymentError extends Error { +// code: string | undefined +// type: string + +// constructor(error: StripeError) { +// const message = error.message || 'Unexptected error from stripe'; +// super(message); + +// console.log('stripe error:', error); +// this.code = error.code; +// this.type = error.type; +// } +// } + +class StripeMissingPaymentIntent extends Error { + constructor() { + const message = 'Stripe responded without payment intent'; + super(message); + } +} + +// Calls backend for a client secret. order_id & customer_no +// is sent to attach to stripe payment intent +function clientSecret(order_id: string, customer_no: string): Promise { + const url = buildApiUrl('/api/v1/payment/stripe'); + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ order_id, customer_no }) + }; + + return fetch(url, options) + .then((resp) => resp.json()) + .then((data) => data?.clientSecret); +} + +// Creates stripe instance from package: @stripe/stripe-js +async function load(apiKey: string): Promise { + if (stripeInstance) return stripeInstance; + + loadStripe.setLoadParameters({ advancedFraudSignals: false }); + const stripe: Stripe | null = await loadStripe(apiKey); + if (!stripe) { + console.error('Unable to load stripe payment'); + return; + } + + stripeInstance = stripe; + return stripe; +} + +// Uses @stripe/stripe-js to make payment request from +// stripe Elements directly to Stripe. Server receives +// async webhook updates from Stripe. +function pay(secret: string, data: ConfirmCardPaymentData) { + if (!clientSecret || !stripeInstance) { + throw new StripeUnableToAccepPaymentsError(); + } + + return stripeInstance + .confirmCardPayment(secret, data) + .then((stripePaymentResponse: PaymentIntentResult) => { + const { error, paymentIntent } = stripePaymentResponse; + + if (error) { + throw error; + } else if (paymentIntent) { + console.log('payment intent from stripe:', paymentIntent); + return paymentIntent.status === 'succeeded'; + } + + throw new StripeMissingPaymentIntent(); + }); +} + +export default { + clientSecret, + load, + pay +}; diff --git a/src/lib/utils/apiUrl.ts b/src/lib/utils/apiUrl.ts new file mode 100644 index 0000000..6783bfd --- /dev/null +++ b/src/lib/utils/apiUrl.ts @@ -0,0 +1,14 @@ +import { dev, browser } from '$app/environment'; + +const LOCALHOST_API = 'http://localhost:30010'; + +export function buildApiUrl(path: string) { + let localhostApi = false; + + if (dev) localhostApi = true; + if (browser && window?.location?.href.includes('localhost')) { + localhostApi = true; + } + + return localhostApi ? LOCALHOST_API.concat(path) : path; +} diff --git a/src/routes/checkout/+page.server.ts b/src/routes/checkout/+page.server.ts new file mode 100644 index 0000000..0fee320 --- /dev/null +++ b/src/routes/checkout/+page.server.ts @@ -0,0 +1,8 @@ +import { env } from '$env/dynamic/private'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = () => { + return { + stripeApiKey: env.STRIPE_API_KEY || '' + }; +}; diff --git a/src/routes/checkout/+page.svelte b/src/routes/checkout/+page.svelte index afdc8dd..918d689 100644 --- a/src/routes/checkout/+page.svelte +++ b/src/routes/checkout/+page.svelte @@ -1,36 +1,140 @@ @@ -39,8 +143,8 @@ description="Kasse for bestilling og betaling av produkter i handlekurven" /> -

Checkout

-
+

Kassen

+

Leveringsaddresse

@@ -48,31 +152,30 @@

Din ordre

- - - +

Betalingsinformasjon

- +
+
+ {#if paymentPromise} + + {/if} +
+ + diff --git a/src/routes/checkout/OrderSection.svelte b/src/routes/checkout/OrderSection.svelte index 33744ab..e61d68c 100644 --- a/src/routes/checkout/OrderSection.svelte +++ b/src/routes/checkout/OrderSection.svelte @@ -4,76 +4,78 @@ import { cart, subTotal } from '$lib/cartStore'; import { decrementProductInCart, incrementProductInCart } from '$lib/websocketCart'; - // $: totalPrice = $cart - // .map((order: IOrderLineItem) => order?.price * order.quantity) - // .reduce((a, b) => a + b); const shippingPrice = 75; + $: totalPrice = $subTotal + shippingPrice; + + function lineItemClass(id: number) { + return `lineitem-${id}`; + } - - - - - - - - - - - {#if $cart.length} - {#each $cart as cartItem} - - - - - - {/each} - {:else} - - - - +
+
VarenavnAntallPris
-
- {cartItem.name} - Størrelse: {cartItem.size} -
-
- - Nok {cartItem.quantity * cartItem.price}
(ingen produkter)0Nok 0
+ + + + + - {/if} + - - - - - + + {#if $cart.length} + {#each $cart as cartItem} + + + + + + {/each} + {:else} + + + + + + {/if} - - - - - + + + + + - - - - - - -
VarenavnAntallPris
Totalpris:Nok {$subTotal}
+
+ {cartItem.name} + Størrelse: {cartItem.size} +
+
+ + Nok {cartItem.quantity * cartItem.price}
(ingen produkter)0Nok 0
Frakt:Nok {shippingPrice}
Totalsum:Nok {$subTotal}
Totalsum:Nok {$subTotal}
+ + Frakt: + + Nok {shippingPrice} + + + + Pris: + + Nok {totalPrice} + + + + -