Skip to content

Commit

Permalink
feat(composables): price format using Intl (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkucmus committed Apr 11, 2023
1 parent 9e28fa9 commit 1fd1962
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-turkeys-brake.md
@@ -0,0 +1,5 @@
---
"@shopware-pwa/composables-next": patch
---

Intl as a price formatter
4 changes: 2 additions & 2 deletions apps/docs/src/getting-started/use-product-price.md
Expand Up @@ -98,13 +98,13 @@ Tier prices presented as a table with range labeled by "to" and "from":

## Format price according to current context

There are additional metadata available in current API context. One of them is current currency. In order to display price together with currency symbol applied to the current context, use `getFormattedPrice` helper:
There are additional metadata available in current API context. One of them is current currency. In order to display price together with currency symbol applied to the current context, use `getFormattedPrice` helper from [usePrice](../packages/composables/usePrice.md) composable:

```ts
const price = 12.95;
const { getFormattedPrice } = usePrice();
const priceWithCurrency = getFormattedPrice(price);
// output: 12.95 $
// output: $12.95
```

Thanks to this, the `priceWithCurrency` will have the current currency symbol prefixed or suffixed, according to the configuration.
14 changes: 14 additions & 0 deletions apps/docs/src/packages/composables/usePrice.md
Expand Up @@ -4,4 +4,18 @@ category: CMS

# usePrice

Internally, `usePrice` composable uses [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) in order to format a price according to the right currency standard, for corresponding locale and symbol.

```js
const { init, getFormattedPrice } = usePrice();
init({
currencyCode: 'EUR'
localeCode: 'de-DE' // value taken from browser's navigator.language variable if localeCode is not provided
})

const regularPrice = getFormattedPrice(49.95);
// regularPrice: '49,95 €'
```


<!-- PLACEHOLDER_DESCRIPTION -->
23 changes: 12 additions & 11 deletions packages/composables/src/usePrice.test.ts
Expand Up @@ -21,31 +21,32 @@ export function withSetup(composable: any) {
describe("usePrice", () => {
const { init, getFormattedPrice } = usePrice();
init({
currencyPosition: 1,
currencySymbol: "$",
localeCode: "en-US",
currencyCode: "USD",
});

it("should be defined", () => {
expect(usePrice).toBeDefined();
});

it("should init price object", () => {
expect(getFormattedPrice("2")).toBe("2.00 $");
expect(getFormattedPrice("2")).toBe("$2.00");
});

it("should update config", () => {
init({
currencyPosition: 0,
currencySymbol: "$",
localeCode: "de-DE",
currencyCode: "EUR",
});
expect(getFormattedPrice("4")).toBe("$ 4.00");
// applied workaround for non-breaking space that is inserted by Intl.NumberFormat
expect(getFormattedPrice(4.1).replace(/\s/g, " ")).toBe("4,10 €");
});

it("should return empty string", () => {
it("should return price with language locale code taken from navigator", () => {
init({
currencyPosition: 0,
currencySymbol: "$",
});
expect(getFormattedPrice(undefined)).toBe("");
currencyCode: "USD",
currencyLocale: undefined,
} as any);
expect(getFormattedPrice(2.55)).toStrictEqual(`$2.55`);
});
});
40 changes: 20 additions & 20 deletions packages/composables/src/usePrice.ts
@@ -1,16 +1,16 @@
import { ref } from "vue";

const currencySymbol = ref<string>("");
const currencyPosition = ref<number>(1);
const currencyLocale = ref<string>("");
const currencyCode = ref<string>("");

// @ToDo make sure why there is no decimal precision in api response
const decimalPrecision = 2;

export type UsePriceReturn = {
/**
* Set init data: currencySymbol & currencyPosition
* Set init data: localeCode & currencyCode
*/
init(options: { currencySymbol: string; currencyPosition: number }): void;
init(options: { localeCode: string | undefined; currencyCode: string }): void;
/**
* Format price i.e. (2) -> 2.00 $
*/
Expand All @@ -26,41 +26,41 @@ export function usePrice(): UsePriceReturn {
/**
* Set init data from backend response
*
* as a fallback for params.localeCode is navigator?.language
* @param params
*/
function init(params: {
currencySymbol: string;
currencyPosition: number;
localeCode: string | undefined;
currencyCode: string;
}): void {
_setCurrencySymbol(params.currencySymbol);
_setCurrencyPosition(params.currencyPosition);
_setCurrencyCode(params.currencyCode);
_setLocaleCode(params.localeCode || navigator?.language);
}

function _setCurrencySymbol(symbol: string) {
currencySymbol.value = symbol;
function _setCurrencyCode(code: string) {
currencyCode.value = code;
}

function _setCurrencyPosition(position: number) {
currencyPosition.value = position;
function _setLocaleCode(locale: string) {
currencyLocale.value = locale;
}

/**
* Format price (2) -> 2.00 $
* Format price (2) -> $ 2.00
*/
function getFormattedPrice(value: number | string | undefined): string {
if (typeof value === "undefined") {
return "";
}
let formattedPrice = [
(+value).toFixed(decimalPrecision),
currencySymbol.value,
];

if (currencyPosition.value === 0) {
formattedPrice = formattedPrice.reverse();
if (!currencyLocale.value) {
return value.toString();
}

return formattedPrice.join(" ");
return new Intl.NumberFormat(currencyLocale.value, {
style: "currency",
currency: currencyCode.value,
}).format(+value);
}

return {
Expand Down
5 changes: 3 additions & 2 deletions packages/composables/src/useSessionContext.ts
Expand Up @@ -111,9 +111,10 @@ export function useSessionContext(
try {
const context = await getSessionContext(apiInstance);
_sessionContext.value = context;

init({
currencyPosition: context.currency.position,
currencySymbol: context.currency.symbol,
currencyCode: context.currency?.isoCode,
localeCode: context.salesChannel?.language?.locale?.code,
});
} catch (e) {
console.error("[UseSessionContext][refreshSessionContext]", e);
Expand Down

2 comments on commit 1fd1962

@vercel
Copy link

@vercel vercel bot commented on 1fd1962 Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

frontends-demo – ./templates/vue-demo-store

frontends-demo-shopware-frontends.vercel.app
frontends-demo-git-main-shopware-frontends.vercel.app
frontends-demo.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1fd1962 Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.