@rpg-engine/long-bow 0.8.233 → 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.
Files changed (25) hide show
  1. package/dist/components/DCWallet/DCTransferPanel.d.ts +3 -0
  2. package/dist/components/DCWallet/DCWalletContent.d.ts +3 -0
  3. package/dist/components/Marketplace/CharacterListingModal.d.ts +1 -0
  4. package/dist/components/Marketplace/CharacterTradePanel.d.ts +1 -0
  5. package/dist/components/Marketplace/ManagmentPanel.d.ts +1 -0
  6. package/dist/components/Marketplace/Marketplace.d.ts +1 -0
  7. package/dist/components/Marketplace/MyCharacterListingsPanel.d.ts +1 -0
  8. package/dist/long-bow.cjs.development.js +90 -50
  9. package/dist/long-bow.cjs.development.js.map +1 -1
  10. package/dist/long-bow.cjs.production.min.js +1 -1
  11. package/dist/long-bow.cjs.production.min.js.map +1 -1
  12. package/dist/long-bow.esm.js +90 -50
  13. package/dist/long-bow.esm.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/components/DCWallet/DCTransferPanel.tsx +34 -4
  16. package/src/components/DCWallet/DCWalletContent.tsx +9 -1
  17. package/src/components/DCWallet/__test__/DCTransferPanel.spec.tsx +115 -0
  18. package/src/components/Marketplace/CharacterListingModal.tsx +16 -0
  19. package/src/components/Marketplace/CharacterTradePanel.tsx +3 -0
  20. package/src/components/Marketplace/ManagmentPanel.tsx +38 -1
  21. package/src/components/Marketplace/Marketplace.tsx +2 -1
  22. package/src/components/Marketplace/MyCharacterListingsPanel.tsx +3 -0
  23. package/src/components/Marketplace/__test__/CharacterListingModal.spec.tsx +114 -0
  24. package/src/components/Marketplace/__test__/ManagmentPanel.spec.tsx +125 -0
  25. package/src/stories/Features/trading/Marketplace.stories.tsx +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.233",
3
+ "version": "0.8.234",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -15,9 +15,15 @@ export interface IDCTransferCharacterResult {
15
15
  export interface IDCTransferPanelProps {
16
16
  dcBalance: number;
17
17
  transferLoading: boolean;
18
- transferResult: { success: boolean; message: string } | null;
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
- if (parsedAmount > dcBalance) {
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={`Transfer ${parseInt(amount, 10)} DC to "${recipientName.trim()}"?`}
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: { success: boolean; message: string } | null;
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
+ });