From 39738a08a18e17f9482cef756c4adaff9aa84856 Mon Sep 17 00:00:00 2001
From: Patrick Keenum <5631043+prof197@users.noreply.github.com>
Date: Wed, 22 Jan 2025 08:04:01 -0500
Subject: [PATCH] Revert "rollback starter pack changes"
This reverts commit 7cf16cd99c2133922d5982717256314d07f6c193.
---
.../details/crewAssignments/Create.js | 20 +-
src/game/launcher/Store.js | 31 +-
src/game/launcher/store/StarterPackSKU.js | 459 +++---------------
3 files changed, 112 insertions(+), 398 deletions(-)
diff --git a/src/game/interface/details/crewAssignments/Create.js b/src/game/interface/details/crewAssignments/Create.js
index d139d697a..f841ecc04 100644
--- a/src/game/interface/details/crewAssignments/Create.js
+++ b/src/game/interface/details/crewAssignments/Create.js
@@ -34,6 +34,7 @@ import useNameAvailability from '~/hooks/useNameAvailability';
import usePriceConstants from '~/hooks/usePriceConstants';
import usePriceHelper from '~/hooks/usePriceHelper';
import useSimulationEnabled from '~/hooks/useSimulationEnabled';
+import useStarterPacks from '~/hooks/useStarterPacks';
import useStore from '~/hooks/useStore';
import useWalletPurchasableBalances from '~/hooks/useWalletPurchasableBalances';
import { useSwayBalance } from '~/hooks/useWalletTokenBalance';
@@ -832,6 +833,7 @@ const TraitSelector = ({ crewmate, currentTraits, onUpdateTraits, onClose, trait
const CrewAssignmentCreate = ({ backLocation, bookSession, coverImage, crewId, crewmateId, locationId, pendingCrewmate }) => {
const history = useHistory();
+ const starterPacks = useStarterPacks();
const simulationEnabled = useSimulationEnabled();
const dispatchSimulationState = useStore((s) => s.dispatchSimulationState);
@@ -1013,8 +1015,10 @@ const CrewAssignmentCreate = ({ backLocation, bookSession, coverImage, crewId, c
// always show prompt while processing (so can see "loading")
if (isPurchasingPack || isPackPurchaseIsProcessing) return true;
+ // else, show prompt when no sway, crewmate credits, or crewmates (if not already dismissed)
+ return !(swayBalance > 0n || adalianRecruits.length > 0 || Object.keys(crewmateMap || {}).length > 0) && !packPromptDismissed
// else, show prompt when no sway and not using a credit (if not already dismissed)
- return !(swayBalance > 0n || !!crewmate?.id) && !packPromptDismissed;
+ // return !(swayBalance > 0n || !!crewmate?.id) && !packPromptDismissed;
}, [!!crewmate?.id, isPurchasingPack, packPromptDismissed, pendingTransactions, swayBalance]);
// init appearance options as desired
@@ -1626,9 +1630,15 @@ const CrewAssignmentCreate = ({ backLocation, bookSession, coverImage, crewId, c
Select
-
-
-
+ {(starterPacks || []).map((product, i) => (
+ 0 ? { marginLeft: 15 } : {}} />
+ ))}
)}
@@ -1638,7 +1648,7 @@ const CrewAssignmentCreate = ({ backLocation, bookSession, coverImage, crewId, c
}}
confirmText="Proceed with crewmate only"
onReject={() => setConfirming(false)}
- style={{ width: 960 }}
+ style={{ width: Math.max(480, starterPacks?.length * 320 + Math.max(0, (starterPacks?.length - 1) * 15) + 60) }}
/>
)}
{confirming && !shouldPromptForPack && (
diff --git a/src/game/launcher/Store.js b/src/game/launcher/Store.js
index 16ccc762c..a31ae6102 100644
--- a/src/game/launcher/Store.js
+++ b/src/game/launcher/Store.js
@@ -16,12 +16,13 @@ import StarterPackSKU from './store/StarterPackSKU';
import SwaySKU from './store/SwaySKU';
import SKULayout from './store/components/SKULayout';
import { appConfig } from '~/appConfig';
+import { useSwayBalance } from '~/hooks/useWalletTokenBalance';
const storeAssets = {
packs: 'Starter Packs',
- sway: 'Sway',
- crewmates: 'Crewmates',
asteroids: 'Asteroids',
+ crewmates: 'Crewmates',
+ sway: 'Sway',
};
if (appConfig.get('Starknet.chainId') === '0x534e5f5345504f4c4941') {
storeAssets.faucets = 'Faucets';
@@ -34,22 +35,33 @@ const coverImages = {
sway: SwayHeroImage,
};
-
const Store = () => {
- const { crew } = useCrewContext();
+ const { crew, crewmateMap, adalianRecruits } = useCrewContext();
+ const { data: swayBalance } = useSwayBalance();
const { data: priceConstants, isLoading } = usePriceConstants();
const initialSubpage = useStore(s => s.launcherSubpage);
+ // force to starter packs if !(hasSway || hasCrewmateCredits || hasCrewmates)
+ const isStarterPackUser = useMemo(
+ () => !(swayBalance > 0n || adalianRecruits.length > 0 || Object.keys(crewmateMap || {}).length > 0),
+ [adalianRecruits, crewmateMap, swayBalance]
+ );
+
+ const eligibleAssetKeys = useMemo(() => {
+ return Object.keys(storeAssets)
+ .filter((asset) => isStarterPackUser ? ['packs', 'faucets'].includes(asset) : (asset !== 'packs'))
+ }, [isStarterPackUser]);
+
const initialSelection = useMemo(() => {
// use specified starting page, or default (starter packs for new users, sway for existing)
- let selectionKey = initialSubpage || (!!crew ? 'sway' : 'packs');
- const linkedSelectionIndex = Object.keys(storeAssets).indexOf(selectionKey);
+ let selectionKey = initialSubpage || (isStarterPackUser ? 'packs' : 'sway');
+ const linkedSelectionIndex = eligibleAssetKeys.indexOf(selectionKey);
return linkedSelectionIndex >= 0 ? linkedSelectionIndex : 0;
- }, [!crew, initialSubpage]);
+ }, [!crew, initialSubpage, isStarterPackUser]);
const panes = useMemo(() => {
- return Object.keys(storeAssets).map((asset) => ({
+ return eligibleAssetKeys.map((asset) => ({
label: storeAssets[asset],
pane: (
@@ -63,9 +75,10 @@ const Store = () => {
),
}))
- }, []);
+ }, [isStarterPackUser]);
if (!priceConstants?.ADALIAN_PURCHASE_PRICE) return isLoading ? : null;
+ if (panes.length === 1) return ;
return (
{
- return buildingIds.reduce((acc, b) => {
- const inputs = Process.TYPES[Building.TYPES[b].processType].inputs;
- Object.keys(inputs).forEach((product) => {
- if (!acc[product]) acc[product] = 0;
- acc[product] += inputs[product];
- });
- return acc;
- }, {});
-};
-
-const introBuildings = [Building.IDS.WAREHOUSE];
-const basicBuildings = [Building.IDS.WAREHOUSE, Building.IDS.EXTRACTOR, Building.IDS.REFINERY];
-const advBuildings = [...basicBuildings, Building.IDS.BIOREACTOR, Building.IDS.FACTORY];
-const introBuildingShoppingList = buildingIdsToShoppingList(introBuildings);
-const basicBuildingShoppingList = buildingIdsToShoppingList(basicBuildings);
-const advBuildingShoppingList = buildingIdsToShoppingList(advBuildings);
-const uniqueProductIds = Array.from(
- new Set([
- ...Object.keys(introBuildingShoppingList),
- ...Object.keys(basicBuildingShoppingList),
- ...Object.keys(advBuildingShoppingList)
- ])
-);
-
-export const barebonesCrewmateAppearance = '0x1200010000000000041';
-
-export const packUI = {
- intro: {
- checkMarks: [
- `${introPackCrewmates}x Crewmate${introPackCrewmates === 1 ? '' : 's'} to perform game tasks (Recommended Miner and Engineer)`,
- `SWAY to construct 1x Warehouse (Production buildings may be leased from other players)`
- ],
- color: theme.colors.glowGreen,
- colorLabel: 'green',
- crewmateAppearance: barebonesCrewmateAppearance,
- flavorText: 'A pair of hands and a plan are all you need to get going in the Belt!',
- flairIcon: ,
- name: 'Explorer',
- },
- basic: {
- checkMarks: [
- `${basicPackCrewmates}x Crewmate${basicPackCrewmates === 1 ? '' : 's'} to perform game tasks (Recommended Miner and Engineer)`,
- `SWAY to construct 1x Warehouse, 1x Extractor, and 1x Refinery`
- ],
- color: theme.colors.main,
- colorLabel: undefined,
- crewmateAppearance: '0x2700020002000300032', //'0x22000200070002000a2'
- flavorText: 'A self-sufficient starter kit for your own mining and refining operation!',
- flairIcon: ,
- name: 'Strategist',
- },
- advanced: {
- checkMarks: [
- `${advPackCrewmates}x Crewmate${advPackCrewmates === 1 ? '' : 's'} to form a full crew and perform game tasks efficiently`,
- `SWAY to construct all Strategist pack buildings, plus 1x Bioreactor and 1x Factory`
- ],
- color: theme.colors.lightPurple,
- colorLabel: 'purple',
- crewmateAppearance: '0x30001000400070002000a2', //'0x3000100030002000300032'
- flavorText: 'Ready to take on the Belt with a specialized crew and expanded production capabilities!',
- flairIcon: ,
- name: 'Industrialist',
- }
-};
+import StripeCheckout from './StripeCheckout';
+import useStarterPacks from '~/hooks/useStarterPacks';
const StarterPacksOuter = styled.div`
display: flex;
@@ -111,10 +27,12 @@ const StarterPacksOuter = styled.div`
`;
const StarterPackPurchaseForm = styled(PurchaseForm)`
- flex-basis: 290px;
+ flex-basis: 320px;
+ max-width: 480px;
& > h2 {
text-align: left;
padding-left: 100px;
+ white-space: nowrap;
}
`;
@@ -195,284 +113,56 @@ const FlairCard = styled.div`
filter: drop-shadow(2px 2px 6px black);
`;
-const useStarterPackPricing = () => {
- const { data: priceConstants } = usePriceConstants();
- const priceHelper = usePriceHelper();
- const { data: wallet } = useWalletPurchasableBalances();
-
- const adalianPrice = useMemo(() => {
- return priceHelper.from(
- priceConstants?.ADALIAN_PURCHASE_PRICE || 0n,
- priceConstants?.ADALIAN_PURCHASE_TOKEN || TOKEN.USDC
- );
- }, [priceConstants]);
-
- const {
- data: resourceMarketplaces,
- dataUpdatedAt: resourceMarketplacesUpdatedAt,
- } = useShoppingListData(1, 0, uniqueProductIds);
-
- // TODO: could just add adv building difference to basic to avoid repeating all those calcs for shared buildings
- const getMarketCostForBuildingList = useCallback((buildingIds) => {
- if (!resourceMarketplaces) return 0;
-
- // get instance of resourceMarketplaces that we can be destructive with
- const dynamicMarketplaces = cloneDeep(resourceMarketplaces);
-
- // split building list into granular orders
- const allOrders = buildingIds.reduce((acc, b) => {
- const inputs = Process.TYPES[Building.TYPES[b].processType].inputs;
- Object.keys(inputs).forEach((productId) => {
- acc.push({ productId, amount: inputs[productId] });
- });
- return acc;
- }, []);
-
- // sort by size desc
- allOrders.sort((a, b) => b.amount - a.amount);
-
- // walk through orders... for each, get best remaining price, then continue
- allOrders.forEach((order) => {
- let totalFilled = 0;
- let totalPaid = 0;
- if (dynamicMarketplaces[order.productId]) {
-
- // for each marketplace, set _dynamicUnitPrice for min(target, avail)
- dynamicMarketplaces[order.productId].forEach((row) => {
- let marketFills = ordersToFills(
- 'buy',
- row.orders,
- Math.min(row.supply, order.amount),
- row.marketplace?.Exchange?.takerFee || 0,
- 1,
- row.feeEnforcement || 1
- );
- const marketplaceCost = marketFills.reduce((acc, cur) => acc + cur.fillPaymentTotal, 0);
- const marketplaceFilled = marketFills.reduce((acc, cur) => acc + cur.fillAmount, 0);
- row._dynamicUnitPrice = marketplaceCost / marketplaceFilled;
- });
-
- // sort by _dynamicUnitPrice (asc)
- dynamicMarketplaces[order.productId].sort((a, b) => a._dynamicUnitPrice - b._dynamicUnitPrice);
-
- // process orders destructively until target met
- dynamicMarketplaces[order.productId].every((row) => {
- let marketFills = ordersToFills(
- 'buy',
- row.orders,
- Math.min(row.supply, order.amount - totalFilled),
- row.marketplace?.Exchange?.takerFee || 0,
- 1,
- row.feeEnforcement || 1,
- true
- );
- const marketplaceCost = marketFills.reduce((acc, cur) => acc + cur.fillPaymentTotal, 0);
- const marketplaceFilled = marketFills.reduce((acc, cur) => acc + cur.fillAmount, 0);
-
- row.supply -= marketplaceFilled;
- totalPaid += marketplaceCost;
- totalFilled += marketplaceFilled;
- return (totalFilled < order.amount);
- });
-
- }
-
- order.cost = totalPaid / TOKEN_SCALE[TOKEN.SWAY];
- if (totalFilled < order.amount) {
- console.warn(`Unable to fill productId ${order.productId}! ${totalFilled} < ${order.amount}`);
- }
- });
-
- return allOrders.reduce((acc, o) => acc + o.cost, 0);
- }, [resourceMarketplacesUpdatedAt]);
-
- const introPackSwayMin = useMemo(() => {
- const marketCost = getMarketCostForBuildingList(introBuildings);
- return (1 + MARKET_BUFFER) * marketCost;
- }, [getMarketCostForBuildingList]);
-
- const basicPackSwayMin = useMemo(() => {
- const marketCost = getMarketCostForBuildingList(basicBuildings);
- return (1 + MARKET_BUFFER) * marketCost;
- }, [getMarketCostForBuildingList]);
-
- const advPackSwayMin = useMemo(() => {
- const marketCost = getMarketCostForBuildingList(advBuildings);
- return (1 + MARKET_BUFFER) * marketCost;
- }, [getMarketCostForBuildingList]);
-
- return useMemo(() => {
- const introMinPrice = priceHelper.from(introPackPriceUSD * TOKEN_SCALE[TOKEN.USDC], TOKEN.USDC);
- const introMinSwayValue = priceHelper.from(introPackSwayMin * TOKEN_SCALE[TOKEN.SWAY], TOKEN.SWAY);
- const introCrewmatesValue = priceHelper.from(introPackCrewmates * adalianPrice.usdcValue, TOKEN.USDC);
- const introEthValue = priceHelper.from(wallet?.shouldMaintainEthGasReserve ? GAS_BUFFER_VALUE_USDC : 0, TOKEN.USDC);
- const introSwayValue = priceHelper.from(
- Math.max(
- introMinSwayValue.usdcValue,
- introMinPrice.usdcValue - introCrewmatesValue.usdcValue - introEthValue.usdcValue
- ),
- TOKEN.USDC
- );
- const introPrice = priceHelper.from(introCrewmatesValue.usdcValue + introEthValue.usdcValue + introSwayValue.usdcValue, TOKEN.USDC);
-
- const basicMinPrice = priceHelper.from(basicPackPriceUSD * TOKEN_SCALE[TOKEN.USDC], TOKEN.USDC);
- const basicMinSwayValue = priceHelper.from(basicPackSwayMin * TOKEN_SCALE[TOKEN.SWAY], TOKEN.SWAY);
- const basicCrewmatesValue = priceHelper.from(basicPackCrewmates * adalianPrice.usdcValue, TOKEN.USDC);
- const basicEthValue = priceHelper.from(wallet?.shouldMaintainEthGasReserve ? GAS_BUFFER_VALUE_USDC : 0, TOKEN.USDC);
- const basicSwayValue = priceHelper.from(
- Math.max(
- basicMinSwayValue.usdcValue,
- basicMinPrice.usdcValue - basicCrewmatesValue.usdcValue - basicEthValue.usdcValue
- ),
- TOKEN.USDC
- );
- const basicPrice = priceHelper.from(basicCrewmatesValue.usdcValue + basicEthValue.usdcValue + basicSwayValue.usdcValue, TOKEN.USDC);
-
- const advMinPrice = priceHelper.from(advPackPriceUSD * TOKEN_SCALE[TOKEN.USDC], TOKEN.USDC);
- const advMinSwayValue = priceHelper.from(advPackSwayMin * TOKEN_SCALE[TOKEN.SWAY], TOKEN.SWAY);
- const advCrewmatesValue = priceHelper.from(advPackCrewmates * adalianPrice.usdcValue, TOKEN.USDC);
- const advEthValue = basicEthValue;
- const advSwayValue = priceHelper.from(
- Math.max(
- advMinSwayValue.usdcValue,
- advMinPrice.usdcValue - advCrewmatesValue.usdcValue - advEthValue.usdcValue
- ),
- TOKEN.USDC
- );
- const advPrice = priceHelper.from(advCrewmatesValue.usdcValue + advEthValue.usdcValue + advSwayValue.usdcValue, TOKEN.USDC);
-
- return {
- intro: {
- price: introPrice,
- crewmates: introPackCrewmates,
- crewmatesValue: introCrewmatesValue,
- ethFormatted: introEthValue.to(TOKEN.ETH, TOKEN_FORMAT.VERBOSE),
- ethValue: introEthValue,
- swayFormatted: introSwayValue.to(TOKEN.SWAY, TOKEN_FORMAT.VERBOSE),
- swayValue: introSwayValue
- },
- basic: {
- price: basicPrice,
- crewmates: basicPackCrewmates,
- crewmatesValue: basicCrewmatesValue,
- ethFormatted: basicEthValue.to(TOKEN.ETH, TOKEN_FORMAT.VERBOSE),
- ethValue: basicEthValue,
- swayFormatted: basicSwayValue.to(TOKEN.SWAY, TOKEN_FORMAT.VERBOSE),
- swayValue: basicSwayValue
- },
- advanced: {
- price: advPrice,
- crewmates: advPackCrewmates,
- crewmatesValue: advCrewmatesValue,
- ethFormatted: advEthValue.to(TOKEN.ETH, TOKEN_FORMAT.VERBOSE),
- ethValue: advEthValue,
- swayFormatted: advSwayValue.to(TOKEN.SWAY, TOKEN_FORMAT.VERBOSE),
- swayValue: advSwayValue
- }
- };
- }, [adalianPrice, advPackSwayMin, basicPackSwayMin, introPackSwayMin, priceConstants, priceHelper]);
-};
-
-export const StarterPack = ({
- packLabel,
- shouldMaintainEthGasReserve = false,
- ...props
-}) => {
- const { execute } = useContext(ChainTransactionContext);
- const { data: ethBalanceSource } = useEthBalance();
- const priceHelper = usePriceHelper();
- const { buildMultiswapFromSellAmount } = useSwapHelper();
- const packs = useStarterPackPricing();
- const { accountAddress } = useSession();
-
- // have to do it this way because purchase function is memoized before funding event
- const ethBalanceRef = useRef();
- ethBalanceRef.current = ethBalanceSource;
+export const StarterPack = ({ product, ...props }) => {
+ const { accountAddress, login } = useSession();
const { pendingTransactions } = useCrewContext();
- const { fundingPrompt, onVerifyFunds } = useFundingFlow();
const [isPurchasing, setIsPurchasing] = useState();
const createAlert = useStore(s => s.dispatchAlertLogged);
- const { pack, color, colorLabel, checkMarks, crewmateAppearance, flairIcon, flavorText, name } = useMemo(() => ({
- pack: packs[packLabel],
- ...packUI[packLabel]
- }), [packLabel]);
+ // pack
+ const { color, colorLabel, checkMarks, crewmateAppearance, flairIcon, flavorText } = product.ui;
const isPurchasingStarterPack = useMemo(() => {
return isPurchasing || (pendingTransactions || []).find(tx => tx.key === 'PurchaseStarterPack');
}, [isPurchasing, pendingTransactions]);
-
- const onPurchase = useCallback(async (setIsPurchasing) => {
- const totalPrice = pack.price;
- const crewmateTally = pack.crewmates;
- const purchaseEth_usd = pack.ethValue.to(TOKEN.USDC);
- const purchaseSway_usd = pack.swayValue.to(TOKEN.USDC);
-
- if (setIsPurchasing) setIsPurchasing(true);
-
- let ethSwapCalls = await buildMultiswapFromSellAmount(purchaseEth_usd, TOKEN.ETH);
- // this means has no usdc (likely) OR illiquid swap (unlikely)... we'll assume the former.
- // can still allow the transaction to go through as long as has enough ETH to cover the
- // cost (and subsequent swaps will not leave without any gas buffer)
- if (ethSwapCalls === false) {
- if (priceHelper.from(ethBalanceRef.current, TOKEN.ETH).to(TOKEN.USDC) >= totalPrice.usdcValue) {
- ethSwapCalls = [];
- } else {
- createAlert({
- type: 'GenericAlert',
- data: { content: 'Insufficient ETH or USDC/ETH swap liquidity!' },
- level: 'warning',
- duration: 5000
- });
- if (setIsPurchasing) setIsPurchasing(false);
- return;
- }
- }
- const swaySwapCalls = await buildMultiswapFromSellAmount(purchaseSway_usd, TOKEN.SWAY);
- if (swaySwapCalls === false) {
- createAlert({
- type: 'GenericAlert',
- data: { content: 'Insufficient SWAY swap liquidity!' },
- level: 'warning',
- duration: 5000
- });
- if (setIsPurchasing) setIsPurchasing(false);
- return;
- }
-
- fireTrackingEvent('purchase', {
- category: 'purchase',
- currency: 'USD',
- externalId: accountAddress,
- value: Number(crewmateTally) * 5,
- items: [{
- item_name: `starter_pack_${packLabel}`
- }]
- });
-
- await execute('PurchaseStarterPack', {
- collection: Crewmate.COLLECTION_IDS.ADALIAN,
- crewmateTally,
- swapCalls: [ ...ethSwapCalls, ...swaySwapCalls ]
- });
-
- if (setIsPurchasing) setIsPurchasing(false);
- }, [accountAddress, buildMultiswapFromSellAmount, execute, pack, priceHelper]);
- const onClick = useCallback(() => {
- onVerifyFunds(
- pack.price,
- () => onPurchase((which) => {
- setIsPurchasing(which);
- if (props.setIsPurchasing) {
- props.setIsPurchasing(which);
- }
- })
- )
- }, [onVerifyFunds, onPurchase, packLabel, pack.price, props.setIsPurchasing]);
+ const [isSelected, setIsSelected] = useState();
+ const onSelect = useCallback(() => {
+ if (!accountAddress) return login();
+ setIsSelected(true);
+ }, [accountAddress, login]);
+
+ const onPurchase = useCallback(() => {
+
+ // if (setIsPurchasing) setIsPurchasing(true);
+ // createAlert({
+ // type: 'GenericAlert',
+ // data: { content: 'Insufficient ETH or USDC/ETH swap liquidity!' },
+ // level: 'warning',
+ // duration: 5000
+ // });
+ // if (setIsPurchasing) setIsPurchasing(false);
+
+ // fireTrackingEvent('purchase', {
+ // category: 'purchase',
+ // currency: 'USD',
+ // externalId: accountAddress,
+ // value: Number(crewmateTally) * 5,
+ // items: [{
+ // item_name: `starter_pack_${packLabel}`
+ // }]
+ // });
+
+ // createAlert({
+ // type: 'GenericAlert',
+ // data: { content: 'Insufficient SWAY swap liquidity!' },
+ // level: 'warning',
+ // duration: 5000
+ // });
+ }, []);
// NOTE: for tinkering...
// const overwriteAppearance = useMemo(() => Crewmate.packAppearance({
@@ -488,11 +178,10 @@ export const StarterPack = ({
<>
- {flairIcon}
+ {flairIcon}
- {name}
+ {product.name} Pack
{flavorText}
-
- {pack.swayFormatted}
+
+ {product.buildings.length} Building{product.buildings.length === 1 ? '' : 's'}
- {pack.crewmates} Crewmate{pack.crewmates === 1 ? '' : 's'}
+ {product.crewmates} Crewmate{product.crewmates === 1 ? '' : 's'}
- {shouldMaintainEthGasReserve && (
-
-
-
- )}
{checkMarks.map((checkText, i) => (
@@ -544,27 +225,37 @@ export const StarterPack = ({
{!props.asButton && (
)}
- {fundingPrompt}
+ {isSelected && (
+ setIsSelected(false)}
+ price={product.price}
+ productId={product.id}
+ productName={`${product.name} Pack`} />
+ )}
>
);
};
// TODO: wrap in launch feature flag
const StarterPackSKU = () => {
+ const starterPacks = useStarterPacks();
return (
-
-
Buy Starter Packs
-
-
-
-
-
-
+
+ Choose a Starter Pack
+ 2 ? {} : { justifyContent: 'space-between' }}>
+ {(starterPacks || []).map((product, i) => (
+ 0 ? { marginLeft: 15 } : {}} />
+ ))}
);