@laioutr/app-shopware 0.15.4 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +7 -2
- package/dist/runtime/app/components/ShopwareEmbedFrame.d.vue.ts +13 -3
- package/dist/runtime/app/components/ShopwareEmbedFrame.vue +48 -5
- package/dist/runtime/app/components/ShopwareEmbedFrame.vue.d.ts +13 -3
- package/dist/runtime/app/composables/useShopwareEmbedBridge.d.ts +3 -1
- package/dist/runtime/app/composables/useShopwareEmbedBridge.js +7 -3
- package/dist/runtime/app/const/bridge.d.ts +20 -0
- package/dist/runtime/app/const/bridge.js +7 -0
- package/dist/runtime/app/lib/createBridgeHandler.d.ts +3 -0
- package/dist/runtime/app/lib/createBridgeHandler.js +7 -1
- package/dist/runtime/app/lib/createOrderHandoffRefresher.d.ts +24 -0
- package/dist/runtime/app/lib/createOrderHandoffRefresher.js +21 -0
- package/dist/runtime/app/lib/parseBridgeMessage.js +2 -1
- package/dist/runtime/app/sections/SectionShopwareCheckout.vue +42 -4
- package/dist/runtime/server/const/checkout.d.ts +1 -1
- package/dist/runtime/server/const/checkout.js +10 -2
- package/dist/runtime/server/orchestr/product/base.resolver.js +18 -2
- package/dist/runtime/server/orchestr/product/variants.link.js +6 -4
- package/dist/runtime/server/orchestr/product-variant/base.resolver.js +9 -4
- package/dist/runtime/server/orchestr-helper/requestedFields.d.ts +1 -3
- package/dist/runtime/server/orchestr-helper/requestedFields.js +54 -42
- package/dist/runtime/server/routes/checkout.js +12 -1
- package/dist/runtime/server/routes/order-handoff.post.d.ts +14 -0
- package/dist/runtime/server/routes/order-handoff.post.js +33 -0
- package/dist/runtime/server/shopware-helper/fetchAllProductVariants.d.ts +2 -1
- package/dist/runtime/server/shopware-helper/fetchAllProductVariants.js +39 -11
- package/dist/runtime/server/shopware-helper/optionGroupsMapper.d.ts +23 -0
- package/dist/runtime/server/shopware-helper/optionGroupsMapper.js +37 -0
- package/dist/runtime/server/shopware-helper/resolveCheckout.d.ts +2 -0
- package/dist/runtime/server/shopware-helper/resolveCheckout.js +4 -3
- package/dist/runtime/server/shopware-helper/sessionHandoff.d.ts +6 -0
- package/dist/runtime/server/shopware-helper/sessionHandoff.js +6 -1
- package/dist/runtime/server/shopware-helper/wellKnownOptionName.d.ts +2 -0
- package/dist/runtime/server/shopware-helper/wellKnownOptionName.js +278 -0
- package/dist/runtime/shared/const/checkout.d.ts +33 -0
- package/dist/runtime/shared/const/checkout.js +5 -0
- package/package.json +3 -3
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addServerHandler, addPlugin, installModule } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
|
-
import { CHECKOUT_ENDPOINT_PATH, ADOPT_SESSION_ENDPOINT_PATH } from '../dist/runtime/server/const/checkout.js';
|
|
3
|
+
import { CHECKOUT_ENDPOINT_PATH, ADOPT_SESSION_ENDPOINT_PATH, ORDER_HANDOFF_ENDPOINT_PATH } from '../dist/runtime/server/const/checkout.js';
|
|
4
4
|
import { registerLaioutrApp } from '@laioutr-core/kit';
|
|
5
5
|
|
|
6
6
|
const name = "@laioutr/app-shopware";
|
|
7
|
-
const version = "0.
|
|
7
|
+
const version = "0.17.0";
|
|
8
8
|
|
|
9
9
|
const module$1 = defineNuxtModule({
|
|
10
10
|
meta: {
|
|
@@ -36,6 +36,11 @@ const module$1 = defineNuxtModule({
|
|
|
36
36
|
method: "post",
|
|
37
37
|
handler: resolveRuntimeModule("./server/routes/adopt-session.post")
|
|
38
38
|
});
|
|
39
|
+
addServerHandler({
|
|
40
|
+
route: ORDER_HANDOFF_ENDPOINT_PATH,
|
|
41
|
+
method: "post",
|
|
42
|
+
handler: resolveRuntimeModule("./server/routes/order-handoff.post")
|
|
43
|
+
});
|
|
39
44
|
await registerLaioutrApp({
|
|
40
45
|
name,
|
|
41
46
|
version,
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** Absolute URL of the page a completed order lands on. */
|
|
3
|
+
finishUrl?: string;
|
|
4
|
+
/** Absolute URL of this checkout page, used to re-frame a failed payment's retry. */
|
|
5
|
+
checkoutUrl?: string;
|
|
6
|
+
/** Order whose payment failed, re-framed on the storefront's order-edit page. */
|
|
7
|
+
retryOrderId?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
10
|
+
refreshHandoff: () => void;
|
|
11
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
12
|
"checkout-finish": (orderId: string) => any;
|
|
3
13
|
"auth-changed": (payload: {
|
|
4
14
|
from: string;
|
|
5
15
|
code?: string;
|
|
6
16
|
}) => any;
|
|
7
|
-
}, string, import("vue").PublicProps, Readonly<
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
18
|
"onCheckout-finish"?: ((orderId: string) => any) | undefined;
|
|
9
19
|
"onAuth-changed"?: ((payload: {
|
|
10
20
|
from: string;
|
|
11
21
|
code?: string;
|
|
12
22
|
}) => any) | undefined;
|
|
13
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions,
|
|
23
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
24
|
export default _default;
|
|
@@ -1,21 +1,56 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref, useRuntimeConfig } from "#imports";
|
|
3
|
-
import {
|
|
2
|
+
import { computed, onBeforeUnmount, ref, useRuntimeConfig } from "#imports";
|
|
3
|
+
import {
|
|
4
|
+
CHECKOUT_CONFIRM_ROUTE,
|
|
5
|
+
CHECKOUT_ENDPOINT_PATH,
|
|
6
|
+
CHECKOUT_RETRY_ROUTE,
|
|
7
|
+
ORDER_HANDOFF_ENDPOINT_PATH,
|
|
8
|
+
RETRY_ORDER_QUERY_KEY
|
|
9
|
+
} from "../../shared/const/checkout";
|
|
4
10
|
import { useShopwareEmbedBridge } from "../composables/useShopwareEmbedBridge";
|
|
11
|
+
import { createOrderHandoffRefresher } from "../lib/createOrderHandoffRefresher";
|
|
5
12
|
const emit = defineEmits(["checkout-finish", "auth-changed"]);
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
finishUrl: { type: String, required: false },
|
|
15
|
+
checkoutUrl: { type: String, required: false },
|
|
16
|
+
retryOrderId: { type: String, required: false }
|
|
17
|
+
});
|
|
6
18
|
const storefrontOrigin = computed(() => useRuntimeConfig().public["@laioutr/app-shopware"]?.storefrontOrigin ?? "");
|
|
7
19
|
const frameRef = ref(null);
|
|
8
20
|
const height = ref();
|
|
9
21
|
const loaded = ref(false);
|
|
22
|
+
const handoff = createOrderHandoffRefresher({
|
|
23
|
+
mint: async () => {
|
|
24
|
+
try {
|
|
25
|
+
const response = await $fetch(ORDER_HANDOFF_ENDPOINT_PATH, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: { finishUrl: props.finishUrl, checkoutUrl: props.checkoutUrl }
|
|
28
|
+
});
|
|
29
|
+
return response.code;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
deliver: (code) => sendOrderHandoff(code)
|
|
35
|
+
});
|
|
10
36
|
let hasShownFramePage = false;
|
|
11
|
-
|
|
37
|
+
let handoffActive = false;
|
|
38
|
+
const onFramePageLoaded = (payload) => {
|
|
12
39
|
loaded.value = true;
|
|
40
|
+
const canReturn = Boolean(props.finishUrl) || payload.returnFallback;
|
|
41
|
+
const submitsAnOrder = payload.route === CHECKOUT_CONFIRM_ROUTE || payload.route === CHECKOUT_RETRY_ROUTE;
|
|
42
|
+
handoffActive = submitsAnOrder && canReturn;
|
|
43
|
+
if (handoffActive) {
|
|
44
|
+
handoff.start();
|
|
45
|
+
} else {
|
|
46
|
+
handoff.stop();
|
|
47
|
+
}
|
|
13
48
|
if (hasShownFramePage) {
|
|
14
49
|
window.scrollTo({ top: 0, left: 0 });
|
|
15
50
|
}
|
|
16
51
|
hasShownFramePage = true;
|
|
17
52
|
};
|
|
18
|
-
const { sendInit } = useShopwareEmbedBridge(frameRef, {
|
|
53
|
+
const { sendInit, sendOrderHandoff } = useShopwareEmbedBridge(frameRef, {
|
|
19
54
|
storefrontOrigin: storefrontOrigin.value,
|
|
20
55
|
onResize: (value) => height.value = value,
|
|
21
56
|
onPageLoaded: onFramePageLoaded,
|
|
@@ -23,10 +58,18 @@ const { sendInit } = useShopwareEmbedBridge(frameRef, {
|
|
|
23
58
|
onAuthChanged: (payload) => emit("auth-changed", payload)
|
|
24
59
|
// laioutr:pw-recovery is received but unused in v1.
|
|
25
60
|
});
|
|
61
|
+
const refreshHandoff = () => {
|
|
62
|
+
if (handoffActive) handoff.start();
|
|
63
|
+
};
|
|
64
|
+
defineExpose({ refreshHandoff });
|
|
65
|
+
onBeforeUnmount(() => handoff.stop());
|
|
26
66
|
const onFrameLoad = () => {
|
|
27
67
|
loaded.value = true;
|
|
28
68
|
sendInit();
|
|
29
69
|
};
|
|
70
|
+
const frameSrc = computed(
|
|
71
|
+
() => props.retryOrderId ? `${CHECKOUT_ENDPOINT_PATH}?${RETRY_ORDER_QUERY_KEY}=${encodeURIComponent(props.retryOrderId)}` : CHECKOUT_ENDPOINT_PATH
|
|
72
|
+
);
|
|
30
73
|
const frameStyle = computed(() => ({ height: height.value ? `${height.value}px` : "600px" }));
|
|
31
74
|
</script>
|
|
32
75
|
|
|
@@ -42,7 +85,7 @@ const frameStyle = computed(() => ({ height: height.value ? `${height.value}px`
|
|
|
42
85
|
</div>
|
|
43
86
|
<iframe
|
|
44
87
|
ref="frameRef"
|
|
45
|
-
:src="
|
|
88
|
+
:src="frameSrc"
|
|
46
89
|
title="Checkout"
|
|
47
90
|
class="shopware-embed-frame__iframe"
|
|
48
91
|
:style="frameStyle"
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** Absolute URL of the page a completed order lands on. */
|
|
3
|
+
finishUrl?: string;
|
|
4
|
+
/** Absolute URL of this checkout page, used to re-frame a failed payment's retry. */
|
|
5
|
+
checkoutUrl?: string;
|
|
6
|
+
/** Order whose payment failed, re-framed on the storefront's order-edit page. */
|
|
7
|
+
retryOrderId?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
10
|
+
refreshHandoff: () => void;
|
|
11
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
12
|
"checkout-finish": (orderId: string) => any;
|
|
3
13
|
"auth-changed": (payload: {
|
|
4
14
|
from: string;
|
|
5
15
|
code?: string;
|
|
6
16
|
}) => any;
|
|
7
|
-
}, string, import("vue").PublicProps, Readonly<
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
18
|
"onCheckout-finish"?: ((orderId: string) => any) | undefined;
|
|
9
19
|
"onAuth-changed"?: ((payload: {
|
|
10
20
|
from: string;
|
|
11
21
|
code?: string;
|
|
12
22
|
}) => any) | undefined;
|
|
13
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions,
|
|
23
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
24
|
export default _default;
|
|
@@ -17,8 +17,10 @@ export type UseShopwareEmbedBridgeOptions = {
|
|
|
17
17
|
*
|
|
18
18
|
* Returns `sendInit` — call it on the iframe's `load` event to proactively complete the
|
|
19
19
|
* handshake, since the plugin's one-shot `ready` can fire before this listener attaches
|
|
20
|
-
* (the SSR iframe loads before Vue hydrates)
|
|
20
|
+
* (the SSR iframe loads before Vue hydrates) — and `sendOrderHandoff`, which hands the
|
|
21
|
+
* storefront a freshly minted code for its top-level order submit.
|
|
21
22
|
*/
|
|
22
23
|
export declare const useShopwareEmbedBridge: (frameRef: Ref<HTMLIFrameElement | null>, options: UseShopwareEmbedBridgeOptions) => {
|
|
23
24
|
sendInit: () => void;
|
|
25
|
+
sendOrderHandoff: (code: string) => void;
|
|
24
26
|
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { onBeforeUnmount, onMounted } from "vue";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildInitMessage,
|
|
4
|
+
buildOrderHandoffMessage
|
|
5
|
+
} from "../const/bridge.js";
|
|
3
6
|
import { createBridgeHandler } from "../lib/createBridgeHandler.js";
|
|
4
7
|
export const useShopwareEmbedBridge = (frameRef, options) => {
|
|
5
|
-
const { handleMessage, sendInit } = createBridgeHandler({
|
|
8
|
+
const { handleMessage, sendInit, sendOrderHandoff } = createBridgeHandler({
|
|
6
9
|
getFrameWindow: () => frameRef.value?.contentWindow ?? null,
|
|
7
10
|
storefrontOrigin: options.storefrontOrigin,
|
|
8
11
|
postInit: (frameWindow, targetOrigin) => frameWindow.postMessage(buildInitMessage(), targetOrigin),
|
|
12
|
+
postOrderHandoff: (frameWindow, targetOrigin, code) => frameWindow.postMessage(buildOrderHandoffMessage(code), targetOrigin),
|
|
9
13
|
onResize: options.onResize,
|
|
10
14
|
onPageLoaded: options.onPageLoaded,
|
|
11
15
|
onCheckoutFinish: options.onCheckoutFinish,
|
|
@@ -18,5 +22,5 @@ export const useShopwareEmbedBridge = (frameRef, options) => {
|
|
|
18
22
|
sendInit();
|
|
19
23
|
});
|
|
20
24
|
onBeforeUnmount(() => window.removeEventListener("message", listener));
|
|
21
|
-
return { sendInit };
|
|
25
|
+
return { sendInit, sendOrderHandoff };
|
|
22
26
|
};
|
|
@@ -10,6 +10,15 @@ export declare const BRIDGE_SOURCE = "laioutr-shopware";
|
|
|
10
10
|
export declare const BRIDGE_VERSION = 1;
|
|
11
11
|
/** Handshake reply this (parent) frame posts to pin the storefront origin. */
|
|
12
12
|
export declare const BRIDGE_INIT_TYPE = "laioutr:init";
|
|
13
|
+
/**
|
|
14
|
+
* Message this (parent) frame posts to hand the storefront a fresh single-use handoff code.
|
|
15
|
+
*
|
|
16
|
+
* The storefront's confirm form submits into the top-level window, which carries no storefront
|
|
17
|
+
* session of its own when the two sit on different registrable domains. The code travels in the
|
|
18
|
+
* form so the storefront can install one before the order is placed. Codes expire in about a
|
|
19
|
+
* minute, so this is posted repeatedly while the frame sits on the confirm page.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BRIDGE_ORDER_HANDOFF_TYPE = "laioutr:order-handoff";
|
|
13
22
|
/** Messages the storefront bridge sends to this (parent) frame. */
|
|
14
23
|
export type BridgeInboundMessage = {
|
|
15
24
|
type: 'laioutr:ready';
|
|
@@ -26,6 +35,8 @@ export type BridgeInboundMessage = {
|
|
|
26
35
|
route: string | null;
|
|
27
36
|
navigationId: string | null;
|
|
28
37
|
salesChannelId: string | null;
|
|
38
|
+
/** Whether the storefront has a configured return URL of its own to fall back on. */
|
|
39
|
+
returnFallback: boolean;
|
|
29
40
|
};
|
|
30
41
|
} | {
|
|
31
42
|
type: 'laioutr:checkout-finish';
|
|
@@ -61,3 +72,12 @@ export declare const buildInitMessage: () => {
|
|
|
61
72
|
type: string;
|
|
62
73
|
payload: {};
|
|
63
74
|
};
|
|
75
|
+
/** A freshly minted handoff code, posted to the pinned storefront origin. */
|
|
76
|
+
export declare const buildOrderHandoffMessage: (code: string) => {
|
|
77
|
+
source: string;
|
|
78
|
+
version: number;
|
|
79
|
+
type: string;
|
|
80
|
+
payload: {
|
|
81
|
+
code: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export const BRIDGE_SOURCE = "laioutr-shopware";
|
|
2
2
|
export const BRIDGE_VERSION = 1;
|
|
3
3
|
export const BRIDGE_INIT_TYPE = "laioutr:init";
|
|
4
|
+
export const BRIDGE_ORDER_HANDOFF_TYPE = "laioutr:order-handoff";
|
|
4
5
|
export const buildInitMessage = () => ({
|
|
5
6
|
source: BRIDGE_SOURCE,
|
|
6
7
|
version: BRIDGE_VERSION,
|
|
7
8
|
type: BRIDGE_INIT_TYPE,
|
|
8
9
|
payload: {}
|
|
9
10
|
});
|
|
11
|
+
export const buildOrderHandoffMessage = (code) => ({
|
|
12
|
+
source: BRIDGE_SOURCE,
|
|
13
|
+
version: BRIDGE_VERSION,
|
|
14
|
+
type: BRIDGE_ORDER_HANDOFF_TYPE,
|
|
15
|
+
payload: { code }
|
|
16
|
+
});
|
|
@@ -12,6 +12,8 @@ export type CreateBridgeHandlerOptions = {
|
|
|
12
12
|
storefrontOrigin: string;
|
|
13
13
|
/** Post the contentless handshake reply to the (validated) origin. */
|
|
14
14
|
postInit: (frameWindow: Window, targetOrigin: string) => void;
|
|
15
|
+
/** Post a freshly minted order-handoff code to the (validated) origin. */
|
|
16
|
+
postOrderHandoff: (frameWindow: Window, targetOrigin: string, code: string) => void;
|
|
15
17
|
onResize?: (height: number) => void;
|
|
16
18
|
onPageLoaded?: (payload: BridgePageLoadedPayload) => void;
|
|
17
19
|
onCheckoutFinish?: (orderId: string) => void;
|
|
@@ -41,4 +43,5 @@ export type CreateBridgeHandlerOptions = {
|
|
|
41
43
|
export declare const createBridgeHandler: (options: CreateBridgeHandlerOptions) => {
|
|
42
44
|
handleMessage: (event: BridgeMessageEvent) => void;
|
|
43
45
|
sendInit: () => void;
|
|
46
|
+
sendOrderHandoff: (code: string) => void;
|
|
44
47
|
};
|
|
@@ -7,6 +7,12 @@ export const createBridgeHandler = (options) => {
|
|
|
7
7
|
options.postInit(frameWindow, options.storefrontOrigin);
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
|
+
const sendOrderHandoff = (code) => {
|
|
11
|
+
const frameWindow = options.getFrameWindow();
|
|
12
|
+
if (frameWindow && options.storefrontOrigin) {
|
|
13
|
+
options.postOrderHandoff(frameWindow, options.storefrontOrigin, code);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
10
16
|
const handleMessage = (event) => {
|
|
11
17
|
const frameWindow = options.getFrameWindow();
|
|
12
18
|
if (!frameWindow || event.source !== frameWindow) return;
|
|
@@ -36,5 +42,5 @@ export const createBridgeHandler = (options) => {
|
|
|
36
42
|
message;
|
|
37
43
|
}
|
|
38
44
|
};
|
|
39
|
-
return { handleMessage, sendInit };
|
|
45
|
+
return { handleMessage, sendInit, sendOrderHandoff };
|
|
40
46
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CreateOrderHandoffRefresherOptions = {
|
|
2
|
+
/** Mint a fresh single-use code, or resolve `null` when the mint failed. */
|
|
3
|
+
mint: () => Promise<string | null>;
|
|
4
|
+
/** Hand a freshly minted code to the storefront frame. */
|
|
5
|
+
deliver: (code: string) => void;
|
|
6
|
+
/** Overrides the re-mint cadence; must stay under the storefront's code lifetime. */
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Keeps a fresh order-handoff code in the storefront frame for as long as the shopper sits on
|
|
11
|
+
* the confirm page.
|
|
12
|
+
*
|
|
13
|
+
* A code cannot be minted at submit time: minting is a round trip, and awaiting it inside the
|
|
14
|
+
* click handler outlives the transient user activation the top-level form submit depends on.
|
|
15
|
+
* Minting once on page load does not work either, since a shopper reading the terms outlives
|
|
16
|
+
* the code. So mint on a cadence and let the frame hold the newest one.
|
|
17
|
+
*
|
|
18
|
+
* Failures are swallowed by design — the frame keeps the previous code, and leaves the form
|
|
19
|
+
* submitting in-frame if it never received one.
|
|
20
|
+
*/
|
|
21
|
+
export declare const createOrderHandoffRefresher: (options: CreateOrderHandoffRefresherOptions) => {
|
|
22
|
+
start: () => void;
|
|
23
|
+
stop: () => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const DEFAULT_INTERVAL_MS = 4e4;
|
|
2
|
+
export const createOrderHandoffRefresher = (options) => {
|
|
3
|
+
let timer = null;
|
|
4
|
+
const tick = () => {
|
|
5
|
+
options.mint().then((code) => {
|
|
6
|
+
if (code) options.deliver(code);
|
|
7
|
+
}).catch(() => {
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
const stop = () => {
|
|
11
|
+
if (!timer) return;
|
|
12
|
+
clearInterval(timer);
|
|
13
|
+
timer = null;
|
|
14
|
+
};
|
|
15
|
+
const start = () => {
|
|
16
|
+
stop();
|
|
17
|
+
tick();
|
|
18
|
+
timer = setInterval(tick, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
19
|
+
};
|
|
20
|
+
return { start, stop };
|
|
21
|
+
};
|
|
@@ -20,7 +20,8 @@ export const parseBridgeMessage = (data) => {
|
|
|
20
20
|
path: payload.path,
|
|
21
21
|
route: typeof payload.route === "string" ? payload.route : null,
|
|
22
22
|
navigationId: typeof payload.navigationId === "string" ? payload.navigationId : null,
|
|
23
|
-
salesChannelId: typeof payload.salesChannelId === "string" ? payload.salesChannelId : null
|
|
23
|
+
salesChannelId: typeof payload.salesChannelId === "string" ? payload.salesChannelId : null,
|
|
24
|
+
returnFallback: payload.returnFallback === true
|
|
24
25
|
}
|
|
25
26
|
} : null;
|
|
26
27
|
case "laioutr:auth-changed":
|
|
@@ -16,6 +16,12 @@ export const definition = defineSection({
|
|
|
16
16
|
name: "finishLink",
|
|
17
17
|
label: "Order Confirmation Page",
|
|
18
18
|
description: "If set, the customer is redirected here after completing an order, with the order id appended as ?order=. If left empty, Shopware\u2019s own order confirmation page is shown inside the checkout frame instead."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: "link",
|
|
22
|
+
name: "checkoutLink",
|
|
23
|
+
label: "Checkout Page",
|
|
24
|
+
description: "The page this section sits on. Used to bring the customer back here when a payment fails or is cancelled, so they can choose another method without leaving the site."
|
|
19
25
|
}
|
|
20
26
|
]
|
|
21
27
|
}
|
|
@@ -24,21 +30,53 @@ export const definition = defineSection({
|
|
|
24
30
|
</script>
|
|
25
31
|
|
|
26
32
|
<script setup>
|
|
27
|
-
import {
|
|
28
|
-
|
|
33
|
+
import {
|
|
34
|
+
computed,
|
|
35
|
+
defineSection,
|
|
36
|
+
definitionToProps,
|
|
37
|
+
linkResolver,
|
|
38
|
+
navigateTo,
|
|
39
|
+
ref,
|
|
40
|
+
refreshNuxtData,
|
|
41
|
+
useRoute
|
|
42
|
+
} from "#imports";
|
|
43
|
+
import { ADOPT_SESSION_ENDPOINT_PATH, RETRY_ORDER_QUERY_KEY } from "../../shared/const/checkout";
|
|
29
44
|
import ShopwareEmbedFrame from "../components/ShopwareEmbedFrame.vue";
|
|
30
45
|
const props = defineProps(definitionToProps(definition));
|
|
46
|
+
const route = useRoute();
|
|
47
|
+
const frame = ref(null);
|
|
48
|
+
const toAbsolute = (path) => import.meta.client ? new URL(path, window.location.origin).href : path;
|
|
49
|
+
const finishUrl = computed(() => props.finishLink ? toAbsolute(linkResolver.resolve(props.finishLink)) : void 0);
|
|
50
|
+
const checkoutUrl = computed(
|
|
51
|
+
() => props.checkoutLink ? toAbsolute(linkResolver.resolve(props.checkoutLink)) : void 0
|
|
52
|
+
);
|
|
53
|
+
const retryOrderId = computed(() => {
|
|
54
|
+
const value = route.query[RETRY_ORDER_QUERY_KEY];
|
|
55
|
+
return typeof value === "string" ? value : void 0;
|
|
56
|
+
});
|
|
31
57
|
const onCheckoutFinish = async (orderId) => {
|
|
32
58
|
if (!props.finishLink) return;
|
|
33
59
|
const path = linkResolver.resolve(props.finishLink);
|
|
34
60
|
await navigateTo({ path, query: { order: orderId } });
|
|
35
61
|
};
|
|
36
62
|
const onAuthChanged = async (payload) => {
|
|
37
|
-
|
|
63
|
+
try {
|
|
64
|
+
await $fetch(ADOPT_SESSION_ENDPOINT_PATH, { method: "POST", body: { code: payload.code } });
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
console.warn("[app-shopware] could not adopt the storefront session", cause);
|
|
67
|
+
}
|
|
38
68
|
await refreshNuxtData();
|
|
69
|
+
frame.value?.refreshHandoff();
|
|
39
70
|
};
|
|
40
71
|
</script>
|
|
41
72
|
|
|
42
73
|
<template>
|
|
43
|
-
<ShopwareEmbedFrame
|
|
74
|
+
<ShopwareEmbedFrame
|
|
75
|
+
ref="frame"
|
|
76
|
+
:finish-url="finishUrl"
|
|
77
|
+
:checkout-url="checkoutUrl"
|
|
78
|
+
:retry-order-id="retryOrderId"
|
|
79
|
+
@checkout-finish="onCheckoutFinish"
|
|
80
|
+
@auth-changed="onAuthChanged"
|
|
81
|
+
/>
|
|
44
82
|
</template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ADOPT_SESSION_ENDPOINT_PATH, CHECKOUT_ENDPOINT_PATH } from '../../shared/const/checkout.js';
|
|
1
|
+
export { ADOPT_SESSION_ENDPOINT_PATH, CHECKOUT_ENDPOINT_PATH, CHECKOUT_RETRY_ROUTE, ORDER_HANDOFF_ENDPOINT_PATH, RETRY_FRAME_MARKER_KEY, RETRY_ORDER_QUERY_KEY, } from '../../shared/const/checkout.js';
|
|
2
2
|
/**
|
|
3
3
|
* Internal Shopware route the handoff lands on after the storefront session adopts
|
|
4
4
|
* the cart context. Resolved server-side by the `LaioutrConnector` plugin's
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { CHECKOUT_CONFIRM_ROUTE } from "../../shared/const/checkout.js";
|
|
2
|
+
export {
|
|
3
|
+
ADOPT_SESSION_ENDPOINT_PATH,
|
|
4
|
+
CHECKOUT_ENDPOINT_PATH,
|
|
5
|
+
CHECKOUT_RETRY_ROUTE,
|
|
6
|
+
ORDER_HANDOFF_ENDPOINT_PATH,
|
|
7
|
+
RETRY_FRAME_MARKER_KEY,
|
|
8
|
+
RETRY_ORDER_QUERY_KEY
|
|
9
|
+
} from "../../shared/const/checkout.js";
|
|
10
|
+
export const CHECKOUT_REDIRECT_ROUTE = CHECKOUT_CONFIRM_ROUTE;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ProductFlags,
|
|
7
7
|
ProductInfo,
|
|
8
8
|
ProductMedia,
|
|
9
|
+
ProductOptionGroups,
|
|
9
10
|
ProductPrices,
|
|
10
11
|
ProductRating,
|
|
11
12
|
ProductSeo
|
|
@@ -16,6 +17,7 @@ import { defineShopwareComponentResolver } from "../../middleware/defineShopware
|
|
|
16
17
|
import { fetchAllProducts } from "../../shopware-helper/fetchAllProductVariants.js";
|
|
17
18
|
import { entitySlug } from "../../shopware-helper/mappers/slugMapper.js";
|
|
18
19
|
import { mapMedia } from "../../shopware-helper/mediaMapper.js";
|
|
20
|
+
import { mapProductOptionGroups } from "../../shopware-helper/optionGroupsMapper.js";
|
|
19
21
|
import { swTranslated } from "../../shopware-helper/swTranslated.js";
|
|
20
22
|
export default defineShopwareComponentResolver({
|
|
21
23
|
label: "Shopware Product Connector",
|
|
@@ -29,6 +31,7 @@ export default defineShopwareComponentResolver({
|
|
|
29
31
|
ProductSeo,
|
|
30
32
|
ProductDescription,
|
|
31
33
|
ProductDefaultVariant,
|
|
34
|
+
ProductOptionGroups,
|
|
32
35
|
ProductRating
|
|
33
36
|
],
|
|
34
37
|
resolve: async ({ entityIds, context, $entity, passthrough }) => {
|
|
@@ -40,7 +43,8 @@ export default defineShopwareComponentResolver({
|
|
|
40
43
|
const response = await fetchAllProducts(context.storefrontClient, {
|
|
41
44
|
productIds: missingIds,
|
|
42
45
|
loadVariants: false,
|
|
43
|
-
resolveCriteria: context.resolveCriteria
|
|
46
|
+
resolveCriteria: context.resolveCriteria,
|
|
47
|
+
maxLimit: context.settings.maxLimit
|
|
44
48
|
});
|
|
45
49
|
loadedVariants.push(...response);
|
|
46
50
|
}
|
|
@@ -64,8 +68,15 @@ export default defineShopwareComponentResolver({
|
|
|
64
68
|
slug: entitySlug(rawProduct)
|
|
65
69
|
},
|
|
66
70
|
defaultVariant: {
|
|
67
|
-
id: rawVariant.id
|
|
71
|
+
id: rawVariant.id,
|
|
72
|
+
sku: rawVariant.productNumber ?? void 0,
|
|
73
|
+
// Typed locally because the resolver hands `rawVariant` back as `any`.
|
|
74
|
+
options: rawVariant.options?.map((option) => swTranslated(option, "name") ?? "").filter(Boolean),
|
|
75
|
+
status: rawVariant.available ? "inStock" : "outOfStock"
|
|
68
76
|
},
|
|
77
|
+
// The configurator hangs off the parent, never the variant the rest of this
|
|
78
|
+
// projection reads from.
|
|
79
|
+
optionGroups: mapProductOptionGroups(rawProduct),
|
|
69
80
|
info: {
|
|
70
81
|
cover: mappedCover,
|
|
71
82
|
shortDescription: swTranslated(rawProduct, "description") || swTranslated(rawVariant, "description"),
|
|
@@ -119,6 +130,11 @@ export default defineShopwareComponentResolver({
|
|
|
119
130
|
components: {
|
|
120
131
|
prices: {
|
|
121
132
|
ttl: "15 minutes"
|
|
133
|
+
},
|
|
134
|
+
// Half of it is stock-derived, so it decays like prices rather than living for
|
|
135
|
+
// the resolver's full day. `optionGroups` needs no override: nothing in it is.
|
|
136
|
+
defaultVariant: {
|
|
137
|
+
ttl: "15 minutes"
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
}
|
|
@@ -6,16 +6,18 @@ export default defineShopwareLink(ProductVariantsLink, async ({ entityIds, conte
|
|
|
6
6
|
const allVariants = passthrough.get(productVariantsToken) ?? await fetchAllProducts(context.storefrontClient, {
|
|
7
7
|
productIds: entityIds,
|
|
8
8
|
loadVariants: true,
|
|
9
|
-
resolveCriteria: context.resolveCriteria
|
|
9
|
+
resolveCriteria: context.resolveCriteria,
|
|
10
|
+
maxLimit: context.settings.maxLimit
|
|
10
11
|
});
|
|
11
12
|
passthrough.set(productVariantsToken, allVariants);
|
|
12
13
|
return {
|
|
13
14
|
links: entityIds.map((productId) => {
|
|
14
|
-
const
|
|
15
|
+
const variants = allVariants.filter((variant) => variant.parentId === productId);
|
|
15
16
|
return {
|
|
16
17
|
sourceId: productId,
|
|
17
|
-
// A
|
|
18
|
-
|
|
18
|
+
// A product's variants are the children this sales channel sells. With none of its own, the
|
|
19
|
+
// product is its single variant.
|
|
20
|
+
targetIds: variants.length > 0 ? variants.map((variant) => variant.id) : [productId]
|
|
19
21
|
};
|
|
20
22
|
})
|
|
21
23
|
};
|
|
@@ -15,6 +15,7 @@ import { resolveProductVariantFields } from "../../orchestr-helper/requestedFiel
|
|
|
15
15
|
import { toRequestCriteria } from "../../shopware-helper/criteria.js";
|
|
16
16
|
import { mapMedia } from "../../shopware-helper/mediaMapper.js";
|
|
17
17
|
import { swTranslated } from "../../shopware-helper/swTranslated.js";
|
|
18
|
+
import { guessWellKnownName } from "../../shopware-helper/wellKnownOptionName.js";
|
|
18
19
|
export default defineShopwareComponentResolver({
|
|
19
20
|
entityType: "ProductVariant",
|
|
20
21
|
label: "Shopware Product Variant Connector",
|
|
@@ -119,10 +120,14 @@ export default defineShopwareComponentResolver({
|
|
|
119
120
|
required: true
|
|
120
121
|
}),
|
|
121
122
|
options: () => ({
|
|
122
|
-
selected: entity.options?.map((option) =>
|
|
123
|
-
name
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
selected: entity.options?.map((option) => {
|
|
124
|
+
const name = swTranslated(option.group, "name") ?? swTranslated(option, "name") ?? "Unknown Property";
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
wellKnownName: guessWellKnownName(name),
|
|
128
|
+
value: swTranslated(option, "name") ?? swTranslated(option, "option") ?? "Unknown Value"
|
|
129
|
+
};
|
|
130
|
+
}) ?? []
|
|
126
131
|
})
|
|
127
132
|
})
|
|
128
133
|
)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { ResolveCriteria, ShopwareCriteria } from '../types/criteria.js';
|
|
2
|
-
export declare const resolveProductFields: ({ loadVariants }: {
|
|
3
|
-
loadVariants: boolean;
|
|
4
|
-
}, resolveCriteria: ResolveCriteria) => Promise<ShopwareCriteria>;
|
|
5
2
|
export declare const resolveProductVariantFields: (resolveCriteria: ResolveCriteria) => Promise<ShopwareCriteria>;
|
|
3
|
+
export declare const resolveProductFields: (resolveCriteria: ResolveCriteria) => Promise<ShopwareCriteria>;
|