@rpg-engine/long-bow 0.8.66 → 0.8.68

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 (37) hide show
  1. package/dist/components/Character/SkinSelectionGrid.d.ts +11 -0
  2. package/dist/components/Store/CartView.d.ts +1 -0
  3. package/dist/components/Store/MetadataCollector.d.ts +9 -0
  4. package/dist/components/Store/Store.d.ts +13 -1
  5. package/dist/components/Store/StoreCharacterSkinRow.d.ts +11 -0
  6. package/dist/components/Store/StoreItemRow.d.ts +1 -1
  7. package/dist/components/Store/hooks/useStoreCart.d.ts +6 -2
  8. package/dist/components/Store/hooks/useStoreMetadata.d.ts +15 -0
  9. package/dist/components/Store/sections/StoreItemsSection.d.ts +1 -1
  10. package/dist/index.d.ts +5 -1
  11. package/dist/long-bow.cjs.development.js +1581 -193
  12. package/dist/long-bow.cjs.development.js.map +1 -1
  13. package/dist/long-bow.cjs.production.min.js +1 -1
  14. package/dist/long-bow.cjs.production.min.js.map +1 -1
  15. package/dist/long-bow.esm.js +1549 -165
  16. package/dist/long-bow.esm.js.map +1 -1
  17. package/dist/stories/Character/SkinSelectionGrid.stories.d.ts +1 -0
  18. package/dist/stories/Character/character/CharacterSkinSelectionModal.stories.d.ts +1 -5
  19. package/dist/stories/Features/store/MetadataCollector.stories.d.ts +1 -0
  20. package/package.json +2 -2
  21. package/src/components/Character/CharacterSkinSelectionModal.tsx +18 -71
  22. package/src/components/Character/SkinSelectionGrid.tsx +179 -0
  23. package/src/components/Store/CartView.tsx +66 -7
  24. package/src/components/Store/MetadataCollector.tsx +48 -0
  25. package/src/components/Store/Store.tsx +69 -7
  26. package/src/components/Store/StoreCharacterSkinRow.tsx +286 -0
  27. package/src/components/Store/StoreItemRow.tsx +1 -1
  28. package/src/components/Store/__test__/MetadataCollector.spec.tsx +228 -0
  29. package/src/components/Store/__test__/useStoreMetadata.spec.tsx +181 -0
  30. package/src/components/Store/hooks/useStoreCart.ts +89 -44
  31. package/src/components/Store/hooks/useStoreMetadata.ts +55 -0
  32. package/src/components/Store/sections/StoreItemsSection.tsx +30 -11
  33. package/src/index.tsx +6 -3
  34. package/src/stories/Character/SkinSelectionGrid.stories.tsx +106 -0
  35. package/src/stories/Character/character/CharacterSkinSelectionModal.stories.tsx +86 -25
  36. package/src/stories/Features/store/MetadataCollector.stories.tsx +94 -0
  37. package/src/stories/Features/store/Store.stories.tsx +103 -3
@@ -1,5 +1 @@
1
- import { Meta } from '@storybook/react';
2
- import { ICharacterSkinSelectionModalProps } from '../../../components/Character/CharacterSkinSelectionModal';
3
- declare const meta: Meta;
4
- export default meta;
5
- export declare const KnightSkins: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, ICharacterSkinSelectionModalProps>;
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.66",
3
+ "version": "0.8.68",
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.9.122",
87
+ "@rpg-engine/shared": "^0.9.123",
88
88
  "dayjs": "^1.11.2",
89
89
  "font-awesome": "^4.7.0",
90
90
  "fs-extra": "^10.1.0",
@@ -1,12 +1,8 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { Button, ButtonTypes } from '../Button';
4
- import { ErrorBoundary } from '../Item/Inventory/ErrorBoundary';
5
- import PropertySelect, {
6
- IPropertiesProps,
7
- } from '../PropertySelect/PropertySelect';
8
- import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
9
4
  import { ICharacterProps } from './CharacterSelection';
5
+ import { SkinSelectionGrid } from './SkinSelectionGrid';
10
6
 
11
7
  export interface ICharacterSkinSelectionModalProps {
12
8
  isOpen: boolean;
@@ -27,51 +23,21 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
27
23
  atlasIMG,
28
24
  initialSelectedSkin = '',
29
25
  }) => {
30
- // Convert availableCharacters to the format used by PropertySelect
31
- const propertySelectValues = availableCharacters.map(item => ({
32
- id: item.textureKey,
33
- name: item.name,
34
- }));
26
+ const [selectedSkin, setSelectedSkin] = useState(initialSelectedSkin);
35
27
 
36
- // State to store the selected skin and the sprite key
37
- const [selectedValue, setSelectedValue] = useState<
38
- IPropertiesProps | undefined
39
- >();
40
- const [selectedSpriteKey, setSelectedSpriteKey] = useState('');
41
-
42
- // Update sprite when the selected value changes
43
- const updateSelectedSpriteKey = () => {
44
- const textureKey = selectedValue ? selectedValue.id : '';
45
- const spriteKey = textureKey ? textureKey + '/down/standing/0.png' : '';
46
-
47
- if (spriteKey === selectedSpriteKey) {
48
- return;
49
- }
50
-
51
- setSelectedSpriteKey(spriteKey);
52
- };
53
-
54
- // Update sprite when selectedValue changes
55
- useEffect(() => {
56
- updateSelectedSpriteKey();
57
- }, [selectedValue]);
58
-
59
- // Initialize selectedValue
28
+ // Initialize selected skin
60
29
  useEffect(() => {
61
30
  if (initialSelectedSkin) {
62
- const initialProperty = propertySelectValues.find(
63
- prop => prop.id === initialSelectedSkin
64
- );
65
- setSelectedValue(initialProperty || propertySelectValues[0]);
66
- } else if (propertySelectValues.length > 0) {
67
- setSelectedValue(propertySelectValues[0]);
31
+ setSelectedSkin(initialSelectedSkin);
32
+ } else if (availableCharacters.length > 0) {
33
+ setSelectedSkin(availableCharacters[0].textureKey);
68
34
  }
69
35
  }, [initialSelectedSkin, availableCharacters]);
70
36
 
71
37
  // Functions to handle confirmation and cancellation
72
38
  const handleConfirm = () => {
73
- if (selectedValue) {
74
- onConfirm(selectedValue.id);
39
+ if (selectedSkin) {
40
+ onConfirm(selectedSkin);
75
41
  onClose();
76
42
  }
77
43
  };
@@ -84,32 +50,12 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
84
50
 
85
51
  return (
86
52
  <Container>
87
- {selectedSpriteKey && atlasIMG && atlasJSON && (
88
- <ErrorBoundary>
89
- <SpriteFromAtlas
90
- spriteKey={selectedSpriteKey}
91
- atlasIMG={atlasIMG}
92
- atlasJSON={atlasJSON}
93
- imgScale={4}
94
- height={80}
95
- width={64}
96
- containerStyle={{
97
- display: 'flex',
98
- alignItems: 'center',
99
- paddingBottom: '15px',
100
- }}
101
- imgStyle={{
102
- left: '22px',
103
- }}
104
- />
105
- </ErrorBoundary>
106
- )}
107
-
108
- <PropertySelect
109
- availableProperties={propertySelectValues}
110
- onChange={value => {
111
- setSelectedValue(value);
112
- }}
53
+ <SkinSelectionGrid
54
+ availableCharacters={availableCharacters}
55
+ initialSelectedSkin={selectedSkin}
56
+ onChange={setSelectedSkin}
57
+ atlasJSON={atlasJSON}
58
+ atlasIMG={atlasIMG}
113
59
  />
114
60
 
115
61
  <ButtonsContainer>
@@ -119,7 +65,7 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
119
65
  <Button
120
66
  buttonType={ButtonTypes.RPGUIButton}
121
67
  onClick={handleConfirm}
122
- disabled={!selectedValue}
68
+ disabled={!selectedSkin}
123
69
  >
124
70
  Confirm
125
71
  </Button>
@@ -129,11 +75,12 @@ export const CharacterSkinSelectionModal: React.FC<ICharacterSkinSelectionModalP
129
75
  };
130
76
 
131
77
  // Styled components
132
-
133
78
  const Container = styled.div`
134
79
  display: flex;
135
80
  flex-direction: column;
136
81
  align-items: center;
82
+ width: 400px;
83
+ max-width: 100%;
137
84
  image-rendering: pixelated;
138
85
  `;
139
86
 
@@ -142,7 +89,7 @@ const ButtonsContainer = styled.div`
142
89
  justify-content: center;
143
90
  gap: 0.8rem;
144
91
  width: 100%;
145
- margin: 3rem 0 0.75rem;
92
+ margin: 2rem 0 0.75rem;
146
93
 
147
94
  button {
148
95
  min-width: 95px;
@@ -0,0 +1,179 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import SelectArrow from '../Arrow/SelectArrow';
4
+ import { ErrorBoundary } from '../Item/Inventory/ErrorBoundary';
5
+ import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
6
+ import { ICharacterProps } from './CharacterSelection';
7
+
8
+ export interface ISkinSelectionGridProps {
9
+ availableCharacters: ICharacterProps[];
10
+ initialSelectedSkin?: string;
11
+ onChange: (skinKey: string) => void;
12
+ atlasJSON: any;
13
+ atlasIMG: any;
14
+ }
15
+
16
+ export const SkinSelectionGrid: React.FC<ISkinSelectionGridProps> = ({
17
+ availableCharacters,
18
+ initialSelectedSkin = '',
19
+ onChange,
20
+ atlasJSON,
21
+ atlasIMG,
22
+ }) => {
23
+ const [currentIndex, setCurrentIndex] = useState(0);
24
+
25
+ // Find the initial index based on initialSelectedSkin
26
+ useEffect(() => {
27
+ if (initialSelectedSkin && availableCharacters.length > 0) {
28
+ const initialIndex = availableCharacters.findIndex(
29
+ character => character.textureKey === initialSelectedSkin
30
+ );
31
+ if (initialIndex !== -1) {
32
+ setCurrentIndex(initialIndex);
33
+ }
34
+ }
35
+ }, [initialSelectedSkin, availableCharacters]);
36
+
37
+ // Update the selected skin when currentIndex changes
38
+ useEffect(() => {
39
+ if (availableCharacters.length > 0) {
40
+ const selectedCharacter = availableCharacters[currentIndex];
41
+ onChange(selectedCharacter.textureKey);
42
+ }
43
+ }, [currentIndex, availableCharacters, onChange]);
44
+
45
+ const handlePrevious = () => {
46
+ setCurrentIndex((prevIndex) =>
47
+ prevIndex === 0 ? availableCharacters.length - 1 : prevIndex - 1
48
+ );
49
+ };
50
+
51
+ const handleNext = () => {
52
+ setCurrentIndex((prevIndex) =>
53
+ prevIndex === availableCharacters.length - 1 ? 0 : prevIndex + 1
54
+ );
55
+ };
56
+
57
+ const getSpriteKey = (textureKey: string) => {
58
+ return textureKey + '/down/standing/0.png';
59
+ };
60
+
61
+ if (availableCharacters.length === 0) {
62
+ return <Container>No skins available</Container>;
63
+ }
64
+
65
+ const currentCharacter = availableCharacters[currentIndex];
66
+
67
+ return (
68
+ <Container>
69
+ <Header>Select Your Character Skin</Header>
70
+ <CarouselContainer>
71
+ <NavButtonWrapper>
72
+ <SelectArrow
73
+ direction="left"
74
+ onPointerDown={handlePrevious}
75
+ />
76
+ </NavButtonWrapper>
77
+
78
+ <SkinPreview>
79
+ {currentCharacter && (
80
+ <>
81
+ <ErrorBoundary>
82
+ <SpriteFromAtlas
83
+ spriteKey={getSpriteKey(currentCharacter.textureKey)}
84
+ atlasIMG={atlasIMG}
85
+ atlasJSON={atlasJSON}
86
+ imgScale={4}
87
+ height={80}
88
+ width={64}
89
+ containerStyle={{
90
+ display: 'flex',
91
+ alignItems: 'center',
92
+ justifyContent: 'center',
93
+ }}
94
+ imgStyle={{
95
+ position: 'relative',
96
+ left: 0,
97
+ }}
98
+ />
99
+ </ErrorBoundary>
100
+ <SkinName>{currentCharacter.name}</SkinName>
101
+ </>
102
+ )}
103
+ </SkinPreview>
104
+
105
+ <NavButtonWrapper>
106
+ <SelectArrow
107
+ direction="right"
108
+ onPointerDown={handleNext}
109
+ />
110
+ </NavButtonWrapper>
111
+ </CarouselContainer>
112
+
113
+ <CounterIndicator>
114
+ {currentIndex + 1} / {availableCharacters.length}
115
+ </CounterIndicator>
116
+ </Container>
117
+ );
118
+ };
119
+
120
+ const Container = styled.div`
121
+ display: flex;
122
+ flex-direction: column;
123
+ align-items: center;
124
+ width: 100%;
125
+ margin-top: 1rem;
126
+ image-rendering: pixelated;
127
+ `;
128
+
129
+ const Header = styled.h3`
130
+ margin: 0 0 1rem 0;
131
+ color: white;
132
+ text-align: center;
133
+ font-size: 1.1rem;
134
+ `;
135
+
136
+ const CarouselContainer = styled.div`
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: space-between;
140
+ width: 100%;
141
+ margin-bottom: 0.5rem;
142
+ position: relative;
143
+ `;
144
+
145
+ const SkinPreview = styled.div`
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ justify-content: center;
150
+ padding: 1rem;
151
+ background-color: rgba(245, 158, 11, 0.1);
152
+ border: 2px solid #f59e0b;
153
+ border-radius: 8px;
154
+ width: 140px;
155
+ height: 150px;
156
+ margin: 0 1.5rem;
157
+ `;
158
+
159
+ const SkinName = styled.span`
160
+ color: white;
161
+ font-size: 1rem;
162
+ margin-top: 0.8rem;
163
+ text-align: center;
164
+ font-weight: bold;
165
+ `;
166
+
167
+ const NavButtonWrapper = styled.div`
168
+ width: 40px;
169
+ height: 42px;
170
+ position: relative;
171
+ `;
172
+
173
+ const CounterIndicator = styled.div`
174
+ color: white;
175
+ font-size: 0.8rem;
176
+ margin-top: 0.5rem;
177
+ `;
178
+
179
+ export default SkinSelectionGrid;
@@ -1,13 +1,17 @@
1
- import { IStoreItem } from '@rpg-engine/shared';
1
+ import { IStoreItem, MetadataType } from '@rpg-engine/shared';
2
2
  import React, { useState } from 'react';
3
- import { FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
3
+ import { FaInfoCircle, FaShoppingBag, FaTimes, FaTrash } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../shared/CTAButton/CTAButton';
6
6
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
7
7
 
8
8
 
9
9
  interface ICartViewProps {
10
- cartItems: { item: IStoreItem; quantity: number }[];
10
+ cartItems: {
11
+ item: IStoreItem;
12
+ quantity: number;
13
+ metadata?: Record<string, any>;
14
+ }[];
11
15
  onRemoveFromCart: (itemKey: string) => void;
12
16
  onClose: () => void;
13
17
  onPurchase: () => Promise<boolean>;
@@ -15,6 +19,26 @@ interface ICartViewProps {
15
19
  atlasIMG: string;
16
20
  }
17
21
 
22
+ const MetadataDisplay: React.FC<{
23
+ type: MetadataType;
24
+ metadata: Record<string, any>;
25
+ }> = ({ type, metadata }) => {
26
+ switch (type) {
27
+ case MetadataType.CharacterSkin:
28
+ return (
29
+ <MetadataInfo>
30
+ <MetadataLabel>
31
+ <FaInfoCircle />
32
+ <span>Skin:</span>
33
+ </MetadataLabel>
34
+ <MetadataValue>{metadata.selectedSkin?.name || 'Custom skin'}</MetadataValue>
35
+ </MetadataInfo>
36
+ );
37
+ default:
38
+ return null;
39
+ }
40
+ };
41
+
18
42
  export const CartView: React.FC<ICartViewProps> = ({
19
43
  cartItems,
20
44
  onRemoveFromCart,
@@ -87,6 +111,13 @@ export const CartView: React.FC<ICartViewProps> = ({
87
111
  ${formatPrice(cartItem.item.price * cartItem.quantity)}
88
112
  </span>
89
113
  </ItemInfo>
114
+
115
+ {cartItem.metadata && cartItem.item.metadataType && (
116
+ <MetadataDisplay
117
+ type={cartItem.item.metadataType}
118
+ metadata={cartItem.metadata}
119
+ />
120
+ )}
90
121
  </ItemDetails>
91
122
 
92
123
  <CTAButton
@@ -243,19 +274,22 @@ const Footer = styled.div`
243
274
  gap: 1.5rem;
244
275
  padding-top: 1.5rem;
245
276
  border-top: 1px solid rgba(255, 255, 255, 0.1);
277
+ flex-shrink: 0;
246
278
  `;
247
279
 
248
280
  const TotalInfo = styled.div`
249
281
  display: flex;
250
282
  flex-direction: column;
251
- gap: 0.75rem;
283
+ gap: 0.5rem;
252
284
  `;
253
285
 
254
286
  const TotalRow = styled.div`
255
287
  display: flex;
288
+ align-items: center;
256
289
  justify-content: space-between;
290
+ gap: 1rem;
257
291
  font-family: 'Press Start 2P', cursive;
258
- font-size: 0.875rem;
292
+ font-size: 1rem;
259
293
  color: #ffffff;
260
294
 
261
295
  span:last-child {
@@ -265,8 +299,33 @@ const TotalRow = styled.div`
265
299
 
266
300
  const ErrorMessage = styled.div`
267
301
  color: #ef4444;
302
+ font-size: 0.875rem;
268
303
  font-family: 'Press Start 2P', cursive;
269
- font-size: 0.75rem;
270
- margin-top: 0.5rem;
271
304
  text-align: center;
272
305
  `;
306
+
307
+ const MetadataInfo = styled.div`
308
+ display: flex;
309
+ align-items: center;
310
+ margin-top: 0.5rem;
311
+ gap: 0.5rem;
312
+ font-size: 0.75rem;
313
+ color: #a3e635;
314
+ background: rgba(163, 230, 53, 0.1);
315
+ padding: 0.25rem 0.5rem;
316
+ border-radius: 4px;
317
+ `;
318
+
319
+ const MetadataLabel = styled.div`
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 0.25rem;
323
+ font-weight: bold;
324
+ color: #d9f99d;
325
+ `;
326
+
327
+ const MetadataValue = styled.div`
328
+ overflow: hidden;
329
+ text-overflow: ellipsis;
330
+ white-space: nowrap;
331
+ `;
@@ -0,0 +1,48 @@
1
+ import { MetadataType } from "@rpg-engine/shared";
2
+ import React, { useEffect } from "react";
3
+ import { CharacterSkinSelectionModal } from "../Character/CharacterSkinSelectionModal";
4
+
5
+ export interface IMetadataCollectorProps {
6
+ metadataType: MetadataType;
7
+ config: Record<string, any>;
8
+ onCollect: (metadata: Record<string, any>) => void;
9
+ onCancel: () => void;
10
+ }
11
+
12
+ export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
13
+ metadataType,
14
+ config,
15
+ onCollect,
16
+ onCancel,
17
+ }) => {
18
+ // Make sure we clean up if unmounted without collecting
19
+ useEffect(() => {
20
+ return () => {
21
+ // If we're unmounting without explicitly collecting or canceling,
22
+ // make sure to call onCancel to prevent any hanging promises
23
+ if (window.__metadataResolvers) {
24
+ onCancel();
25
+ }
26
+ };
27
+ }, [onCancel]);
28
+
29
+ // Use string comparison instead of direct property access
30
+ if (metadataType === 'CharacterSkin') {
31
+ return (
32
+ <CharacterSkinSelectionModal
33
+ isOpen={true}
34
+ onClose={onCancel}
35
+ onConfirm={(selectedSkin: any) => onCollect({ selectedSkin })}
36
+ availableCharacters={config.availableCharacters || []}
37
+ atlasJSON={config.atlasJSON}
38
+ atlasIMG={config.atlasIMG}
39
+ initialSelectedSkin={config.initialSelectedSkin}
40
+ />
41
+ );
42
+ } else {
43
+ console.warn(`No collector implemented for metadata type: ${metadataType}`);
44
+ // Auto-cancel for unhandled types to prevent hanging promises
45
+ setTimeout(onCancel, 0);
46
+ return null;
47
+ }
48
+ };
@@ -1,6 +1,6 @@
1
- import { IItemPack, IStoreItem, IStoreProps, ItemRarities, ItemSubType, ItemType } from '@rpg-engine/shared';
1
+ import { IItemPack, IPurchase, IStoreItem, ItemRarities, ItemSubType, ItemType, UserAccountTypes } from '@rpg-engine/shared';
2
2
  import React, { useMemo, useState } from 'react';
3
- import { FaShoppingCart } from 'react-icons/fa';
3
+ import { FaHistory, FaShoppingCart } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { uiColors } from '../../constants/uiColors';
6
6
  import { DraggableContainer } from '../DraggableContainer';
@@ -9,16 +9,32 @@ import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
9
9
  import { CTAButton } from '../shared/CTAButton/CTAButton';
10
10
  import { CartView } from './CartView';
11
11
  import { useStoreCart } from './hooks/useStoreCart';
12
+ import { MetadataCollector } from './MetadataCollector';
12
13
  import { StoreItemsSection } from './sections/StoreItemsSection';
13
14
  import { StorePacksSection } from './sections/StorePacksSection';
14
15
  import { StoreItemDetails } from './StoreItemDetails';
15
16
 
17
+ // Define IStoreProps locally as a workaround
18
+ export interface IStoreProps {
19
+ items: IStoreItem[];
20
+ packs?: IItemPack[];
21
+ atlasJSON: any;
22
+ atlasIMG: string;
23
+ onPurchase: (purchase: Partial<IPurchase>) => Promise<boolean>;
24
+ onShowHistory?: () => void; // Add the new optional prop
25
+ userAccountType: UserAccountTypes;
26
+ loading?: boolean;
27
+ error?: string;
28
+ onClose?: () => void;
29
+ }
30
+
16
31
  export const Store: React.FC<IStoreProps> = ({
17
32
  items,
18
33
  packs = [],
19
34
  atlasJSON,
20
35
  atlasIMG,
21
36
  onPurchase,
37
+ onShowHistory, // Destructure the new prop
22
38
  userAccountType,
23
39
  loading = false,
24
40
  error,
@@ -37,6 +53,8 @@ export const Store: React.FC<IStoreProps> = ({
37
53
  getTotalPrice,
38
54
  isCartOpen,
39
55
  } = useStoreCart();
56
+ const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
57
+ const [currentMetadataItem, setCurrentMetadataItem] = useState<IStoreItem | null>(null);
40
58
 
41
59
  const handleAddPackToCart = (pack: IItemPack) => {
42
60
  const packItem: IStoreItem = {
@@ -87,6 +105,29 @@ export const Store: React.FC<IStoreProps> = ({
87
105
  [items]
88
106
  );
89
107
 
108
+ const handleMetadataCollected = (metadata: Record<string, any>) => {
109
+ if (currentMetadataItem && window.__metadataResolvers) {
110
+ // Resolve the promise in the useStoreMetadata hook
111
+ window.__metadataResolvers.resolve(metadata);
112
+
113
+ // Reset the metadata collection state
114
+ setCurrentMetadataItem(null);
115
+ // Removed unused setPendingMetadataQuantity call
116
+ }
117
+ };
118
+
119
+ const handleMetadataCancel = () => {
120
+ if (window.__metadataResolvers) {
121
+ // Resolve with null to indicate cancellation
122
+ window.__metadataResolvers.resolve(null);
123
+ }
124
+
125
+ // Reset the metadata collection state
126
+ setCurrentMetadataItem(null);
127
+ // Removed unused setPendingMetadataQuantity call
128
+ setIsCollectingMetadata(false);
129
+ };
130
+
90
131
  if (loading) {
91
132
  return <LoadingMessage>Loading...</LoadingMessage>;
92
133
  }
@@ -141,9 +182,16 @@ export const Store: React.FC<IStoreProps> = ({
141
182
  minWidth="600px"
142
183
  height="auto"
143
184
  type={RPGUIContainerTypes.Framed}
144
- cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails']"
185
+ cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails'], .close-button"
145
186
  >
146
- {isCartOpen ? (
187
+ {isCollectingMetadata && currentMetadataItem && currentMetadataItem.metadataType ? (
188
+ <MetadataCollector
189
+ metadataType={currentMetadataItem.metadataType}
190
+ config={currentMetadataItem.metadataConfig || {}}
191
+ onCollect={handleMetadataCollected}
192
+ onCancel={handleMetadataCancel}
193
+ />
194
+ ) : isCartOpen ? (
147
195
  <CartView
148
196
  cartItems={cartItems}
149
197
  onRemoveFromCart={handleRemoveFromCart}
@@ -169,12 +217,19 @@ export const Store: React.FC<IStoreProps> = ({
169
217
  ) : (
170
218
  <Container>
171
219
  <TopBar>
220
+ <HistoryButton>
221
+ {onShowHistory && (
222
+ <CTAButton
223
+ icon={<FaHistory />}
224
+ label="History"
225
+ onClick={onShowHistory}
226
+ />
227
+ )}
228
+ </HistoryButton>
172
229
  <CartButton>
173
230
  <CTAButton
174
231
  icon={<FaShoppingCart />}
175
- label={`${getTotalItems()} items ($${getTotalPrice().toFixed(
176
- 2
177
- )})`}
232
+ label={`${getTotalItems()} items ($${getTotalPrice().toFixed(2)})`}
178
233
  onClick={openCart}
179
234
  />
180
235
  </CartButton>
@@ -223,6 +278,7 @@ const Container = styled.div`
223
278
  width: 100%;
224
279
  height: 100%;
225
280
  gap: 1rem;
281
+ position: relative;
226
282
  `;
227
283
 
228
284
  const TopBar = styled.div`
@@ -232,6 +288,12 @@ const TopBar = styled.div`
232
288
  gap: 1rem;
233
289
  padding: 0 1rem;
234
290
  flex-shrink: 0;
291
+ margin-top: 0.5rem;
292
+ `;
293
+
294
+ const HistoryButton = styled.div`
295
+ min-width: fit-content;
296
+ margin-right: auto;
235
297
  `;
236
298
 
237
299
  const CartButton = styled.div`