@akinon/pz-masterpass-rest 2.0.18 → 2.0.19-rc.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/CHANGELOG.md +10 -0
- package/docs/USAGE.md +332 -61
- package/package.json +2 -2
- package/src/components/card-list.tsx +72 -2
- package/src/components/credit-card-form.tsx +1 -1
- package/src/components/installment-list.tsx +1 -1
- package/src/components/otp-modal.tsx +12 -4
- package/src/components/reward-selection-modal.tsx +194 -0
- package/src/hooks/useMasterpassAccount.ts +21 -5
- package/src/hooks/useMasterpassPayment.ts +112 -13
- package/src/index.ts +2 -0
- package/src/redux/api.ts +82 -1
- package/src/redux/reducer.ts +35 -3
- package/src/types/custom-render.types.ts +47 -1
- package/src/types/payment.types.ts +13 -0
- package/src/utils/card-utils.ts +1 -1
- package/src/utils/reward-utils.ts +41 -0
- package/src/views/masterpass-rest-option.tsx +133 -9
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { Input, Icon } from '@akinon/next/components';
|
|
3
3
|
import { detectCardType, getCardIcon, getCvcLength } from '../utils/card-utils';
|
|
4
|
+
import { getCappedRewardTotal } from '../utils/reward-utils';
|
|
4
5
|
import type { CardListProps } from '../types/custom-render.types';
|
|
5
6
|
import MasterpassSecurityInfo from './masterpass-security-info';
|
|
6
7
|
|
|
@@ -13,6 +14,12 @@ const CardList: React.FC<CardListProps> = ({
|
|
|
13
14
|
cvc = '',
|
|
14
15
|
onCvcChange,
|
|
15
16
|
cvcRequired = false,
|
|
17
|
+
availableRewards = [],
|
|
18
|
+
selectedRewards = [],
|
|
19
|
+
isLoadingRewards = false,
|
|
20
|
+
onOpenRewardModal,
|
|
21
|
+
rewardCurrency,
|
|
22
|
+
rewardPayableAmount,
|
|
16
23
|
texts
|
|
17
24
|
}) => {
|
|
18
25
|
const [localCvc, setLocalCvc] = useState(cvc);
|
|
@@ -23,9 +30,21 @@ const CardList: React.FC<CardListProps> = ({
|
|
|
23
30
|
onCvcChange?.(numericValue);
|
|
24
31
|
};
|
|
25
32
|
|
|
33
|
+
const orderedCards = useMemo(() => {
|
|
34
|
+
if (!selectedCard) return cards;
|
|
35
|
+
const idx = cards.findIndex(
|
|
36
|
+
(c) => c.uniqueCardNumber === selectedCard.uniqueCardNumber
|
|
37
|
+
);
|
|
38
|
+
if (idx <= 0) return cards;
|
|
39
|
+
const reordered = [...cards];
|
|
40
|
+
const [picked] = reordered.splice(idx, 1);
|
|
41
|
+
reordered.unshift(picked);
|
|
42
|
+
return reordered;
|
|
43
|
+
}, [cards, selectedCard]);
|
|
44
|
+
|
|
26
45
|
return (
|
|
27
46
|
<div className="flex flex-col gap-4">
|
|
28
|
-
{
|
|
47
|
+
{orderedCards.map((card) => {
|
|
29
48
|
const isSelected =
|
|
30
49
|
selectedCard?.uniqueCardNumber === card.uniqueCardNumber;
|
|
31
50
|
const expectedCvcLength = getCvcLength(detectCardType(card.cardBin));
|
|
@@ -119,6 +138,57 @@ const CardList: React.FC<CardListProps> = ({
|
|
|
119
138
|
)}
|
|
120
139
|
</div>
|
|
121
140
|
|
|
141
|
+
{isSelected &&
|
|
142
|
+
(isLoadingRewards || availableRewards.length > 0) && (
|
|
143
|
+
<div
|
|
144
|
+
className="border-t border-[#eeeeee] p-4 bg-[#fafafa] flex items-center justify-between gap-3"
|
|
145
|
+
onClick={(e) => e.stopPropagation()}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
148
|
+
<Icon name="gift" size={16} className="text-[#000000]" />
|
|
149
|
+
<div className="min-w-0">
|
|
150
|
+
<p className="text-sm font-medium text-[#4a4f54] truncate">
|
|
151
|
+
{texts.rewardOpenButtonText}
|
|
152
|
+
</p>
|
|
153
|
+
{selectedRewards.length > 0 ? (
|
|
154
|
+
<p className="text-xs text-[#00a63d] truncate">
|
|
155
|
+
{(texts.rewardSelectedSummaryText || '').replace(
|
|
156
|
+
'{count}',
|
|
157
|
+
String(selectedRewards.length)
|
|
158
|
+
)}
|
|
159
|
+
{' · '}
|
|
160
|
+
{getCappedRewardTotal(
|
|
161
|
+
selectedRewards,
|
|
162
|
+
rewardPayableAmount
|
|
163
|
+
).toFixed(2)}{' '}
|
|
164
|
+
{(rewardCurrency || '').toUpperCase()}
|
|
165
|
+
</p>
|
|
166
|
+
) : (
|
|
167
|
+
<p className="text-xs text-[#9d9d9d] truncate">
|
|
168
|
+
{(texts.rewardAvailableSummaryText || '').replace(
|
|
169
|
+
'{count}',
|
|
170
|
+
String(availableRewards.length)
|
|
171
|
+
)}
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={(e) => {
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
onOpenRewardModal?.();
|
|
181
|
+
}}
|
|
182
|
+
disabled={isLoadingRewards}
|
|
183
|
+
className="px-3 py-1.5 text-xs border border-[#000000] text-[#000000] hover:bg-[#000000] hover:text-white transition-colors disabled:opacity-50 shrink-0"
|
|
184
|
+
>
|
|
185
|
+
{selectedRewards.length > 0
|
|
186
|
+
? texts.rewardModalConfirmText
|
|
187
|
+
: texts.rewardOpenButtonText}
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
122
192
|
{isSelected && cvcRequired && (
|
|
123
193
|
<div
|
|
124
194
|
className="border-t border-[#eeeeee] p-4 bg-[#f7f7f7]"
|
|
@@ -184,7 +184,7 @@ const CreditCardForm: React.FC<CreditCardFormProps> = ({
|
|
|
184
184
|
setValue('cardNumber', formatted, { shouldValidate: true });
|
|
185
185
|
}}
|
|
186
186
|
error={errors.cardNumber}
|
|
187
|
-
maxLength={
|
|
187
|
+
maxLength={19}
|
|
188
188
|
className="pr-20"
|
|
189
189
|
/>
|
|
190
190
|
{cardType !== 'unknown' && (
|
|
@@ -118,7 +118,7 @@ const InstallmentList: React.FC<InstallmentListProps> = ({
|
|
|
118
118
|
<Button
|
|
119
119
|
onClick={onProceedToPayment}
|
|
120
120
|
disabled={isLoading || paymentLoading}
|
|
121
|
-
className="w-full cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
|
121
|
+
className="w-full cursor-pointer bg-[#000000] text-[#ffffff] hover:bg-[#ffffff] hover:text-[#000000] border border-solid border-[#000000] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
122
122
|
>
|
|
123
123
|
{paymentLoading ? (
|
|
124
124
|
<div className="flex items-center justify-center">
|
|
@@ -204,15 +204,21 @@ const OTPModal: React.FC<OTPModalProps> = ({
|
|
|
204
204
|
>
|
|
205
205
|
<div className="px-6">
|
|
206
206
|
{getTitle() && (
|
|
207
|
-
<h3 className="text-center mt-4 text-lg font-semibold">
|
|
207
|
+
<h3 className="text-center mt-4 text-lg font-semibold">
|
|
208
|
+
{getTitle()}
|
|
209
|
+
</h3>
|
|
208
210
|
)}
|
|
209
|
-
<p className="text-center mt-2 text-sm text-gray-600">
|
|
211
|
+
<p className="text-center mt-2 text-sm text-gray-600">
|
|
212
|
+
{getDescription()}
|
|
213
|
+
</p>
|
|
210
214
|
<div className="flex flex-col gap-3 p-5 w-3/4 m-auto">
|
|
211
215
|
<Input
|
|
212
216
|
type="text"
|
|
213
217
|
value={otp}
|
|
214
218
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
215
|
-
setOtp(
|
|
219
|
+
setOtp(
|
|
220
|
+
e.target.value.replace(/\D/g, '').slice(0, getMaxLength())
|
|
221
|
+
);
|
|
216
222
|
setError(null);
|
|
217
223
|
}}
|
|
218
224
|
maxLength={getMaxLength()}
|
|
@@ -222,7 +228,9 @@ const OTPModal: React.FC<OTPModalProps> = ({
|
|
|
222
228
|
placeholder={getPlaceholder()}
|
|
223
229
|
/>
|
|
224
230
|
{getHelperText() && (
|
|
225
|
-
<p className="text-xs text-gray-500 text-center">
|
|
231
|
+
<p className="text-xs text-gray-500 text-center">
|
|
232
|
+
{getHelperText()}
|
|
233
|
+
</p>
|
|
226
234
|
)}
|
|
227
235
|
{error && (
|
|
228
236
|
<p className="text-[#d72b01] text-xs text-center">{error}</p>
|
|
@@ -0,0 +1,194 @@
|
|
|
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;
|
|
@@ -40,7 +40,16 @@ export const useMasterpassAccount = () => {
|
|
|
40
40
|
|
|
41
41
|
const initializeAccount = useCallback(
|
|
42
42
|
async (newTokenData: any, rawToken?: string) => {
|
|
43
|
-
if (!newTokenData?.AccountKey)
|
|
43
|
+
if (!newTokenData?.AccountKey) {
|
|
44
|
+
dispatch(
|
|
45
|
+
setAccountStatus({
|
|
46
|
+
isAccountNotFound: false,
|
|
47
|
+
isAccountNotLinked: true,
|
|
48
|
+
shouldShowDirectForm: true
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
44
53
|
|
|
45
54
|
const accountService = new AccountService(rawToken || token, newTokenData?.MerchantId);
|
|
46
55
|
|
|
@@ -126,7 +135,10 @@ export const useMasterpassAccount = () => {
|
|
|
126
135
|
dispatch(resetState());
|
|
127
136
|
}, [dispatch]);
|
|
128
137
|
|
|
129
|
-
const refreshToken = async (options?: {
|
|
138
|
+
const refreshToken = async (options?: {
|
|
139
|
+
three_d?: boolean;
|
|
140
|
+
skipReset?: boolean;
|
|
141
|
+
}) => {
|
|
130
142
|
if (options?.three_d !== false && !options?.skipReset) {
|
|
131
143
|
resetData();
|
|
132
144
|
}
|
|
@@ -218,8 +230,12 @@ export const useMasterpassAccount = () => {
|
|
|
218
230
|
informationModalData: {
|
|
219
231
|
type: 'warning',
|
|
220
232
|
title: texts?.sessionExpiredTitle || 'Session Expired',
|
|
221
|
-
message:
|
|
222
|
-
|
|
233
|
+
message:
|
|
234
|
+
texts?.sessionExpiredMessage ||
|
|
235
|
+
'Your session has expired due to inactivity. Please restart the process to continue.',
|
|
236
|
+
secondaryMessage:
|
|
237
|
+
texts?.sessionExpiredSecondaryMessage ||
|
|
238
|
+
'For security reasons, verification codes are only valid for a limited time.',
|
|
223
239
|
buttonText: texts?.sessionExpiredButton || 'Start Again'
|
|
224
240
|
}
|
|
225
241
|
})
|
|
@@ -335,7 +351,7 @@ export const useMasterpassAccount = () => {
|
|
|
335
351
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
336
352
|
|
|
337
353
|
if (!activeTokenData?.AccountKey) {
|
|
338
|
-
|
|
354
|
+
return { success: false, message: '' };
|
|
339
355
|
}
|
|
340
356
|
|
|
341
357
|
const accountService = new AccountService(activeToken, activeMerchantId);
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
useSetMasterpassRestBinNumberMutation,
|
|
6
6
|
useCheckoutMasterpassInstallmentMutation,
|
|
7
7
|
usePrepareMasterpassOrderMutation,
|
|
8
|
-
useFinalizeMasterpassOrderMutation
|
|
8
|
+
useFinalizeMasterpassOrderMutation,
|
|
9
|
+
useQueryMasterpassRewardsMutation,
|
|
10
|
+
useSelectMasterpassRewardsMutation
|
|
9
11
|
} from '../redux/api';
|
|
10
12
|
import {
|
|
11
13
|
updatePaymentState,
|
|
@@ -14,7 +16,12 @@ import {
|
|
|
14
16
|
setInstallments,
|
|
15
17
|
setCardType,
|
|
16
18
|
setOrderData,
|
|
17
|
-
setOrderCompleted
|
|
19
|
+
setOrderCompleted,
|
|
20
|
+
setAvailableRewards,
|
|
21
|
+
setSelectedRewards,
|
|
22
|
+
setLoadingRewards,
|
|
23
|
+
setShowRewardModal,
|
|
24
|
+
clearRewards
|
|
18
25
|
} from '../redux/reducer';
|
|
19
26
|
import { useMasterpassToken } from './useMasterpassToken';
|
|
20
27
|
import { PaymentService } from '../services/payment';
|
|
@@ -24,8 +31,9 @@ import {
|
|
|
24
31
|
getTransactionType
|
|
25
32
|
} from '../utils/payment-utils';
|
|
26
33
|
import { handleMasterpassResponse } from '../utils/response-handler';
|
|
27
|
-
import type { Installment } from '../types/payment.types';
|
|
34
|
+
import type { Installment, RewardItem } from '../types/payment.types';
|
|
28
35
|
import type { CardModel } from '../types/account.types';
|
|
36
|
+
import { getCappedRewardAmounts } from '../utils/reward-utils';
|
|
29
37
|
|
|
30
38
|
export type PaymentResult = {
|
|
31
39
|
success: boolean;
|
|
@@ -35,7 +43,14 @@ export type PaymentResult = {
|
|
|
35
43
|
message?: any;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
export
|
|
46
|
+
export interface UseMasterpassPaymentOptions {
|
|
47
|
+
enableRewards?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const useMasterpassPayment = (
|
|
51
|
+
options: UseMasterpassPaymentOptions = {}
|
|
52
|
+
) => {
|
|
53
|
+
const { enableRewards = false } = options;
|
|
39
54
|
const dispatch = useDispatch();
|
|
40
55
|
const {
|
|
41
56
|
paymentState,
|
|
@@ -46,6 +61,13 @@ export const useMasterpassPayment = () => {
|
|
|
46
61
|
tokenData
|
|
47
62
|
} = useAppSelector((state) => state.masterpassRest);
|
|
48
63
|
|
|
64
|
+
const payableAmount = useAppSelector(
|
|
65
|
+
(state) =>
|
|
66
|
+
state.checkout?.preOrder?.unpaid_amount ??
|
|
67
|
+
state.checkout?.preOrder?.total_amount_with_interest ??
|
|
68
|
+
null
|
|
69
|
+
);
|
|
70
|
+
|
|
49
71
|
const { refetch: refetchToken } = useMasterpassToken({
|
|
50
72
|
useThreeD: paymentState.useThreeD
|
|
51
73
|
});
|
|
@@ -62,6 +84,12 @@ export const useMasterpassPayment = () => {
|
|
|
62
84
|
const [finalizeMasterpassOrder, { isLoading: isFinalizeLoading }] =
|
|
63
85
|
useFinalizeMasterpassOrderMutation();
|
|
64
86
|
|
|
87
|
+
const [queryMasterpassRewards, { isLoading: isRewardsQueryLoading }] =
|
|
88
|
+
useQueryMasterpassRewardsMutation();
|
|
89
|
+
|
|
90
|
+
const [selectMasterpassRewards, { isLoading: isRewardsSelectLoading }] =
|
|
91
|
+
useSelectMasterpassRewardsMutation();
|
|
92
|
+
|
|
65
93
|
const refreshTokenForPayment = useCallback(async () => {
|
|
66
94
|
const result = await refetchToken({ skipReset: true });
|
|
67
95
|
if (result?.data?.token) {
|
|
@@ -97,12 +125,38 @@ export const useMasterpassPayment = () => {
|
|
|
97
125
|
[checkoutMasterpassInstallment, dispatch]
|
|
98
126
|
);
|
|
99
127
|
|
|
128
|
+
const fetchRewardsForCard = useCallback(
|
|
129
|
+
async (card: CardModel) => {
|
|
130
|
+
if (!card?.cardAlias) return;
|
|
131
|
+
|
|
132
|
+
dispatch(setLoadingRewards(true));
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const result = await queryMasterpassRewards({
|
|
136
|
+
card_alias: card.cardAlias
|
|
137
|
+
}).unwrap();
|
|
138
|
+
|
|
139
|
+
const rewards =
|
|
140
|
+
result.context_list?.[0]?.page_context?.rewards || [];
|
|
141
|
+
dispatch(setAvailableRewards(rewards));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
dispatch(setAvailableRewards([]));
|
|
144
|
+
} finally {
|
|
145
|
+
dispatch(setLoadingRewards(false));
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[queryMasterpassRewards, dispatch]
|
|
149
|
+
);
|
|
150
|
+
|
|
100
151
|
const handleCardSelect = useCallback(
|
|
101
152
|
async (card: CardModel) => {
|
|
102
153
|
dispatch(setSelectedCard(card));
|
|
103
154
|
dispatch(setInstallments([]));
|
|
104
155
|
dispatch(setSelectedInstallment(null));
|
|
105
156
|
dispatch(setCardType(null));
|
|
157
|
+
if (enableRewards) {
|
|
158
|
+
dispatch(clearRewards());
|
|
159
|
+
}
|
|
106
160
|
|
|
107
161
|
try {
|
|
108
162
|
const result = await setMasterpassRestBinNumber({
|
|
@@ -119,11 +173,53 @@ export const useMasterpassPayment = () => {
|
|
|
119
173
|
await handleInstallmentSelect(pageContext.installments[0]);
|
|
120
174
|
}
|
|
121
175
|
}
|
|
176
|
+
|
|
177
|
+
if (enableRewards) {
|
|
178
|
+
void fetchRewardsForCard(card);
|
|
179
|
+
}
|
|
122
180
|
} catch (error) {
|
|
123
181
|
// Silently handle errors (e.g., mutation aborted due to rapid selection)
|
|
124
182
|
}
|
|
125
183
|
},
|
|
126
|
-
[
|
|
184
|
+
[
|
|
185
|
+
enableRewards,
|
|
186
|
+
setMasterpassRestBinNumber,
|
|
187
|
+
handleInstallmentSelect,
|
|
188
|
+
fetchRewardsForCard,
|
|
189
|
+
dispatch
|
|
190
|
+
]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const openRewardModal = useCallback(() => {
|
|
194
|
+
dispatch(setShowRewardModal(true));
|
|
195
|
+
}, [dispatch]);
|
|
196
|
+
|
|
197
|
+
const closeRewardModal = useCallback(() => {
|
|
198
|
+
dispatch(setShowRewardModal(false));
|
|
199
|
+
}, [dispatch]);
|
|
200
|
+
|
|
201
|
+
const confirmRewards = useCallback(
|
|
202
|
+
async (selected: RewardItem[]) => {
|
|
203
|
+
const capped = getCappedRewardAmounts(selected, payableAmount);
|
|
204
|
+
const special = capped.special.toFixed(2);
|
|
205
|
+
const general = capped.general.toFixed(2);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await selectMasterpassRewards({ special, general }).unwrap();
|
|
209
|
+
dispatch(setSelectedRewards(selected));
|
|
210
|
+
dispatch(setShowRewardModal(false));
|
|
211
|
+
return { success: true };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
message:
|
|
216
|
+
error instanceof Error
|
|
217
|
+
? error.message
|
|
218
|
+
: 'Failed to apply rewards'
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
[selectMasterpassRewards, payableAmount, dispatch]
|
|
127
223
|
);
|
|
128
224
|
|
|
129
225
|
const processPayment = useCallback(async (): Promise<PaymentResult> => {
|
|
@@ -251,10 +347,6 @@ export const useMasterpassPayment = () => {
|
|
|
251
347
|
const activeToken = freshResult?.token;
|
|
252
348
|
const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId;
|
|
253
349
|
|
|
254
|
-
if (!activeTokenData?.AccountKey) {
|
|
255
|
-
return { success: false, message: 'Token data not available' };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
350
|
const prepareResult = await prepareMasterpassOrder({
|
|
259
351
|
use_three_d: paymentState.useThreeD
|
|
260
352
|
}).unwrap();
|
|
@@ -268,13 +360,13 @@ export const useMasterpassPayment = () => {
|
|
|
268
360
|
cardHolderName: newCardFormData.cardholderName || '',
|
|
269
361
|
cardAlias: newCardFormData.cardAlias || '',
|
|
270
362
|
expiryDate: newCardFormData.expiryDate,
|
|
271
|
-
accountKey: activeTokenData
|
|
363
|
+
accountKey: activeTokenData?.AccountKey || '',
|
|
272
364
|
amount: prepareResult.pre_order?.total_amount_with_interest,
|
|
273
365
|
orderNo: prepareResult.context_list?.[0]?.page_context?.order_no,
|
|
274
366
|
currencyCode: currency,
|
|
275
367
|
installmentCount: selectedInstallment.installment_count,
|
|
276
|
-
authenticationMethod: activeTokenData
|
|
277
|
-
secure3DModel: activeTokenData
|
|
368
|
+
authenticationMethod: activeTokenData?.AuthenticationMethod,
|
|
369
|
+
secure3DModel: activeTokenData?.Secure3dType,
|
|
278
370
|
terminalGroupId,
|
|
279
371
|
acquirerIcaNumber: bankIca,
|
|
280
372
|
additionalFields:
|
|
@@ -346,11 +438,18 @@ export const useMasterpassPayment = () => {
|
|
|
346
438
|
isInstallmentLoading,
|
|
347
439
|
isPrepareLoading,
|
|
348
440
|
isFinalizeLoading,
|
|
441
|
+
isRewardsQueryLoading,
|
|
442
|
+
isRewardsSelectLoading,
|
|
443
|
+
payableAmount,
|
|
349
444
|
|
|
350
445
|
updatePaymentState: updatePaymentStateAction,
|
|
351
446
|
handleCardSelect,
|
|
352
447
|
handleInstallmentSelect,
|
|
353
448
|
processPayment,
|
|
354
|
-
processDirectPayment
|
|
449
|
+
processDirectPayment,
|
|
450
|
+
fetchRewardsForCard,
|
|
451
|
+
openRewardModal,
|
|
452
|
+
closeRewardModal,
|
|
453
|
+
confirmRewards
|
|
355
454
|
};
|
|
356
455
|
};
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { default as MasterpassRestOption } from './views/masterpass-rest-option'
|
|
|
4
4
|
|
|
5
5
|
export { default as CreditCardForm } from './components/credit-card-form';
|
|
6
6
|
export { default as PaymentMethodSelector } from './components/payment-method-selector';
|
|
7
|
+
export { default as RewardSelectionModal } from './components/reward-selection-modal';
|
|
7
8
|
|
|
8
9
|
export { default as masterpassRestReducer } from './redux/reducer';
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ export { useMasterpassScript } from './hooks/useMasterpassScript';
|
|
|
14
15
|
|
|
15
16
|
export * from './utils/payment-constants';
|
|
16
17
|
export * from './utils/payment-utils';
|
|
18
|
+
export * from './utils/reward-utils';
|
|
17
19
|
export * from './utils/card-utils';
|
|
18
20
|
export * from './utils/validation-schemas';
|
|
19
21
|
export * from './utils/masterpass-sdk';
|