@rpg-engine/long-bow 0.8.233 → 0.8.235

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 +86 -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 +86 -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 +26 -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 +119 -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.235",
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,12 @@ export const ManagmentPanel: React.FC<IManagmentPanelProps> = ({
135
147
  }}
136
148
  />
137
149
  </Flex>
150
+ {price && Number(price) > 0 && commissionPercent > 0 && (
151
+ <NetProceeds>
152
+ You'll receive: {netProceedsLabel}
153
+ {' '}(after {commissionPercent * 100}% commission)
154
+ </NetProceeds>
155
+ )}
138
156
  <CurrencyToggleRow>
139
157
  <CurrencyToggleLabel>Accept:</CurrencyToggleLabel>
140
158
  {LISTING_CURRENCY_OPTIONS.map(opt => (
@@ -266,7 +284,7 @@ const PriceInputWrapper = styled.div`
266
284
  flex-direction: column;
267
285
  flex-grow: 1;
268
286
 
269
- p {
287
+ > p:first-child {
270
288
  margin: 0 0 8px 0;
271
289
  font-size: 0.6rem;
272
290
  color: #888;
@@ -287,6 +305,13 @@ const PriceInputWrapper = styled.div`
287
305
  }
288
306
  `;
289
307
 
308
+ const NetProceeds = styled.p`
309
+ margin: 8px 0 0;
310
+ color: #fef08a;
311
+ font-size: 8px !important;
312
+ line-height: 1.5 !important;
313
+ `;
314
+
290
315
  const GoldIcon = styled.div`
291
316
  position: relative;
292
317
  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,119 @@
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('hides net proceeds when commission is zero', () => {
97
+ renderPanel();
98
+
99
+ act(() => {
100
+ Simulate.change(container.querySelector('.price-input'), {
101
+ target: { value: '1000' },
102
+ });
103
+ });
104
+
105
+ expect(container.textContent).not.toMatch(/You'll receive/i);
106
+ });
107
+
108
+ it('shows net DC proceeds for a DC-only listing', () => {
109
+ renderPanel(0.05, MarketplaceAcceptedCurrency.Dc);
110
+
111
+ act(() => {
112
+ Simulate.change(container.querySelector('.price-input'), {
113
+ target: { value: '1000' },
114
+ });
115
+ });
116
+
117
+ expect(container.textContent).toMatch(/You'll receive: 0\.17 DC/i);
118
+ });
119
+ });
@@ -171,7 +171,7 @@ const Template: Story = () => {
171
171
  onDCCoinClick={() => console.log('open store: packs tab')}
172
172
  onFulfillBuyOrder={buyOrderId => console.log('fulfill buy order:', buyOrderId)}
173
173
  availableGold={0}
174
- selectedItemToSell={null}
174
+ selectedItemToSell={items[0]}
175
175
  onSelectedItemToSellRemove={() => console.log('click')}
176
176
  onYourPanelToggle={() => console.log('toggle')}
177
177
  onAddItemToMarketplace={() => console.log('add')}
@@ -179,6 +179,7 @@ const Template: Story = () => {
179
179
  onMoneyWithdraw={() => console.log('withdraw')}
180
180
  dcBalance={50}
181
181
  dcToGoldSwapRate={5500}
182
+ commissionPercent={0.05}
182
183
  totalItems={100}
183
184
  currentPage={page}
184
185
  itemsPerPage={30}