@rpg-engine/long-bow 0.8.225 → 0.8.227
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/Marketplace/characterListingUtils.d.ts +3 -0
- package/dist/components/Spellbook/Spell.d.ts +1 -0
- package/dist/components/Store/hooks/useStoreCart.d.ts +1 -1
- package/dist/long-bow.cjs.development.js +75 -51
- 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 +75 -51
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Marketplace/CharacterListingForm.tsx +2 -3
- package/src/components/Marketplace/CharacterListingModal.tsx +7 -5
- package/src/components/Marketplace/MyCharacterListingsPanel.tsx +2 -4
- package/src/components/Marketplace/__test__/characterListingUtils.spec.ts +55 -0
- package/src/components/Marketplace/characterListingUtils.ts +38 -0
- package/src/components/Spellbook/Spell.tsx +5 -1
- package/src/components/Store/Store.tsx +1 -1
- package/src/components/Store/__test__/Store.spec.tsx +16 -0
- package/src/components/Store/__test__/useStoreCart.spec.tsx +98 -0
- package/src/components/Store/hooks/useStoreCart.ts +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpg-engine/long-bow",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.227",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"@capacitor/core": "^6.1.0",
|
|
86
86
|
"@rollup/plugin-image": "^2.1.1",
|
|
87
|
-
"@rpg-engine/shared": "^0.10.
|
|
87
|
+
"@rpg-engine/shared": "^0.10.119",
|
|
88
88
|
"dayjs": "^1.11.2",
|
|
89
89
|
"font-awesome": "^4.7.0",
|
|
90
90
|
"fs-extra": "^10.1.0",
|
|
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
|
|
4
4
|
import styled from 'styled-components';
|
|
5
5
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
6
6
|
import { CharacterListingModal } from './CharacterListingModal';
|
|
7
|
+
import { getEligibleCharactersForListing } from './characterListingUtils';
|
|
7
8
|
|
|
8
9
|
export interface ICharacterListingFormProps {
|
|
9
10
|
accountCharacters: ICharacter[];
|
|
@@ -30,9 +31,7 @@ export const CharacterListingForm: React.FC<ICharacterListingFormProps> = ({
|
|
|
30
31
|
}) => {
|
|
31
32
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
32
33
|
|
|
33
|
-
const eligibleCount = accountCharacters.
|
|
34
|
-
c => !c.isListedForSale && !c.tradedAt
|
|
35
|
-
).length;
|
|
34
|
+
const eligibleCount = getEligibleCharactersForListing(accountCharacters).length;
|
|
36
35
|
|
|
37
36
|
return (
|
|
38
37
|
<Wrapper>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ICharacter } from '@rpg-engine/shared';
|
|
2
2
|
import { ShoppingBag } from 'pixelarticons/react/ShoppingBag';
|
|
3
|
-
import React, { useCallback, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
4
|
import { FaTimes } from 'react-icons/fa';
|
|
5
5
|
import styled, { keyframes } from 'styled-components';
|
|
6
6
|
import ModalPortal from '../Abstractions/ModalPortal';
|
|
@@ -10,6 +10,7 @@ import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
|
10
10
|
import { DCRateStrip } from '../shared/DCRateStrip';
|
|
11
11
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
12
12
|
import { MMORPGNumber } from '../../components/shared/MMORPGNumber';
|
|
13
|
+
import { getEligibleCharactersForListing } from './characterListingUtils';
|
|
13
14
|
|
|
14
15
|
export interface ICharacterListingModalProps {
|
|
15
16
|
isOpen: boolean;
|
|
@@ -52,12 +53,13 @@ export const CharacterListingModal: React.FC<ICharacterListingModalProps> = ({
|
|
|
52
53
|
[]
|
|
53
54
|
);
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
c => !c.isListedForSale
|
|
56
|
+
const eligibleCharacters = useMemo(
|
|
57
|
+
() => getEligibleCharactersForListing(accountCharacters, activeCharacterId),
|
|
58
|
+
[accountCharacters, activeCharacterId]
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
+
if (!isOpen) return null;
|
|
62
|
+
|
|
61
63
|
const isActiveCharacter = (c: ICharacter) => !!activeCharacterId && c._id === activeCharacterId;
|
|
62
64
|
|
|
63
65
|
const canList = !!selectedId && Number(price) > 0 && selectedId !== activeCharacterId;
|
|
@@ -9,6 +9,7 @@ import { Pagination } from '../shared/Pagination/Pagination';
|
|
|
9
9
|
import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
|
|
10
10
|
import { CTAButton } from '../shared/CTAButton/CTAButton';
|
|
11
11
|
import { CharacterListingModal } from './CharacterListingModal';
|
|
12
|
+
import { getEligibleCharactersForListing } from './characterListingUtils';
|
|
12
13
|
|
|
13
14
|
export interface IMyCharacterListingsPanelProps {
|
|
14
15
|
myCharacterListings: ICharacterListing[];
|
|
@@ -65,9 +66,7 @@ export const MyCharacterListingsPanel: React.FC<IMyCharacterListingsPanelProps>
|
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
const eligibleCount = accountCharacters.
|
|
69
|
-
c => !c.isListedForSale && !c.tradedAt
|
|
70
|
-
).length;
|
|
69
|
+
const eligibleCount = getEligibleCharactersForListing(accountCharacters).length;
|
|
71
70
|
|
|
72
71
|
return (
|
|
73
72
|
<>
|
|
@@ -358,4 +357,3 @@ const EmptySubtext = styled.span`
|
|
|
358
357
|
max-width: 260px;
|
|
359
358
|
line-height: 1.6;
|
|
360
359
|
`;
|
|
361
|
-
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
import {
|
|
3
|
+
getEligibleCharactersForListing,
|
|
4
|
+
isCharacterEligibleForListing,
|
|
5
|
+
} from '../characterListingUtils';
|
|
6
|
+
|
|
7
|
+
describe('characterListingUtils', () => {
|
|
8
|
+
it('keeps traded characters eligible as long as they are not currently listed', () => {
|
|
9
|
+
const tradedCharacter = {
|
|
10
|
+
_id: 'traded-character',
|
|
11
|
+
isListedForSale: false,
|
|
12
|
+
tradedAt: '2026-04-17T00:00:00.000Z',
|
|
13
|
+
} as ICharacter;
|
|
14
|
+
|
|
15
|
+
expect(isCharacterEligibleForListing(tradedCharacter)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('sorts recently traded characters first and moves the active character to the end', () => {
|
|
19
|
+
const characters = [
|
|
20
|
+
{
|
|
21
|
+
_id: 'older-character',
|
|
22
|
+
name: 'Older',
|
|
23
|
+
isListedForSale: false,
|
|
24
|
+
tradedAt: '2026-04-10T00:00:00.000Z',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
_id: 'newly-traded-character',
|
|
28
|
+
name: 'Newly Traded',
|
|
29
|
+
isListedForSale: false,
|
|
30
|
+
tradedAt: '2026-04-17T00:00:00.000Z',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
_id: 'already-listed-character',
|
|
34
|
+
name: 'Already Listed',
|
|
35
|
+
isListedForSale: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
_id: 'active-character',
|
|
39
|
+
name: 'Active',
|
|
40
|
+
isListedForSale: false,
|
|
41
|
+
},
|
|
42
|
+
] as ICharacter[];
|
|
43
|
+
|
|
44
|
+
const eligibleCharacters = getEligibleCharactersForListing(
|
|
45
|
+
characters,
|
|
46
|
+
'active-character'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(eligibleCharacters.map(character => character._id)).toEqual([
|
|
50
|
+
'newly-traded-character',
|
|
51
|
+
'older-character',
|
|
52
|
+
'active-character',
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ICharacter } from '@rpg-engine/shared';
|
|
2
|
+
|
|
3
|
+
const toTimestamp = (value?: Date | string | null): number => {
|
|
4
|
+
if (!value) return 0;
|
|
5
|
+
|
|
6
|
+
const timestamp = new Date(value).getTime();
|
|
7
|
+
|
|
8
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const isCharacterEligibleForListing = (character: ICharacter): boolean =>
|
|
12
|
+
!character.isListedForSale;
|
|
13
|
+
|
|
14
|
+
export const getEligibleCharactersForListing = (
|
|
15
|
+
accountCharacters: ICharacter[],
|
|
16
|
+
activeCharacterId?: string
|
|
17
|
+
): ICharacter[] =>
|
|
18
|
+
accountCharacters
|
|
19
|
+
.filter(isCharacterEligibleForListing)
|
|
20
|
+
.sort((left, right) => {
|
|
21
|
+
const leftIsActive =
|
|
22
|
+
!!activeCharacterId && left._id === activeCharacterId;
|
|
23
|
+
const rightIsActive =
|
|
24
|
+
!!activeCharacterId && right._id === activeCharacterId;
|
|
25
|
+
|
|
26
|
+
if (leftIsActive !== rightIsActive) {
|
|
27
|
+
return leftIsActive ? 1 : -1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tradedAtDiff =
|
|
31
|
+
toTimestamp(right.tradedAt) - toTimestamp(left.tradedAt);
|
|
32
|
+
|
|
33
|
+
if (tradedAtDiff !== 0) {
|
|
34
|
+
return tradedAtDiff;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 0;
|
|
38
|
+
});
|
|
@@ -13,6 +13,7 @@ export interface ISpellProps {
|
|
|
13
13
|
charMagicLevel: number;
|
|
14
14
|
charSkillLevels?: Record<string, number>;
|
|
15
15
|
onPointerUp?: (spellKey: string) => void;
|
|
16
|
+
onClick?: (spellKey: string) => void;
|
|
16
17
|
isSettingShortcut?: boolean;
|
|
17
18
|
spellKey: string;
|
|
18
19
|
spell: ISpell;
|
|
@@ -43,6 +44,7 @@ export const Spell: React.FC<ISpellProps> = ({
|
|
|
43
44
|
charMagicLevel,
|
|
44
45
|
charSkillLevels,
|
|
45
46
|
onPointerUp,
|
|
47
|
+
onClick,
|
|
46
48
|
isSettingShortcut,
|
|
47
49
|
spell,
|
|
48
50
|
activeCooldown,
|
|
@@ -77,10 +79,12 @@ export const Spell: React.FC<ISpellProps> = ({
|
|
|
77
79
|
const CONTAINER_STYLE = { width: '32px', height: '32px' };
|
|
78
80
|
const IMAGE_SCALE = 2;
|
|
79
81
|
|
|
82
|
+
const handleClick = onClick ?? onPointerUp;
|
|
83
|
+
|
|
80
84
|
return (
|
|
81
85
|
<SpellInfoWrapper spell={spell}>
|
|
82
86
|
<Container
|
|
83
|
-
|
|
87
|
+
onClick={handleClick?.bind(null, spellKey)}
|
|
84
88
|
isSettingShortcut={isSettingShortcut && !disabled}
|
|
85
89
|
className="spell"
|
|
86
90
|
>
|
|
@@ -316,7 +316,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
316
316
|
cartItems={cartItems}
|
|
317
317
|
onRemoveFromCart={handleRemoveFromCartTracked}
|
|
318
318
|
onClose={closeCart}
|
|
319
|
-
onPurchase={
|
|
319
|
+
onPurchase={() => handleCartPurchase(onPurchase)}
|
|
320
320
|
atlasJSON={atlasJSON}
|
|
321
321
|
atlasIMG={atlasIMG}
|
|
322
322
|
onCheckoutStart={onCheckoutStart}
|
|
@@ -127,6 +127,22 @@ jest.mock('pixelarticons/react/Wallet', () => ({
|
|
|
127
127
|
Wallet: () => <span>wallet</span>,
|
|
128
128
|
}));
|
|
129
129
|
|
|
130
|
+
jest.mock('pixelarticons/react/DateTime', () => ({
|
|
131
|
+
DateTime: () => <span>datetime</span>,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
jest.mock('pixelarticons/react/Receipt', () => ({
|
|
135
|
+
Receipt: () => <span>receipt</span>,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
jest.mock('pixelarticons/react/ShoppingCart', () => ({
|
|
139
|
+
ShoppingCart: () => <span>shopping-cart</span>,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
jest.mock('pixelarticons/react/Users', () => ({
|
|
143
|
+
Users: () => <span>users</span>,
|
|
144
|
+
}));
|
|
145
|
+
|
|
130
146
|
jest.mock('react-icons/fa', () => ({
|
|
131
147
|
FaHistory: () => <span>history</span>,
|
|
132
148
|
FaShoppingCart: () => <span>cart</span>,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import { PurchaseType } from '@rpg-engine/shared';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import ReactDOM from 'react-dom';
|
|
8
|
+
import { act } from 'react-dom/test-utils';
|
|
9
|
+
import { useStoreCart } from '../hooks/useStoreCart';
|
|
10
|
+
|
|
11
|
+
jest.mock('../hooks/useStoreMetadata', () => ({
|
|
12
|
+
useStoreMetadata: () => ({
|
|
13
|
+
collectMetadata: jest.fn(),
|
|
14
|
+
resolveMetadata: jest.fn(),
|
|
15
|
+
isCollectingMetadata: false,
|
|
16
|
+
currentMetadataItem: null,
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
let hookResult;
|
|
21
|
+
const TestComponent = () => {
|
|
22
|
+
hookResult = useStoreCart();
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('useStoreCart', () => {
|
|
27
|
+
let container;
|
|
28
|
+
|
|
29
|
+
const baseItem = {
|
|
30
|
+
key: 'account-email-change',
|
|
31
|
+
name: 'Account Email Change',
|
|
32
|
+
price: 2.99,
|
|
33
|
+
type: PurchaseType.Service,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
container = document.createElement('div');
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
act(() => {
|
|
40
|
+
ReactDOM.render(<TestComponent />, container);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
46
|
+
document.body.removeChild(container);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('clears and closes the cart after a successful purchase', async () => {
|
|
50
|
+
await act(async () => {
|
|
51
|
+
await hookResult.handleAddToCart(baseItem, 1, { inputValue: 'new@example.com' });
|
|
52
|
+
});
|
|
53
|
+
act(() => {
|
|
54
|
+
hookResult.openCart();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const onPurchase = jest.fn().mockResolvedValue(true);
|
|
58
|
+
let result;
|
|
59
|
+
|
|
60
|
+
await act(async () => {
|
|
61
|
+
result = await hookResult.handlePurchase(onPurchase);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toBe(true);
|
|
65
|
+
expect(onPurchase).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
purchases: [
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
purchaseKey: 'account-email-change',
|
|
70
|
+
metadata: { inputValue: 'new@example.com' },
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
expect(hookResult.cartItems).toEqual([]);
|
|
76
|
+
expect(hookResult.isCartOpen).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the cart intact when purchase returns false', async () => {
|
|
80
|
+
await act(async () => {
|
|
81
|
+
await hookResult.handleAddToCart(baseItem, 1, { inputValue: 'new@example.com' });
|
|
82
|
+
});
|
|
83
|
+
act(() => {
|
|
84
|
+
hookResult.openCart();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const onPurchase = jest.fn().mockResolvedValue(false);
|
|
88
|
+
let result;
|
|
89
|
+
|
|
90
|
+
await act(async () => {
|
|
91
|
+
result = await hookResult.handlePurchase(onPurchase);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result).toBe(false);
|
|
95
|
+
expect(hookResult.cartItems).toHaveLength(1);
|
|
96
|
+
expect(hookResult.isCartOpen).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -19,7 +19,7 @@ interface IUseStoreCart {
|
|
|
19
19
|
isCartOpen: boolean;
|
|
20
20
|
handleAddToCart: (item: IProductBlueprint, quantity: number, metadata?: Record<string, any>) => void;
|
|
21
21
|
handleRemoveFromCart: (itemKey: string) => void;
|
|
22
|
-
handlePurchase: (onPurchase: (purchase: IPurchase) =>
|
|
22
|
+
handlePurchase: (onPurchase: (purchase: IPurchase) => Promise<boolean>) => Promise<boolean>;
|
|
23
23
|
openCart: () => void;
|
|
24
24
|
closeCart: () => void;
|
|
25
25
|
getTotalItems: () => number;
|
|
@@ -104,7 +104,7 @@ export const useStoreCart = (): IUseStoreCart => {
|
|
|
104
104
|
);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
const handlePurchase = (onPurchase: (purchase: IPurchase) =>
|
|
107
|
+
const handlePurchase = async (onPurchase: (purchase: IPurchase) => Promise<boolean>): Promise<boolean> => {
|
|
108
108
|
const purchaseUnits: IPurchaseUnit[] = cartItems.map(cartItem => ({
|
|
109
109
|
purchaseKey: cartItem.item.key,
|
|
110
110
|
qty: cartItem.quantity,
|
|
@@ -122,12 +122,14 @@ export const useStoreCart = (): IUseStoreCart => {
|
|
|
122
122
|
purchases: purchaseUnits,
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
onPurchase(purchase);
|
|
125
|
+
const success = await onPurchase(purchase);
|
|
126
126
|
|
|
127
|
-
if (isMounted.current) {
|
|
127
|
+
if (success && isMounted.current) {
|
|
128
128
|
setCartItems([]);
|
|
129
129
|
setIsCartOpen(false);
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
return success;
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
const openCart = () => setIsCartOpen(true);
|