diff --git a/ios/Rainbow.xcodeproj/project.pbxproj b/ios/Rainbow.xcodeproj/project.pbxproj index abe72d6389a..9c1069d705a 100644 --- a/ios/Rainbow.xcodeproj/project.pbxproj +++ b/ios/Rainbow.xcodeproj/project.pbxproj @@ -41,7 +41,9 @@ A4277DA323CFE85F0042BAF4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4277DA223CFE85F0042BAF4 /* Theme.swift */; }; A4D04BA923D12F99008C1DEC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4D04BA823D12F99008C1DEC /* Button.swift */; }; A4D04BAC23D12FD5008C1DEC /* ButtonManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A4D04BAB23D12FD5008C1DEC /* ButtonManager.m */; }; - A9F312A50F28216E0C2FC393 /* libPods-PriceWidgetExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 73790DA1833435B55F6617C8 /* libPods-PriceWidgetExtension.a */; }; + AA0B1CBB2B00C5E100EAF77D /* SF-Mono-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA0B1CB82B00C5E100EAF77D /* SF-Mono-Semibold.otf */; }; + AA0B1CBD2B00C5E100EAF77D /* SF-Mono-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA0B1CB92B00C5E100EAF77D /* SF-Mono-Bold.otf */; }; + AA0B1CBF2B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA0B1CBA2B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf */; }; AA6228EF24272F510078BDAA /* SF-Pro-Rounded-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA6228EB24272B200078BDAA /* SF-Pro-Rounded-Bold.otf */; }; AA6228F024272F510078BDAA /* SF-Pro-Rounded-Heavy.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA6228EC24272B200078BDAA /* SF-Pro-Rounded-Heavy.otf */; }; AA6228F124272F510078BDAA /* SF-Pro-Rounded-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = AA6228ED24272B200078BDAA /* SF-Pro-Rounded-Medium.otf */; }; @@ -259,14 +261,14 @@ A4277DA223CFE85F0042BAF4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; A4D04BA823D12F99008C1DEC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; A4D04BAB23D12FD5008C1DEC /* ButtonManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ButtonManager.m; sourceTree = ""; }; - A56EC7992F140DBE3A0BC44D /* Pods-PriceWidgetExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PriceWidgetExtension.debug.xcconfig"; path = "Target Support Files/Pods-PriceWidgetExtension/Pods-PriceWidgetExtension.debug.xcconfig"; sourceTree = ""; }; - A873FCD38CDC7FEB5E93833F /* Pods-Rainbow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rainbow.debug.xcconfig"; path = "Target Support Files/Pods-Rainbow/Pods-Rainbow.debug.xcconfig"; sourceTree = ""; }; + AA0B1CB82B00C5E100EAF77D /* SF-Mono-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Mono-Semibold.otf"; path = "../src/assets/fonts/SF-Mono-Semibold.otf"; sourceTree = ""; }; + AA0B1CB92B00C5E100EAF77D /* SF-Mono-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Mono-Bold.otf"; path = "../src/assets/fonts/SF-Mono-Bold.otf"; sourceTree = ""; }; + AA0B1CBA2B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Black.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Black.otf"; sourceTree = ""; }; AA6228EA24272B200078BDAA /* SF-Pro-Rounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Semibold.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Semibold.otf"; sourceTree = ""; }; AA6228EB24272B200078BDAA /* SF-Pro-Rounded-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Bold.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Bold.otf"; sourceTree = ""; }; AA6228EC24272B200078BDAA /* SF-Pro-Rounded-Heavy.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Heavy.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Heavy.otf"; sourceTree = ""; }; AA6228ED24272B200078BDAA /* SF-Pro-Rounded-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Medium.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Medium.otf"; sourceTree = ""; }; AA6228EE24272B200078BDAA /* SF-Pro-Rounded-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Rounded-Regular.otf"; path = "../src/assets/fonts/SF-Pro-Rounded-Regular.otf"; sourceTree = ""; }; - AD490CBDAC6554605BF1847A /* Pods-PriceWidgetExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PriceWidgetExtension.release.xcconfig"; path = "Target Support Files/Pods-PriceWidgetExtension/Pods-PriceWidgetExtension.release.xcconfig"; sourceTree = ""; }; B0C692B061D7430D8194DC98 /* ToolTipMenuTests.xctest */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = ToolTipMenuTests.xctest; sourceTree = ""; }; B50C9AE92A9D18DC00EB0019 /* adworld@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "adworld@3x.png"; sourceTree = ""; }; B50C9AEA2A9D18DC00EB0019 /* adworld@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "adworld@2x.png"; sourceTree = ""; }; @@ -699,6 +701,9 @@ DCAC1D8CC45E468FBB7E1395 /* Resources */ = { isa = PBXGroup; children = ( + AA0B1CB92B00C5E100EAF77D /* SF-Mono-Bold.otf */, + AA0B1CB82B00C5E100EAF77D /* SF-Mono-Semibold.otf */, + AA0B1CBA2B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf */, AA6228EB24272B200078BDAA /* SF-Pro-Rounded-Bold.otf */, AA6228EC24272B200078BDAA /* SF-Pro-Rounded-Heavy.otf */, AA6228ED24272B200078BDAA /* SF-Pro-Rounded-Medium.otf */, @@ -871,6 +876,7 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, B5D7F2F129E8D41E003D6A54 /* finiliar@2x.png in Resources */, B54C1D1029358946007560D9 /* golddoge@3x.png in Resources */, + AA0B1CBD2B00C5E100EAF77D /* SF-Mono-Bold.otf in Resources */, 24979E8920F84250007EB0DA /* GoogleService-Info.plist in Resources */, B54C1D162935A54F007560D9 /* zora@3x.png in Resources */, B5CE8FFE29A5758100EB1EFA /* pooly@2x.png in Resources */, @@ -879,9 +885,11 @@ 15CF49BD2889AF7C005F92C9 /* optimism@2x.png in Resources */, 1539422824C7C7E200E4A9D1 /* Settings.bundle in Resources */, 15C398832880EDFF006033AC /* og@2x.png in Resources */, + AA0B1CBB2B00C5E100EAF77D /* SF-Mono-Semibold.otf in Resources */, B50C9AEC2A9D18DC00EB0019 /* adworld@2x.png in Resources */, B54C1D1129358946007560D9 /* golddoge@2x.png in Resources */, B54C1D1229358946007560D9 /* raindoge@3x.png in Resources */, + AA0B1CBF2B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf in Resources */, 15CF49C12889AFAD005F92C9 /* pixel@2x.png in Resources */, C04D10F025AFC8C1003BEF7A /* Extras.json in Resources */, B5CC6D3A2A7873300037D5A3 /* poolboy@3x.png in Resources */, @@ -910,6 +918,7 @@ files = ( C1AA308F27338F2B00136A9A /* SF-Pro-Rounded-Bold.otf in Resources */, C1AA309027338F2B00136A9A /* SF-Pro-Rounded-Regular.otf in Resources */, + AA0B1CC02B00C5E100EAF77D /* SF-Pro-Rounded-Black.otf in Resources */, C1C61A83272CBDA100E5C0B3 /* Images.xcassets in Resources */, C1AA309227338F2B00136A9A /* SF-Pro-Rounded-Medium.otf in Resources */, C1AA309527338F2B00136A9A /* SF-Pro-Rounded-Heavy.otf in Resources */, diff --git a/ios/Rainbow/Info.plist b/ios/Rainbow/Info.plist index 6356572531d..3bca00d1552 100644 --- a/ios/Rainbow/Info.plist +++ b/ios/Rainbow/Info.plist @@ -215,6 +215,9 @@ UIAppFonts + SF-Mono-Bold.otf + SF-Mono-Semibold.otf + SF-Pro-Rounded-Black.otf SF-Pro-Rounded-Bold.otf SF-Pro-Rounded-Heavy.otf SF-Pro-Rounded-Medium.otf diff --git a/ios/SF-Mono-Bold.otf b/ios/SF-Mono-Bold.otf new file mode 100755 index 00000000000..088f548f641 Binary files /dev/null and b/ios/SF-Mono-Bold.otf differ diff --git a/ios/SF-Mono-Semibold.otf b/ios/SF-Mono-Semibold.otf new file mode 100755 index 00000000000..ac9fb96393c Binary files /dev/null and b/ios/SF-Mono-Semibold.otf differ diff --git a/ios/SF-Pro-Rounded-Black.otf b/ios/SF-Pro-Rounded-Black.otf new file mode 100755 index 00000000000..0c4bdf06932 Binary files /dev/null and b/ios/SF-Pro-Rounded-Black.otf differ diff --git a/src/assets/fonts/SF-Mono-Bold.otf b/src/assets/fonts/SF-Mono-Bold.otf new file mode 100755 index 00000000000..088f548f641 Binary files /dev/null and b/src/assets/fonts/SF-Mono-Bold.otf differ diff --git a/src/assets/fonts/SF-Mono-Semibold.otf b/src/assets/fonts/SF-Mono-Semibold.otf new file mode 100755 index 00000000000..ac9fb96393c Binary files /dev/null and b/src/assets/fonts/SF-Mono-Semibold.otf differ diff --git a/src/assets/fonts/SF-Pro-Rounded-Black.otf b/src/assets/fonts/SF-Pro-Rounded-Black.otf new file mode 100755 index 00000000000..0c4bdf06932 Binary files /dev/null and b/src/assets/fonts/SF-Pro-Rounded-Black.otf differ diff --git a/src/assets/fonts/SFMono-Medium.otf b/src/assets/fonts/SFMono-Medium.otf deleted file mode 100644 index 93262ff4540..00000000000 Binary files a/src/assets/fonts/SFMono-Medium.otf and /dev/null differ diff --git a/src/assets/fonts/SFMono-Regular.otf b/src/assets/fonts/SFMono-Regular.otf deleted file mode 100644 index 1294dbb78bf..00000000000 Binary files a/src/assets/fonts/SFMono-Regular.otf and /dev/null differ diff --git a/src/components/animations/AnimatePresence.tsx b/src/components/animations/AnimatePresence.tsx new file mode 100644 index 00000000000..43a97627f32 --- /dev/null +++ b/src/components/animations/AnimatePresence.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import Animated, { + Easing, + EasingFunctionFactory, + FadeIn, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; + +interface AnimatePresenceProps { + children: React.ReactNode; + condition: boolean | null | undefined; + duration?: number; + easing?: EasingFunctionFactory; + exitDuration?: number; + exitEasing?: EasingFunctionFactory; + enterAnimation?: typeof FadeIn; +} + +const defaultTimingConfig = { + duration: 225, + easing: Easing.bezier(0.2, 0, 0, 1), +}; + +const exitTimingConfig = { + duration: 125, + easing: Easing.bezier(0.3, 0, 1, 1), +}; + +export const AnimatePresence = ({ + children, + condition, + duration = defaultTimingConfig.duration, + easing = defaultTimingConfig.easing, + exitEasing = exitTimingConfig.easing, + exitDuration = exitTimingConfig.duration, + enterAnimation = FadeIn, +}: AnimatePresenceProps) => { + const [isMounted, setIsMounted] = useState(condition); + const [isExiting, setIsExiting] = useState(false); + + const enterAnimationStyle = useMemo(() => { + return enterAnimation.duration(duration).easing(easing.factory()); + }, [enterAnimation, duration, easing]); + + useEffect(() => { + if (condition) { + setIsExiting(false); + setIsMounted(true); + } else { + setIsExiting(true); + // Wait for exit animation completion + setTimeout(() => { + setIsMounted(false); + }, exitDuration || duration); + } + }, [condition, duration, exitDuration]); + + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: withTiming(isExiting ? 0 : 1, { + duration: exitDuration, + easing: exitEasing, + }), + }; + }, [isExiting, duration, easing]); + + return isMounted ? ( + + {children} + + ) : null; +}; diff --git a/src/graphql/queries/metadata.graphql b/src/graphql/queries/metadata.graphql index 76801b8d3bf..fdea8b73999 100644 --- a/src/graphql/queries/metadata.graphql +++ b/src/graphql/queries/metadata.graphql @@ -254,6 +254,10 @@ query simulateMessage( query getPointsDataForWallet($address: String!) { points(address: $address) { + error { + message + type + } meta { distribution { next @@ -311,7 +315,6 @@ mutation onboardPoints( } } user { - referralCode earnings { total } @@ -320,6 +323,23 @@ mutation onboardPoints( current } } + onboarding { + earnings { + total + } + categories { + data { + usd_amount + total_collections + owned_collections + } + type + display_type + earnings { + total + } + } + } } error { message diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 61a1c5a1dbb..0c5a81c2236 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1236,7 +1236,7 @@ }, "claim": { "title": "Claim your points", - "subtitle": "Points are here. Find out how many you've been awarded", + "subtitle": "Points are here. Find out how many you've been awarded.", "get_started": "Get Started", "use_referral_code": "Use Referral Code" }, @@ -1267,6 +1267,28 @@ "error": "Oops!\nPlease pull to refresh.", "points": "points", "link_copied": "Link copied" + }, + "console": { + "claim_bonus_paragraph": "To claim the rest of your bonus points, swap at least $100 through Rainbow.", + "bonus_points": "Bonus Points", + "registration_complete": "Registration complete", + "account": "Account", + "points_earned": "Points Earned", + "calculation_complete": "Calculation complete", + "metamask_swaps": "MetaMask Swaps", + "wallet_balance": "Wallet Balance", + "rainbow_nfts_owned": "Rainbow NFTs Owned", + "rainbow_swaps": "Rainbow Swaps", + "calculating_points": "Calculating points", + "access_granted": "Access granted", + "sign_in_with_wallet": "Sign in with your wallet", + "auth_required": "Authorization required", + "sign_in": "Sign In", + "try_a_swap": "Try a Swap", + "get_some_eth": "Get Some ETH", + "generic_alert": "Something went wrong, please try again.", + "existing_user_alert": "Points already claimed. Please restart the app to refresh.", + "invalid_referral_code_alert": "Invalid referral code. Please double check your code and try again." } }, "pools": { diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index c9f90dae4d3..c233435b070 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -54,6 +54,7 @@ import { addCashSheet, nftSingleOfferSheetPreset, walletconnectBottomSheetPreset, + consoleSheetPreset, } from './effects'; import { InitialRouteContext } from './initialRoute'; import { onNavigationStateChange } from './onNavigationStateChange'; @@ -83,6 +84,7 @@ import MintSheet from '@/screens/mints/MintSheet'; import { MintsSheet } from '@/screens/MintsSheet/MintsSheet'; import { SignTransactionSheet } from '@/screens/SignTransactionSheet'; import { RemotePromoSheet } from '@/components/remote-promo-sheet/RemotePromoSheet'; +import { ConsoleSheet } from '@/screens/points/ConsoleSheet'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -387,6 +389,11 @@ function BSNavigator() { name={Routes.CONFIRM_REQUEST} options={walletconnectBottomSheetPreset} /> + ); } diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index d43d0c22c79..724d1481594 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -36,6 +36,7 @@ import { backupSheetConfig, basicSheetConfig, hardwareWalletTxNavigatorConfig, + consoleSheetConfig, customGasSheetConfig, defaultScreenStackOptions, ensAdditionalRecordsSheetConfig, @@ -97,6 +98,7 @@ import { NFTSingleOfferSheet } from '@/screens/NFTSingleOfferSheet'; import MintSheet from '@/screens/mints/MintSheet'; import { MintsSheet } from '@/screens/MintsSheet/MintsSheet'; import { RemotePromoSheet } from '@/components/remote-promo-sheet/RemotePromoSheet'; +import { ConsoleSheet } from '@/screens/points/ConsoleSheet'; type StackNavigatorParams = { [Routes.SEND_SHEET]: unknown; @@ -451,6 +453,11 @@ function NativeStackNavigator() { component={MintsSheet} {...mintsSheetConfig} /> + ); } diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index cb0feffb403..7a193411288 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -197,6 +197,19 @@ export const mintsSheetConfig = { }), }; +export const consoleSheetConfig = { + options: ({ route: { params = {} } }) => ({ + ...buildCoolModalConfig({ + ...params, + backgroundOpacity: 1, + cornerRadius: 0, + springDamping: 1, + topOffset: 0, + transitionDuration: 0.3, + }), + }), +}; + export const signTransactionSheetConfig = { options: ({ route: { params = {} } }) => ({ ...buildCoolModalConfig({ diff --git a/src/navigation/effects.tsx b/src/navigation/effects.tsx index b67ea6bbc44..70c4941da2d 100644 --- a/src/navigation/effects.tsx +++ b/src/navigation/effects.tsx @@ -466,6 +466,11 @@ export const walletconnectBottomSheetPreset: BottomSheetNavigationOptions = { height: '100%', }; +export const consoleSheetPreset: BottomSheetNavigationOptions = { + backdropColor: 'black', + backdropOpacity: 1, +}; + export const expandedPresetWithSmallGestureResponseDistance: StackNavigationOptions & BottomSheetNavigationOptions = { ...expandedPreset, diff --git a/src/navigation/routesNames.ts b/src/navigation/routesNames.ts index b51e2a155c7..18f86ccbda4 100644 --- a/src/navigation/routesNames.ts +++ b/src/navigation/routesNames.ts @@ -13,6 +13,7 @@ const Routes = { CHANGE_WALLET_SHEET_NAVIGATOR: 'ChangeWalletSheetNavigator', CONFIRM_REQUEST: 'ConfirmRequest', CONNECTED_DAPPS: 'ConnectedDapps', + CONSOLE_SHEET: 'ConsoleSheet', CURRENCY_SELECT_SCREEN: 'CurrencySelectScreen', CUSTOM_GAS_SHEET: 'CustomGasSheet', DIAGNOSTICS_SHEET: 'DiagnosticsSheet', diff --git a/src/screens/points/ConsoleSheet.tsx b/src/screens/points/ConsoleSheet.tsx new file mode 100644 index 00000000000..a131cac5bc2 --- /dev/null +++ b/src/screens/points/ConsoleSheet.tsx @@ -0,0 +1,1113 @@ +/* eslint-disable no-nested-ternary */ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { StyleSheet, Text as RNText, View } from 'react-native'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; +import Animated, { + Easing, + cancelAnimation, + runOnJS, + useAnimatedReaction, + useSharedValue, + withRepeat, + withSequence, + withTiming, +} from 'react-native-reanimated'; +import { AnimatePresence } from '@/components/animations/AnimatePresence'; +import ButtonPressAnimation from '@/components/animations/ButtonPressAnimation'; +import { + Bleed, + Box, + Cover, + Inset, + Stack, + Text, + globalColors, + useForegroundColor, +} from '@/design-system'; +import { alignHorizontalToFlexAlign } from '@/design-system/layout/alignment'; +import { IS_DEV } from '@/env'; +import { + useAccountProfile, + useDimensions, + useSwapCurrencyHandlers, +} from '@/hooks'; +import { fonts } from '@/styles'; +import { useTheme } from '@/theme'; +import { safeAreaInsetValues } from '@/utils'; +import { HapticFeedbackType } from '@/utils/haptics'; +import { getNativeAssetForNetwork } from '@/utils/ethereumUtils'; +import { metadataClient } from '@/graphql'; +import { signPersonalMessage } from '@/model/wallet'; +import { RouteProp, useRoute } from '@react-navigation/native'; +import { WrappedAlert as Alert } from '@/helpers/alert'; +import { RainbowError, logger } from '@/logger'; +import { + OnboardPointsMutation, + PointsErrorType, +} from '@/graphql/__generated__/metadata'; +import { pointsQueryKey } from '@/resources/points'; +import { queryClient } from '@/react-query'; +import { Network } from '@/networks/types'; +import { useNavigation } from '@/navigation'; +import Routes from '@/navigation/routesNames'; +import { CurrencySelectionTypes, ExchangeModalTypes } from '@/helpers'; +import * as i18n from '@/languages'; +import { delay } from '@/utils/delay'; +import { abbreviateNumber } from '@/helpers/utilities'; +import { + address as formatAddress, + abbreviateEnsForDisplay, +} from '@/utils/abbreviations'; + +const SCREEN_BOTTOM_INSET = safeAreaInsetValues.bottom + 20; +const CHARACTER_WIDTH = 9.2725; + +type ConsoleSheetParams = { + ConsoleSheet: { + referralCode?: string; + }; +}; + +export const ConsoleSheet = () => { + const { params } = useRoute>(); + const referralCode = params?.referralCode; + + const [didConfirmOwnership, setDidConfirmOwnership] = useState(false); + const [showSignInButton, setShowSignInButton] = useState(false); + const [showSwapOrBuyButton, setShowSwapOrBuyButton] = useState(false); + const [pointsProfile, setPointsProfile] = useState(); + const [hasEth, setHasEth] = useState(false); + + const { accountAddress } = useAccountProfile(); + const { goBack, navigate } = useNavigation(); + const { updateInputCurrency } = useSwapCurrencyHandlers({ + shouldUpdate: false, + type: ExchangeModalTypes.swap, + }); + + useEffect(() => { + (async () => { + const ethAsset = await getNativeAssetForNetwork( + Network.mainnet, + accountAddress + ); + setHasEth(!!Number(ethAsset?.balance?.amount)); + })(); + }, [accountAddress]); + + useEffect(() => { + if (IS_DEV) { + setDidConfirmOwnership(false); + setShowSignInButton(false); + setShowSwapOrBuyButton(false); + } + }, []); + + const signIn = useCallback(async () => { + let points; + let signature; + let challenge; + const challengeResponse = await metadataClient.getPointsOnboardChallenge({ + address: accountAddress, + referral: referralCode, + }); + challenge = challengeResponse?.pointsOnboardChallenge; + if (challenge) { + const signatureResponse = await signPersonalMessage(challenge); + signature = signatureResponse?.result; + if (signature) { + points = await metadataClient.onboardPoints({ + address: accountAddress, + signature, + referral: referralCode, + }); + } + } + if (!points) { + logger.error(new RainbowError('Error onboarding points user'), { + referralCode, + challenge, + signature, + }); + Alert.alert(i18n.t(i18n.l.points.console.generic_alert)); + } else { + if (points.onboardPoints?.error) { + const errorType = points.onboardPoints?.error?.type; + if (errorType === PointsErrorType.ExistingUser) { + Alert.alert(i18n.t(i18n.l.points.console.existing_user_alert)); + } else if (errorType === PointsErrorType.InvalidReferralCode) { + Alert.alert( + i18n.t(i18n.l.points.console.invalid_referral_code_alert) + ); + } + } else { + setDidConfirmOwnership(true); + setPointsProfile(points); + queryClient.setQueryData( + pointsQueryKey({ address: accountAddress }), + points + ); + } + } + }, [accountAddress, referralCode]); + + const swap = useCallback(async () => { + goBack(); + await delay(1000); + navigate(Routes.WALLET_SCREEN); + await delay(1000); + navigate(Routes.EXCHANGE_MODAL, { + fromDiscover: true, + params: { + fromDiscover: true, + onSelectCurrency: updateInputCurrency, + title: i18n.t(i18n.l.swap.modal_types.swap), + type: CurrencySelectionTypes.input, + }, + screen: Routes.CURRENCY_SELECT_SCREEN, + }); + }, [goBack, navigate, updateInputCurrency]); + + const getEth = useCallback(async () => { + goBack(); + await delay(1000); + navigate(Routes.WALLET_SCREEN); + await delay(1000); + navigate(Routes.ADD_CASH_SHEET); + }, [goBack, navigate]); + + return ( + + + + + + + + + + + + + + + ); +}; + +const NeonButton = ({ + color, + label, + onPress, +}: { + color?: string; + label: string; + onPress?: () => void; +}) => { + const { width: deviceWidth } = useDimensions(); + const { colors } = useTheme(); + const green = useForegroundColor('green'); + + return ( + + + + + + + + + {label} + + + + + + ); +}; + +const ClaimRetroactivePointsFlow = ({ + didConfirmOwnership, + onComplete, + pointsProfile, + setShowSignInButton, + setShowSwapOrBuyButton, +}: { + didConfirmOwnership: boolean; + onComplete?: () => void; + pointsProfile?: OnboardPointsMutation; + setShowSignInButton: React.Dispatch>; + setShowSwapOrBuyButton: React.Dispatch>; +}) => { + const { accountENS, accountAddress } = useAccountProfile(); + const [animationKey, setAnimationKey] = useState(0); + const [animationPhase, setAnimationPhase] = useState(0); + const [isCalculationComplete, setIsCalculationComplete] = useState(false); + + const accountName = (abbreviateEnsForDisplay(accountENS, 10) || + formatAddress(accountAddress, 4, 5)) as string; + + useEffect(() => { + if (IS_DEV) { + setAnimationKey(prevKey => prevKey + 1); + setAnimationPhase(0); + setIsCalculationComplete(false); + } + }, []); + + const onboardingData = pointsProfile?.onboardPoints?.user?.onboarding; + const rainbowSwaps = onboardingData?.categories?.find( + c => c.type === 'rainbow-swaps' + ); + const metamaskSwaps = onboardingData?.categories?.find( + c => c.type === 'metamask-swaps' + ); + const rainbowBridges = onboardingData?.categories?.find( + c => c.type === 'metamask-swaps' + ); + const nftCollections = onboardingData?.categories?.find( + c => c.type === 'nft-collections' + ); + const historicBalance = onboardingData?.categories?.find( + c => c.type === 'historic-balance' + ); + const bonus = onboardingData?.categories?.find(c => c.type === 'bonus'); + + const hasRetroactivePoints = + rainbowSwaps?.earnings?.total || + metamaskSwaps?.earnings?.total || + rainbowBridges?.earnings?.total || + nftCollections?.earnings?.total || + historicBalance?.earnings?.total; + + return ( + + {animationPhase === 0 && ( + <> + + + + + + ${i18n.t(i18n.l.points.console.auth_required)}`} + weight="normal" + /> + setShowSignInButton(true)} + textContent={`> ${i18n.t( + i18n.l.points.console.sign_in_with_wallet + )}`} + weight="normal" + /> + ${i18n.t(i18n.l.points.console.access_granted)}`} + /> + + + + + + + + + + + + { + const beginNextPhase = setTimeout(() => { + setAnimationKey(prevKey => prevKey + 1); + setAnimationPhase(1); + }, 2500); + onComplete?.(); + return () => clearTimeout(beginNextPhase); + }} + textContent={rainbowText.row9} + typingSpeed={100} + /> + + + + )} + {animationPhase === 1 && + (hasRetroactivePoints ? ( + }> + + + + + + + ${i18n.t( + i18n.l.points.console.calculating_points + )}`} + weight="normal" + /> + + + + }> + + + + + + + + + + + + + + + + + + + setIsCalculationComplete(true)} + textAlign="right" + textContent={`+ ${bonus?.earnings?.total}`} + typingSpeed={100} + /> + + + + ${i18n.t( + i18n.l.points.console.calculation_complete + )}`} + weight="normal" + /> + + + + + + + ) : ( + }> + + + + + + ${i18n.t( + i18n.l.points.console.registration_complete + )}`} + /> + + + + + + setShowSwapOrBuyButton(true)} + weight="normal" + multiline + textContent={i18n.t(i18n.l.points.console.claim_bonus_paragraph)} + /> + + ))} + + ); +}; + +const AnimationContext = createContext({ + currentSequenceIndex: 0, + getNextAnimationIndex: () => { + return; + }, + incrementSequence: () => { + return; + }, +}); + +export const useAnimationContext = () => useContext(AnimationContext); + +export const TypingAnimation = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [currentSequenceIndex, setCurrentSequenceIndex] = useState(0); + const animationIndexRef = useRef(0); + + const getNextAnimationIndex = useCallback(() => { + const currentIndex = animationIndexRef.current; + animationIndexRef.current += 1; + return currentIndex; + }, []); + + const incrementSequence = useCallback(() => { + setCurrentSequenceIndex(prevIndex => prevIndex + 1); + }, []); + + useEffect(() => { + if (IS_DEV) { + setCurrentSequenceIndex(0); + animationIndexRef.current = 0; + } + }, []); + + return ( + + {children} + + ); +}; + +type AnimatedTextProps = { + color?: { text: string; shadow: string }; + delayStart?: number; + disableShadow?: boolean; + enableHapticTyping?: boolean; + hapticType?: HapticFeedbackType; + multiline?: boolean; + onComplete?: () => void; + opacity?: number; + rainbowText?: boolean; + repeat?: boolean; + shadowOpacity?: number; + skipAnimation?: boolean; + startWhenTrue?: boolean; + textAlign?: 'center' | 'left' | 'right'; + textContent: string; + typingSpeed?: number; + weight?: 'bold' | 'normal'; +} & ({ color: { text: string; shadow: string } } | { rainbowText: boolean }); + +const AnimatedText = ({ + color, + delayStart, + disableShadow, + enableHapticTyping, + hapticType = 'selection', + multiline, + onComplete, + opacity, + rainbowText, + repeat, + shadowOpacity, + skipAnimation, + startWhenTrue, + textAlign, + textContent, + typingSpeed = 20, + weight = 'bold', +}: AnimatedTextProps) => { + const { colors } = useTheme(); + const { + currentSequenceIndex, + getNextAnimationIndex, + incrementSequence, + } = useAnimationContext(); + const index = useRef(getNextAnimationIndex()).current; + const displayedCharacters = useSharedValue( + skipAnimation ? textContent.length : 0 + ); + const [displayedText, setDisplayedText] = useState( + skipAnimation ? textContent : '' + ); + + const rainbowTextColors = useMemo( + () => (rainbowText ? generateRainbowColors(textContent) : undefined), + [rainbowText, textContent] + ); + + const getRainbowTextStyle = useCallback( + (i: number) => ({ + color: rainbowTextColors?.[i]?.text, + opacity, + textAlign, + textShadowColor: disableShadow + ? 'transparent' + : shadowOpacity && rainbowTextColors?.[i]?.shadow + ? colors.alpha(rainbowTextColors?.[i]?.shadow, shadowOpacity) + : rainbowTextColors?.[i]?.shadow, + }), + [ + colors, + disableShadow, + opacity, + rainbowTextColors, + shadowOpacity, + textAlign, + ] + ); + + const textStyle = useMemo( + () => ({ + color: rainbowText ? undefined : color?.text, + fontWeight: weight, + opacity, + textAlign, + textShadowColor: disableShadow + ? 'transparent' + : rainbowText + ? undefined + : shadowOpacity && color?.shadow + ? colors.alpha(color?.shadow, shadowOpacity) + : color?.shadow, + }), + [ + color, + colors, + disableShadow, + opacity, + rainbowText, + shadowOpacity, + textAlign, + weight, + ] + ); + + const animationConfig = useMemo( + () => ({ + duration: textContent.length * typingSpeed, + easing: Easing.linear, + }), + [textContent, typingSpeed] + ); + + const onAnimationComplete = useCallback( + (isFinished?: boolean) => { + 'worklet'; + if (isFinished) { + if (onComplete) { + runOnJS(onComplete)(); + } + runOnJS(incrementSequence)(); + + if (repeat) { + displayedCharacters.value = withRepeat( + withSequence( + withTiming(textContent.length, { duration: typingSpeed }), + withTiming(0, { duration: 0 }), + withTiming(0, { duration: typingSpeed }), + withTiming(textContent.length, animationConfig) + ), + -1, + false + ); + } + } + }, + [ + animationConfig, + displayedCharacters, + incrementSequence, + onComplete, + repeat, + textContent.length, + typingSpeed, + ] + ); + + useAnimatedReaction( + () => ({ displayedValue: displayedCharacters.value, repeat }), + (current, previous) => { + if ( + !previous?.displayedValue || + Math.round(current.displayedValue) !== + Math.round(previous?.displayedValue) + ) { + const newText = + textContent.slice(0, Math.round(current.displayedValue)) || ' '; + + if (current.repeat === false && newText === textContent) { + runOnJS(setDisplayedText)(newText); + cancelAnimation(displayedCharacters); + displayedCharacters.value = textContent.length; + return; + } + + runOnJS(setDisplayedText)(newText); + if ( + enableHapticTyping && + Math.round(current.displayedValue) && + newText[newText.length - 1] !== ' ' + ) { + runOnJS(triggerHapticFeedback)(hapticType); + } + } + } + ); + + useEffect(() => { + if ( + index !== undefined && + currentSequenceIndex === index && + (startWhenTrue === undefined || startWhenTrue) + ) { + if (!skipAnimation) { + const timer = setTimeout(() => { + displayedCharacters.value = 0; + displayedCharacters.value = withTiming( + textContent.length, + animationConfig, + onAnimationComplete + ); + }, delayStart || 0); + + return () => { + clearTimeout(timer); + }; + } else { + onComplete?.(); + incrementSequence(); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentSequenceIndex, index, startWhenTrue, textContent]); + + return ( + + + {rainbowText + ? displayedText.split('').map((char, i) => ( + + {char} + + )) + : displayedText} + + + ); +}; + +const Line = ({ + alignHorizontal, + children, + gap = 10, + leftIndent = 0, +}: { + alignHorizontal?: 'center' | 'justify' | 'left' | 'right'; + children: React.ReactNode; + gap?: number; + leftIndent?: number; +}) => { + return ( + + {children} + + ); +}; + +const LineBreak = ({ lines = 1 }: { lines?: number }) => { + return ; +}; + +const Paragraph = ({ + children, + gap = 15, + leftIndent = 0, +}: { + children: React.ReactNode; + gap?: number; + leftIndent?: number; +}) => { + return ( + + {children} + + ); +}; + +const generateRainbowColors = ( + text: string +): Array<{ text: string; shadow: string }> | undefined => { + let colorIndex = 0; + let repeatCount = 0; + const colorKeys: string[] = Object.keys(rainbowColors); + const colors: Array<{ text: string; shadow: string }> = []; + const repeatLength: number = Math.floor(text.length / (colorKeys.length * 2)); + + text.split('').forEach(() => { + if (repeatCount >= repeatLength + Math.round(Math.random())) { + repeatCount = 0; + colorIndex = (colorIndex + 1) % colorKeys.length; + } + colors.push( + rainbowColors[colorKeys[colorIndex] as keyof typeof rainbowColors] + ); + repeatCount += 1; + }); + + return colors; +}; + +const triggerHapticFeedback = (hapticType: HapticFeedbackType) => + ReactNativeHapticFeedback.trigger(hapticType); + +const styles = StyleSheet.create({ + neonButtonWrapper: { + alignSelf: 'center', + }, + neonButton: { + alignContent: 'center', + backgroundColor: '#191A1C', + borderCurve: 'continuous', + borderRadius: 12, + borderWidth: 1.5, + height: 48, + justifyContent: 'center', + shadowOffset: { width: 0, height: 13 }, + shadowOpacity: 0.2, + shadowRadius: 26, + }, + neonButtonFill: { + marginLeft: -((1 / 3) * 2), + marginTop: -((1 / 3) * 2), + }, + neonButtonText: { + margin: -16, + padding: 16, + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 12, + }, + sheet: { + backgroundColor: '#191A1C', + borderColor: 'rgba(245, 248, 255, 0.06)', + borderCurve: 'continuous', + borderRadius: 28, + borderWidth: 1.5, + height: 444, + gap: 45, + paddingHorizontal: 30, + paddingVertical: 45, + width: '100%', + zIndex: -1, + }, + text: { + fontFamily: fonts.family.SFMono, + fontSize: 15, + fontWeight: 'bold', + lineHeight: 11, + overflow: 'visible', + padding: 16, + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 12, + }, +}); + +const rainbowColors = { + blue: { text: '#31BCC4', shadow: 'rgba(49, 188, 196, 0.8)' }, + green: { text: '#57EA5F', shadow: 'rgba(87, 234, 95, 0.8)' }, + yellow: { text: '#F0D83F', shadow: 'rgba(240, 216, 63, 0.8)' }, + red: { text: '#DF5337', shadow: 'rgba(223, 83, 55, 0.8)' }, + purple: { text: '#B756A7', shadow: 'rgba(183, 86, 167, 0.8)' }, +}; + +const textColors = { + account: { text: '#FEC101', shadow: 'rgba(254, 193, 1, 0.8)' }, + gray: { text: '#94969B', shadow: 'rgba(148, 150, 155, 0.8)' }, + green: { text: '#3ECF5B', shadow: 'rgba(62, 207, 91, 0.8)' }, + white: { text: '#FFFFFF', shadow: 'rgba(255, 255, 255, 0.8)' }, +}; + +const rainbowText = { + row1: '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row2: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row3: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row4: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row5: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row6: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row7: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row8: ' \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', + row9: ' WELCOME TO POINTS ', +}; diff --git a/src/screens/points/PointsScreen.tsx b/src/screens/points/PointsScreen.tsx index 4dc670e877f..b084d07efcd 100644 --- a/src/screens/points/PointsScreen.tsx +++ b/src/screens/points/PointsScreen.tsx @@ -18,6 +18,7 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-ta import { deviceUtils } from '@/utils'; import ClaimContent from './content/ClaimContent'; import ReferralContent from './content/ReferralContent'; +import { PointsErrorType } from '@/graphql/__generated__/metadata'; const Swipe = createMaterialTopTabNavigator(); @@ -35,7 +36,8 @@ export default function PointsScreen() { walletAddress: accountAddress, }); - const isOnboarded = !!data?.points?.user?.referralCode; + const isOnboarded = + data?.points?.error?.type !== PointsErrorType.NonExistingUser; return ( @@ -68,18 +70,21 @@ export default function PointsScreen() { title={i18n.t(i18n.l.account.tab_points)} /> {pointsFullyEnabled ? ( - null} - > - - - - + isOnboarded ? ( + + ) : ( + null} + > + + + + ) ) : ( )} diff --git a/src/screens/points/content/ClaimContent.tsx b/src/screens/points/content/ClaimContent.tsx index 300df84a24b..841a84c504a 100644 --- a/src/screens/points/content/ClaimContent.tsx +++ b/src/screens/points/content/ClaimContent.tsx @@ -13,6 +13,7 @@ import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; import React from 'react'; import Svg, { Path } from 'react-native-svg'; import * as i18n from '@/languages'; +import Routes from '@/navigation/routesNames'; export default function ClaimContent() { const { accentColor } = useAccountAccentColor(); @@ -70,6 +71,7 @@ export default function ClaimContent() { alignItems: 'center', justifyContent: 'center', }} + onPress={() => navigate(Routes.CONSOLE_SHEET)} > + ( 'incomplete' ); @@ -64,10 +67,15 @@ export default function ReferralContent() { if (res.onboardPoints.error.type === 'INVALID_REFERRAL_CODE') { setStatus('invalid'); haptics.notificationError(); + } else { + logger.error(new RainbowError('Error validating referral code'), { + referralCode, + }); + Alert.alert(i18n.t(i18n.l.points.referral.error)); } - Alert.alert(i18n.t(i18n.l.points.referral.error)); } else { setStatus('valid'); + setReferralCode(referralCode); textInputRef.current?.blur(); haptics.notificationSuccess(); } @@ -281,7 +289,7 @@ export default function ReferralContent() { > - {`􀆉 ${i18n.t(i18n.l.points.referral.back)}`}` + {`􀆉 ${i18n.t(i18n.l.points.referral.back)}`} @@ -297,6 +305,7 @@ export default function ReferralContent() { alignItems: 'center', justifyContent: 'center', }} + onPress={() => navigate(Routes.CONSOLE_SHEET, { referralCode })} > => {