-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(default-theme): promotion code functionality #1155
Changes from 12 commits
1774853
e62deee
1196cec
250bab4
c472945
2ca922a
f25c9da
1b98965
f4a3d15
ccb06bd
04130c0
7597f97
352f393
499012a
be85189
37bee8f
7b68155
7c12f93
6b9b7ef
a2b093d
e82b924
17f16c9
ec90bc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,23 +2,32 @@ import { ref, Ref, computed } from "@vue/composition-api"; | |
import { | ||
getCart, | ||
addProductToCart, | ||
addPromotionCode, | ||
removeCartItem, | ||
changeCartItemQuantity, | ||
} from "@shopware-pwa/shopware-6-client"; | ||
import { ClientApiError } from "@shopware-pwa/commons/interfaces/errors/ApiError"; | ||
import { Cart } from "@shopware-pwa/commons/interfaces/models/checkout/cart/Cart"; | ||
import { Product } from "@shopware-pwa/commons/interfaces/models/content/product/Product"; | ||
import { LineItem } from "@shopware-pwa/commons/interfaces/models/checkout/cart/line-item/LineItem"; | ||
import { getApplicationContext } from "@shopware-pwa/composables"; | ||
import { | ||
getApplicationContext, | ||
useNotifications, | ||
} from "@shopware-pwa/composables"; | ||
import { ApplicationVueContext } from "../../appContext"; | ||
|
||
const TYPE_PROMOTION = "promotion"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could be a type in
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right and there even is a CartItemType interface already with the needed types... I'll use that one. |
||
const TYPE_PRODUCT = "product"; | ||
|
||
/** | ||
* interface for {@link useCart} composable | ||
* | ||
* @beta | ||
*/ | ||
export interface IUseCart { | ||
addProduct: ({ id, quantity }: { id: string; quantity?: number }) => void; | ||
addPromotionCode: (promoCode: string) => void; | ||
appliedPromotionCodes: Readonly<Ref<Readonly<LineItem[]>>>; | ||
niklaswolf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cart: Readonly<Ref<Readonly<Cart>>>; | ||
cartItems: Readonly<Ref<Readonly<LineItem[]>>>; | ||
changeProductQuantity: ({ | ||
|
@@ -33,6 +42,7 @@ export interface IUseCart { | |
loading: Readonly<Ref<Readonly<boolean>>>; | ||
refreshCart: () => void; | ||
removeProduct: ({ id }: Partial<Product>) => void; | ||
removePromotionCode: ({ id }: Partial<Product>) => void; | ||
niklaswolf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
totalPrice: Readonly<Ref<Readonly<number>>>; | ||
subtotal: Readonly<Ref<Readonly<number>>>; | ||
} | ||
|
@@ -51,6 +61,8 @@ export const useCart = (rootContext: ApplicationVueContext): IUseCart => { | |
const loading: Ref<boolean> = ref(false); | ||
const error: Ref<any> = ref(null); | ||
|
||
const { pushSuccess, pushError } = useNotifications(rootContext); | ||
|
||
async function refreshCart(): Promise<void> { | ||
loading.value = true; | ||
try { | ||
|
@@ -85,6 +97,39 @@ export const useCart = (rootContext: ApplicationVueContext): IUseCart => { | |
vuexStore.commit("SET_CART", result); | ||
} | ||
|
||
async function submitPromotionCode(promoCode: string) { | ||
try { | ||
const result = await addPromotionCode(promoCode, apiInstance); | ||
vuexStore.commit("SET_CART", result); | ||
|
||
// It's strange that success also ends up as an error in the API response | ||
const err = <any>Object.values(result.errors)[0]; | ||
switch (err.messageKey) { | ||
case "promotion-discount-added": | ||
pushSuccess(rootContext.$t("Promotion code added successfully")); | ||
break; | ||
case "promotion-not-found": | ||
pushError(rootContext.$t("Promotion code does not exist")); | ||
break; | ||
default: | ||
pushError(err.message.toString()); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic should go into |
||
} catch (e) { | ||
const err: ClientApiError = e; | ||
error.value = err.message; | ||
} | ||
} | ||
|
||
async function removePromotionCode(lineItem: Product) { | ||
await removeProduct(lineItem); | ||
} | ||
|
||
const appliedPromotionCodes = computed(() => { | ||
return cartItems.value.filter( | ||
(cartItem) => cartItem.type === TYPE_PROMOTION | ||
); | ||
}); | ||
|
||
const cart: Readonly<Ref<Readonly<Cart>>> = computed(() => { | ||
return vuexStore.getters.getCart; | ||
}); | ||
|
@@ -96,7 +141,7 @@ export const useCart = (rootContext: ApplicationVueContext): IUseCart => { | |
const count = computed(() => { | ||
return cartItems.value.reduce( | ||
(accumulator: number, lineItem: LineItem) => | ||
lineItem.type === "product" | ||
lineItem.type === TYPE_PRODUCT | ||
? lineItem.quantity + accumulator | ||
: accumulator, | ||
0 | ||
|
@@ -116,6 +161,8 @@ export const useCart = (rootContext: ApplicationVueContext): IUseCart => { | |
|
||
return { | ||
addProduct, | ||
addPromotionCode: submitPromotionCode, | ||
appliedPromotionCodes, | ||
cart, | ||
cartItems, | ||
changeProductQuantity, | ||
|
@@ -124,6 +171,7 @@ export const useCart = (rootContext: ApplicationVueContext): IUseCart => { | |
loading, | ||
refreshCart, | ||
removeProduct, | ||
removePromotionCode, | ||
totalPrice, | ||
subtotal, | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
<template> | ||
<div class="promo-code"> | ||
<div class="promo-code__input-wrapper"> | ||
<SwInput | ||
v-model="promoCode" | ||
name="promoCode" | ||
:label="$t('Enter promo code')" | ||
class="sf-input--filled promo-code__input" | ||
@keyup.enter="addPromotionCode(promoCode)" | ||
/> | ||
<SfCircleIcon | ||
class="promo-code__circle-icon" | ||
icon="check" | ||
@click="addPromotionCode(promoCode)" | ||
/> | ||
</div> | ||
<div v-if="showPromotionCodes" class="applied-codes"> | ||
<SfHeading | ||
:title="$t('Applied promo codes:')" | ||
:level="4" | ||
class="sf-heading--left sf-heading--no-underline title" | ||
/> | ||
<ul class="applied-codes__list"> | ||
<SwPromoCodeItem | ||
v-for="appliedPromotionCode in appliedPromotionCodes" | ||
:key="appliedPromotionCode.id" | ||
:code="appliedPromotionCode" | ||
:remove-promotion-code="removePromotionCode" | ||
/> | ||
</ul> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
import SwInput from "@/components/atoms/SwInput" | ||
import { SfCircleIcon, SfHeading } from "@storefront-ui/vue" | ||
import SwPromoCodeItem from "@/components/SwPromoCodeItem" | ||
import { useCart } from "@shopware-pwa/composables" | ||
import { computed } from "@vue/composition-api" | ||
|
||
export default { | ||
name: "SwPromoCode", | ||
setup(props, { root }) { | ||
const { | ||
appliedPromotionCodes, | ||
addPromotionCode, | ||
removePromotionCode, | ||
} = useCart(root) | ||
|
||
const showPromotionCodes = computed( | ||
() => appliedPromotionCodes.value.length > 0 | ||
) | ||
|
||
return { | ||
appliedPromotionCodes, | ||
addPromotionCode, | ||
removePromotionCode, | ||
showPromotionCodes, | ||
} | ||
}, | ||
components: { | ||
SwPromoCodeItem, | ||
SfHeading, | ||
SwInput, | ||
SfCircleIcon, | ||
}, | ||
data: () => { | ||
return { | ||
promoCode: "", | ||
} | ||
}, | ||
} | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
@import "@/assets/scss/variables"; | ||
|
||
.promo-code { | ||
padding: var(--spacer-lg) 0 var(--spacer-base) 0; | ||
&__input-wrapper { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: flex-start; | ||
|
||
.promo-code__circle-icon { | ||
--button-size: 2rem; | ||
--icon-size: 0.6875rem; | ||
} | ||
|
||
.promo-code__input { | ||
--input-background: var(--c-white); | ||
flex: 1; | ||
margin: 0 var(--spacer-lg) 0 0; | ||
} | ||
} | ||
.applied-codes { | ||
&__list { | ||
list-style: none; | ||
padding: 0; | ||
} | ||
} | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<template> | ||
<li class="promo-code-item"> | ||
<span>{{ code.label }}</span> | ||
<SfCircleIcon | ||
class="promo-code-item__remove" | ||
icon="cross" | ||
@click="removePromotionCode(code)" | ||
/> | ||
</li> | ||
</template> | ||
|
||
<script> | ||
import { SfCircleIcon } from "@storefront-ui/vue" | ||
|
||
export default { | ||
name: "SwPromoCodeItem", | ||
components: { | ||
SfCircleIcon, | ||
}, | ||
props: { | ||
code: { | ||
type: Object, | ||
}, | ||
removePromotionCode: { | ||
type: Function, | ||
}, | ||
}, | ||
} | ||
</script> | ||
|
||
<style lang="scss" scoped> | ||
.promo-code-item { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
&__remove { | ||
--button-size: 2rem; | ||
--icon-size: 0.6875rem; | ||
} | ||
} | ||
</style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notifications Should not be used inside other composables. Users need to have a choice of how they want to react on events across the system. Use interceptors instead: https://shopware-pwa-docs.vuestorefront.io/landing/concepts/interceptor.html#events-interceptor