@akinon/pz-masterpass-rest 2.0.14-rc.1 → 2.0.14
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/CHANGELOG.md +1 -14
- package/docs/USAGE.md +61 -332
- package/package.json +2 -2
- package/src/components/card-list.tsx +2 -72
- package/src/components/credit-card-form.tsx +1 -1
- package/src/components/installment-list.tsx +1 -1
- package/src/components/otp-modal.tsx +4 -12
- package/src/hooks/useMasterpassAccount.ts +5 -21
- package/src/hooks/useMasterpassPayment.ts +13 -112
- package/src/index.ts +0 -2
- package/src/redux/api.ts +1 -82
- package/src/redux/reducer.ts +3 -35
- package/src/types/custom-render.types.ts +1 -47
- package/src/types/payment.types.ts +0 -13
- package/src/utils/card-utils.ts +1 -1
- package/src/views/masterpass-rest-option.tsx +9 -133
- package/src/components/reward-selection-modal.tsx +0 -194
- package/src/utils/reward-utils.ts +0 -41
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import type { CreditCardFormData } from '../utils/validation-schemas';
|
|
3
|
-
import { CardType, Installment
|
|
3
|
+
import { CardType, Installment } from './payment.types';
|
|
4
4
|
|
|
5
5
|
export type PaymentMethodSelectorProps = {
|
|
6
6
|
cards: any[];
|
|
@@ -18,14 +18,6 @@ export type CardListProps = {
|
|
|
18
18
|
cvc?: string;
|
|
19
19
|
onCvcChange?: (cvc: string) => void;
|
|
20
20
|
cvcRequired?: boolean;
|
|
21
|
-
availableRewards?: RewardItem[];
|
|
22
|
-
selectedRewards?: RewardItem[];
|
|
23
|
-
isLoadingRewards?: boolean;
|
|
24
|
-
isConfirmingRewards?: boolean;
|
|
25
|
-
onOpenRewardModal?: () => void;
|
|
26
|
-
onConfirmRewards?: (selected: RewardItem[]) => Promise<void> | void;
|
|
27
|
-
rewardCurrency?: string;
|
|
28
|
-
rewardPayableAmount?: string | number | null;
|
|
29
21
|
texts: MasterpassRestOptionTexts;
|
|
30
22
|
};
|
|
31
23
|
|
|
@@ -66,18 +58,6 @@ export type OTPModalProps = {
|
|
|
66
58
|
texts: MasterpassRestOptionTexts;
|
|
67
59
|
};
|
|
68
60
|
|
|
69
|
-
export type RewardSelectionModalProps = {
|
|
70
|
-
open: boolean;
|
|
71
|
-
onClose: () => void;
|
|
72
|
-
onConfirm: (selected: RewardItem[]) => Promise<void> | void;
|
|
73
|
-
rewards: RewardItem[];
|
|
74
|
-
selectedRewards: RewardItem[];
|
|
75
|
-
isLoading?: boolean;
|
|
76
|
-
currency?: string;
|
|
77
|
-
payableAmount?: string | number | null;
|
|
78
|
-
texts: MasterpassRestOptionTexts;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
61
|
export type ConfirmationModalProps = {
|
|
82
62
|
open: boolean;
|
|
83
63
|
onClose: () => void;
|
|
@@ -139,14 +119,6 @@ export type MasterpassRestOptionRenderProps = {
|
|
|
139
119
|
isInstallmentLoading: boolean;
|
|
140
120
|
isPrepareLoading: boolean;
|
|
141
121
|
isFinalizeLoading: boolean;
|
|
142
|
-
isProcessingPayment: boolean;
|
|
143
|
-
isRewardsQueryLoading: boolean;
|
|
144
|
-
isRewardsSelectLoading: boolean;
|
|
145
|
-
|
|
146
|
-
openRewardModal: () => void;
|
|
147
|
-
closeRewardModal: () => void;
|
|
148
|
-
handleConfirmRewards: (selected: RewardItem[]) => Promise<void>;
|
|
149
|
-
payableAmount: string | null;
|
|
150
122
|
|
|
151
123
|
texts: MasterpassRestOptionTexts;
|
|
152
124
|
|
|
@@ -158,7 +130,6 @@ export type MasterpassRestOptionRenderProps = {
|
|
|
158
130
|
LinkModal: React.ComponentType<LinkModalProps>;
|
|
159
131
|
OTPModal: React.ComponentType<OTPModalProps>;
|
|
160
132
|
ConfirmationModal: React.ComponentType<ConfirmationModalProps>;
|
|
161
|
-
RewardSelectionModal: React.ComponentType<RewardSelectionModalProps>;
|
|
162
133
|
};
|
|
163
134
|
};
|
|
164
135
|
|
|
@@ -170,7 +141,6 @@ export type MasterpassRestOptionCustomRender = {
|
|
|
170
141
|
linkModal?: (props: LinkModalProps) => ReactElement;
|
|
171
142
|
otpModal?: (props: OTPModalProps) => ReactElement;
|
|
172
143
|
confirmationModal?: (props: ConfirmationModalProps) => ReactElement;
|
|
173
|
-
rewardSelectionModal?: (props: RewardSelectionModalProps) => ReactElement;
|
|
174
144
|
errorDisplay?: (props: ErrorDisplayProps) => ReactElement;
|
|
175
145
|
loadingState?: (props: LoadingStateProps) => ReactElement;
|
|
176
146
|
emptyState?: (props: EmptyStateProps) => ReactElement;
|
|
@@ -307,20 +277,4 @@ export type MasterpassRestOptionTexts = {
|
|
|
307
277
|
sessionExpiredMessage?: string;
|
|
308
278
|
sessionExpiredSecondaryMessage?: string;
|
|
309
279
|
sessionExpiredButton?: string;
|
|
310
|
-
|
|
311
|
-
rewardModalTitle?: string;
|
|
312
|
-
rewardModalDescription?: string;
|
|
313
|
-
rewardModalEmptyMessage?: string;
|
|
314
|
-
rewardModalConfirmText?: string;
|
|
315
|
-
rewardModalCancelText?: string;
|
|
316
|
-
rewardModalLoadingText?: string;
|
|
317
|
-
rewardOpenButtonText?: string;
|
|
318
|
-
rewardSelectedSummaryText?: string;
|
|
319
|
-
rewardAvailableSummaryText?: string;
|
|
320
|
-
rewardClearText?: string;
|
|
321
|
-
rewardCategorySpecialText?: string;
|
|
322
|
-
rewardCategoryGeneralText?: string;
|
|
323
|
-
rewardFailedToLoadText?: string;
|
|
324
|
-
rewardFailedToSelectText?: string;
|
|
325
|
-
rewardCappedNoticeText?: string;
|
|
326
280
|
};
|
|
@@ -69,15 +69,6 @@ export type TransactionType =
|
|
|
69
69
|
| 'DIRECT_PURCHASE'
|
|
70
70
|
| 'DIRECT_PURCHASE_3D';
|
|
71
71
|
|
|
72
|
-
export type RewardName = 'FBB' | 'BNS';
|
|
73
|
-
export type RewardCategory = 'special' | 'general';
|
|
74
|
-
|
|
75
|
-
export interface RewardItem {
|
|
76
|
-
type: RewardCategory;
|
|
77
|
-
amount: number | string;
|
|
78
|
-
name?: RewardName;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
72
|
export interface PaymentState {
|
|
82
73
|
selectedCard: any | null;
|
|
83
74
|
selectedInstallment: Installment | null;
|
|
@@ -87,9 +78,6 @@ export interface PaymentState {
|
|
|
87
78
|
orderCompleted: boolean;
|
|
88
79
|
cvc: string;
|
|
89
80
|
useThreeD: boolean;
|
|
90
|
-
availableRewards: RewardItem[];
|
|
91
|
-
selectedRewards: RewardItem[];
|
|
92
|
-
isLoadingRewards: boolean;
|
|
93
81
|
}
|
|
94
82
|
|
|
95
83
|
export type InformationModalType = 'warning' | 'error' | 'success' | 'info';
|
|
@@ -107,7 +95,6 @@ export interface ModalState {
|
|
|
107
95
|
showOTPModal: boolean;
|
|
108
96
|
show3DSecureModal: boolean;
|
|
109
97
|
showInformationModal: boolean;
|
|
110
|
-
showRewardModal: boolean;
|
|
111
98
|
informationModalData: InformationModalData | null;
|
|
112
99
|
otpType: OTPType;
|
|
113
100
|
verificationData: any | null;
|
package/src/utils/card-utils.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const formatCardNumber = (value: string): string => {
|
|
|
9
9
|
|
|
10
10
|
const formatted = cleanValue.replace(/(\d{4})(?=\d)/g, '$1 ');
|
|
11
11
|
|
|
12
|
-
return formatted.substring(0,
|
|
12
|
+
return formatted.substring(0, 23);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const formatExpiryDate = (value: string): string => {
|
|
@@ -29,8 +29,6 @@ import InstallmentList from '../components/installment-list';
|
|
|
29
29
|
import CreditCardForm from '../components/credit-card-form';
|
|
30
30
|
import PaymentMethodSelector from '../components/payment-method-selector';
|
|
31
31
|
import InformationModal from '../components/information-modal';
|
|
32
|
-
import RewardSelectionModal from '../components/reward-selection-modal';
|
|
33
|
-
import type { RewardItem } from '../types/payment.types';
|
|
34
32
|
import mpBlackLogo from '../assets/img/masterpass-black-logo.png';
|
|
35
33
|
|
|
36
34
|
interface MasterpassRestOptionProps {
|
|
@@ -40,7 +38,6 @@ interface MasterpassRestOptionProps {
|
|
|
40
38
|
environment?: MasterpassEnvironment;
|
|
41
39
|
texts?: MasterpassRestOptionTexts;
|
|
42
40
|
customRender?: MasterpassRestOptionCustomRender;
|
|
43
|
-
enableRewards?: boolean;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
const defaultTexts: MasterpassRestOptionTexts = {
|
|
@@ -161,23 +158,7 @@ const defaultTexts: MasterpassRestOptionTexts = {
|
|
|
161
158
|
sessionExpiredTitle: 'Session Expired',
|
|
162
159
|
sessionExpiredMessage: 'Your session has expired due to inactivity. Please restart the process to continue.',
|
|
163
160
|
sessionExpiredSecondaryMessage: 'For security reasons, sessions are only valid for a limited time.',
|
|
164
|
-
sessionExpiredButton: 'Start Again'
|
|
165
|
-
|
|
166
|
-
rewardModalTitle: 'Use Card Rewards',
|
|
167
|
-
rewardModalDescription: 'Select the rewards you want to apply to this order.',
|
|
168
|
-
rewardModalEmptyMessage: 'No rewards available for this card.',
|
|
169
|
-
rewardModalConfirmText: 'Apply',
|
|
170
|
-
rewardModalCancelText: 'Cancel',
|
|
171
|
-
rewardModalLoadingText: 'Applying...',
|
|
172
|
-
rewardOpenButtonText: 'Use Rewards',
|
|
173
|
-
rewardSelectedSummaryText: '{count} reward(s) applied',
|
|
174
|
-
rewardAvailableSummaryText: '{count} reward(s) available',
|
|
175
|
-
rewardClearText: 'Clear',
|
|
176
|
-
rewardCategorySpecialText: 'Special Rewards',
|
|
177
|
-
rewardCategoryGeneralText: 'General Rewards',
|
|
178
|
-
rewardFailedToLoadText: 'Failed to load rewards',
|
|
179
|
-
rewardFailedToSelectText: 'Failed to apply rewards',
|
|
180
|
-
rewardCappedNoticeText: '{amount} will be applied'
|
|
161
|
+
sessionExpiredButton: 'Start Again'
|
|
181
162
|
};
|
|
182
163
|
|
|
183
164
|
const mergeTexts = (
|
|
@@ -196,8 +177,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
196
177
|
sdkUrl,
|
|
197
178
|
environment,
|
|
198
179
|
texts = defaultTexts,
|
|
199
|
-
customRender
|
|
200
|
-
enableRewards = false
|
|
180
|
+
customRender
|
|
201
181
|
}) => {
|
|
202
182
|
const mergedTexts = React.useMemo(
|
|
203
183
|
() => mergeTexts(texts, defaultTexts),
|
|
@@ -221,7 +201,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
221
201
|
'stored_card' | 'new_card'
|
|
222
202
|
>('stored_card');
|
|
223
203
|
const [cvc, setCvc] = useState<string>('');
|
|
224
|
-
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
|
225
204
|
|
|
226
205
|
const [setMasterpassRestBinNumber] = useSetMasterpassRestBinNumberMutation();
|
|
227
206
|
|
|
@@ -230,18 +209,12 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
230
209
|
isInstallmentLoading,
|
|
231
210
|
isPrepareLoading,
|
|
232
211
|
isFinalizeLoading,
|
|
233
|
-
isRewardsQueryLoading,
|
|
234
|
-
isRewardsSelectLoading,
|
|
235
212
|
updatePaymentState,
|
|
236
213
|
handleCardSelect,
|
|
237
214
|
handleInstallmentSelect,
|
|
238
215
|
processPayment,
|
|
239
|
-
processDirectPayment
|
|
240
|
-
|
|
241
|
-
closeRewardModal,
|
|
242
|
-
confirmRewards,
|
|
243
|
-
payableAmount
|
|
244
|
-
} = useMasterpassPayment({ enableRewards });
|
|
216
|
+
processDirectPayment
|
|
217
|
+
} = useMasterpassPayment();
|
|
245
218
|
|
|
246
219
|
const {
|
|
247
220
|
accountData,
|
|
@@ -409,10 +382,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
409
382
|
}, [dispatch, handleAddCard, mergedTexts.failedToSaveCardText]);
|
|
410
383
|
|
|
411
384
|
const handleProceedToPayment = useCallback(async () => {
|
|
412
|
-
if (isProcessingPayment) return;
|
|
413
|
-
|
|
414
|
-
setIsProcessingPayment(true);
|
|
415
|
-
|
|
416
385
|
try {
|
|
417
386
|
dispatch(clearAllErrors());
|
|
418
387
|
|
|
@@ -439,10 +408,8 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
439
408
|
const errorMessage =
|
|
440
409
|
error instanceof Error ? error.message : mergedTexts.paymentFailedText;
|
|
441
410
|
dispatch(setError(errorMessage));
|
|
442
|
-
} finally {
|
|
443
|
-
setIsProcessingPayment(false);
|
|
444
411
|
}
|
|
445
|
-
}, [
|
|
412
|
+
}, [dispatch, paymentMethod, processDirectPayment, processPayment, updateModalState, mergedTexts.paymentFailedText]);
|
|
446
413
|
|
|
447
414
|
const handleOTPSubmitWithErrorHandling = useCallback(async (otp: string) => {
|
|
448
415
|
if (!handleOTPSubmit)
|
|
@@ -482,18 +449,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
482
449
|
}
|
|
483
450
|
}, [dispatch, handleInstallmentSelect, mergedTexts.failedToSelectInstallmentText]);
|
|
484
451
|
|
|
485
|
-
const handleConfirmRewards = useCallback(
|
|
486
|
-
async (selected: RewardItem[]) => {
|
|
487
|
-
const result = await confirmRewards(selected);
|
|
488
|
-
if (!result.success) {
|
|
489
|
-
dispatch(
|
|
490
|
-
setError(result.message || mergedTexts.rewardFailedToSelectText)
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
},
|
|
494
|
-
[confirmRewards, dispatch, mergedTexts.rewardFailedToSelectText]
|
|
495
|
-
);
|
|
496
|
-
|
|
497
452
|
const handleCvcChange = useCallback((newCvc: string) => {
|
|
498
453
|
setCvc(newCvc);
|
|
499
454
|
updatePaymentState({ cvc: newCvc });
|
|
@@ -531,8 +486,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
531
486
|
InstallmentList: wrapRenderFunction(customRenderRef.current?.installmentList) || InstallmentList,
|
|
532
487
|
LinkModal: wrapRenderFunction(customRenderRef.current?.linkModal) || LinkModal,
|
|
533
488
|
OTPModal: wrapRenderFunction(customRenderRef.current?.otpModal) || OTPModal,
|
|
534
|
-
ConfirmationModal: wrapRenderFunction(customRenderRef.current?.confirmationModal) || ConfirmationModal
|
|
535
|
-
RewardSelectionModal: wrapRenderFunction(customRenderRef.current?.rewardSelectionModal) || RewardSelectionModal
|
|
489
|
+
ConfirmationModal: wrapRenderFunction(customRenderRef.current?.confirmationModal) || ConfirmationModal
|
|
536
490
|
};
|
|
537
491
|
}, [
|
|
538
492
|
customRender?.paymentMethodSelector,
|
|
@@ -541,8 +495,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
541
495
|
customRender?.installmentList,
|
|
542
496
|
customRender?.linkModal,
|
|
543
497
|
customRender?.otpModal,
|
|
544
|
-
customRender?.confirmationModal
|
|
545
|
-
customRender?.rewardSelectionModal
|
|
498
|
+
customRender?.confirmationModal
|
|
546
499
|
]);
|
|
547
500
|
|
|
548
501
|
const hasStoredCards =
|
|
@@ -588,14 +541,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
588
541
|
isInstallmentLoading,
|
|
589
542
|
isPrepareLoading,
|
|
590
543
|
isFinalizeLoading,
|
|
591
|
-
isProcessingPayment,
|
|
592
|
-
isRewardsQueryLoading,
|
|
593
|
-
isRewardsSelectLoading,
|
|
594
|
-
|
|
595
|
-
openRewardModal,
|
|
596
|
-
closeRewardModal,
|
|
597
|
-
handleConfirmRewards,
|
|
598
|
-
payableAmount,
|
|
599
544
|
|
|
600
545
|
texts: mergedTexts,
|
|
601
546
|
|
|
@@ -628,13 +573,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
628
573
|
isInstallmentLoading,
|
|
629
574
|
isPrepareLoading,
|
|
630
575
|
isFinalizeLoading,
|
|
631
|
-
isProcessingPayment,
|
|
632
|
-
isRewardsQueryLoading,
|
|
633
|
-
isRewardsSelectLoading,
|
|
634
|
-
openRewardModal,
|
|
635
|
-
closeRewardModal,
|
|
636
|
-
handleConfirmRewards,
|
|
637
|
-
payableAmount,
|
|
638
576
|
mergedTexts,
|
|
639
577
|
memoizedComponents
|
|
640
578
|
]);
|
|
@@ -713,20 +651,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
713
651
|
removingCardId: modalState?.removingCardId,
|
|
714
652
|
cvc: cvc,
|
|
715
653
|
onCvcChange: handleCvcChange,
|
|
716
|
-
...(enableRewards
|
|
717
|
-
? {
|
|
718
|
-
availableRewards: paymentState.availableRewards,
|
|
719
|
-
selectedRewards: paymentState.selectedRewards,
|
|
720
|
-
isLoadingRewards:
|
|
721
|
-
paymentState.isLoadingRewards ||
|
|
722
|
-
isRewardsQueryLoading,
|
|
723
|
-
isConfirmingRewards: isRewardsSelectLoading,
|
|
724
|
-
onOpenRewardModal: openRewardModal,
|
|
725
|
-
onConfirmRewards: handleConfirmRewards,
|
|
726
|
-
rewardCurrency: stateCurrency || currency,
|
|
727
|
-
rewardPayableAmount: payableAmount
|
|
728
|
-
}
|
|
729
|
-
: {}),
|
|
730
654
|
texts: mergedTexts
|
|
731
655
|
})
|
|
732
656
|
) : (
|
|
@@ -738,20 +662,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
738
662
|
removingCardId={modalState?.removingCardId}
|
|
739
663
|
cvc={cvc}
|
|
740
664
|
onCvcChange={handleCvcChange}
|
|
741
|
-
{...(enableRewards
|
|
742
|
-
? {
|
|
743
|
-
availableRewards: paymentState.availableRewards,
|
|
744
|
-
selectedRewards: paymentState.selectedRewards,
|
|
745
|
-
isLoadingRewards:
|
|
746
|
-
paymentState.isLoadingRewards ||
|
|
747
|
-
isRewardsQueryLoading,
|
|
748
|
-
isConfirmingRewards: isRewardsSelectLoading,
|
|
749
|
-
onOpenRewardModal: openRewardModal,
|
|
750
|
-
onConfirmRewards: handleConfirmRewards,
|
|
751
|
-
rewardCurrency: stateCurrency || currency,
|
|
752
|
-
rewardPayableAmount: payableAmount
|
|
753
|
-
}
|
|
754
|
-
: {})}
|
|
755
665
|
texts={mergedTexts}
|
|
756
666
|
/>
|
|
757
667
|
)}
|
|
@@ -797,10 +707,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
797
707
|
selectedInstallment: paymentState.selectedInstallment,
|
|
798
708
|
isLoading: isInstallmentLoading || isPrepareLoading,
|
|
799
709
|
onProceedToPayment: handleProceedToPayment,
|
|
800
|
-
paymentLoading:
|
|
801
|
-
isProcessingPayment ||
|
|
802
|
-
isPrepareLoading ||
|
|
803
|
-
isFinalizeLoading,
|
|
710
|
+
paymentLoading: isFinalizeLoading,
|
|
804
711
|
texts: mergedTexts
|
|
805
712
|
})
|
|
806
713
|
) : (
|
|
@@ -811,11 +718,7 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
811
718
|
selectedInstallment={paymentState.selectedInstallment}
|
|
812
719
|
isLoading={isInstallmentLoading || isPrepareLoading}
|
|
813
720
|
onProceedToPayment={handleProceedToPayment}
|
|
814
|
-
paymentLoading={
|
|
815
|
-
isProcessingPayment ||
|
|
816
|
-
isPrepareLoading ||
|
|
817
|
-
isFinalizeLoading
|
|
818
|
-
}
|
|
721
|
+
paymentLoading={isFinalizeLoading}
|
|
819
722
|
texts={mergedTexts}
|
|
820
723
|
/>
|
|
821
724
|
)
|
|
@@ -991,33 +894,6 @@ const MasterpassRestOption: React.FC<MasterpassRestOptionProps> = ({
|
|
|
991
894
|
onClose={() => updateModalState({ showInformationModal: false, informationModalData: null })}
|
|
992
895
|
data={modalState?.informationModalData}
|
|
993
896
|
/>
|
|
994
|
-
|
|
995
|
-
{enableRewards &&
|
|
996
|
-
(customRender?.rewardSelectionModal ? (
|
|
997
|
-
customRender.rewardSelectionModal({
|
|
998
|
-
open: modalState?.showRewardModal ?? false,
|
|
999
|
-
onClose: closeRewardModal,
|
|
1000
|
-
onConfirm: handleConfirmRewards,
|
|
1001
|
-
rewards: paymentState.availableRewards,
|
|
1002
|
-
selectedRewards: paymentState.selectedRewards,
|
|
1003
|
-
isLoading: isRewardsSelectLoading,
|
|
1004
|
-
currency: stateCurrency || currency,
|
|
1005
|
-
payableAmount,
|
|
1006
|
-
texts: mergedTexts
|
|
1007
|
-
})
|
|
1008
|
-
) : (
|
|
1009
|
-
<RewardSelectionModal
|
|
1010
|
-
open={modalState?.showRewardModal ?? false}
|
|
1011
|
-
onClose={closeRewardModal}
|
|
1012
|
-
onConfirm={handleConfirmRewards}
|
|
1013
|
-
rewards={paymentState.availableRewards}
|
|
1014
|
-
selectedRewards={paymentState.selectedRewards}
|
|
1015
|
-
isLoading={isRewardsSelectLoading}
|
|
1016
|
-
currency={stateCurrency || currency}
|
|
1017
|
-
payableAmount={payableAmount}
|
|
1018
|
-
texts={mergedTexts}
|
|
1019
|
-
/>
|
|
1020
|
-
))}
|
|
1021
897
|
</div>
|
|
1022
898
|
);
|
|
1023
899
|
};
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { Modal, Button } from '@akinon/next/components';
|
|
3
|
-
import { RewardSelectionModalProps } from '../types/custom-render.types';
|
|
4
|
-
import type { RewardItem, RewardCategory } from '../types/payment.types';
|
|
5
|
-
import { parseRewardAmount, getCappedRewardAmounts } from '../utils/reward-utils';
|
|
6
|
-
|
|
7
|
-
const formatAmount = (amount: number | string, currency?: string) => {
|
|
8
|
-
const formatted = parseRewardAmount(amount).toFixed(2);
|
|
9
|
-
return currency ? `${formatted} ${currency.toUpperCase()}` : formatted;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const isSameReward = (a: RewardItem, b: RewardItem) => a.type === b.type;
|
|
13
|
-
|
|
14
|
-
const RewardSelectionModal: React.FC<RewardSelectionModalProps> = ({
|
|
15
|
-
open,
|
|
16
|
-
onClose,
|
|
17
|
-
onConfirm,
|
|
18
|
-
rewards,
|
|
19
|
-
selectedRewards,
|
|
20
|
-
isLoading = false,
|
|
21
|
-
currency,
|
|
22
|
-
payableAmount,
|
|
23
|
-
texts
|
|
24
|
-
}) => {
|
|
25
|
-
const [draft, setDraft] = useState<RewardItem[]>(selectedRewards);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (open) {
|
|
29
|
-
setDraft(selectedRewards);
|
|
30
|
-
}
|
|
31
|
-
}, [open, selectedRewards]);
|
|
32
|
-
|
|
33
|
-
const cappedAmounts = useMemo(
|
|
34
|
-
() => getCappedRewardAmounts(draft, payableAmount),
|
|
35
|
-
[draft, payableAmount]
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const grouped = useMemo(() => {
|
|
39
|
-
const map: Record<RewardCategory, RewardItem[]> = {
|
|
40
|
-
special: [],
|
|
41
|
-
general: []
|
|
42
|
-
};
|
|
43
|
-
rewards.forEach((reward) => {
|
|
44
|
-
if (map[reward.type]) {
|
|
45
|
-
map[reward.type].push(reward);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
return map;
|
|
49
|
-
}, [rewards]);
|
|
50
|
-
|
|
51
|
-
const toggleReward = (reward: RewardItem) => {
|
|
52
|
-
setDraft((current) => {
|
|
53
|
-
const exists = current.some((item) => isSameReward(item, reward));
|
|
54
|
-
if (exists) {
|
|
55
|
-
return current.filter((item) => !isSameReward(item, reward));
|
|
56
|
-
}
|
|
57
|
-
const withoutSameType = current.filter(
|
|
58
|
-
(item) => item.type !== reward.type
|
|
59
|
-
);
|
|
60
|
-
return [...withoutSameType, reward];
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const isSelected = (reward: RewardItem) =>
|
|
65
|
-
draft.some((item) => isSameReward(item, reward));
|
|
66
|
-
|
|
67
|
-
const categoryLabel = (category: RewardCategory) =>
|
|
68
|
-
category === 'special'
|
|
69
|
-
? texts.rewardCategorySpecialText
|
|
70
|
-
: texts.rewardCategoryGeneralText;
|
|
71
|
-
|
|
72
|
-
const handleConfirm = async () => {
|
|
73
|
-
await onConfirm(draft);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const hasRewards = rewards.length > 0;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Modal
|
|
80
|
-
portalId="masterpass-reward-modal"
|
|
81
|
-
open={open}
|
|
82
|
-
setOpen={onClose}
|
|
83
|
-
title={texts.rewardModalTitle}
|
|
84
|
-
className="w-full sm:w-[32rem] max-h-[90vh] overflow-y-auto"
|
|
85
|
-
>
|
|
86
|
-
<div className="px-6 pt-4 pb-6">
|
|
87
|
-
{texts.rewardModalDescription && (
|
|
88
|
-
<p className="text-sm text-gray-600 mb-4">
|
|
89
|
-
{texts.rewardModalDescription}
|
|
90
|
-
</p>
|
|
91
|
-
)}
|
|
92
|
-
|
|
93
|
-
{!hasRewards ? (
|
|
94
|
-
<p className="text-center text-gray-500 py-8">
|
|
95
|
-
{texts.rewardModalEmptyMessage}
|
|
96
|
-
</p>
|
|
97
|
-
) : (
|
|
98
|
-
<div className="space-y-5">
|
|
99
|
-
{(Object.keys(grouped) as RewardCategory[])
|
|
100
|
-
.filter((category) => grouped[category].length > 0)
|
|
101
|
-
.map((category) => (
|
|
102
|
-
<div key={category}>
|
|
103
|
-
<h3 className="text-sm font-semibold text-gray-700 mb-2">
|
|
104
|
-
{categoryLabel(category)}
|
|
105
|
-
</h3>
|
|
106
|
-
<ul className="space-y-2">
|
|
107
|
-
{grouped[category].map((reward) => {
|
|
108
|
-
const selected = isSelected(reward);
|
|
109
|
-
const fullValue = parseRewardAmount(reward.amount);
|
|
110
|
-
const capped = cappedAmounts[reward.type];
|
|
111
|
-
const isCapped = selected && capped < fullValue;
|
|
112
|
-
return (
|
|
113
|
-
<li key={`${reward.type}-${reward.name ?? ''}`}>
|
|
114
|
-
<label
|
|
115
|
-
className={`flex items-center justify-between gap-3 px-3 py-3 border cursor-pointer transition-colors ${
|
|
116
|
-
selected
|
|
117
|
-
? 'border-primary bg-primary/5'
|
|
118
|
-
: 'border-gray-200 hover:bg-gray-50'
|
|
119
|
-
}`}
|
|
120
|
-
>
|
|
121
|
-
<div className="flex items-center gap-3">
|
|
122
|
-
<input
|
|
123
|
-
type="checkbox"
|
|
124
|
-
className="h-4 w-4"
|
|
125
|
-
checked={selected}
|
|
126
|
-
onChange={() => toggleReward(reward)}
|
|
127
|
-
disabled={isLoading}
|
|
128
|
-
/>
|
|
129
|
-
<div>
|
|
130
|
-
<p className="font-medium">
|
|
131
|
-
{reward.name || categoryLabel(reward.type)}
|
|
132
|
-
</p>
|
|
133
|
-
{reward.name && (
|
|
134
|
-
<p className="text-xs text-gray-500">
|
|
135
|
-
{categoryLabel(reward.type)}
|
|
136
|
-
</p>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<div className="text-right">
|
|
141
|
-
<span
|
|
142
|
-
className={`font-semibold ${
|
|
143
|
-
isCapped
|
|
144
|
-
? 'text-gray-400 line-through text-sm'
|
|
145
|
-
: ''
|
|
146
|
-
}`}
|
|
147
|
-
>
|
|
148
|
-
{formatAmount(reward.amount, currency)}
|
|
149
|
-
</span>
|
|
150
|
-
{isCapped && (
|
|
151
|
-
<p className="text-xs text-[#00a63d] font-medium mt-0.5">
|
|
152
|
-
{(texts.rewardCappedNoticeText || '').replace(
|
|
153
|
-
'{amount}',
|
|
154
|
-
formatAmount(capped, currency)
|
|
155
|
-
)}
|
|
156
|
-
</p>
|
|
157
|
-
)}
|
|
158
|
-
</div>
|
|
159
|
-
</label>
|
|
160
|
-
</li>
|
|
161
|
-
);
|
|
162
|
-
})}
|
|
163
|
-
</ul>
|
|
164
|
-
</div>
|
|
165
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<div className="flex gap-3 justify-end mt-6">
|
|
170
|
-
<Button
|
|
171
|
-
appearance="outlined"
|
|
172
|
-
className="px-5 py-3 h-auto"
|
|
173
|
-
onClick={onClose}
|
|
174
|
-
disabled={isLoading}
|
|
175
|
-
>
|
|
176
|
-
{texts.rewardModalCancelText}
|
|
177
|
-
</Button>
|
|
178
|
-
<Button
|
|
179
|
-
appearance="filled"
|
|
180
|
-
className="px-5 py-3 h-auto"
|
|
181
|
-
onClick={handleConfirm}
|
|
182
|
-
disabled={isLoading || !hasRewards}
|
|
183
|
-
>
|
|
184
|
-
{isLoading
|
|
185
|
-
? texts.rewardModalLoadingText
|
|
186
|
-
: texts.rewardModalConfirmText}
|
|
187
|
-
</Button>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
</Modal>
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
export default RewardSelectionModal;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { RewardItem, RewardCategory } from '../types/payment.types';
|
|
2
|
-
|
|
3
|
-
export const REWARD_PRIORITY: RewardCategory[] = ['special', 'general'];
|
|
4
|
-
|
|
5
|
-
export const parseRewardAmount = (amount: number | string): number => {
|
|
6
|
-
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
7
|
-
return Number.isFinite(value) ? value : 0;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const getCappedRewardAmounts = (
|
|
11
|
-
selected: RewardItem[],
|
|
12
|
-
payableAmount?: string | number | null
|
|
13
|
-
): Record<RewardCategory, number> => {
|
|
14
|
-
const parsedPayable =
|
|
15
|
-
payableAmount != null ? parseRewardAmount(payableAmount) : NaN;
|
|
16
|
-
let remaining = Number.isFinite(parsedPayable) ? parsedPayable : Infinity;
|
|
17
|
-
|
|
18
|
-
const result: Record<RewardCategory, number> = { special: 0, general: 0 };
|
|
19
|
-
|
|
20
|
-
REWARD_PRIORITY.forEach((type) => {
|
|
21
|
-
const picked = selected.find((item) => item.type === type);
|
|
22
|
-
if (!picked) return;
|
|
23
|
-
|
|
24
|
-
const value = parseRewardAmount(picked.amount);
|
|
25
|
-
if (value <= 0) return;
|
|
26
|
-
|
|
27
|
-
const used = Math.min(value, remaining);
|
|
28
|
-
remaining = Math.max(remaining - used, 0);
|
|
29
|
-
result[type] = used;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const getCappedRewardTotal = (
|
|
36
|
-
selected: RewardItem[],
|
|
37
|
-
payableAmount?: string | number | null
|
|
38
|
-
): number => {
|
|
39
|
-
const amounts = getCappedRewardAmounts(selected, payableAmount);
|
|
40
|
-
return REWARD_PRIORITY.reduce((sum, type) => sum + amounts[type], 0);
|
|
41
|
-
};
|