@rpg-engine/long-bow 0.8.232 → 0.8.234
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/dist/components/DCWallet/DCTransferPanel.d.ts +3 -0
- package/dist/components/DCWallet/DCWalletContent.d.ts +3 -0
- package/dist/components/Marketplace/CharacterListingModal.d.ts +1 -0
- package/dist/components/Marketplace/CharacterTradePanel.d.ts +1 -0
- package/dist/components/Marketplace/ManagmentPanel.d.ts +1 -0
- package/dist/components/Marketplace/Marketplace.d.ts +1 -0
- package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +1 -0
- package/dist/components/Store/CartView.d.ts +11 -1
- package/dist/components/Store/PaymentMethodModal.d.ts +1 -0
- package/dist/components/Store/Store.d.ts +2 -1
- package/dist/components/Store/hooks/useStoreCart.d.ts +2 -1
- package/dist/long-bow.cjs.development.js +146 -82
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +146 -82
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DCWallet/DCTransferPanel.tsx +34 -4
- package/src/components/DCWallet/DCWalletContent.tsx +9 -1
- package/src/components/DCWallet/__test__/DCTransferPanel.spec.tsx +115 -0
- package/src/components/Marketplace/CharacterListingModal.tsx +16 -0
- package/src/components/Marketplace/CharacterTradePanel.tsx +3 -0
- package/src/components/Marketplace/ManagmentPanel.tsx +38 -1
- package/src/components/Marketplace/Marketplace.tsx +2 -1
- package/src/components/Marketplace/MyCharacterListingsPanel.tsx +3 -0
- package/src/components/Marketplace/__test__/CharacterListingModal.spec.tsx +114 -0
- package/src/components/Marketplace/__test__/ManagmentPanel.spec.tsx +125 -0
- package/src/components/Store/CartView.tsx +23 -5
- package/src/components/Store/PaymentMethodModal.tsx +19 -9
- package/src/components/Store/Store.tsx +2 -2
- package/src/components/Store/__test__/CartView.spec.tsx +190 -0
- package/src/components/Store/__test__/PaymentMethodModal.spec.tsx +108 -0
- package/src/components/Store/hooks/useStoreCart.ts +6 -4
- package/src/stories/Features/trading/Marketplace.stories.tsx +2 -1
package/package.json
CHANGED
|
@@ -15,9 +15,15 @@ export interface IDCTransferCharacterResult {
|
|
|
15
15
|
export interface IDCTransferPanelProps {
|
|
16
16
|
dcBalance: number;
|
|
17
17
|
transferLoading: boolean;
|
|
18
|
-
transferResult: {
|
|
18
|
+
transferResult: {
|
|
19
|
+
success: boolean;
|
|
20
|
+
message: string;
|
|
21
|
+
amount?: number;
|
|
22
|
+
fee?: number;
|
|
23
|
+
} | null;
|
|
19
24
|
onSendTransfer: (recipientName: string, amount: number) => void;
|
|
20
25
|
onClearTransferResult: () => void;
|
|
26
|
+
feePercent?: number;
|
|
21
27
|
characterName?: string;
|
|
22
28
|
onInputFocus?: () => void;
|
|
23
29
|
onInputBlur?: () => void;
|
|
@@ -36,6 +42,7 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
36
42
|
onInputBlur,
|
|
37
43
|
onSearchCharacter,
|
|
38
44
|
searchResults,
|
|
45
|
+
feePercent = 0,
|
|
39
46
|
}) => {
|
|
40
47
|
const [recipientName, setRecipientName] = useState<string>('');
|
|
41
48
|
const [amount, setAmount] = useState<string>('');
|
|
@@ -97,7 +104,8 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
97
104
|
return false;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
const fee = Math.floor(parsedAmount * feePercent);
|
|
108
|
+
if (parsedAmount + fee > dcBalance) {
|
|
101
109
|
setValidationError('Insufficient DC balance.');
|
|
102
110
|
return false;
|
|
103
111
|
}
|
|
@@ -109,7 +117,7 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
109
117
|
const handleSendClick = useCallback((): void => {
|
|
110
118
|
if (!validate()) return;
|
|
111
119
|
setShowConfirm(true);
|
|
112
|
-
}, [recipientName, amount, dcBalance, characterName]);
|
|
120
|
+
}, [recipientName, amount, dcBalance, characterName, feePercent]);
|
|
113
121
|
|
|
114
122
|
const handleConfirmTransfer = useCallback((): void => {
|
|
115
123
|
setShowConfirm(false);
|
|
@@ -118,6 +126,10 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
118
126
|
setAmount('');
|
|
119
127
|
}, [recipientName, amount, onSendTransfer]);
|
|
120
128
|
|
|
129
|
+
const transferAmount = parseInt(amount, 10);
|
|
130
|
+
const transferFee = Math.floor(transferAmount * feePercent);
|
|
131
|
+
const transferTotal = transferAmount + transferFee;
|
|
132
|
+
|
|
121
133
|
return (
|
|
122
134
|
<PanelContainer>
|
|
123
135
|
<FieldLabel>Recipient Character Name</FieldLabel>
|
|
@@ -174,6 +186,12 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
174
186
|
disabled={transferLoading}
|
|
175
187
|
/>
|
|
176
188
|
|
|
189
|
+
{feePercent > 0 && (
|
|
190
|
+
<FeeDisclosure>
|
|
191
|
+
{feePercent * 100}% transfer fee is added to the sender debit.
|
|
192
|
+
</FeeDisclosure>
|
|
193
|
+
)}
|
|
194
|
+
|
|
177
195
|
{validationError && <ErrorMessage>{validationError}</ErrorMessage>}
|
|
178
196
|
|
|
179
197
|
<ButtonRow>
|
|
@@ -191,12 +209,17 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
|
|
|
191
209
|
{transferResult && (
|
|
192
210
|
<ResultMessage $success={transferResult.success}>
|
|
193
211
|
{transferResult.message}
|
|
212
|
+
{!!transferResult.fee && ` · ${transferResult.fee.toLocaleString()} DC fee`}
|
|
194
213
|
</ResultMessage>
|
|
195
214
|
)}
|
|
196
215
|
|
|
197
216
|
{showConfirm && (
|
|
198
217
|
<ConfirmModal
|
|
199
|
-
message={
|
|
218
|
+
message={
|
|
219
|
+
feePercent > 0
|
|
220
|
+
? `Transfer ${transferAmount.toLocaleString()} DC to "${recipientName.trim()}"? Fee: ${transferFee.toLocaleString()} DC · Total: ${transferTotal.toLocaleString()} DC`
|
|
221
|
+
: `Transfer ${transferAmount.toLocaleString()} DC to "${recipientName.trim()}"?`
|
|
222
|
+
}
|
|
200
223
|
onConfirm={handleConfirmTransfer}
|
|
201
224
|
onClose={() => setShowConfirm(false)}
|
|
202
225
|
/>
|
|
@@ -275,6 +298,13 @@ const ErrorMessage = styled.div`
|
|
|
275
298
|
font-family: 'Press Start 2P', cursive !important;
|
|
276
299
|
`;
|
|
277
300
|
|
|
301
|
+
const FeeDisclosure = styled.p`
|
|
302
|
+
margin: 0;
|
|
303
|
+
color: rgba(254, 240, 138, 0.65);
|
|
304
|
+
font-size: 7px !important;
|
|
305
|
+
line-height: 1.4 !important;
|
|
306
|
+
`;
|
|
307
|
+
|
|
278
308
|
const AutocompleteWrapper = styled.div`
|
|
279
309
|
position: relative;
|
|
280
310
|
width: 100%;
|
|
@@ -13,7 +13,13 @@ export interface IDCWalletContentProps {
|
|
|
13
13
|
historyLoading: boolean;
|
|
14
14
|
onRequestHistory: (page: number, type?: string) => void;
|
|
15
15
|
transferLoading: boolean;
|
|
16
|
-
transferResult: {
|
|
16
|
+
transferResult: {
|
|
17
|
+
success: boolean;
|
|
18
|
+
message: string;
|
|
19
|
+
amount?: number;
|
|
20
|
+
fee?: number;
|
|
21
|
+
} | null;
|
|
22
|
+
feePercent?: number;
|
|
17
23
|
onSendTransfer: (recipientName: string, amount: number) => void;
|
|
18
24
|
onClearTransferResult: () => void;
|
|
19
25
|
characterName?: string;
|
|
@@ -43,6 +49,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
|
|
|
43
49
|
onBuyDC,
|
|
44
50
|
onSearchCharacter,
|
|
45
51
|
searchResults,
|
|
52
|
+
feePercent = 0,
|
|
46
53
|
}) => {
|
|
47
54
|
const usdValue = (dcBalance / DC_TO_USD).toFixed(2);
|
|
48
55
|
|
|
@@ -55,6 +62,7 @@ export const DCWalletContent: React.FC<IDCWalletContentProps> = ({
|
|
|
55
62
|
dcBalance={dcBalance}
|
|
56
63
|
transferLoading={transferLoading}
|
|
57
64
|
transferResult={transferResult}
|
|
65
|
+
feePercent={feePercent}
|
|
58
66
|
onSendTransfer={onSendTransfer}
|
|
59
67
|
onClearTransferResult={onClearTransferResult}
|
|
60
68
|
characterName={characterName}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom';
|
|
7
|
+
import { act, Simulate } from 'react-dom/test-utils';
|
|
8
|
+
import { DCTransferPanel } from '../DCTransferPanel';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../ConfirmModal', () => ({
|
|
11
|
+
ConfirmModal: ({ message }) => <div data-testid="confirm-modal">{message}</div>,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('../../Input', () => ({
|
|
15
|
+
Input: React.forwardRef((props, ref) => <input ref={ref} {...props} />),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
19
|
+
CTAButton: ({ label, onClick, disabled }) => (
|
|
20
|
+
<button disabled={disabled} onClick={onClick}>
|
|
21
|
+
{label}
|
|
22
|
+
</button>
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock('react-icons/fa', () => ({
|
|
27
|
+
FaPaperPlane: () => <svg />,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('DCTransferPanel fee disclosure', () => {
|
|
31
|
+
let container;
|
|
32
|
+
|
|
33
|
+
const renderPanel = (overrides = {}) => {
|
|
34
|
+
act(() => {
|
|
35
|
+
ReactDOM.render(
|
|
36
|
+
<DCTransferPanel
|
|
37
|
+
dcBalance={2000}
|
|
38
|
+
transferLoading={false}
|
|
39
|
+
transferResult={null}
|
|
40
|
+
onSendTransfer={jest.fn()}
|
|
41
|
+
onClearTransferResult={jest.fn()}
|
|
42
|
+
{...overrides}
|
|
43
|
+
/>,
|
|
44
|
+
container
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const enterTransfer = (recipient, amount) => {
|
|
50
|
+
const inputs = container.querySelectorAll('input');
|
|
51
|
+
act(() => {
|
|
52
|
+
Simulate.change(inputs[0], { target: { value: recipient } });
|
|
53
|
+
Simulate.change(inputs[1], { target: { value: amount } });
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
container = document.createElement('div');
|
|
59
|
+
document.body.appendChild(container);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
64
|
+
container.remove();
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should show the fee in the transfer result', () => {
|
|
69
|
+
renderPanel({
|
|
70
|
+
transferResult: { success: true, message: 'Transfer complete', fee: 50 },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(container.textContent).toMatch(/50 DC fee/i);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should show no fee line when the fee is zero', () => {
|
|
77
|
+
renderPanel({
|
|
78
|
+
transferResult: { success: true, message: 'Transfer complete', fee: 0 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(container.textContent).not.toMatch(/fee/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should block an amount whose fee exceeds the balance', () => {
|
|
85
|
+
const onSendTransfer = jest.fn();
|
|
86
|
+
renderPanel({ dcBalance: 1000, feePercent: 0.05, onSendTransfer });
|
|
87
|
+
enterTransfer('Trader', '1000');
|
|
88
|
+
|
|
89
|
+
act(() => {
|
|
90
|
+
Simulate.click(container.querySelector('button'));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(container.textContent).toMatch(/insufficient DC balance/i);
|
|
94
|
+
expect(container.querySelector('[data-testid="confirm-modal"]')).toBeNull();
|
|
95
|
+
expect(onSendTransfer).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should include the fee and total in the confirm message', () => {
|
|
99
|
+
renderPanel({ feePercent: 0.05 });
|
|
100
|
+
enterTransfer('Trader', '1000');
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
Simulate.click(container.querySelector('button'));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(container.textContent).toMatch(/fee: 50 DC/i);
|
|
107
|
+
expect(container.textContent).toMatch(/total: 1,?050 DC/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should show the transfer fee disclosure beside the amount field', () => {
|
|
111
|
+
renderPanel({ feePercent: 0.05 });
|
|
112
|
+
|
|
113
|
+
expect(container.textContent).toMatch(/5% transfer fee is added to the sender debit/i);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -25,6 +25,7 @@ export interface ICharacterListingModalProps {
|
|
|
25
25
|
characterAtlasJSON: any;
|
|
26
26
|
characterAtlasIMG: any;
|
|
27
27
|
onCharacterList: (characterId: string, price: number) => void;
|
|
28
|
+
commissionPercent?: number;
|
|
28
29
|
enableHotkeys?: () => void;
|
|
29
30
|
disableHotkeys?: () => void;
|
|
30
31
|
}
|
|
@@ -41,6 +42,7 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
|
|
|
41
42
|
onCharacterList,
|
|
42
43
|
enableHotkeys,
|
|
43
44
|
disableHotkeys,
|
|
45
|
+
commissionPercent = 0,
|
|
44
46
|
}) => {
|
|
45
47
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
46
48
|
const [price, setPrice] = useState('');
|
|
@@ -191,6 +193,13 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
|
|
|
191
193
|
<PricePreviewAmount><MMORPGNumber value={Number(price)} /> DC</PricePreviewAmount>
|
|
192
194
|
</PricePreview>
|
|
193
195
|
)}
|
|
196
|
+
{price && Number(price) > 0 && commissionPercent > 0 && (
|
|
197
|
+
<NetProceeds>
|
|
198
|
+
You'll receive:{' '}
|
|
199
|
+
{(Number(price) - Math.floor(Number(price) * commissionPercent)).toLocaleString()} DC
|
|
200
|
+
{' '}(after {commissionPercent * 100}% commission)
|
|
201
|
+
</NetProceeds>
|
|
202
|
+
)}
|
|
194
203
|
<DCRateStrip />
|
|
195
204
|
</PriceSection>
|
|
196
205
|
</ModalContent>
|
|
@@ -237,6 +246,13 @@ const ModalContent = styled.div`
|
|
|
237
246
|
animation: ${scaleIn} 0.15s ease-out;
|
|
238
247
|
`;
|
|
239
248
|
|
|
249
|
+
const NetProceeds = styled.p`
|
|
250
|
+
margin: 0;
|
|
251
|
+
color: #fef08a;
|
|
252
|
+
font-size: 8px !important;
|
|
253
|
+
line-height: 1.5 !important;
|
|
254
|
+
`;
|
|
255
|
+
|
|
240
256
|
const Header = styled.div`
|
|
241
257
|
display: flex;
|
|
242
258
|
align-items: center;
|
|
@@ -28,6 +28,7 @@ export interface ICharacterTradePanelProps {
|
|
|
28
28
|
characterAtlasIMG?: any;
|
|
29
29
|
enableHotkeys?: () => void;
|
|
30
30
|
disableHotkeys?: () => void;
|
|
31
|
+
commissionPercent?: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export const CharacterTradePanel: React.FC<ICharacterTradePanelProps> = ({
|
|
@@ -54,6 +55,7 @@ export const CharacterTradePanel: React.FC<ICharacterTradePanelProps> = ({
|
|
|
54
55
|
characterAtlasIMG,
|
|
55
56
|
enableHotkeys,
|
|
56
57
|
disableHotkeys,
|
|
58
|
+
commissionPercent = 0,
|
|
57
59
|
}) => {
|
|
58
60
|
const [subTab, setSubTab] = useState<'browse' | 'my-listings'>('browse');
|
|
59
61
|
|
|
@@ -113,6 +115,7 @@ export const CharacterTradePanel: React.FC<ICharacterTradePanelProps> = ({
|
|
|
113
115
|
characterAtlasIMG={characterAtlasIMG ?? atlasIMG}
|
|
114
116
|
enableHotkeys={enableHotkeys}
|
|
115
117
|
disableHotkeys={disableHotkeys}
|
|
118
|
+
commissionPercent={commissionPercent}
|
|
116
119
|
/>
|
|
117
120
|
)}
|
|
118
121
|
</>
|
|
@@ -28,6 +28,7 @@ export interface IManagmentPanelProps {
|
|
|
28
28
|
onMoneyWithdraw: () => void;
|
|
29
29
|
currentPage: number;
|
|
30
30
|
dcToGoldSwapRate?: number;
|
|
31
|
+
commissionPercent?: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const LISTING_CURRENCY_OPTIONS: { value: MarketplaceAcceptedCurrency; label: string }[] = [
|
|
@@ -53,6 +54,7 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
|
|
|
53
54
|
onMoneyWithdraw,
|
|
54
55
|
currentPage,
|
|
55
56
|
dcToGoldSwapRate = 0,
|
|
57
|
+
commissionPercent = 0,
|
|
56
58
|
}) => {
|
|
57
59
|
const [price, setPrice] = useState('');
|
|
58
60
|
const [isCreatingOffer, setIsCreatingOffer] = useState(false);
|
|
@@ -67,6 +69,16 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
|
|
|
67
69
|
itemsContainer.current?.scrollTo(0, 0);
|
|
68
70
|
}, [currentPage]);
|
|
69
71
|
|
|
72
|
+
const parsedPrice = Number(price);
|
|
73
|
+
const netGoldProceeds = parsedPrice - Math.floor(parsedPrice * commissionPercent);
|
|
74
|
+
const netDCProceeds = goldToDC(netGoldProceeds);
|
|
75
|
+
const netProceedsLabel =
|
|
76
|
+
listingCurrency === MarketplaceAcceptedCurrency.Gold
|
|
77
|
+
? `${netGoldProceeds.toLocaleString()} gold`
|
|
78
|
+
: listingCurrency === MarketplaceAcceptedCurrency.Dc
|
|
79
|
+
? `${formatDCAmount(netDCProceeds)} DC`
|
|
80
|
+
: `${netGoldProceeds.toLocaleString()} gold or ${formatDCAmount(netDCProceeds)} DC`;
|
|
81
|
+
|
|
70
82
|
return (
|
|
71
83
|
<>
|
|
72
84
|
{isCreatingOffer && (
|
|
@@ -135,6 +147,17 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
|
|
|
135
147
|
}}
|
|
136
148
|
/>
|
|
137
149
|
</Flex>
|
|
150
|
+
{commissionPercent > 0 && (
|
|
151
|
+
<FeeDisclosure>
|
|
152
|
+
{commissionPercent * 100}% marketplace commission is deducted from seller proceeds.
|
|
153
|
+
</FeeDisclosure>
|
|
154
|
+
)}
|
|
155
|
+
{price && Number(price) > 0 && commissionPercent > 0 && (
|
|
156
|
+
<NetProceeds>
|
|
157
|
+
You'll receive: {netProceedsLabel}
|
|
158
|
+
{' '}(after {commissionPercent * 100}% commission)
|
|
159
|
+
</NetProceeds>
|
|
160
|
+
)}
|
|
138
161
|
<CurrencyToggleRow>
|
|
139
162
|
<CurrencyToggleLabel>Accept:</CurrencyToggleLabel>
|
|
140
163
|
{LISTING_CURRENCY_OPTIONS.map(opt => (
|
|
@@ -266,7 +289,7 @@ const PriceInputWrapper = styled.div`
|
|
|
266
289
|
flex-direction: column;
|
|
267
290
|
flex-grow: 1;
|
|
268
291
|
|
|
269
|
-
p {
|
|
292
|
+
> p:first-child {
|
|
270
293
|
margin: 0 0 8px 0;
|
|
271
294
|
font-size: 0.6rem;
|
|
272
295
|
color: #888;
|
|
@@ -287,6 +310,20 @@ const PriceInputWrapper = styled.div`
|
|
|
287
310
|
}
|
|
288
311
|
`;
|
|
289
312
|
|
|
313
|
+
const NetProceeds = styled.p`
|
|
314
|
+
margin: 8px 0 0;
|
|
315
|
+
color: #fef08a;
|
|
316
|
+
font-size: 8px !important;
|
|
317
|
+
line-height: 1.5 !important;
|
|
318
|
+
`;
|
|
319
|
+
|
|
320
|
+
const FeeDisclosure = styled.p`
|
|
321
|
+
margin: 6px 0 0;
|
|
322
|
+
color: rgba(254, 240, 138, 0.65);
|
|
323
|
+
font-size: 7px !important;
|
|
324
|
+
line-height: 1.4 !important;
|
|
325
|
+
`;
|
|
326
|
+
|
|
290
327
|
const GoldIcon = styled.div`
|
|
291
328
|
position: relative;
|
|
292
329
|
display: flex;
|
|
@@ -136,6 +136,7 @@ export interface IMarketPlaceProps {
|
|
|
136
136
|
characterNameFilter?: string;
|
|
137
137
|
onCharacterNameFilterChange?: (name: string) => void;
|
|
138
138
|
fullScreen?: boolean;
|
|
139
|
+
commissionPercent?: number;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
type ActiveTab = 'marketplace' | 'characters' | 'sell' | 'buy-orders' | 'history' | 'wallet' | 'settings';
|
|
@@ -313,6 +314,7 @@ export const Marketplace: React.FC<IMarketPlaceProps> = props => {
|
|
|
313
314
|
characterAtlasIMG={characterAtlasIMG}
|
|
314
315
|
enableHotkeys={props.enableHotkeys}
|
|
315
316
|
disableHotkeys={props.disableHotkeys}
|
|
317
|
+
commissionPercent={props.commissionPercent}
|
|
316
318
|
/>
|
|
317
319
|
)}
|
|
318
320
|
|
|
@@ -404,4 +406,3 @@ const PagerContainer = styled.div`
|
|
|
404
406
|
margin: 6px auto 0 auto;
|
|
405
407
|
padding: 4px 10px;
|
|
406
408
|
`;
|
|
407
|
-
|
|
@@ -31,6 +31,7 @@ export interface IMyCharacterListingsPanelProps {
|
|
|
31
31
|
characterAtlasIMG: any;
|
|
32
32
|
enableHotkeys?: () => void;
|
|
33
33
|
disableHotkeys?: () => void;
|
|
34
|
+
commissionPercent?: number;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps> = ({
|
|
@@ -49,6 +50,7 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
49
50
|
characterAtlasIMG,
|
|
50
51
|
enableHotkeys,
|
|
51
52
|
disableHotkeys,
|
|
53
|
+
commissionPercent = 0,
|
|
52
54
|
}) => {
|
|
53
55
|
const [delistingId, setDelistingId] = useState<string | null>(null);
|
|
54
56
|
const [isListingModalOpen, setIsListingModalOpen] = useState(false);
|
|
@@ -93,6 +95,7 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
93
95
|
}}
|
|
94
96
|
enableHotkeys={enableHotkeys}
|
|
95
97
|
disableHotkeys={disableHotkeys}
|
|
98
|
+
commissionPercent={commissionPercent}
|
|
96
99
|
/>
|
|
97
100
|
|
|
98
101
|
<Toolbar>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom';
|
|
7
|
+
import { act, Simulate } from 'react-dom/test-utils';
|
|
8
|
+
import { CharacterListingModal } from '../CharacterListingModal';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../Abstractions/ModalPortal', () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: ({ children }) => <>{children}</>,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock('../../ConfirmModal', () => ({
|
|
16
|
+
ConfirmModal: ({ message }) => <div>{message}</div>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('../../Input', () => ({
|
|
20
|
+
Input: props => <input {...props} />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
24
|
+
CTAButton: ({ label, onClick, disabled }) => (
|
|
25
|
+
<button disabled={disabled} onClick={onClick}>
|
|
26
|
+
{label}
|
|
27
|
+
</button>
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../../shared/DCRateStrip', () => ({
|
|
32
|
+
DCRateStrip: () => null,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.mock('../../shared/SpriteFromAtlas', () => ({
|
|
36
|
+
SpriteFromAtlas: () => null,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.mock('../../shared/MMORPGNumber', () => ({
|
|
40
|
+
MMORPGNumber: ({ value }) => <>{value}</>,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock('pixelarticons/react/ShoppingBag', () => ({
|
|
44
|
+
ShoppingBag: () => <svg />,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
jest.mock('react-icons/fa', () => ({
|
|
48
|
+
FaTimes: () => <svg />,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe('CharacterListingModal commission disclosure', () => {
|
|
52
|
+
let container;
|
|
53
|
+
|
|
54
|
+
const character = {
|
|
55
|
+
_id: 'character-1',
|
|
56
|
+
name: 'Trader',
|
|
57
|
+
textureKey: 'knight',
|
|
58
|
+
skills: { level: 10 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const renderModal = (commissionPercent = 0) => {
|
|
62
|
+
act(() => {
|
|
63
|
+
ReactDOM.render(
|
|
64
|
+
<CharacterListingModal
|
|
65
|
+
isOpen
|
|
66
|
+
onClose={jest.fn()}
|
|
67
|
+
accountCharacters={[character]}
|
|
68
|
+
atlasJSON={{}}
|
|
69
|
+
atlasIMG=""
|
|
70
|
+
characterAtlasJSON={{}}
|
|
71
|
+
characterAtlasIMG=""
|
|
72
|
+
onCharacterList={jest.fn()}
|
|
73
|
+
commissionPercent={commissionPercent}
|
|
74
|
+
/>,
|
|
75
|
+
container
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
container = document.createElement('div');
|
|
82
|
+
document.body.appendChild(container);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
87
|
+
container.remove();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should show net proceeds after commission', () => {
|
|
91
|
+
renderModal(0.05);
|
|
92
|
+
|
|
93
|
+
act(() => {
|
|
94
|
+
Simulate.change(container.querySelector('.price-input'), {
|
|
95
|
+
target: { value: '1000' },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(container.textContent).toMatch(/You'll receive: 950 DC/i);
|
|
100
|
+
expect(container.textContent).toMatch(/after 5% commission/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should hide net proceeds when commission is zero', () => {
|
|
104
|
+
renderModal();
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
Simulate.change(container.querySelector('.price-input'), {
|
|
108
|
+
target: { value: '1000' },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(container.textContent).not.toMatch(/You'll receive/i);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import { MarketplaceAcceptedCurrency } from '@rpg-engine/shared';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import ReactDOM from 'react-dom';
|
|
8
|
+
import { act, Simulate } from 'react-dom/test-utils';
|
|
9
|
+
import { ManagmentPanel } from '../ManagmentPanel';
|
|
10
|
+
|
|
11
|
+
jest.mock('../../ConfirmModal', () => ({
|
|
12
|
+
ConfirmModal: ({ message }) => <div>{message}</div>,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock('../../Input', () => ({
|
|
16
|
+
Input: props => <input {...props} />,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('../../Item/Inventory/ItemSlot', () => ({
|
|
20
|
+
ItemSlot: () => <div data-testid="item-slot" />,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
24
|
+
CTAButton: ({ label, onClick, disabled }) => (
|
|
25
|
+
<button disabled={disabled} onClick={onClick}>
|
|
26
|
+
{label}
|
|
27
|
+
</button>
|
|
28
|
+
),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../../shared/SpriteFromAtlas', () => ({
|
|
32
|
+
SpriteFromAtlas: () => null,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.mock('../MarketplaceRows', () => ({
|
|
36
|
+
MarketplaceRows: () => <div data-testid="marketplace-row" />,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.mock('pixelarticons/react/ShoppingBag', () => ({
|
|
40
|
+
ShoppingBag: () => <svg />,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock('pixelarticons/react/Wallet', () => ({
|
|
44
|
+
Wallet: () => <svg />,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
describe('ManagmentPanel commission disclosure', () => {
|
|
48
|
+
let container;
|
|
49
|
+
|
|
50
|
+
const renderPanel = (commissionPercent = 0, acceptedCurrency = MarketplaceAcceptedCurrency.GoldOrDc) => {
|
|
51
|
+
act(() => {
|
|
52
|
+
ReactDOM.render(
|
|
53
|
+
<ManagmentPanel
|
|
54
|
+
items={[]}
|
|
55
|
+
atlasJSON={{}}
|
|
56
|
+
atlasIMG=""
|
|
57
|
+
availableGold={0}
|
|
58
|
+
selectedItemToSell={{ _id: 'item-1', name: 'Sword', stackQty: 1 }}
|
|
59
|
+
onSelectedItemToSellRemove={jest.fn()}
|
|
60
|
+
onAddItemToMarketplace={jest.fn()}
|
|
61
|
+
onMoneyWithdraw={jest.fn()}
|
|
62
|
+
onMarketPlaceItemRemove={jest.fn()}
|
|
63
|
+
currentPage={1}
|
|
64
|
+
acceptedCurrency={acceptedCurrency}
|
|
65
|
+
commissionPercent={commissionPercent}
|
|
66
|
+
/>,
|
|
67
|
+
container
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
container = document.createElement('div');
|
|
74
|
+
document.body.appendChild(container);
|
|
75
|
+
HTMLElement.prototype.scrollTo = jest.fn();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
80
|
+
container.remove();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('shows net gold proceeds after commission', () => {
|
|
84
|
+
renderPanel(0.05);
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
Simulate.change(container.querySelector('.price-input'), {
|
|
88
|
+
target: { value: '1000' },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(container.textContent).toMatch(/You'll receive: 950 gold/i);
|
|
93
|
+
expect(container.textContent).toMatch(/after 5% commission/i);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows the commission disclosure before a price is entered', () => {
|
|
97
|
+
renderPanel(0.05);
|
|
98
|
+
|
|
99
|
+
expect(container.textContent).toMatch(/5% marketplace commission is deducted from seller proceeds/i);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('hides net proceeds when commission is zero', () => {
|
|
103
|
+
renderPanel();
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
Simulate.change(container.querySelector('.price-input'), {
|
|
107
|
+
target: { value: '1000' },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(container.textContent).not.toMatch(/You'll receive/i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('shows net DC proceeds for a DC-only listing', () => {
|
|
115
|
+
renderPanel(0.05, MarketplaceAcceptedCurrency.Dc);
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
Simulate.change(container.querySelector('.price-input'), {
|
|
119
|
+
target: { value: '1000' },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(container.textContent).toMatch(/You'll receive: 0\.17 DC/i);
|
|
124
|
+
});
|
|
125
|
+
});
|