@akinon/pz-masterpass-rest 2.0.33-beta.0 → 2.0.33
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 +5 -1
- package/docs/USAGE.md +332 -61
- package/package.json +1 -1
- package/src/components/card-list.tsx +72 -2
- package/src/components/reward-selection-modal.tsx +194 -0
- package/src/hooks/useMasterpassPayment.ts +109 -6
- 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/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]"
|
|
@@ -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;
|
|
@@ -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> => {
|
|
@@ -346,11 +442,18 @@ export const useMasterpassPayment = () => {
|
|
|
346
442
|
isInstallmentLoading,
|
|
347
443
|
isPrepareLoading,
|
|
348
444
|
isFinalizeLoading,
|
|
445
|
+
isRewardsQueryLoading,
|
|
446
|
+
isRewardsSelectLoading,
|
|
447
|
+
payableAmount,
|
|
349
448
|
|
|
350
449
|
updatePaymentState: updatePaymentStateAction,
|
|
351
450
|
handleCardSelect,
|
|
352
451
|
handleInstallmentSelect,
|
|
353
452
|
processPayment,
|
|
354
|
-
processDirectPayment
|
|
453
|
+
processDirectPayment,
|
|
454
|
+
fetchRewardsForCard,
|
|
455
|
+
openRewardModal,
|
|
456
|
+
closeRewardModal,
|
|
457
|
+
confirmRewards
|
|
355
458
|
};
|
|
356
459
|
};
|
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';
|
package/src/redux/api.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { api } from '@akinon/next/data/client/api';
|
|
2
2
|
import { buildClientRequestUrl } from '@akinon/next/utils';
|
|
3
3
|
import { setPaymentStepBusy } from '@akinon/next/redux/reducers/checkout';
|
|
4
|
+
import type { RewardItem } from '../types/payment.types';
|
|
4
5
|
|
|
5
6
|
export interface MasterpassRestTokenRequest {
|
|
6
7
|
three_d?: boolean;
|
|
@@ -97,6 +98,39 @@ interface CardType {
|
|
|
97
98
|
logo: string;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
export interface QueryMasterpassRewardsRequest {
|
|
102
|
+
card_alias: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface QueryMasterpassRewardsResponse {
|
|
106
|
+
context_list: Array<{
|
|
107
|
+
page_context: {
|
|
108
|
+
rewards: RewardItem[];
|
|
109
|
+
};
|
|
110
|
+
page_name: string;
|
|
111
|
+
page_slug: string;
|
|
112
|
+
}>;
|
|
113
|
+
template_name: string;
|
|
114
|
+
errors: any;
|
|
115
|
+
pre_order: any;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface SelectMasterpassRewardsRequest {
|
|
119
|
+
special: string;
|
|
120
|
+
general: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SelectMasterpassRewardsResponse {
|
|
124
|
+
context_list: Array<{
|
|
125
|
+
page_context: any;
|
|
126
|
+
page_name: string;
|
|
127
|
+
page_slug: string;
|
|
128
|
+
}>;
|
|
129
|
+
template_name: string;
|
|
130
|
+
errors: any;
|
|
131
|
+
pre_order: any;
|
|
132
|
+
}
|
|
133
|
+
|
|
100
134
|
export interface SetMasterpassRestBinNumberResponse {
|
|
101
135
|
context_list: Array<{
|
|
102
136
|
page_context: {
|
|
@@ -202,6 +236,51 @@ export const masterpassRestApi = api.injectEndpoints({
|
|
|
202
236
|
dispatch(setPaymentStepBusy(false));
|
|
203
237
|
}
|
|
204
238
|
}),
|
|
239
|
+
queryMasterpassRewards: build.mutation<
|
|
240
|
+
QueryMasterpassRewardsResponse,
|
|
241
|
+
QueryMasterpassRewardsRequest
|
|
242
|
+
>({
|
|
243
|
+
query: (arg) => ({
|
|
244
|
+
url: buildClientRequestUrl('/orders/checkout/', {
|
|
245
|
+
useFormData: true
|
|
246
|
+
}),
|
|
247
|
+
method: 'POST',
|
|
248
|
+
params: {
|
|
249
|
+
page: 'MasterpassRestRewardListPage'
|
|
250
|
+
},
|
|
251
|
+
body: {
|
|
252
|
+
card_alias: arg.card_alias
|
|
253
|
+
}
|
|
254
|
+
}),
|
|
255
|
+
async onQueryStarted(_, { dispatch, queryFulfilled }) {
|
|
256
|
+
dispatch(setPaymentStepBusy(true));
|
|
257
|
+
await queryFulfilled;
|
|
258
|
+
dispatch(setPaymentStepBusy(false));
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
selectMasterpassRewards: build.mutation<
|
|
262
|
+
SelectMasterpassRewardsResponse,
|
|
263
|
+
SelectMasterpassRewardsRequest
|
|
264
|
+
>({
|
|
265
|
+
query: (arg) => ({
|
|
266
|
+
url: buildClientRequestUrl('/orders/checkout/', {
|
|
267
|
+
useFormData: true
|
|
268
|
+
}),
|
|
269
|
+
method: 'POST',
|
|
270
|
+
params: {
|
|
271
|
+
page: 'MasterpassRestRewardSelectionPage'
|
|
272
|
+
},
|
|
273
|
+
body: {
|
|
274
|
+
special: arg.special,
|
|
275
|
+
general: arg.general
|
|
276
|
+
}
|
|
277
|
+
}),
|
|
278
|
+
async onQueryStarted(_, { dispatch, queryFulfilled }) {
|
|
279
|
+
dispatch(setPaymentStepBusy(true));
|
|
280
|
+
await queryFulfilled;
|
|
281
|
+
dispatch(setPaymentStepBusy(false));
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
205
284
|
finalizeMasterpassOrder: build.mutation<
|
|
206
285
|
FinalizeMasterpassOrderResponse,
|
|
207
286
|
FinalizeMasterpassOrderRequest
|
|
@@ -239,5 +318,7 @@ export const {
|
|
|
239
318
|
useSetMasterpassRestBinNumberMutation,
|
|
240
319
|
useCheckoutMasterpassInstallmentMutation,
|
|
241
320
|
usePrepareMasterpassOrderMutation,
|
|
242
|
-
useFinalizeMasterpassOrderMutation
|
|
321
|
+
useFinalizeMasterpassOrderMutation,
|
|
322
|
+
useQueryMasterpassRewardsMutation,
|
|
323
|
+
useSelectMasterpassRewardsMutation
|
|
243
324
|
} = masterpassRestApi;
|
package/src/redux/reducer.ts
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
ModalState,
|
|
6
6
|
Installment,
|
|
7
7
|
CardType,
|
|
8
|
-
OrderData
|
|
8
|
+
OrderData,
|
|
9
|
+
RewardItem
|
|
9
10
|
} from '../types/payment.types';
|
|
10
11
|
import { CreditCardFormData } from '../utils/validation-schemas';
|
|
11
12
|
|
|
@@ -68,13 +69,17 @@ const initialState: MasterpassRestState = {
|
|
|
68
69
|
orderData: null,
|
|
69
70
|
orderCompleted: false,
|
|
70
71
|
cvc: '',
|
|
71
|
-
useThreeD: true
|
|
72
|
+
useThreeD: true,
|
|
73
|
+
availableRewards: [],
|
|
74
|
+
selectedRewards: [],
|
|
75
|
+
isLoadingRewards: false
|
|
72
76
|
},
|
|
73
77
|
modalState: {
|
|
74
78
|
showLinkModal: false,
|
|
75
79
|
showOTPModal: false,
|
|
76
80
|
show3DSecureModal: false,
|
|
77
81
|
showInformationModal: false,
|
|
82
|
+
showRewardModal: false,
|
|
78
83
|
informationModalData: null,
|
|
79
84
|
otpType: 'OTP',
|
|
80
85
|
verificationData: null,
|
|
@@ -169,6 +174,24 @@ const masterpassRestSlice = createSlice({
|
|
|
169
174
|
setOrderCompleted: (state, action: PayloadAction<boolean>) => {
|
|
170
175
|
state.paymentState.orderCompleted = action.payload;
|
|
171
176
|
},
|
|
177
|
+
setAvailableRewards: (state, action: PayloadAction<RewardItem[]>) => {
|
|
178
|
+
state.paymentState.availableRewards = action.payload;
|
|
179
|
+
},
|
|
180
|
+
setSelectedRewards: (state, action: PayloadAction<RewardItem[]>) => {
|
|
181
|
+
state.paymentState.selectedRewards = action.payload;
|
|
182
|
+
},
|
|
183
|
+
setLoadingRewards: (state, action: PayloadAction<boolean>) => {
|
|
184
|
+
state.paymentState.isLoadingRewards = action.payload;
|
|
185
|
+
},
|
|
186
|
+
setShowRewardModal: (state, action: PayloadAction<boolean>) => {
|
|
187
|
+
state.modalState.showRewardModal = action.payload;
|
|
188
|
+
},
|
|
189
|
+
clearRewards: (state) => {
|
|
190
|
+
state.paymentState.availableRewards = [];
|
|
191
|
+
state.paymentState.selectedRewards = [];
|
|
192
|
+
state.paymentState.isLoadingRewards = false;
|
|
193
|
+
state.modalState.showRewardModal = false;
|
|
194
|
+
},
|
|
172
195
|
updateModalState: (state, action: PayloadAction<Partial<ModalState>>) => {
|
|
173
196
|
state.modalState = { ...state.modalState, ...action.payload };
|
|
174
197
|
},
|
|
@@ -234,13 +257,17 @@ const masterpassRestSlice = createSlice({
|
|
|
234
257
|
orderData: null,
|
|
235
258
|
orderCompleted: false,
|
|
236
259
|
cvc: '',
|
|
237
|
-
useThreeD: true
|
|
260
|
+
useThreeD: true,
|
|
261
|
+
availableRewards: [],
|
|
262
|
+
selectedRewards: [],
|
|
263
|
+
isLoadingRewards: false
|
|
238
264
|
};
|
|
239
265
|
state.modalState = {
|
|
240
266
|
showLinkModal: false,
|
|
241
267
|
showOTPModal: false,
|
|
242
268
|
show3DSecureModal: false,
|
|
243
269
|
showInformationModal: false,
|
|
270
|
+
showRewardModal: false,
|
|
244
271
|
informationModalData: null,
|
|
245
272
|
otpType: 'OTP',
|
|
246
273
|
verificationData: null,
|
|
@@ -276,6 +303,11 @@ export const {
|
|
|
276
303
|
setOrderData,
|
|
277
304
|
setCvc,
|
|
278
305
|
setOrderCompleted,
|
|
306
|
+
setAvailableRewards,
|
|
307
|
+
setSelectedRewards,
|
|
308
|
+
setLoadingRewards,
|
|
309
|
+
setShowRewardModal,
|
|
310
|
+
clearRewards,
|
|
279
311
|
updateModalState,
|
|
280
312
|
setShowLinkModal,
|
|
281
313
|
setShowOTPModal,
|