diff --git a/package.json b/package.json
index 9a2e7586a9d..c71e89981f9 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,7 @@
"@react-navigation/material-top-tabs": "6.6.2",
"@react-navigation/native": "6.1.6",
"@react-navigation/stack": "6.3.16",
- "@reservoir0x/reservoir-sdk": "1.2.1",
+ "@reservoir0x/reservoir-sdk": "1.4.5",
"@segment/analytics-react-native": "2.15.0",
"@segment/sovran-react-native": "1.0.4",
"@sentry/react-native": "3.4.1",
diff --git a/src/App.js b/src/App.js
index b92fb27a3e7..2038bfe4a0d 100644
--- a/src/App.js
+++ b/src/App.js
@@ -85,7 +85,7 @@ import { migrate } from '@/migrations';
import { initListeners as initWalletConnectListeners } from '@/walletConnect';
import { saveFCMToken } from '@/notifications/tokens';
import branch from 'react-native-branch';
-import { initializeReservoirClient } from '@/resources/nftOffers/utils';
+import { initializeReservoirClient } from '@/resources/reservoir/client';
if (__DEV__) {
reactNativeDisableYellowBox && LogBox.ignoreAllLogs();
diff --git a/src/analytics/event.ts b/src/analytics/event.ts
index c187be4dc56..5af645fe65a 100644
--- a/src/analytics/event.ts
+++ b/src/analytics/event.ts
@@ -1,6 +1,7 @@
import { CardType } from '@/components/cards/GenericCard';
import { LearnCategory } from '@/components/cards/utils/types';
import { FiatProviderName } from '@/entities/f2c';
+import { Network } from '@/networks/types';
/**
* All events, used by `analytics.track()`
@@ -81,6 +82,7 @@ export const event = {
poapsOpenedMintSheet: 'Opened POAP mint sheet',
poapsMintedPoap: 'Minted POAP',
poapsViewedOnPoap: 'Viewed POAP on poap.gallery',
+
positionsOpenedSheet: 'Opened position Sheet',
positionsOpenedExternalDapp: 'Viewed external dapp',
@@ -89,6 +91,12 @@ export const event = {
mintsPressedMintButton: 'Pressed mint button in mints sheet',
mintsPressedViewAllMintsButton: 'Pressed view all mints button in mints card',
mintsChangedFilter: 'Changed mints filter',
+
+ mintsOpenedSheet: 'Opened NFT Mint Sheet',
+ mintsOpeningMintDotFun: 'Opening Mintdotfun',
+ mintsMintingNFT: 'Minting NFT',
+ mintsMintedNFT: 'Minted NFT',
+ mintsErrorMintingNFT: 'Error Minting NFT',
} as const;
/**
@@ -292,6 +300,37 @@ export type EventProperties = {
rainbowFee: number;
offerCurrency: { symbol: string; contractAddress: string };
};
+ [event.mintsMintingNFT]: {
+ contract: string;
+ chainId: number;
+ quantity: number;
+ collectionName: string;
+ priceInEth: string;
+ };
+ [event.mintsMintedNFT]: {
+ contract: string;
+ chainId: number;
+ quantity: number;
+ collectionName: string;
+ priceInEth: string;
+ };
+ [event.mintsErrorMintingNFT]: {
+ contract: string;
+ chainId: number;
+ quantity: number;
+ collectionName: string;
+ priceInEth: string;
+ };
+ [event.mintsOpenedSheet]: {
+ contract: string;
+ chainId: number;
+ collectionName: string;
+ };
+ [event.mintsOpeningMintDotFun]: {
+ contract: string;
+ chainId: number;
+ collectionName: string;
+ };
[event.poapsMintedPoap]: {
eventId: number;
type: 'qrHash' | 'secretWord';
diff --git a/src/components/cards/FeaturedMintCard.tsx b/src/components/cards/FeaturedMintCard.tsx
index 02149cbca2e..402904ded56 100644
--- a/src/components/cards/FeaturedMintCard.tsx
+++ b/src/components/cards/FeaturedMintCard.tsx
@@ -13,7 +13,7 @@ import {
useColorMode,
useForegroundColor,
} from '@/design-system';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { ButtonPressAnimation } from '../animations';
import { useMints } from '@/resources/mints';
import { useAccountProfile, useDimensions } from '@/hooks';
@@ -24,11 +24,13 @@ import {
convertRawAmountToRoundedDecimal,
} from '@/helpers/utilities';
import { BlurView } from '@react-native-community/blur';
-import { Linking, View } from 'react-native';
+import { View } from 'react-native';
import { IS_IOS } from '@/env';
import { Media } from '../Media';
import { analyticsV2 } from '@/analytics';
import * as i18n from '@/languages';
+import { navigateToMintCollection } from '@/resources/reservoir/mints';
+import { ethereumUtils } from '@/utils';
const IMAGE_SIZE = 111;
@@ -71,6 +73,24 @@ export function FeaturedMintCard() {
useEffect(() => setMediaRendered(false), [imageUrl]);
+ const handlePress = useCallback(() => {
+ if (featuredMint) {
+ analyticsV2.track(analyticsV2.event.mintsPressedFeaturedMintCard, {
+ contractAddress: featuredMint.contractAddress,
+ chainId: featuredMint.chainId,
+ totalMints: featuredMint.totalMints,
+ mintsLastHour: featuredMint.totalMints,
+ priceInEth: convertRawAmountToRoundedDecimal(
+ featuredMint.mintStatus.price,
+ 18,
+ 6
+ ),
+ });
+ const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId);
+ navigateToMintCollection(featuredMint.contract, network);
+ }
+ }, [featuredMint]);
+
return featuredMint ? (
@@ -108,23 +128,7 @@ export function FeaturedMintCard() {
overflow: 'hidden',
padding: 12,
}}
- onPress={() => {
- analyticsV2.track(
- analyticsV2.event.mintsPressedFeaturedMintCard,
- {
- contractAddress: featuredMint.contractAddress,
- chainId: featuredMint.chainId,
- totalMints: featuredMint.totalMints,
- mintsLastHour: featuredMint.totalMints,
- priceInEth: convertRawAmountToRoundedDecimal(
- featuredMint.mintStatus.price,
- 18,
- 6
- ),
- }
- );
- Linking.openURL(featuredMint.externalURL);
- }}
+ onPress={handlePress}
scaleTo={0.96}
>
diff --git a/src/components/cards/MintsCard/CollectionCell.tsx b/src/components/cards/MintsCard/CollectionCell.tsx
index c9eae64f150..2520166f976 100644
--- a/src/components/cards/MintsCard/CollectionCell.tsx
+++ b/src/components/cards/MintsCard/CollectionCell.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { globalColors } from '@/design-system/color/palettes';
import { convertRawAmountToRoundedDecimal } from '@/helpers/utilities';
import { CoinIcon } from '@/components/coin-icon';
@@ -15,12 +15,13 @@ import { ButtonPressAnimation } from '@/components/animations';
import { useTheme } from '@/theme';
import { Linking, View } from 'react-native';
import { MintableCollection } from '@/graphql/__generated__/arc';
-import { getNetworkFromChainId } from '@/utils/ethereumUtils';
+import ethereumUtils, { getNetworkFromChainId } from '@/utils/ethereumUtils';
import { getNetworkObj } from '@/networks';
import { analyticsV2 } from '@/analytics';
import * as i18n from '@/languages';
import { IS_IOS } from '@/env';
import { ImgixImage } from '@/components/images';
+import { navigateToMintCollection } from '@/resources/reservoir/mints';
export const NFT_IMAGE_SIZE = 111;
@@ -90,16 +91,25 @@ export function CollectionCell({
useEffect(() => setMediaRendered(false), [imageUrl]);
+ const handlePress = useCallback(() => {
+ analyticsV2.track(analyticsV2.event.mintsPressedCollectionCell, {
+ contractAddress: collection.contractAddress,
+ chainId: collection.chainId,
+ priceInEth: amount,
+ });
+
+ const network = ethereumUtils.getNetworkFromChainId(collection.chainId);
+ navigateToMintCollection(collection.contract, network);
+ }, [
+ amount,
+ collection.chainId,
+ collection.contract,
+ collection.contractAddress,
+ ]);
+
return (
{
- analyticsV2.track(analyticsV2.event.mintsPressedCollectionCell, {
- contractAddress: collection.contractAddress,
- chainId: collection.chainId,
- priceInEth: amount,
- });
- Linking.openURL(collection.externalURL);
- }}
+ onPress={handlePress}
style={{ width: NFT_IMAGE_SIZE }}
>
+ {item.network !== Network.mainnet && (
+
+ )}
) : (
{
]);
const { isDarkMode } = useTheme();
- const shadows = useMemo(() => buildShadows(color, size, isDarkMode, colors), [
- color,
- size,
- isDarkMode,
- colors,
- ]);
+ const shadows = useMemo(
+ () => buildShadows(color, size, props?.forceDarkMode || isDarkMode, colors),
+ [color, size, props?.forceDarkMode, isDarkMode, colors]
+ );
const backgroundColor =
typeof color === 'number'
diff --git a/src/components/expanded-state/CustomGasState.js b/src/components/expanded-state/CustomGasState.js
index 3e227b74982..5a9f629e525 100644
--- a/src/components/expanded-state/CustomGasState.js
+++ b/src/components/expanded-state/CustomGasState.js
@@ -41,12 +41,17 @@ const FeesPanelTabswrapper = styled(Column)(margin.object(19, 0, 24, 0));
export default function CustomGasState({ asset }) {
const { setParams } = useNavigation();
const {
- params: { longFormHeight, speeds, openCustomOptions } = {},
+ params: { longFormHeight, speeds, openCustomOptions, fallbackColor } = {},
} = useRoute();
const { colors } = useTheme();
const { height: deviceHeight } = useDimensions();
const keyboardHeight = useKeyboardHeight();
- const colorForAsset = useColorForAsset(asset || {}, null, false, true);
+ const colorForAsset = useColorForAsset(
+ asset || {},
+ fallbackColor,
+ false,
+ true
+ );
const { selectedGasFee, currentBlockParams, txNetwork } = useGas();
const [canGoBack, setCanGoBack] = useState(true);
const { tradeDetails } = useSelector(state => state.swap);
diff --git a/src/components/gas/GasSpeedButton.js b/src/components/gas/GasSpeedButton.js
index e1403b9efa0..4bff48b0017 100644
--- a/src/components/gas/GasSpeedButton.js
+++ b/src/components/gas/GasSpeedButton.js
@@ -137,6 +137,7 @@ const GasSpeedButton = ({
asset,
currentNetwork,
horizontalPadding = 19,
+ fallbackColor,
marginBottom = 20,
marginTop = 18,
speeds = null,
@@ -151,7 +152,12 @@ const GasSpeedButton = ({
const { colors } = useTheme();
const { navigate, goBack } = useNavigation();
const { nativeCurrencySymbol, nativeCurrency } = useAccountSettings();
- const rawColorForAsset = useColorForAsset(asset || {}, null, false, true);
+ const rawColorForAsset = useColorForAsset(
+ asset || {},
+ fallbackColor,
+ false,
+ true
+ );
const [isLongWait, setIsLongWait] = useState(false);
const { inputCurrency, outputCurrency } = useSwapCurrencies();
@@ -229,6 +235,7 @@ const GasSpeedButton = ({
if (gasIsNotReady) return;
navigate(Routes.CUSTOM_GAS_SHEET, {
asset,
+ fallbackColor,
flashbotTransaction,
focusTo: shouldOpenCustomGasSheet.focusTo,
openCustomOptions: focusTo => openCustomOptionsRef.current(focusTo),
diff --git a/src/entities/transactions/transactionStatus.ts b/src/entities/transactions/transactionStatus.ts
index f21b112bd72..0449bf04cfa 100644
--- a/src/entities/transactions/transactionStatus.ts
+++ b/src/entities/transactions/transactionStatus.ts
@@ -10,6 +10,8 @@ export enum TransactionStatus {
depositing = 'depositing',
dropped = 'dropped',
failed = 'failed',
+ minted = 'minted',
+ minting = 'minting',
purchased = 'purchased',
purchasing = 'purchasing',
received = 'received',
@@ -39,6 +41,8 @@ export default {
depositing: 'depositing',
dropped: 'dropped',
failed: 'failed',
+ minted: 'minted',
+ minting: 'minting',
purchased: 'purchased',
purchasing: 'purchasing',
received: 'received',
diff --git a/src/entities/transactions/transactionType.ts b/src/entities/transactions/transactionType.ts
index 62ad3f83fea..b64f2275ad5 100644
--- a/src/entities/transactions/transactionType.ts
+++ b/src/entities/transactions/transactionType.ts
@@ -7,6 +7,7 @@ export enum TransactionType {
deposit = 'deposit',
dropped = 'dropped',
execution = 'execution',
+ mint = 'mint',
purchase = 'purchase', // Rainbow-specific type
receive = 'receive',
repay = 'repay',
@@ -25,6 +26,7 @@ export default {
deposit: 'deposit',
dropped: 'dropped',
execution: 'execution',
+ mint: 'mint',
purchase: 'purchase',
receive: 'receive',
repay: 'repay',
diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql
index 62f542bc5ea..c05e9439b19 100644
--- a/src/graphql/queries/arc.graphql
+++ b/src/graphql/queries/arc.graphql
@@ -101,6 +101,51 @@ query claimPoapBySecretWord($walletAddress: String!, $secretWord: String!) {
}
}
+query getReservoirCollection($contractAddress: String!, $chainId: Int!) {
+ getReservoirCollection(contractAddress: $contractAddress, chainId: $chainId) {
+ collection {
+ id
+ chainId
+ createdAt
+ name
+ image
+ description
+ sampleImages
+ tokenCount
+ creator
+ ownerCount
+ isMintingPublicSale
+ publicMintInfo {
+ stage
+ kind
+ price {
+ currency {
+ contract
+ name
+ symbol
+ decimals
+ }
+ amount {
+ raw
+ decimal
+ usd
+ native
+ }
+ netAmount {
+ raw
+ decimal
+ usd
+ native
+ }
+ }
+ startTime
+ endTime
+ maxMintsPerWallet
+ }
+ }
+ }
+}
+
fragment mintStatus on MintStatus {
isMintable
price
diff --git a/src/handlers/transactions.ts b/src/handlers/transactions.ts
index 2d19dc80ca9..10507372fe4 100644
--- a/src/handlers/transactions.ts
+++ b/src/handlers/transactions.ts
@@ -114,6 +114,8 @@ const getConfirmedState = (type?: TransactionType): TransactionStatus => {
return TransactionStatus.approved;
case TransactionTypes.sell:
return TransactionStatus.sold;
+ case TransactionTypes.mint:
+ return TransactionStatus.minted;
case TransactionTypes.deposit:
return TransactionStatus.deposited;
case TransactionTypes.withdraw:
diff --git a/src/helpers/transactions.ts b/src/helpers/transactions.ts
index c38b59ce7f1..dfdbd5eac2a 100644
--- a/src/helpers/transactions.ts
+++ b/src/helpers/transactions.ts
@@ -99,6 +99,8 @@ export const getConfirmedState = (
return TransactionStatus.purchased;
case TransactionTypes.sell:
return TransactionStatus.sold;
+ case TransactionTypes.mint:
+ return TransactionStatus.minted;
default:
return TransactionStatus.sent;
}
diff --git a/src/hooks/useGas.ts b/src/hooks/useGas.ts
index f3e3c200fd9..4e38fe7ffa0 100644
--- a/src/hooks/useGas.ts
+++ b/src/hooks/useGas.ts
@@ -186,6 +186,18 @@ export default function useGas({
[dispatch]
);
+ const getTotalGasPrice = useCallback(() => {
+ const txFee = gasData?.selectedGasFee?.gasFee;
+ const isLegacyGasNetwork =
+ getNetworkObj(gasData?.txNetwork).gas.gasType === 'legacy';
+ const txFeeValue = isLegacyGasNetwork
+ ? (txFee as LegacyGasFee)?.estimatedFee
+ : (txFee as GasFee)?.maxFee;
+
+ const txFeeAmount = fromWei(txFeeValue?.value?.amount);
+ return txFeeAmount;
+ }, [gasData?.selectedGasFee?.gasFee, gasData?.txNetwork]);
+
return {
isGasReady,
isSufficientGas,
@@ -197,6 +209,7 @@ export default function useGas({
updateGasFeeOption,
updateToCustomGasFee,
updateTxFee,
+ getTotalGasPrice,
...gasData,
};
}
diff --git a/src/languages/en_US.json b/src/languages/en_US.json
index de016e00a69..a6e75041b59 100644
--- a/src/languages/en_US.json
+++ b/src/languages/en_US.json
@@ -1155,6 +1155,30 @@
"withdrawal_dropdown_label": "For",
"withdrawal_input_label": "Get"
},
+ "minting": {
+ "hold_to_mint": "Hold To Mint",
+ "minting": "Minting...",
+ "minted": "Minted",
+ "mint_unavailable": "Mint Unavailable",
+ "mint_on_mintdotfun": " Mint on Mint.fun",
+ "error_minting": "Error Minting",
+ "mint_price": "Mint Price",
+ "free": "Free",
+ "by": "By",
+ "unknown": "unknown",
+ "max": "Max",
+ "nft_count": "%{number} NFTs",
+ "description": "Description",
+ "total_minted": "Total Minted",
+ "first_event": "First Event",
+ "last_event": "Last Event",
+ "max_supply": "Max Supply",
+ "contract": "Contract",
+ "network": "Network",
+ "could_not_find_collection": "Could not find collection",
+ "unable_to_find_check_again": "We are unable to find this collection, double check the address and network or try again later",
+ "mintdotfun_unsupported_network": "Mint.fun does not support this network"
+ },
"nfts": {
"selling": "Selling"
},
@@ -1700,6 +1724,8 @@
"failed": "Failed",
"purchased": "Purchased",
"purchasing": "Purchasing",
+ "minting": "Minting",
+ "minted": "Minted",
"received": "Received",
"receiving": "Receiving",
"self": "Self",
diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx
index ccf3e1af69b..c7e79849409 100644
--- a/src/navigation/Routes.android.tsx
+++ b/src/navigation/Routes.android.tsx
@@ -81,6 +81,7 @@ import { NFTSingleOfferSheet } from '@/screens/NFTSingleOfferSheet';
import ShowSecretView from '@/screens/SettingsSheet/components/ShowSecretView';
import PoapSheet from '@/screens/mints/PoapSheet';
import { PositionSheet } from '@/screens/positions/PositionSheet';
+import MintSheet from '@/screens/mints/MintSheet';
import { MintsSheet } from '@/screens/MintsSheet/MintsSheet';
const Stack = createStackNavigator();
@@ -244,6 +245,7 @@ function BSNavigator() {
name={Routes.EXPANDED_ASSET_SHEET}
/>
+
+
{
+ Alert.alert(
+ lang.t(lang.l.minting.could_not_find_collection),
+ lang.t(lang.l.minting.unable_to_find_check_again),
+ [{ text: lang.t(lang.l.button.ok) }],
+ { cancelable: false }
+ );
+};
+export const navigateToMintCollection = async (
+ contractAddress: EthereumAddress,
+ network: Network
+) => {
+ logger.debug('Mints: Navigating to Mint Collection', {
+ contractAddress,
+ network,
+ });
+ try {
+ const chainId = getNetworkObj(network).id;
+ const res = await client.getReservoirCollection({
+ contractAddress,
+ chainId,
+ });
+ if (res?.getReservoirCollection?.collection) {
+ Navigation.handleAction(Routes.MINT_SHEET, {
+ collection: res.getReservoirCollection?.collection,
+ });
+ } else {
+ logger.warn('Mints: No collection found', { contractAddress, network });
+ showAlert();
+ }
+ } catch (e) {
+ logger.warn('Mints: navigateToMintCollection error', {
+ contractAddress,
+ network,
+ error: e,
+ });
+ showAlert();
+ }
+};
diff --git a/src/resources/nftOffers/index.ts b/src/resources/reservoir/nftOffersQuery.ts
similarity index 100%
rename from src/resources/nftOffers/index.ts
rename to src/resources/reservoir/nftOffersQuery.ts
diff --git a/src/resources/reservoir/utils.ts b/src/resources/reservoir/utils.ts
new file mode 100644
index 00000000000..9003a8279d8
--- /dev/null
+++ b/src/resources/reservoir/utils.ts
@@ -0,0 +1,34 @@
+import { Network } from '@/networks/types';
+
+const RAINBOW_FEE_ADDRESS_MAINNET =
+ '0x69d6d375de8c7ade7e44446df97f49e661fdad7d';
+const RAINBOW_FEE_ADDRESS_POLYGON =
+ '0xfb9af3db5e19c4165f413f53fe3bbe6226834548';
+const RAINBOW_FEE_ADDRESS_OPTIMISM =
+ '0x0d9b71891dc86400acc7ead08c80af301ccb3d71';
+const RAINBOW_FEE_ADDRESS_ARBITRUM =
+ '0x0f9259af03052c96afda88add62eb3b5cbc185f1';
+const RAINBOW_FEE_ADDRESS_BASE = '0x1bbe055ad3204fa4468b4e6d3a3c59b9d9ac8c19';
+const RAINBOW_FEE_ADDRESS_BSC = '0x9670271ec2e2937a2e9df536784344bbff2bbea6';
+const RAINBOW_FEE_ADDRESS_ZORA = '0x7a3d05c70581bd345fe117c06e45f9669205384f';
+
+export function getRainbowFeeAddress(network: Network) {
+ switch (network) {
+ case Network.mainnet:
+ return RAINBOW_FEE_ADDRESS_MAINNET;
+ case Network.polygon:
+ return RAINBOW_FEE_ADDRESS_POLYGON;
+ case Network.optimism:
+ return RAINBOW_FEE_ADDRESS_OPTIMISM;
+ case Network.arbitrum:
+ return RAINBOW_FEE_ADDRESS_ARBITRUM;
+ case Network.base:
+ return RAINBOW_FEE_ADDRESS_BASE;
+ case Network.bsc:
+ return RAINBOW_FEE_ADDRESS_BSC;
+ case Network.zora:
+ return RAINBOW_FEE_ADDRESS_ZORA;
+ default:
+ return undefined;
+ }
+}
diff --git a/src/screens/MintsSheet/card/Card.tsx b/src/screens/MintsSheet/card/Card.tsx
index 319f57f95ff..159df6c351b 100644
--- a/src/screens/MintsSheet/card/Card.tsx
+++ b/src/screens/MintsSheet/card/Card.tsx
@@ -26,6 +26,7 @@ import * as i18n from '@/languages';
import ChainBadge from '@/components/coin-icon/ChainBadge';
import { CoinIcon } from '@/components/coin-icon';
import { Network } from '@/helpers';
+import { navigateToMintCollection } from '@/resources/reservoir/mints';
export const NUM_NFTS = 3;
@@ -129,7 +130,7 @@ export function Card({ collection }: { collection: MintableCollection }) {
chainId: collection.chainId,
priceInEth: price,
});
- Linking.openURL(collection.externalURL);
+ navigateToMintCollection(collection.contract, network);
}}
style={{
borderRadius: 99,
diff --git a/src/screens/NFTOffersSheet/index.tsx b/src/screens/NFTOffersSheet/index.tsx
index c2adc2629d3..0615fe8f386 100644
--- a/src/screens/NFTOffersSheet/index.tsx
+++ b/src/screens/NFTOffersSheet/index.tsx
@@ -17,7 +17,10 @@ import { FakeOfferRow, OfferRow } from './OfferRow';
import { useAccountProfile, useDimensions } from '@/hooks';
import { ImgixImage } from '@/components/images';
import { ContactAvatar } from '@/components/contacts';
-import { nftOffersQueryKey, useNFTOffers } from '@/resources/nftOffers';
+import {
+ nftOffersQueryKey,
+ useNFTOffers,
+} from '@/resources/reservoir/nftOffersQuery';
import { convertAmountToNativeDisplay } from '@/helpers/utilities';
import { SortMenu } from '@/components/nft-offers/SortMenu';
import * as i18n from '@/languages';
diff --git a/src/screens/NFTSingleOfferSheet/index.tsx b/src/screens/NFTSingleOfferSheet/index.tsx
index 911123828f4..dc30430a888 100644
--- a/src/screens/NFTSingleOfferSheet/index.tsx
+++ b/src/screens/NFTSingleOfferSheet/index.tsx
@@ -49,46 +49,14 @@ import { Network } from '@/helpers';
import { getNetworkObj } from '@/networks';
import { CardSize } from '@/components/unique-token/CardSize';
import { queryClient } from '@/react-query';
-import { nftOffersQueryKey } from '@/resources/nftOffers';
+import { nftOffersQueryKey } from '@/resources/reservoir/nftOffersQuery';
+import { getRainbowFeeAddress } from '@/resources/reservoir/utils';
const NFT_IMAGE_HEIGHT = 160;
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
const RAINBOW_FEE_BIPS = 85;
const BIPS_TO_DECIMAL_RATIO = 10000;
-const RAINBOW_FEE_ADDRESS_MAINNET =
- '0x69d6d375de8c7ade7e44446df97f49e661fdad7d';
-const RAINBOW_FEE_ADDRESS_POLYGON =
- '0xfb9af3db5e19c4165f413f53fe3bbe6226834548';
-const RAINBOW_FEE_ADDRESS_OPTIMISM =
- '0x0d9b71891dc86400acc7ead08c80af301ccb3d71';
-const RAINBOW_FEE_ADDRESS_ARBITRUM =
- '0x0f9259af03052c96afda88add62eb3b5cbc185f1';
-const RAINBOW_FEE_ADDRESS_BASE = '0x1bbe055ad3204fa4468b4e6d3a3c59b9d9ac8c19';
-const RAINBOW_FEE_ADDRESS_BSC = '0x9670271ec2e2937a2e9df536784344bbff2bbea6';
-const RAINBOW_FEE_ADDRESS_ZORA = '0x7a3d05c70581bd345fe117c06e45f9669205384f';
-
-function getRainbowFeeAddress(network: Network) {
- switch (network) {
- case Network.mainnet:
- return RAINBOW_FEE_ADDRESS_MAINNET;
- case Network.polygon:
- return RAINBOW_FEE_ADDRESS_POLYGON;
- case Network.optimism:
- return RAINBOW_FEE_ADDRESS_OPTIMISM;
- case Network.arbitrum:
- return RAINBOW_FEE_ADDRESS_ARBITRUM;
- case Network.base:
- return RAINBOW_FEE_ADDRESS_BASE;
- case Network.bsc:
- return RAINBOW_FEE_ADDRESS_BSC;
- case Network.zora:
- return RAINBOW_FEE_ADDRESS_ZORA;
- default:
- return undefined;
- }
-}
-
function Row({
symbol,
label,
diff --git a/src/screens/discover/components/DiscoverSearch.js b/src/screens/discover/components/DiscoverSearch.js
index 78649bbd42f..6c8bd913edb 100644
--- a/src/screens/discover/components/DiscoverSearch.js
+++ b/src/screens/discover/components/DiscoverSearch.js
@@ -22,6 +22,7 @@ import { analytics } from '@/analytics';
import { PROFILES, useExperimentalFlag } from '@/config';
import { fetchSuggestions } from '@/handlers/ens';
import {
+ useAccountSettings,
useHardwareBackOnFocus,
usePrevious,
useSwapCurrencyList,
@@ -36,6 +37,7 @@ import {
getPoapAndOpenSheetWithQRHash,
getPoapAndOpenSheetWithSecretWord,
} from '@/utils/poaps';
+import { navigateToMintCollection } from '@/resources/reservoir/mints';
export const SearchContainer = styled(Row)({
height: '100%',
@@ -44,6 +46,7 @@ export const SearchContainer = styled(Row)({
export default function DiscoverSearch() {
const { navigate } = useNavigation();
const dispatch = useDispatch();
+ const { accountAddress } = useAccountSettings();
const {
isSearching,
isFetchingEns,
@@ -144,6 +147,26 @@ export default function DiscoverSearch() {
checkAndHandlePoaps(searchQueryForPoap);
}, [searchQueryForPoap]);
+ useEffect(() => {
+ // probably dont need this entry point but seems worth keeping?
+ // could do the same with zora, etc
+ const checkAndHandleMint = async seachQueryForMint => {
+ if (seachQueryForMint.includes('mint.fun')) {
+ const mintdotfunURL = seachQueryForMint.split('https://mint.fun/');
+ const query = mintdotfunURL[1];
+ let network = query.split('/')[0];
+ if (network === 'ethereum') {
+ network = Network.mainnet;
+ } else if (network === 'op') {
+ network === Network.optimism;
+ }
+ const contractAddress = query.split('/')[1];
+ navigateToMintCollection(contractAddress, network);
+ }
+ };
+ checkAndHandleMint(searchQuery);
+ }, [accountAddress, navigate, searchQuery]);
+
const handlePress = useCallback(
item => {
if (item.ens) {
diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx
new file mode 100644
index 00000000000..1682a368b15
--- /dev/null
+++ b/src/screens/mints/MintSheet.tsx
@@ -0,0 +1,874 @@
+import { BlurView } from '@react-native-community/blur';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useRef,
+ useState,
+} from 'react';
+import { Linking, View } from 'react-native';
+import { useSharedValue } from 'react-native-reanimated';
+import useWallets from '../../hooks/useWallets';
+import { GasSpeedButton } from '@/components/gas';
+import { Execute, getClient } from '@reservoir0x/reservoir-sdk';
+import { privateKeyToAccount } from 'viem/accounts';
+import { createWalletClient, http } from 'viem';
+import { dataAddNewTransaction } from '@/redux/data';
+import { HoldToAuthorizeButton } from '@/components/buttons';
+import Routes from '@/navigation/routesNames';
+import ImgixImage from '../../components/images/ImgixImage';
+import { SlackSheet } from '../../components/sheet';
+import { CardSize } from '../../components/unique-token/CardSize';
+import { WrappedAlert as Alert } from '@/helpers/alert';
+import {
+ Box,
+ ColorModeProvider,
+ Column,
+ Columns,
+ Inline,
+ Inset,
+ Separator,
+ Stack,
+ Text,
+} from '@/design-system';
+import {
+ useAccountProfile,
+ useAccountSettings,
+ useDimensions,
+ useENSAvatar,
+ useGas,
+ usePersistentAspectRatio,
+} from '@/hooks';
+import { useNavigation } from '@/navigation';
+import styled from '@/styled-thing';
+import { position } from '@/styles';
+import { useTheme } from '@/theme';
+import { CoinIcon, abbreviations, ethereumUtils, watchingAlert } from '@/utils';
+import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage';
+import { maybeSignUri } from '@/handlers/imgix';
+import { ButtonPressAnimation } from '@/components/animations';
+import { useFocusEffect, useRoute } from '@react-navigation/native';
+import { ReservoirCollection } from '@/graphql/__generated__/arcDev';
+import { format } from 'date-fns';
+import { TransactionStatus, TransactionType } from '@/entities';
+import * as i18n from '@/languages';
+import { analyticsV2 } from '@/analytics';
+import { event } from '@/analytics/event';
+import { ETH_ADDRESS, ETH_SYMBOL } from '@/references';
+import { RainbowNetworks, getNetworkObj } from '@/networks';
+import { Network } from '@/networks/types';
+import { fetchReverseRecord } from '@/handlers/ens';
+import { ContactAvatar } from '@/components/contacts';
+import { addressHashedColorIndex } from '@/utils/profileUtils';
+import { loadPrivateKey } from '@/model/wallet';
+import { ChainBadge } from '@/components/coin-icon';
+import {
+ add,
+ convertAmountToBalanceDisplay,
+ convertAmountToNativeDisplay,
+ convertRawAmountToBalance,
+ greaterThanOrEqualTo,
+ isZero,
+ multiply,
+} from '@/helpers/utilities';
+import { RainbowError, logger } from '@/logger';
+import { useDispatch } from 'react-redux';
+import { QuantityButton } from './components/QuantityButton';
+import { estimateGas, getProviderForNetwork } from '@/handlers/web3';
+import { getRainbowFeeAddress } from '@/resources/reservoir/utils';
+
+const NFT_IMAGE_HEIGHT = 250;
+// inset * 2 -> 28 *2
+const INSET_OFFSET = 56;
+
+const BackgroundBlur = styled(BlurView).attrs({
+ blurAmount: 100,
+ blurType: 'light',
+})({
+ ...position.coverAsObject,
+});
+
+const BackgroundImage = styled(View)({
+ ...position.coverAsObject,
+});
+
+interface BlurWrapperProps {
+ height: number;
+ width: number;
+}
+
+const BlurWrapper = styled(View).attrs({
+ shouldRasterizeIOS: true,
+})({
+ // @ts-expect-error missing theme types
+ backgroundColor: ({ theme: { colors } }) => colors.trueBlack,
+ height: ({ height }: BlurWrapperProps) => height,
+ left: 0,
+ overflow: 'hidden',
+ position: 'absolute',
+ width: ({ width }: BlurWrapperProps) => width,
+ ...(android ? { borderTopLeftRadius: 30, borderTopRightRadius: 30 } : {}),
+});
+
+interface MintSheetProps {
+ collection: ReservoirCollection;
+ chainId: number;
+}
+
+function MintInfoRow({
+ symbol,
+ label,
+ value,
+}: {
+ symbol: string;
+ label: string;
+ value: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+ {symbol}
+
+
+
+
+ {label}
+
+
+
+ {value}
+
+
+ );
+}
+
+const getFormattedDate = (date: string) => {
+ return format(new Date(date), 'MMMM dd, yyyy');
+};
+
+const MintSheet = () => {
+ const params = useRoute();
+ const { collection: mintCollection } = params.params as MintSheetProps;
+ const { accountAddress } = useAccountProfile();
+ const { nativeCurrency } = useAccountSettings();
+ const { height: deviceHeight, width: deviceWidth } = useDimensions();
+ const { navigate } = useNavigation();
+ const dispatch = useDispatch();
+ const { colors, isDarkMode } = useTheme();
+ const { isReadOnlyWallet, isHardwareWallet } = useWallets();
+ const [insufficientEth, setInsufficientEth] = useState(false);
+ const [showNativePrice, setShowNativePrice] = useState(false);
+ const [gasError, setGasError] = useState(false);
+ const currentNetwork =
+ RainbowNetworks.find(({ id }) => id === mintCollection.chainId)?.value ||
+ Network.mainnet;
+ const [ensName, setENSName] = useState('');
+ const [mintStatus, setMintStatus] = useState<
+ 'none' | 'minting' | 'minted' | 'error'
+ >('none');
+ const txRef = useRef();
+
+ const { data: ensAvatar } = useENSAvatar(ensName, {
+ enabled: Boolean(ensName),
+ });
+
+ const [quantity, setQuantity] = useReducer(
+ (quantity: number, increment: number) => {
+ if (quantity === 1 && increment === -1) {
+ return quantity;
+ }
+ if (
+ maxMintsPerWallet &&
+ quantity === maxMintsPerWallet &&
+ increment === 1
+ ) {
+ return quantity;
+ }
+ return quantity + increment;
+ },
+ 1
+ );
+
+ // if there is no max mint info, we fallback to 1 to be safe
+ const maxMintsPerWallet = Number(
+ mintCollection.publicMintInfo?.maxMintsPerWallet
+ );
+
+ const price = convertRawAmountToBalance(
+ mintCollection.publicMintInfo?.price?.amount?.raw || '0',
+ {
+ decimals: mintCollection.publicMintInfo?.price?.currency?.decimals || 18,
+ symbol: mintCollection.publicMintInfo?.price?.currency?.symbol || 'ETH',
+ }
+ );
+
+ // case where mint isnt eth? prob not with our current entrypoints
+ const mintPriceAmount = multiply(price.amount, quantity);
+ const mintPriceDisplay = convertAmountToBalanceDisplay(
+ multiply(price.amount, quantity),
+ {
+ decimals: mintCollection.publicMintInfo?.price?.currency?.decimals || 18,
+ symbol: mintCollection.publicMintInfo?.price?.currency?.symbol || 'ETH',
+ }
+ );
+
+ const priceOfEth = ethereumUtils.getEthPriceUnit() as number;
+
+ const nativeMintPriceDisplay = convertAmountToNativeDisplay(
+ parseFloat(multiply(price.amount, quantity)) * priceOfEth,
+ nativeCurrency
+ );
+
+ const {
+ updateTxFee,
+ startPollingGasFees,
+ stopPollingGasFees,
+ isSufficientGas,
+ isValidGas,
+ getTotalGasPrice,
+ } = useGas();
+
+ const imageUrl = maybeSignUri(mintCollection.image || '');
+ const { result: aspectRatio } = usePersistentAspectRatio(imageUrl || '');
+
+ // isMintingPublicSale handles if theres a time based mint, otherwise if there is a price we should be able to mint
+ const isMintingAvailable =
+ !(isReadOnlyWallet || isHardwareWallet) &&
+ (mintCollection.isMintingPublicSale || price) &&
+ !gasError;
+
+ const imageColor =
+ usePersistentDominantColorFromImage(imageUrl) ?? colors.paleBlue;
+
+ const sheetRef = useRef();
+ const yPosition = useSharedValue(0);
+
+ useFocusEffect(() => {
+ if (mintCollection.name && mintCollection.id) {
+ analyticsV2.track(event.mintsOpenedSheet, {
+ collectionName: mintCollection?.name,
+ contract: mintCollection.id,
+ chainId: mintCollection.chainId,
+ });
+ }
+ });
+
+ // check address balance
+ useEffect(() => {
+ const checkInsufficientEth = async () => {
+ const nativeBalance =
+ (
+ await ethereumUtils.getNativeAssetForNetwork(
+ currentNetwork,
+ accountAddress
+ )
+ )?.balance?.amount ?? 0;
+ const txFee = getTotalGasPrice();
+ const totalMintPrice = multiply(price.amount, quantity);
+ // gas price + mint price
+ setInsufficientEth(
+ greaterThanOrEqualTo(add(txFee, totalMintPrice), nativeBalance)
+ );
+ };
+ checkInsufficientEth();
+ }, [
+ accountAddress,
+ currentNetwork,
+ getTotalGasPrice,
+ mintCollection.publicMintInfo?.price?.currency?.decimals,
+ mintCollection.publicMintInfo?.price?.currency?.symbol,
+ price,
+ quantity,
+ ]);
+
+ // resolve ens name
+ useEffect(() => {
+ const fetchENSName = async (address: string) => {
+ const ensName = await fetchReverseRecord(address);
+ setENSName(ensName);
+ };
+
+ if (mintCollection.creator) {
+ fetchENSName(mintCollection.creator);
+ }
+ }, [mintCollection.creator]);
+
+ // start poll gas price
+ useEffect(() => {
+ startPollingGasFees(currentNetwork);
+
+ return () => {
+ stopPollingGasFees();
+ };
+ }, [currentNetwork, startPollingGasFees, stopPollingGasFees]);
+
+ // estimate gas limit
+ useEffect(() => {
+ const estimateMintGas = async () => {
+ const networkObj = getNetworkObj(currentNetwork);
+ const provider = await getProviderForNetwork(currentNetwork);
+ const signer = createWalletClient({
+ account: accountAddress,
+ chain: networkObj,
+ transport: http(networkObj.rpc),
+ });
+ try {
+ await getClient()?.actions.buyToken({
+ items: [
+ { fillType: 'mint', collection: mintCollection.id!, quantity },
+ ],
+ wallet: signer!,
+ chainId: networkObj.id,
+ precheck: true,
+ onProgress: async (steps: Execute['steps']) => {
+ steps.forEach(step => {
+ if (step.error) {
+ logger.error(
+ new RainbowError(`NFT Mints: Gas Step Error: ${step.error}`)
+ );
+ return;
+ }
+ step.items?.forEach(async item => {
+ // could add safety here if unable to calc gas limit
+ const tx = {
+ to: item.data?.to,
+ from: item.data?.from,
+ data: item.data?.data,
+ value: multiply(price.amount || '0', quantity),
+ };
+
+ const gas = await estimateGas(tx, provider);
+ if (gas) {
+ setGasError(false);
+ updateTxFee(gas, null);
+ }
+ });
+ });
+ },
+ });
+ } catch (e) {
+ setGasError(true);
+ logger.error(
+ new RainbowError(`NFT Mints: Gas Step Error: ${(e as Error).message}`)
+ );
+ }
+ };
+ estimateMintGas();
+ }, [
+ accountAddress,
+ currentNetwork,
+ mintCollection.id,
+ price,
+ quantity,
+ updateTxFee,
+ ]);
+
+ const deployerDisplay = abbreviations.address(
+ mintCollection.creator || '',
+ 4,
+ 6
+ );
+
+ const contractAddressDisplay = `${abbreviations.address(
+ mintCollection.id || '',
+ 4,
+ 6
+ )} `;
+
+ const buildMintDotFunUrl = (contract: string, network: Network) => {
+ const MintDotFunNetworks = [
+ Network.mainnet,
+ Network.optimism,
+ Network.base,
+ Network.zora,
+ ];
+ if (!MintDotFunNetworks.includes(network)) {
+ Alert.alert(i18n.t(i18n.l.minting.mintdotfun_unsupported_network));
+ }
+
+ let chainSlug = 'ethereum';
+ switch (network) {
+ case Network.optimism:
+ chainSlug = 'op';
+ break;
+ case Network.base:
+ chainSlug = 'base';
+ break;
+ case Network.zora:
+ chainSlug = 'zora';
+ break;
+ }
+ return `https://mint.fun/${chainSlug}/${contract}`;
+ };
+
+ const actionOnPress = useCallback(async () => {
+ if (isReadOnlyWallet) {
+ watchingAlert();
+ return;
+ }
+
+ // link to mint.fun if reservoir not supporting
+ if (!isMintingAvailable) {
+ analyticsV2.track(event.mintsOpeningMintDotFun, {
+ collectionName: mintCollection.name || '',
+ contract: mintCollection.id || '',
+ chainId: mintCollection.chainId,
+ });
+ Linking.openURL(buildMintDotFunUrl(mintCollection.id!, currentNetwork));
+ return;
+ }
+
+ logger.info('Minting NFT', { name: mintCollection.name });
+ analyticsV2.track(event.mintsMintingNFT, {
+ collectionName: mintCollection.name || '',
+ contract: mintCollection.id || '',
+ chainId: mintCollection.chainId,
+ quantity,
+ priceInEth: mintPriceAmount,
+ });
+ setMintStatus('minting');
+
+ const privateKey = await loadPrivateKey(accountAddress, false);
+ // @ts-ignore
+ const account = privateKeyToAccount(privateKey);
+ const networkObj = getNetworkObj(currentNetwork);
+ const signer = createWalletClient({
+ account,
+ chain: networkObj,
+ transport: http(networkObj.rpc),
+ });
+
+ const feeAddress = getRainbowFeeAddress(currentNetwork);
+ try {
+ await getClient()?.actions.buyToken({
+ items: [
+ {
+ fillType: 'mint',
+ collection: mintCollection.id!,
+ quantity,
+ ...(feeAddress && { referrer: feeAddress }),
+ },
+ ],
+ wallet: signer!,
+ chainId: networkObj.id,
+ onProgress: (steps: Execute['steps']) => {
+ steps.forEach(step => {
+ if (step.error) {
+ logger.error(
+ new RainbowError(`Error minting NFT: ${step.error}`)
+ );
+ setMintStatus('error');
+ return;
+ }
+ step.items?.forEach(item => {
+ if (
+ item.txHash &&
+ txRef.current !== item.txHash &&
+ item.status === 'incomplete'
+ ) {
+ const tx = {
+ to: item.data?.to,
+ from: item.data?.from,
+ hash: item.txHash,
+ network: currentNetwork,
+ amount: mintPriceAmount,
+ asset: {
+ address: ETH_ADDRESS,
+ symbol: ETH_SYMBOL,
+ },
+ nft: {
+ predominantColor: imageColor,
+ collection: {
+ image: imageUrl,
+ },
+ lowResUrl: imageUrl,
+ name: mintCollection.name,
+ },
+ type: TransactionType.mint,
+ status: TransactionStatus.minting,
+ };
+
+ txRef.current = tx.hash;
+ // @ts-expect-error TODO: fix when we overhaul tx list, types are not good
+ dispatch(dataAddNewTransaction(tx));
+ analyticsV2.track(event.mintsMintedNFT, {
+ collectionName: mintCollection.name || '',
+ contract: mintCollection.id || '',
+ chainId: mintCollection.chainId,
+ quantity,
+ priceInEth: mintPriceAmount,
+ });
+ navigate(Routes.PROFILE_SCREEN);
+ setMintStatus('minted');
+ }
+ });
+ });
+ },
+ });
+ } catch (e) {
+ setMintStatus('error');
+ analyticsV2.track(event.mintsErrorMintingNFT, {
+ collectionName: mintCollection.name || '',
+ contract: mintCollection.id || '',
+ chainId: mintCollection.chainId,
+ quantity,
+ priceInEth: mintPriceAmount,
+ });
+ logger.error(
+ new RainbowError(`Error minting NFT: ${(e as Error).message}`)
+ );
+ }
+ }, [
+ accountAddress,
+ currentNetwork,
+ dispatch,
+ imageColor,
+ imageUrl,
+ isMintingAvailable,
+ isReadOnlyWallet,
+ mintCollection.chainId,
+ mintCollection.id,
+ mintCollection.name,
+ mintPriceAmount,
+ navigate,
+ quantity,
+ ]);
+
+ const buttonLabel = useMemo(() => {
+ if (!isMintingAvailable) {
+ return i18n.t(i18n.l.minting.mint_on_mintdotfun);
+ }
+ if (insufficientEth) {
+ return i18n.t(i18n.l.button.confirm_exchange.insufficient_eth);
+ }
+
+ if (mintStatus === 'minting') {
+ return i18n.t(i18n.l.minting.minting);
+ } else if (mintStatus === 'minted') {
+ return i18n.t(i18n.l.minting.minted);
+ } else if (mintStatus === 'error') {
+ return i18n.t(i18n.l.minting.error_minting);
+ }
+
+ return i18n.t(i18n.l.minting.hold_to_mint);
+ }, [insufficientEth, isMintingAvailable, mintStatus]);
+
+ return (
+ <>
+ {ios && (
+
+
+
+
+
+
+ )}
+ {/* @ts-expect-error JavaScript component */}
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ {mintCollection.name}
+
+
+
+ {`${i18n.t(i18n.l.minting.by)} `}
+
+
+ {ensAvatar?.imageUrl ? (
+
+ ) : (
+
+ )}
+
+ {` ${
+ ensName ||
+ deployerDisplay ||
+ i18n.t(i18n.l.minting.unknown)
+ }`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.t(i18n.l.minting.mint_price)}
+
+ setShowNativePrice(!showNativePrice)}
+ >
+
+
+ {isZero(mintPriceAmount)
+ ? i18n.t(i18n.l.minting.free)
+ : showNativePrice
+ ? nativeMintPriceDisplay
+ : mintPriceDisplay}
+
+
+
+
+
+
+
+
+ {
+
+ {quantity === Number(maxMintsPerWallet)
+ ? i18n.t(i18n.l.minting.max)
+ : ''}
+
+ }
+
+ setQuantity(1)}
+ minusAction={() => setQuantity(-1)}
+ buttonColor={imageColor}
+ disabled={!isMintingAvailable}
+ maxValue={Number(maxMintsPerWallet)}
+ />
+
+
+
+
+ {/* @ts-ignore */}
+
+
+
+ {/* @ts-ignore */}
+
+
+
+
+
+ {mintCollection.description && (
+
+
+ {i18n.t(i18n.l.minting.description)}
+
+
+ {mintCollection.description}
+
+
+ )}
+
+ {mintCollection?.tokenCount && (
+
+ {i18n.t(i18n.l.minting.nft_count, {
+ number: mintCollection.tokenCount,
+ })}
+
+ }
+ />
+ )}
+ {mintCollection?.createdAt && (
+
+ {getFormattedDate(mintCollection?.createdAt)}
+
+ }
+ />
+ )}
+
+ {mintCollection?.id && (
+
+ ethereumUtils.openAddressInBlockExplorer(
+ mintCollection.id!,
+ currentNetwork
+ )
+ }
+ >
+
+ {contractAddressDisplay}
+
+
+ }
+ />
+ )}
+
+
+
+ {currentNetwork === Network.mainnet ? (
+
+ ) : (
+
+ )}
+
+ {`${getNetworkObj(currentNetwork).name}`}
+
+
+
+ }
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+export default MintSheet;
diff --git a/src/screens/mints/components/QuantityButton.tsx b/src/screens/mints/components/QuantityButton.tsx
new file mode 100644
index 00000000000..68a7413d0af
--- /dev/null
+++ b/src/screens/mints/components/QuantityButton.tsx
@@ -0,0 +1,197 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
+import { delay } from '@/utils/delay';
+import { usePrevious } from '@/hooks';
+import styled from '@/styled-thing';
+import { ButtonPressAnimation } from '@/components/animations';
+import Row from '@/components/layout/Row';
+import { Box, Inline, Text } from '@/design-system';
+import { useTheme } from '@/theme';
+
+const PLUS_ACTION_TYPE = 'plus';
+const MINUS_ACTION_TYPE = 'minus';
+const LONG_PRESS_DELAY_THRESHOLD = 69;
+const MIN_LONG_PRESS_DELAY_THRESHOLD = 200;
+
+const Wrapper = styled(Row)({});
+
+const StepButtonWrapper = styled(ButtonPressAnimation).attrs(() => ({
+ paddingHorizontal: 7,
+ scaleTo: 0.75,
+}))({});
+
+type StepButtonProps = {
+ type: 'plus' | 'minus';
+ onLongPress: () => void;
+ onLongPressEnded: () => void;
+ onPress: () => void;
+ shouldLongPressHoldPress: boolean;
+ buttonColor: string;
+ disabled?: boolean;
+ threshold?: number;
+ value: number;
+};
+const StepButton = ({
+ type,
+ onLongPress,
+ onLongPressEnded,
+ onPress,
+ shouldLongPressHoldPress,
+ buttonColor,
+ disabled = false,
+ threshold,
+ value,
+}: StepButtonProps) => {
+ const { colors, lightScheme } = useTheme();
+ // should prob change the color here maybe :thinky:
+ const atThreshold = type === 'plus' ? value === threshold : value === 1;
+ const color = disabled || atThreshold ? lightScheme.grey : buttonColor;
+
+ return (
+
+
+
+ {type === 'plus' ? '' : ''}
+
+
+
+ );
+};
+
+type StepButtonInputProps = {
+ value: number;
+ plusAction: () => void;
+ minusAction: () => void;
+ buttonColor: string;
+ disabled?: boolean;
+ maxValue: number;
+};
+export function QuantityButton({
+ value,
+ plusAction,
+ minusAction,
+ buttonColor,
+ disabled = false,
+ maxValue,
+}: StepButtonInputProps) {
+ const longPressHandle = useRef(null);
+ const [trigger, setTrigger] = useState(false);
+ const [actionType, setActionType] = useState<'plus' | 'minus' | null>(null);
+ const prevTrigger = usePrevious(trigger);
+
+ const onMinusPress = useCallback(() => {
+ longPressHandle.current = false;
+ minusAction();
+ }, [minusAction]);
+
+ const onPlusPress = useCallback(() => {
+ longPressHandle.current = false;
+ plusAction();
+ }, [plusAction]);
+
+ const onLongPressEnded = useCallback(() => {
+ longPressHandle.current = false;
+ setActionType(null);
+ }, [longPressHandle]);
+
+ const onLongPressLoop = useCallback(async () => {
+ setTrigger(true);
+ setTrigger(false);
+ await delay(LONG_PRESS_DELAY_THRESHOLD);
+ longPressHandle.current && onLongPressLoop();
+ }, []);
+
+ const onLongPress = useCallback(async () => {
+ longPressHandle.current = true;
+ onLongPressLoop();
+ }, [onLongPressLoop]);
+
+ const onPlusLongPress = useCallback(() => {
+ setActionType(PLUS_ACTION_TYPE);
+ onLongPress();
+ }, [onLongPress]);
+
+ const onMinusLongPress = useCallback(() => {
+ setActionType(MINUS_ACTION_TYPE);
+ onLongPress();
+ }, [onLongPress]);
+
+ useEffect(() => {
+ if (!prevTrigger && trigger) {
+ if (actionType === PLUS_ACTION_TYPE) {
+ plusAction();
+ if (!android) {
+ ReactNativeHapticFeedback.trigger('selection');
+ }
+ } else if (actionType === MINUS_ACTION_TYPE) {
+ minusAction();
+ if (!android) {
+ ReactNativeHapticFeedback.trigger('selection');
+ }
+ }
+ }
+ }, [trigger, prevTrigger, actionType, plusAction, minusAction]);
+
+ return (
+
+
+
+
+ {value}
+
+
+
+
+ );
+}
diff --git a/yarn.lock b/yarn.lock
index fe54973101a..2dbe3180cbc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4069,10 +4069,10 @@
color "^4.2.3"
warn-once "^0.1.0"
-"@reservoir0x/reservoir-sdk@1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@reservoir0x/reservoir-sdk/-/reservoir-sdk-1.2.1.tgz#8f1a1330c06c658b98e4fd66d0de994c398197ba"
- integrity sha512-+FN4mN5m5zEPAW41ZkO23NBc1ANQA8gIJr7bWE44fOm7T0dMtbBKEHuGsLtwbuKUx+IslbjmdhytkXRfFTPMqQ==
+"@reservoir0x/reservoir-sdk@1.4.5":
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/@reservoir0x/reservoir-sdk/-/reservoir-sdk-1.4.5.tgz#6ff0fd61fea203de4cdde019048fa9d0b0ab08e9"
+ integrity sha512-Yi4Z0c85fUfPB2mo1FdeCpEgrpN36fDV8gU4RDmsl6IuFYD8pwA/SMtxoo5M0uEN3at+VpVV9utyvB8XlHUFMQ==
dependencies:
axios "^0.27.2"