@rpg-engine/long-bow 0.8.218 → 0.8.220

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 (33) hide show
  1. package/dist/components/Store/MetadataCollector.d.ts +2 -2
  2. package/dist/components/Store/Store.d.ts +10 -21
  3. package/dist/components/Store/StoreHeader.d.ts +14 -0
  4. package/dist/components/Store/hooks/useStoreCart.d.ts +2 -0
  5. package/dist/components/Store/hooks/useStoreMetadata.d.ts +4 -11
  6. package/dist/components/Store/hooks/useStoreTabs.d.ts +20 -0
  7. package/dist/components/Store/internal/packToBlueprint.d.ts +2 -0
  8. package/dist/components/Store/sections/StoreItemsSection.d.ts +5 -3
  9. package/dist/hooks/useStoreFiltering.d.ts +7 -4
  10. package/dist/long-bow.cjs.development.js +346 -375
  11. package/dist/long-bow.cjs.development.js.map +1 -1
  12. package/dist/long-bow.cjs.production.min.js +1 -1
  13. package/dist/long-bow.cjs.production.min.js.map +1 -1
  14. package/dist/long-bow.esm.js +348 -377
  15. package/dist/long-bow.esm.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/components/Store/CartView.tsx +7 -2
  18. package/src/components/Store/MetadataCollector.tsx +60 -40
  19. package/src/components/Store/Store.tsx +75 -270
  20. package/src/components/Store/StoreHeader.tsx +74 -0
  21. package/src/components/Store/__test__/MetadataCollector.spec.tsx +94 -164
  22. package/src/components/Store/__test__/Store.spec.tsx +4 -0
  23. package/src/components/Store/__test__/useStoreMetadata.spec.tsx +58 -156
  24. package/src/components/Store/__test__/useStoreTabs.spec.tsx +69 -0
  25. package/src/components/Store/hooks/useStoreCart.ts +5 -2
  26. package/src/components/Store/hooks/useStoreMetadata.ts +30 -48
  27. package/src/components/Store/hooks/useStoreTabs.ts +104 -0
  28. package/src/components/Store/internal/packToBlueprint.ts +21 -0
  29. package/src/components/Store/sections/StoreItemsSection.tsx +19 -60
  30. package/src/components/Store/sections/StorePacksSection.tsx +0 -1
  31. package/src/components/shared/ScrollableContent/ScrollableContent.tsx +3 -6
  32. package/src/hooks/useStoreFiltering.spec.tsx +79 -0
  33. package/src/hooks/useStoreFiltering.ts +27 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.218",
3
+ "version": "0.8.220",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -260,14 +260,17 @@ const Container = styled.div`
260
260
  display: flex;
261
261
  flex-direction: column;
262
262
  width: 100%;
263
- gap: 1rem;
263
+ height: 100%;
264
264
  padding: 1rem;
265
+ overflow: hidden;
266
+ box-sizing: border-box;
265
267
  `;
266
268
 
267
269
  const Header = styled.div`
268
270
  display: flex;
269
271
  justify-content: space-between;
270
272
  align-items: center;
273
+ flex-shrink: 0;
271
274
  `;
272
275
 
273
276
  const Title = styled.h2`
@@ -293,8 +296,10 @@ const CartItems = styled.div`
293
296
  display: flex;
294
297
  flex-direction: column;
295
298
  gap: 0.5rem;
299
+ flex: 1;
300
+ min-height: 0;
296
301
  overflow-y: auto;
297
- max-height: 250px;
302
+ margin: 1rem 0;
298
303
  padding-right: 0.5rem;
299
304
 
300
305
  &::-webkit-scrollbar {
@@ -1,48 +1,68 @@
1
- import { MetadataType } from "@rpg-engine/shared";
2
- import React, { useEffect } from "react";
3
- import { CharacterSkinSelectionModal } from "../Character/CharacterSkinSelectionModal";
1
+ import { MetadataType } from '@rpg-engine/shared';
2
+ import React, { useCallback, useEffect, useRef } from 'react';
3
+ import { CharacterSkinSelectionModal } from '../Character/CharacterSkinSelectionModal';
4
4
 
5
5
  export interface IMetadataCollectorProps {
6
- metadataType: MetadataType;
7
- config: Record<string, any>;
8
- onCollect: (metadata: Record<string, any>) => void;
9
- onCancel: () => void;
6
+ metadataType: MetadataType;
7
+ config: Record<string, any>;
8
+ onCollect: (metadata: Record<string, any>) => void;
9
+ onCancel: () => void;
10
10
  }
11
11
 
12
12
  export const MetadataCollector: React.FC<IMetadataCollectorProps> = ({
13
- metadataType,
14
- config,
15
- onCollect,
16
- onCancel,
13
+ metadataType,
14
+ config,
15
+ onCollect,
16
+ onCancel,
17
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;
18
+ const isPendingRef = useRef(true);
19
+
20
+ const finalize = useCallback((callback: () => void) => {
21
+ if (!isPendingRef.current) {
22
+ return;
23
+ }
24
+
25
+ isPendingRef.current = false;
26
+ callback();
27
+ }, []);
28
+
29
+ const handleCollect = useCallback((metadata: Record<string, any>) => {
30
+ finalize(() => onCollect(metadata));
31
+ }, [finalize, onCollect]);
32
+
33
+ const handleCancel = useCallback(() => {
34
+ finalize(onCancel);
35
+ }, [finalize, onCancel]);
36
+
37
+ useEffect(() => {
38
+ return () => {
39
+ handleCancel();
40
+ };
41
+ }, [handleCancel]);
42
+
43
+ useEffect(() => {
44
+ if (metadataType === MetadataType.CharacterSkin) {
45
+ return undefined;
47
46
  }
47
+
48
+ const timer = window.setTimeout(handleCancel, 0);
49
+ return () => window.clearTimeout(timer);
50
+ }, [handleCancel, metadataType]);
51
+
52
+ if (metadataType === MetadataType.CharacterSkin) {
53
+ return (
54
+ <CharacterSkinSelectionModal
55
+ isOpen
56
+ onClose={handleCancel}
57
+ onConfirm={(selectedSkin: any) => handleCollect({ selectedSkin })}
58
+ availableCharacters={config.availableCharacters || []}
59
+ atlasJSON={config.atlasJSON}
60
+ atlasIMG={config.atlasIMG}
61
+ initialSelectedSkin={config.initialSelectedSkin}
62
+ />
63
+ );
64
+ }
65
+
66
+ console.warn(`No collector implemented for metadata type: ${metadataType}`);
67
+ return null;
48
68
  };
@@ -1,4 +1,4 @@
1
- import { IItemPack, IPurchase, IProductBlueprint, ItemRarities, ItemSubType, ItemType, UserAccountTypes, PaymentCurrency, PurchaseType } from '@rpg-engine/shared';
1
+ import { IItemPack, IPurchase, IProductBlueprint, UserAccountTypes } from '@rpg-engine/shared';
2
2
  import { Crown } from 'pixelarticons/react/Crown';
3
3
  import { Gift } from 'pixelarticons/react/Gift';
4
4
  import { Package } from 'pixelarticons/react/Package';
@@ -9,22 +9,20 @@ import styled from 'styled-components';
9
9
  import { uiColors } from '../../constants/uiColors';
10
10
  import { DraggableContainer } from '../DraggableContainer';
11
11
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
12
- import { Tabs } from '../shared/Tabs';
13
- import { LabelPill } from '../shared/LabelPill/LabelPill';
14
12
  import { CTAButton } from '../shared/CTAButton/CTAButton';
13
+ import { LabelPill } from '../shared/LabelPill/LabelPill';
15
14
  import { CartView } from './CartView';
16
15
  import { FeaturedBanner, IFeaturedItem } from './FeaturedBanner';
16
+ import { packToBlueprint } from './internal/packToBlueprint';
17
17
  import { useStoreCart } from './hooks/useStoreCart';
18
+ import { TabId, useStoreTabs } from './hooks/useStoreTabs';
18
19
  import { MetadataCollector } from './MetadataCollector';
20
+ import { StoreHeader } from './StoreHeader';
19
21
  import { StoreItemsSection } from './sections/StoreItemsSection';
20
22
  import { StorePacksSection } from './sections/StorePacksSection';
21
23
  import { StoreRedeemSection } from './sections/StoreRedeemSection';
22
24
  import { StoreItemDetails } from './StoreItemDetails';
23
25
 
24
- // Define TabId union type for tab identifiers
25
- type TabId = 'premium' | 'packs' | 'items' | 'characters' | 'wallet' | 'history' | 'redeem';
26
-
27
- // Define IStoreProps locally as a workaround
28
26
  export interface IStoreProps {
29
27
  items: IProductBlueprint[];
30
28
  packs?: IItemPack[];
@@ -47,47 +45,33 @@ export interface IStoreProps {
47
45
  customCharactersContent?: React.ReactNode;
48
46
  customWalletContent?: React.ReactNode;
49
47
  customHistoryContent?: React.ReactNode;
50
- /** When true the store renders full-screen (useful on mobile). */
51
48
  fullScreen?: boolean;
52
- /** Override the DraggableContainer width (e.g. "90vw"). Defaults to "1000px". */
53
- containerWidth?: string;
54
- /** Override the DraggableContainer height (e.g. "80vh"). Defaults to "80vh" so the inner scroll area is bounded. */
55
- containerHeight?: string;
56
49
  packsBadge?: string;
57
50
  featuredItems?: IFeaturedItem[];
58
51
  onQuickBuy?: (item: IProductBlueprint, quantity?: number) => void;
59
52
  itemBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
60
53
  packBadges?: Record<string, { badges?: import('./StoreBadges').IStoreBadge[]; buyCount?: number; viewersCount?: number; saleEndsAt?: string; originalPrice?: number }>;
61
- /** Fires when an item row becomes visible (on mount). Useful for store_item_viewed analytics. */
62
54
  onItemView?: (item: IProductBlueprint, position: number) => void;
63
- /** Fires when a pack row becomes visible (on mount). Useful for pack_viewed analytics. */
64
55
  onPackView?: (pack: IItemPack, position: number) => void;
65
- /** Fires when the active store tab changes (e.g. 'items', 'packs', 'premium'). */
66
56
  onTabChange?: (tab: string, itemsShown: number) => void;
67
- /** Fires when the category filter changes in the items tab. */
68
57
  onCategoryChange?: (category: string, itemsShown: number) => void;
69
- /** Fires when the cart is opened. */
70
58
  onCartOpen?: () => void;
71
- /** Fires when any item or pack is added to the cart. */
72
59
  onAddToCart?: (item: IProductBlueprint, quantity: number) => void;
73
- /** Fires when an item is removed from the cart. */
74
60
  onRemoveFromCart?: (itemKey: string) => void;
75
- /** Fires when the user taps "Pay" — before the purchase resolves. */
76
61
  onCheckoutStart?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
77
- /** Fires after a successful purchase. */
78
62
  onPurchaseSuccess?: (items: Array<{ key: string; name: string; quantity: number }>, total: number) => void;
79
- /** Fires when a purchase fails. */
80
63
  onPurchaseError?: (error: string) => void;
81
- /** Called when the DC nudge in CartView is tapped — open the DC purchase flow. */
82
64
  onBuyDC?: () => void;
83
- /** Currency symbol to display (e.g. "$" for USD, "R$" for BRL). Defaults to "$". */
84
65
  currencySymbol?: string;
85
- /** Callback to redeem a voucher code. When provided, the Redeem tab is shown. */
86
66
  onRedeem?: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
87
- /** Called when the voucher code input gains focus. */
88
67
  onRedeemInputFocus?: () => void;
89
- /** Called when the voucher code input loses focus. */
90
68
  onRedeemInputBlur?: () => void;
69
+ /** Override the modal width. Defaults to '1000px'. */
70
+ width?: string;
71
+ /** Override the modal height. Defaults to 'min(85vh, 900px)'. */
72
+ height?: string;
73
+ /** Override the item category filter pills. Auto-derived from item.itemType when omitted. */
74
+ itemCategoryOptions?: Array<{ value: string; label: string }>;
91
75
  }
92
76
 
93
77
  export type { IFeaturedItem };
@@ -114,8 +98,6 @@ export const Store: React.FC<IStoreProps> = ({
114
98
  customWalletContent,
115
99
  customHistoryContent,
116
100
  fullScreen = false,
117
- containerWidth,
118
- containerHeight,
119
101
  packsTabLabel = 'Packs',
120
102
  packsBadge,
121
103
  featuredItems,
@@ -137,23 +119,12 @@ export const Store: React.FC<IStoreProps> = ({
137
119
  onRedeem,
138
120
  onRedeemInputFocus,
139
121
  onRedeemInputBlur,
122
+ width = '1000px',
123
+ height = 'min(85vh, 900px)',
124
+ itemCategoryOptions,
140
125
  }) => {
141
- const defaultTabOrder: TabId[] = ['premium', 'packs', 'items'];
142
126
  const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
143
- const [activeTab, setActiveTab] = useState<TabId>(() => {
144
- const allTabIds: TabId[] = [
145
- ...(tabOrder ?? defaultTabOrder),
146
- ...(customCharactersContent ? ['characters' as TabId] : []),
147
- ...(onRedeem ? ['redeem' as TabId] : []),
148
- ...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
149
- ...((onShowHistory || customHistoryContent) ? ['history' as TabId] : []),
150
- ];
151
- const validTabs = Array.from(new Set(allTabIds.filter(id => !(hidePremiumTab && id === 'premium'))));
152
- if (defaultActiveTab && validTabs.includes(defaultActiveTab)) {
153
- return defaultActiveTab;
154
- }
155
- return validTabs[0] ?? (hidePremiumTab ? 'items' : 'premium');
156
- });
127
+
157
128
  const {
158
129
  cartItems,
159
130
  handleAddToCart,
@@ -164,9 +135,32 @@ export const Store: React.FC<IStoreProps> = ({
164
135
  getTotalItems,
165
136
  getTotalPrice,
166
137
  isCartOpen,
138
+ isCollectingMetadata,
139
+ currentMetadataItem,
140
+ resolveMetadata,
167
141
  } = useStoreCart();
168
- const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
169
- const [currentMetadataItem, setCurrentMetadataItem] = useState<IProductBlueprint | null>(null);
142
+
143
+ const filteredItems = useMemo(() => ({
144
+ items: items,
145
+ premium: items.filter(item => (item.requiredAccountType?.length ?? 0) > 0),
146
+ }), [items]);
147
+
148
+ const { availableTabIds, activeTab, handleTabChange } = useStoreTabs({
149
+ tabOrder,
150
+ defaultActiveTab,
151
+ hidePremiumTab,
152
+ hasCharacters: !!customCharactersContent,
153
+ hasRedeem: !!onRedeem,
154
+ hasWallet: !!(onShowWallet || customWalletContent),
155
+ hasHistory: !!(onShowHistory || customHistoryContent),
156
+ onTabChange: onTabChange as ((tab: TabId, itemsShown: number) => void) | undefined,
157
+ getItemCount: (tab) => {
158
+ if (tab === 'items') return filteredItems.items.length;
159
+ if (tab === 'premium') return filteredItems.premium.length;
160
+ if (tab === 'packs') return packs.length;
161
+ return 0;
162
+ },
163
+ });
170
164
 
171
165
  const handleOpenCart = () => {
172
166
  openCart();
@@ -184,87 +178,14 @@ export const Store: React.FC<IStoreProps> = ({
184
178
  };
185
179
 
186
180
  const handleAddPackToCart = (pack: IItemPack, quantity: number = 1) => {
187
- const packItem: IProductBlueprint = {
188
- key: pack.key,
189
- name: pack.title,
190
- description: pack.description || '',
191
- price: pack.priceUSD,
192
- currency: PaymentCurrency.USD,
193
- texturePath: pack.image.default || pack.image.src,
194
- type: PurchaseType.Pack,
195
- onPurchase: async () => {},
196
- itemType: ItemType.Consumable,
197
- itemSubType: ItemSubType.Other,
198
- rarity: ItemRarities.Common,
199
- weight: 0,
200
- isStackable: false,
201
- maxStackSize: 1,
202
- isUsable: false,
203
- };
204
- handleAddToCart(packItem, quantity);
205
- onAddToCart?.(packItem, quantity);
206
- };
207
-
208
- const filterItems = (
209
- itemsToFilter: IProductBlueprint[],
210
- type: 'items' | 'premium'
211
- ): IProductBlueprint[] => {
212
- return itemsToFilter.filter(item => {
213
- if (type === 'premium') {
214
- return item.requiredAccountType?.length ?? 0 > 0;
215
- }
216
- return true;
217
- });
218
- };
219
-
220
- const filteredItems = useMemo(
221
- () => ({
222
- items: filterItems(items, 'items'),
223
- premium: filterItems(items, 'premium'),
224
- }),
225
- [items]
226
- );
227
-
228
- const handleMetadataCollected = (metadata: Record<string, any>) => {
229
- if (currentMetadataItem && window.__metadataResolvers) {
230
- // Resolve the promise in the useStoreMetadata hook
231
- window.__metadataResolvers.resolve(metadata);
232
-
233
- // Reset the metadata collection state
234
- setCurrentMetadataItem(null);
235
- // Removed unused setPendingMetadataQuantity call
236
- }
181
+ const bp = packToBlueprint(pack);
182
+ handleAddToCart(bp, quantity);
183
+ onAddToCart?.(bp, quantity);
237
184
  };
238
185
 
239
- const handleMetadataCancel = () => {
240
- if (window.__metadataResolvers) {
241
- // Resolve with null to indicate cancellation
242
- window.__metadataResolvers.resolve(null);
243
- }
244
-
245
- // Reset the metadata collection state
246
- setCurrentMetadataItem(null);
247
- // Removed unused setPendingMetadataQuantity call
248
- setIsCollectingMetadata(false);
249
- };
250
-
251
- if (loading) {
252
- return <LoadingMessage>Loading...</LoadingMessage>;
253
- }
254
-
255
- if (error) {
256
- return <ErrorMessage>{error}</ErrorMessage>;
257
- }
258
-
259
- // Build tabs dynamically based on props
260
- const tabIds: TabId[] = [
261
- ...(tabOrder ?? defaultTabOrder),
262
- ...(customCharactersContent ? ['characters' as TabId] : []),
263
- ...(onRedeem ? ['redeem' as TabId] : []),
264
- ...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
265
- ...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
266
- ];
267
- const availableTabIds: TabId[] = Array.from(new Set(tabIds.filter(id => !(hidePremiumTab && id === 'premium'))));
186
+ const makePackQuickBuy = onQuickBuy
187
+ ? (pack: IItemPack, qty?: number) => onQuickBuy(packToBlueprint(pack), qty)
188
+ : undefined;
268
189
 
269
190
  const tabsMap: Record<string, { id: TabId; title: ReactNode; icon: ReactNode; content: ReactNode }> = {
270
191
  premium: {
@@ -275,26 +196,7 @@ export const Store: React.FC<IStoreProps> = ({
275
196
  <StorePacksSection
276
197
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
277
198
  onAddToCart={handleAddPackToCart}
278
- onQuickBuy={onQuickBuy ? (pack, qty) => {
279
- const bp: IProductBlueprint = {
280
- key: pack.key,
281
- name: pack.title,
282
- description: pack.description || '',
283
- price: pack.priceUSD,
284
- currency: PaymentCurrency.USD,
285
- texturePath: pack.image.default || pack.image.src,
286
- type: PurchaseType.Pack,
287
- onPurchase: async () => {},
288
- itemType: ItemType.Consumable,
289
- itemSubType: ItemSubType.Other,
290
- rarity: ItemRarities.Common,
291
- weight: 0,
292
- isStackable: false,
293
- maxStackSize: 1,
294
- isUsable: false,
295
- };
296
- onQuickBuy(bp, qty);
297
- } : undefined}
199
+ onQuickBuy={makePackQuickBuy}
298
200
  onSelectPack={setSelectedPack}
299
201
  atlasJSON={atlasJSON}
300
202
  atlasIMG={atlasIMG}
@@ -317,26 +219,7 @@ export const Store: React.FC<IStoreProps> = ({
317
219
  <StorePacksSection
318
220
  packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
319
221
  onAddToCart={handleAddPackToCart}
320
- onQuickBuy={onQuickBuy ? (pack, qty) => {
321
- const bp: IProductBlueprint = {
322
- key: pack.key,
323
- name: pack.title,
324
- description: pack.description || '',
325
- price: pack.priceUSD,
326
- currency: PaymentCurrency.USD,
327
- texturePath: pack.image.default || pack.image.src,
328
- type: PurchaseType.Pack,
329
- onPurchase: async () => {},
330
- itemType: ItemType.Consumable,
331
- itemSubType: ItemSubType.Other,
332
- rarity: ItemRarities.Common,
333
- weight: 0,
334
- isStackable: false,
335
- maxStackSize: 1,
336
- isUsable: false,
337
- };
338
- onQuickBuy(bp, qty);
339
- } : undefined}
222
+ onQuickBuy={makePackQuickBuy}
340
223
  onSelectPack={setSelectedPack}
341
224
  atlasJSON={atlasJSON}
342
225
  atlasIMG={atlasIMG}
@@ -354,7 +237,7 @@ export const Store: React.FC<IStoreProps> = ({
354
237
  <StoreItemsSection
355
238
  items={filteredItems.items}
356
239
  onAddToCart={handleAddToCartTracked}
357
- onQuickBuy={onQuickBuy ? (item, qty, _meta) => onQuickBuy(item, qty) : undefined}
240
+ onQuickBuy={onQuickBuy ? (item, qty) => onQuickBuy(item, qty) : undefined}
358
241
  atlasJSON={atlasJSON}
359
242
  atlasIMG={atlasIMG}
360
243
  userAccountType={userAccountType}
@@ -363,6 +246,7 @@ export const Store: React.FC<IStoreProps> = ({
363
246
  onItemView={onItemView}
364
247
  onCategoryChange={onCategoryChange}
365
248
  currencySymbol={currencySymbol}
249
+ categoryOptions={itemCategoryOptions}
366
250
  />
367
251
  ),
368
252
  },
@@ -406,33 +290,32 @@ export const Store: React.FC<IStoreProps> = ({
406
290
  },
407
291
  };
408
292
 
293
+ if (loading) return <LoadingMessage>Loading...</LoadingMessage>;
294
+ if (error) return <ErrorMessage>{error}</ErrorMessage>;
295
+
409
296
  return (
410
297
  <DraggableContainer
411
- title="Store"
412
298
  onCloseButton={onClose}
413
- width={containerWidth ?? "1000px"}
299
+ width={width}
414
300
  minWidth="700px"
415
- height={containerHeight ?? "80vh"}
301
+ height={height}
416
302
  type={RPGUIContainerTypes.Framed}
417
- cancelDrag="[class*='Store__Container'], [class*='CartView'], [class*='StoreItemDetails'], .close-button"
303
+ cancelDrag=".store-scroll-area, [class*='CartView'], [class*='StoreItemDetails'], .close-button"
418
304
  isFullScreen={fullScreen}
419
305
  >
420
- {isCollectingMetadata && currentMetadataItem && currentMetadataItem.metadataType ? (
306
+ {isCollectingMetadata && currentMetadataItem?.metadataType ? (
421
307
  <MetadataCollector
422
308
  metadataType={currentMetadataItem.metadataType}
423
309
  config={currentMetadataItem.metadataConfig || {}}
424
- onCollect={handleMetadataCollected}
425
- onCancel={handleMetadataCancel}
310
+ onCollect={resolveMetadata}
311
+ onCancel={() => resolveMetadata(null)}
426
312
  />
427
313
  ) : isCartOpen ? (
428
314
  <CartView
429
315
  cartItems={cartItems}
430
316
  onRemoveFromCart={handleRemoveFromCartTracked}
431
317
  onClose={closeCart}
432
- onPurchase={async () => {
433
- handleCartPurchase(onPurchase);
434
- return true;
435
- }}
318
+ onPurchase={async () => { handleCartPurchase(onPurchase); return true; }}
436
319
  atlasJSON={atlasJSON}
437
320
  atlasIMG={atlasIMG}
438
321
  onCheckoutStart={onCheckoutStart}
@@ -454,7 +337,7 @@ export const Store: React.FC<IStoreProps> = ({
454
337
  currencySymbol={currencySymbol}
455
338
  />
456
339
  ) : (
457
- <Container>
340
+ <Container className="store-scroll-area">
458
341
  {featuredItems && featuredItems.length > 0 && (
459
342
  <FeaturedBanner
460
343
  items={featuredItems}
@@ -466,62 +349,23 @@ export const Store: React.FC<IStoreProps> = ({
466
349
  }}
467
350
  onQuickBuy={onQuickBuy ? item => {
468
351
  const blueprint = items.find(bp => bp.key === item.key);
469
- if (blueprint) onQuickBuy(blueprint, 1);
470
- else {
352
+ if (blueprint) {
353
+ onQuickBuy(blueprint, 1);
354
+ } else {
471
355
  const pack = packs.find(p => p.key === item.key);
472
- if (pack) {
473
- const packBlueprint: IProductBlueprint = {
474
- key: pack.key,
475
- name: pack.title,
476
- description: pack.description || '',
477
- price: pack.priceUSD,
478
- currency: PaymentCurrency.USD,
479
- texturePath: pack.image.default || pack.image.src,
480
- type: PurchaseType.Pack,
481
- onPurchase: async () => {},
482
- itemType: ItemType.Consumable,
483
- itemSubType: ItemSubType.Other,
484
- rarity: ItemRarities.Common,
485
- weight: 0,
486
- isStackable: false,
487
- maxStackSize: 1,
488
- isUsable: false,
489
- };
490
- onQuickBuy(packBlueprint, 1);
491
- }
356
+ if (pack) onQuickBuy(packToBlueprint(pack), 1);
492
357
  }
493
358
  } : undefined}
494
359
  />
495
360
  )}
496
361
  <MainContent>
497
- <HeaderRow>
498
- <TabsFlexWrapper>
499
- <Tabs
500
- options={availableTabIds.map(id => ({ id, label: tabsMap[id]?.title, icon: tabsMap[id]?.icon }))}
501
- activeTabId={activeTab}
502
- onTabChange={(tabId) => {
503
- const nextTab = tabId as TabId;
504
- setActiveTab(nextTab);
505
- if (onTabChange) {
506
- const itemCount = nextTab === 'items' ? filteredItems.items.length
507
- : nextTab === 'premium' ? filteredItems.premium.length
508
- : nextTab === 'packs' ? packs.length
509
- : 0;
510
- onTabChange(nextTab, itemCount);
511
- }
512
- }}
513
- />
514
- </TabsFlexWrapper>
515
- <CartButtonWrapper>
516
- <CTAButton
517
- icon={<FaShoppingCart />}
518
- onClick={handleOpenCart}
519
- />
520
- {getTotalItems() > 0 && (
521
- <CartBadge>{getTotalItems()}</CartBadge>
522
- )}
523
- </CartButtonWrapper>
524
- </HeaderRow>
362
+ <StoreHeader
363
+ tabs={availableTabIds.map(id => ({ id, label: tabsMap[id]?.title, icon: tabsMap[id]?.icon }))}
364
+ activeTabId={activeTab}
365
+ onTabChange={handleTabChange}
366
+ cartItemCount={getTotalItems()}
367
+ onOpenCart={handleOpenCart}
368
+ />
525
369
  <TabContent>
526
370
  {tabsMap[activeTab]?.content}
527
371
  </TabContent>
@@ -530,7 +374,7 @@ export const Store: React.FC<IStoreProps> = ({
530
374
  <Footer>
531
375
  <CTAButton
532
376
  icon={<FaShoppingCart />}
533
- label={`Checkout · ${getTotalItems()} item${getTotalItems() !== 1 ? 's' : ''} (${currencySymbol}${getTotalPrice().toFixed(2)})`}
377
+ label={`Proceed to Checkout (${currencySymbol}${getTotalPrice().toFixed(2)})`}
534
378
  onClick={handleOpenCart}
535
379
  fullWidth
536
380
  />
@@ -546,50 +390,12 @@ const Container = styled.div`
546
390
  display: flex;
547
391
  flex-direction: column;
548
392
  width: 100%;
549
- height: calc(100% - 48px);
393
+ height: 100%;
550
394
  gap: 0.5rem;
551
395
  position: relative;
552
396
  overflow: hidden;
553
397
  `;
554
398
 
555
- const HeaderRow = styled.div`
556
- display: flex;
557
- align-items: flex-end;
558
- justify-content: space-between;
559
- margin-bottom: 0.25rem;
560
- padding-top: 10px;
561
- padding-right: 12px;
562
- `;
563
-
564
- const TabsFlexWrapper = styled.div`
565
- flex: 1;
566
- min-width: 0;
567
- `;
568
-
569
- const CartButtonWrapper = styled.div`
570
- position: relative;
571
- flex-shrink: 0;
572
- `;
573
-
574
- const CartBadge = styled.div`
575
- position: absolute;
576
- top: -8px;
577
- right: -8px;
578
- background: #ef4444; /* red */
579
- color: white;
580
- font-family: 'Press Start 2P', cursive;
581
- font-size: 0.5rem;
582
- padding: 4px;
583
- border-radius: 50%;
584
- border: 2px solid #000;
585
- display: flex;
586
- align-items: center;
587
- justify-content: center;
588
- min-width: 16px;
589
- min-height: 16px;
590
- box-sizing: content-box;
591
- `;
592
-
593
399
  const MainContent = styled.div`
594
400
  flex: 1;
595
401
  display: flex;
@@ -604,7 +410,7 @@ const TabContent = styled.div`
604
410
  `;
605
411
 
606
412
  const Footer = styled.div`
607
- padding: 0.5rem;
413
+ padding: 1rem;
608
414
  border-top: 2px solid #f59e0b;
609
415
  background: rgba(0, 0, 0, 0.2);
610
416
  flex-shrink: 0;
@@ -616,7 +422,6 @@ const TabLabelWithBadge = styled.span`
616
422
  gap: 5px;
617
423
  `;
618
424
 
619
-
620
425
  const LoadingMessage = styled.div`
621
426
  text-align: center;
622
427
  color: ${uiColors.white};