@akinon/pz-blik 1.126.5-v1-rc.1

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/.prettierrc ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "bracketSameLine": false,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "jsxSingleQuote": false,
6
+ "bracketSpacing": true,
7
+ "semi": true,
8
+ "useTabs": false,
9
+ "arrowParens": "always",
10
+ "endOfLine": "lf",
11
+ "proseWrap": "never",
12
+ "trailingComma": "none"
13
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @akinon/pz-blik
2
+
3
+ ## 1.126.5-v1-rc.1
4
+
5
+ ### Patch Changes
6
+
7
+ - cc4e80d: ZERO-4623: Add PayU Europe BLIK digital-wallet payment package
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Project Zero - BLIK Plugin
2
+
3
+ PayU Europe **BLIK** payment method for the Project Zero storefront. The shopper enters the 6-digit one-time BLIK code generated in their banking app; the code is submitted through the generic digital-wallet checkout flow (server-to-server, no redirect) and the order is finalized on PayU's asynchronous order-confirmation webhook.
4
+
5
+ Markets: Poland (and Slovakia when enabled on the POS).
6
+
7
+ ## Installation
8
+
9
+ You can use the following command to install the extension with the latest plugins:
10
+
11
+ ```bash
12
+ npx @akinon/projectzero@latest --plugins
13
+ ```
14
+
15
+ ## Adding BLIK to Checkout
16
+
17
+ BLIK arrives from the backend as a wallet payment option (`payment_type: "wallet"`). All PayU Europe wallet methods share that type, so the storefront selects the BLIK component by the payment option's **`slug`** through the brand `PaymentOptionViews` registry. That registry is resolved before the generic wallet branches, so no core change is required.
18
+
19
+ ### 1. Create the BLIK option file
20
+
21
+ **views/checkout/steps/payment/options/blik.tsx**
22
+
23
+ ```tsx
24
+ import Blik from '@akinon/pz-blik';
25
+
26
+ export default function BlikOption() {
27
+ return <Blik />;
28
+ }
29
+ ```
30
+
31
+ ### 2. Register it in the payment step
32
+
33
+ **views/checkout/steps/payment/index.tsx**
34
+
35
+ ```tsx
36
+ import BlikOption from './options/blik';
37
+
38
+ export const PaymentOptionViews: Array<CheckoutPaymentOption> = [
39
+ {
40
+ slug: 'blik', // must match the BLIK payment option slug configured in the Akinon backend
41
+ view: BlikOption
42
+ }
43
+ // Other payment methods can be added here
44
+ ];
45
+ ```
46
+
47
+ > The `slug` must exactly match the BLIK payment option `slug` provisioned on the POS in the Akinon backend.
48
+
49
+ ## Translations
50
+
51
+ The default UI reads the following localization keys — add them to the brand translations:
52
+
53
+ | Key | Example (en) |
54
+ | --------------------------------- | -------------------------------------- |
55
+ | `checkout.blik.code_label` | BLIK code |
56
+ | `checkout.blik.code_placeholder` | 6-digit code |
57
+ | `checkout.blik.pay_button` | Pay |
58
+ | `checkout.blik.processing` | Processing... |
59
+ | `checkout.blik.errors.context` | Could not proceed to the payment step. |
60
+ | `checkout.blik.errors.incomplete` | Payment could not be completed. |
61
+ | `checkout.blik.errors.unexpected` | An unexpected error occurred. |
62
+
63
+ ## Props
64
+
65
+ | Prop | Type | Description |
66
+ | --- | --- | --- |
67
+ | `className` | `string` | Applied to the default wrapper element. |
68
+ | `customUIRender` | `(props) => JSX.Element \| null` | Fully replaces the default UI. Receives `{ code, setCode, handleSubmit, isProcessing, isValid, paymentErrors }`. |
69
+
70
+ ### customUIRender example
71
+
72
+ ```tsx
73
+ <Blik
74
+ customUIRender={({
75
+ code,
76
+ setCode,
77
+ handleSubmit,
78
+ isProcessing,
79
+ isValid,
80
+ paymentErrors
81
+ }) => (
82
+ <div>
83
+ <input
84
+ value={code}
85
+ inputMode="numeric"
86
+ onChange={(event) => setCode(event.target.value)}
87
+ />
88
+ <button onClick={handleSubmit} disabled={!isValid || isProcessing}>
89
+ Pay with BLIK
90
+ </button>
91
+ {paymentErrors && <p>{paymentErrors}</p>}
92
+ </div>
93
+ )}
94
+ />
95
+ ```
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@akinon/pz-blik",
3
+ "version": "1.126.5-v1-rc.1",
4
+ "license": "MIT",
5
+ "main": "src/index.tsx",
6
+ "peerDependencies": {
7
+ "react": "^18.0.0",
8
+ "react-dom": "^18.0.0"
9
+ },
10
+ "devDependencies": {
11
+ "@types/node": "^18.7.8",
12
+ "@types/react": "^18.0.17",
13
+ "@types/react-dom": "^18.0.6",
14
+ "prettier": "^3.0.3",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0",
17
+ "typescript": "^5.2.2"
18
+ }
19
+ }
@@ -0,0 +1,120 @@
1
+ import { checkoutApi } from '@akinon/next/data/client/checkout';
2
+ import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
3
+ import { getCookie } from '@akinon/next/utils';
4
+ import { getUrlPathWithLocale } from '@akinon/next/utils/localization';
5
+ import { useLocalization } from '@akinon/next/hooks';
6
+ import { RootState } from '@theme/redux/store';
7
+ import { useMemo, useState } from 'react';
8
+
9
+ export default function useBlik() {
10
+ const dispatch = useAppDispatch();
11
+ const { preOrder } = useAppSelector((state: RootState) => state.checkout);
12
+ const { t } = useLocalization();
13
+ const [errors, setErrors] = useState<any>(null);
14
+ const [isProcessing, setIsProcessing] = useState(false);
15
+
16
+ const paymentErrors = useMemo(() => {
17
+ if (typeof errors === 'string') return errors;
18
+ if (Array.isArray(errors)) return errors.join(', ');
19
+ if (typeof errors === 'object')
20
+ return Object.values(errors ?? {}).join(', ');
21
+ return null;
22
+ }, [errors]);
23
+
24
+ const redirectToThankYouPage = (contextList: any[]) => {
25
+ const thankYouPageContext = contextList?.find(
26
+ (c: any) => c.page_name === 'ThankYouPage'
27
+ );
28
+
29
+ if (thankYouPageContext) {
30
+ const redirectUrl =
31
+ thankYouPageContext.page_context.context_data.redirect_url;
32
+ const redirectUrlWithLocale = `${
33
+ window.location.origin
34
+ }${getUrlPathWithLocale(redirectUrl, getCookie('pz-locale'))}`;
35
+ window.location.href = redirectUrlWithLocale;
36
+ }
37
+ };
38
+
39
+ const handleBlikSubmit = async (authorizationCode: string) => {
40
+ if (isProcessing) return;
41
+
42
+ setErrors(null);
43
+ setIsProcessing(true);
44
+
45
+ try {
46
+ const walletSelectionPageResponse = await dispatch(
47
+ checkoutApi.endpoints.setWalletSelectionPage.initiate({
48
+ payment_option: preOrder.payment_option?.pk
49
+ })
50
+ ).unwrap();
51
+
52
+ const walletPaymentPageContext =
53
+ walletSelectionPageResponse.context_list?.find(
54
+ (c: any) => c.page_name === 'WalletPaymentPage'
55
+ );
56
+
57
+ if (!walletPaymentPageContext) {
58
+ setErrors(t('checkout.blik.errors.context'));
59
+ return;
60
+ }
61
+
62
+ // The BLIK 6-digit authorization code travels in the generic wallet
63
+ // `payment_token` slot; the backend maps it into pre_order.data['token'].
64
+ const walletPaymentPageResponse = await dispatch(
65
+ checkoutApi.endpoints.setWalletPaymentPage.initiate({
66
+ payment_token: authorizationCode
67
+ })
68
+ ).unwrap();
69
+
70
+ if (walletPaymentPageResponse.errors) {
71
+ setErrors(walletPaymentPageResponse.errors);
72
+ return;
73
+ }
74
+
75
+ // BLIK's authorization-code flow completes server-to-server (no redirect),
76
+ // but honour a redirect context if the backend ever returns one.
77
+ const redirectPageContext = walletPaymentPageResponse.context_list?.find(
78
+ (c: any) => c.page_name === 'WalletRedirectCompletePage'
79
+ );
80
+
81
+ if (redirectPageContext) {
82
+ window.location.href = redirectPageContext.page_context.redirect_url;
83
+ return;
84
+ }
85
+
86
+ const completePageContext = walletPaymentPageResponse.context_list?.find(
87
+ (c: any) => c.page_name === 'WalletCompletePage'
88
+ );
89
+
90
+ if (completePageContext) {
91
+ const paymentCompleteResponse = await dispatch(
92
+ checkoutApi.endpoints.setWalletCompletePage.initiate({
93
+ success: true
94
+ })
95
+ ).unwrap();
96
+
97
+ if (paymentCompleteResponse.errors) {
98
+ setErrors(paymentCompleteResponse.errors);
99
+ return;
100
+ }
101
+
102
+ redirectToThankYouPage(paymentCompleteResponse.context_list);
103
+ } else {
104
+ setErrors(t('checkout.blik.errors.incomplete'));
105
+ }
106
+ } catch (error) {
107
+ console.error('BLIK Flow Error:', error);
108
+ setErrors(t('checkout.blik.errors.unexpected'));
109
+ } finally {
110
+ setIsProcessing(false);
111
+ }
112
+ };
113
+
114
+ return {
115
+ paymentErrors,
116
+ setPaymentErrors: setErrors,
117
+ handleBlikSubmit,
118
+ isProcessing
119
+ };
120
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export * from './views';
2
+ export { default } from './views';
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { ReactNode, useState } from 'react';
4
+ import { useLocalization } from '@akinon/next/hooks';
5
+ import useBlik from '../hooks/use-blik';
6
+
7
+ const BLIK_CODE_LENGTH = 6;
8
+ const BLIK_CODE_REGEX = /^\d{6}$/;
9
+
10
+ export interface BlikProps {
11
+ customUIRender?: (props: {
12
+ code: string;
13
+ setCode: (code: string) => void;
14
+ handleSubmit: () => void;
15
+ isProcessing: boolean;
16
+ isValid: boolean;
17
+ paymentErrors?: string | null;
18
+ }) => ReactNode;
19
+ className?: string;
20
+ }
21
+
22
+ export const Blik = ({ customUIRender, className }: BlikProps) => {
23
+ const { t } = useLocalization();
24
+ const { paymentErrors, handleBlikSubmit, isProcessing } = useBlik();
25
+ const [code, setCode] = useState('');
26
+
27
+ const isValid = BLIK_CODE_REGEX.test(code);
28
+
29
+ const handleCodeChange = (value: string) => {
30
+ setCode(value.replace(/\D/g, '').slice(0, BLIK_CODE_LENGTH));
31
+ };
32
+
33
+ const handleSubmit = () => {
34
+ if (!isValid || isProcessing) return;
35
+ handleBlikSubmit(code);
36
+ };
37
+
38
+ if (customUIRender) {
39
+ return (
40
+ <>
41
+ {customUIRender({
42
+ code,
43
+ setCode: handleCodeChange,
44
+ handleSubmit,
45
+ isProcessing,
46
+ isValid,
47
+ paymentErrors
48
+ })}
49
+ </>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <div className={className}>
55
+ <label htmlFor="blik-code" className="block text-sm font-medium mb-2">
56
+ {t('checkout.blik.code_label')}
57
+ </label>
58
+
59
+ <input
60
+ id="blik-code"
61
+ type="text"
62
+ inputMode="numeric"
63
+ autoComplete="one-time-code"
64
+ maxLength={BLIK_CODE_LENGTH}
65
+ value={code}
66
+ onChange={(event) => handleCodeChange(event.target.value)}
67
+ placeholder={t('checkout.blik.code_placeholder')}
68
+ className="w-full border rounded px-3 py-2 text-center tracking-[0.5em]"
69
+ />
70
+
71
+ <button
72
+ type="button"
73
+ onClick={handleSubmit}
74
+ disabled={!isValid || isProcessing}
75
+ className="mt-4 w-full rounded bg-black py-3 text-white disabled:opacity-50"
76
+ >
77
+ {isProcessing
78
+ ? t('checkout.blik.processing')
79
+ : t('checkout.blik.pay_button')}
80
+ </button>
81
+
82
+ {paymentErrors && (
83
+ <div className="text-error-100 text-xs mt-5">{paymentErrors}</div>
84
+ )}
85
+ </div>
86
+ );
87
+ };
88
+
89
+ export default Blik;