diff --git a/assets/icons/rakki-ticket.svg b/assets/icons/rakki-ticket.svg new file mode 100644 index 0000000000..2f0c53237a --- /dev/null +++ b/assets/icons/rakki-ticket.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ticket.svg b/assets/icons/ticket.svg new file mode 100644 index 0000000000..f95928c5c4 --- /dev/null +++ b/assets/icons/ticket.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/logos/rakki-ticket.png b/assets/logos/rakki-ticket.png new file mode 100644 index 0000000000..498e7e67dd Binary files /dev/null and b/assets/logos/rakki-ticket.png differ diff --git a/assets/logos/rakki-ticket.svg b/assets/logos/rakki-ticket.svg deleted file mode 100644 index f7a3c37364..0000000000 --- a/assets/logos/rakki-ticket.svg +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/components/gradientText/GradientText.tsx b/packages/components/gradientText/GradientText.tsx index 846dbd7f63..b34ba177f0 100644 --- a/packages/components/gradientText/GradientText.tsx +++ b/packages/components/gradientText/GradientText.tsx @@ -17,8 +17,8 @@ import { gradientColorPurple, gradientColorSalmon, gradientColorTurquoise, - rakkiYellow, - rakkiYellowLight, + gradientColorRakkiYellow, + gradientColorRakkiYellowLight, } from "../../utils/style/colors"; import { BrandText } from "../BrandText"; @@ -115,7 +115,7 @@ const gradient = (type: GradientType): LinearGradientProps => { }; case "yellow": return { - colors: [rakkiYellow, rakkiYellowLight], + colors: [gradientColorRakkiYellow, gradientColorRakkiYellowLight], start, end, }; diff --git a/packages/components/gradientText/GradientText.web.tsx b/packages/components/gradientText/GradientText.web.tsx index e7ddd31819..4612a21696 100644 --- a/packages/components/gradientText/GradientText.web.tsx +++ b/packages/components/gradientText/GradientText.web.tsx @@ -13,10 +13,10 @@ import { gradientColorLightLavender, gradientColorPink, gradientColorPurple, + gradientColorRakkiYellow, + gradientColorRakkiYellowLight, gradientColorSalmon, gradientColorTurquoise, - rakkiYellow, - rakkiYellowLight, } from "../../utils/style/colors"; import { exoFontFamilyFromFontWeight } from "../../utils/style/fonts"; @@ -43,7 +43,7 @@ const gradient = (type: GradientType) => { case "grayLight": return `90deg, ${gradientColorLighterGray} 0%, ${gradientColorLightLavender} 100%`; case "yellow": - return `267deg, ${rakkiYellow} 0%, ${rakkiYellowLight} 100%`; + return `267deg, ${gradientColorRakkiYellow} 0%, ${gradientColorRakkiYellowLight} 100%`; case getMapPostTextGradientType(PostCategory.Normal): return getMapPostTextGradientString(PostCategory.Normal); case getMapPostTextGradientType(PostCategory.Article): diff --git a/packages/hooks/rakki/useRakkiTicketsByUser.ts b/packages/hooks/rakki/useRakkiTicketsByUser.ts new file mode 100644 index 0000000000..9894c09ece --- /dev/null +++ b/packages/hooks/rakki/useRakkiTicketsByUser.ts @@ -0,0 +1,43 @@ +import { useQuery } from "@tanstack/react-query"; + +import { RakkiQueryClient } from "../../contracts-clients/rakki/Rakki.client"; +import { + NetworkFeature, + getNetworkFeature, + getNonSigningCosmWasmClient, + parseUserId, +} from "../../networks"; + +export const useRakkiTicketsCountByUser = (userId?: string) => { + const { data: ticketsCount = null, ...other } = useQuery( + ["rakkiTicketsCountByUser", userId], + async () => { + if (!userId) { + return null; + } + const [network, userAddress] = parseUserId(userId); + const networkId = network?.id; + if (!networkId) { + return null; + } + const rakkiFeature = getNetworkFeature( + networkId, + NetworkFeature.CosmWasmRakki, + ); + if (!rakkiFeature) { + return null; + } + const cosmWasmClient = await getNonSigningCosmWasmClient(networkId); + if (!cosmWasmClient) { + return null; + } + const client = new RakkiQueryClient( + cosmWasmClient, + rakkiFeature.contractAddress, + ); + return await client.ticketsCountByUser({ userAddr: userAddress }); + }, + { staleTime: Infinity, refetchInterval: 10000, enabled: !!userId }, + ); + return { ticketsCount, ...other }; +}; diff --git a/packages/screens/Rakki/RakkiScreen.tsx b/packages/screens/Rakki/RakkiScreen.tsx index c3c2ba1510..46324c8db5 100644 --- a/packages/screens/Rakki/RakkiScreen.tsx +++ b/packages/screens/Rakki/RakkiScreen.tsx @@ -1,51 +1,22 @@ -import { useQueryClient } from "@tanstack/react-query"; -import Long from "long"; -import moment from "moment"; -import { useEffect, useState } from "react"; -import { StyleProp, TextInput, TextStyle, View, ViewStyle } from "react-native"; - -import rakkiTicketSVG from "../../../assets/logos/rakki-ticket.svg"; -import { BrandText } from "../../components/BrandText"; -import { SVG } from "../../components/SVG"; -import { ScreenContainer } from "../../components/ScreenContainer"; -import { Box, BoxStyle } from "../../components/boxes/Box"; -import { TertiaryBox } from "../../components/boxes/TertiaryBox"; -import { PrimaryButton } from "../../components/buttons/PrimaryButton"; -import { SecondaryButton } from "../../components/buttons/SecondaryButton"; -import { GradientText } from "../../components/gradientText"; -import { UserAvatarWithFrame } from "../../components/images/AvatarWithFrame"; -import { GridList } from "../../components/layout/GridList"; -import { LoaderFullSize } from "../../components/loaders/LoaderFullScreen"; -import ModalBase from "../../components/modals/ModalBase"; -import { Username } from "../../components/user/Username"; -import { useFeedbacks } from "../../context/FeedbacksProvider"; -import { Info } from "../../contracts-clients/rakki/Rakki.types"; -import { useRakkiHistory } from "../../hooks/rakki/useRakkiHistory"; -import { useRakkiInfo } from "../../hooks/rakki/useRakkiInfo"; -import { useBalances } from "../../hooks/useBalances"; -import { useMaxResolution } from "../../hooks/useMaxResolution"; -import { useSelectedNetworkId } from "../../hooks/useSelectedNetwork"; -import useSelectedWallet from "../../hooks/useSelectedWallet"; -import { NetworkFeature, getNetworkFeature } from "../../networks"; -import { prettyPrice } from "../../utils/coins"; -import { ScreenFC } from "../../utils/navigation"; -import { errorColor } from "../../utils/style/colors"; -import { - fontMedium10, - fontSemibold12, - fontSemibold13, - fontSemibold14, - fontSemibold16, - fontSemibold28, -} from "../../utils/style/fonts"; -import { modalMarginPadding } from "../../utils/style/modals"; -import { joinElements } from "../Multisig/components/MultisigRightSection"; - -import { RakkiClient } from "@/contracts-clients/rakki"; -import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; - -// TODO: replace all placeholders text with real values -// TODO: jap gradient +import { View } from "react-native"; + +import { NetworkFeature } from "../../networks"; + +import { BrandText } from "@/components/BrandText"; +import { ScreenContainer } from "@/components/ScreenContainer"; +import { LoaderFullSize } from "@/components/loaders/LoaderFullScreen"; +import { useRakkiInfo } from "@/hooks/rakki/useRakkiInfo"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; +import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; +import { GameBox } from "@/screens/Rakki/components/GameBox"; +import { Help } from "@/screens/Rakki/components/Help"; +import { PrizeInfo } from "@/screens/Rakki/components/PrizeInfo"; +import { RakkiHistory } from "@/screens/Rakki/components/RakkiHistory"; +import { RakkiLogo } from "@/screens/Rakki/components/RakkiLogo"; +import { TicketsRemaining } from "@/screens/Rakki/components/TicketsRamaining"; +import { sectionLabelCStyle } from "@/screens/Rakki/styles"; +import { ScreenFC } from "@/utils/navigation"; +import { layout } from "@/utils/style/layout"; export const RakkiScreen: ScreenFC<"Rakki"> = () => { const networkId = useSelectedNetworkId(); @@ -66,6 +37,7 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => { width: "100%", alignItems: "center", justifyContent: "center", + marginTop: 100, }} > @@ -82,17 +54,24 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => { networkId={networkId} style={{ marginTop: 50 }} /> - + - - + ); @@ -102,744 +81,9 @@ export const RakkiScreen: ScreenFC<"Rakki"> = () => { footerChildren={rakkiInfo === undefined ? <> : undefined} forceNetworkFeatures={[NetworkFeature.CosmWasmRakki]} > - {content} - - ); -}; - -const BuyTicketsButton: React.FC<{ networkId: string; info: Info }> = ({ - networkId, - info, -}) => { - const selectedWallet = useSelectedWallet(); - const [modalVisible, setModalVisible] = useState(false); - const remainingTickets = info.config.max_tickets - info.current_tickets_count; - const [ticketAmount, setTicketAmount] = useState("1"); - const queryClient = useQueryClient(); - const ticketAmountNumber = Long.fromString(ticketAmount || "0"); - useEffect(() => { - if (remainingTickets > 0 && ticketAmountNumber.gt(remainingTickets)) { - setTicketAmount(remainingTickets.toString()); - } - }, [ticketAmountNumber, remainingTickets]); - const totalPrice = ticketAmountNumber.mul( - Long.fromString(info.config.ticket_price.amount), - ); - const { balances } = useBalances(networkId, selectedWallet?.address); - const ticketDenomBalance = - balances.find((b) => b.denom === info.config.ticket_price.denom)?.amount || - "0"; - const canPay = Long.fromString(ticketDenomBalance).gte(totalPrice); - const canBuy = ticketAmountNumber.gt(0) && canPay; - const { wrapWithFeedback } = useFeedbacks(); - return ( - - setModalVisible(true)} - textStyle={{ fontWeight: "400" }} - text="Buy tickets" - size="XS" - /> - setModalVisible(false)} - > - - - - - - 1 ticket price{" "} - - {prettyPrice( - networkId, - info.config.ticket_price.amount, - info.config.ticket_price.denom, - )} - - - - - - Number of Lottery Tickets - - - { - if (!newAmount) { - setTicketAmount(newAmount); - return; - } - const newAmountNumber = +newAmount; - if (isNaN(newAmountNumber)) { - return; - } - if (newAmountNumber > remainingTickets) { - return; - } - setTicketAmount(newAmountNumber.toString()); - }} - style={[ - fontSemibold16, - { - paddingLeft: 16, - paddingRight: 10, - fontWeight: "400", - color: "white", - }, - { outlineStyle: "none" } as TextStyle, - ]} - /> - - - Total price - - - {prettyPrice( - networkId, - totalPrice.toString(), - info.config.ticket_price.denom, - )} - - - - - - - Available Balance{" "} - - {prettyPrice( - networkId, - ticketDenomBalance, - info.config.ticket_price.denom, - )} - - - - - - setModalVisible(false)} - /> - { - if (!selectedWallet?.address) { - throw new Error("No wallet with valid address selected"); - } - const cosmWasmClient = - await getKeplrSigningCosmWasmClient(networkId); - const feature = getNetworkFeature( - networkId, - NetworkFeature.CosmWasmRakki, - ); - if (feature?.type !== NetworkFeature.CosmWasmRakki) { - throw new Error("Rakki not supported on this network"); - } - const rakkiClient = new RakkiClient( - cosmWasmClient, - selectedWallet.address, - feature.contractAddress, - ); - const count = ticketAmountNumber.toNumber(); - const price = { - amount: Long.fromString(info.config.ticket_price.amount) - .multiply(count) - .toString(), - denom: info.config.ticket_price.denom, - }; - await rakkiClient.buyTickets( - { - count, - }, - "auto", - undefined, - [price], - ); - await Promise.all([ - queryClient.invalidateQueries(["rakkiInfo", networkId]), - queryClient.invalidateQueries([ - "balances", - networkId, - selectedWallet.address, - ]), - queryClient.invalidateQueries(["rakkiHistory", networkId]), - ]); - setModalVisible(false); - })} - text="Buy Tickets" - size="M" - /> - - - - - - ); -}; - -const History: React.FC<{ - style?: StyleProp; - networkId: string; - info: Info; -}> = ({ style, networkId, info }) => { - const { width } = useMaxResolution(); - const isSmallScreen = width < 400; - const { rakkiHistory } = useRakkiHistory(networkId); - if (!rakkiHistory?.length) { - return null; - } - return ( - - RAKKi Finished Rounds - - - - Rounds - - - {rakkiHistory.length} - - - {joinElements( - rakkiHistory.map((historyItem) => { - return ( - - - - - - - Drawn{" "} - {moment(historyItem.date.getTime()).format( - "MMM D, YYYY, h:mm A", - )} - - - ); - }), - , - )} - - - - - - - ); -}; - -interface HelpBoxDefinition { - title: string; - description: string; -} - -const Help: React.FC<{ style?: StyleProp }> = ({ style }) => { - const helpBoxes: HelpBoxDefinition[] = [ - { - title: "Buy Tickets", - description: - "Prices are $10 USDC per ticket.\nGamblers can buy multiple tickets.", - }, - { - title: "Wait for the Draw", - description: - "Players just have to wait until the cash prize pool is reached.", - }, - { - title: "Check for Prizes", - description: - "Once the cashprize pool is reached, the winner receive the $10,000 transaction directly!", - }, - ]; - return ( - - How to Play RAKKi - - {`When the community lottery pool reaches the 10k USDC amount, only one will be the winner!\nSimple!`} - - - - minElemWidth={280} - gap={14} - keyExtractor={(item) => item.title} - noFixedHeight - data={helpBoxes} - renderItem={({ item, index }, width) => { - return ( - - - - {item.title} - - - Step {index + 1} - - - - {item.description} - - - ); - }} - /> - - - ); -}; - -const GameBox: React.FC<{ - networkId: string; - info: Info; - style?: StyleProp; -}> = ({ networkId, info, style }) => { - const totalPrizeAmount = Long.fromString(info.config.ticket_price.amount).mul( - info.current_tickets_count, - ); - const feePrizeAmount = totalPrizeAmount - .mul(info.config.fee_per10k) - .div(10000); - const winnerPrizeAmount = totalPrizeAmount.sub(feePrizeAmount); - return ( - - - - Next Draw - - - When the {info.config.max_tickets - info.current_tickets_count}{" "} - remaining tickets will be sold out. - - - - Prize Pot - - - ~ - {prettyPrice( - networkId, - winnerPrizeAmount.toString(), - info.config.ticket_price.denom, - )} - - - ({info.current_tickets_count} TICKETS) - - + + {content} - - Your tickets - - - - ); -}; - -const GetTicketCTA: React.FC<{ info: Info; style?: StyleProp }> = ({ - info, - style, -}) => { - return ( - - Get your tickets now! - - - {info.config.max_tickets - info.current_tickets_count} - - - tickets - - - remaining - - - - ); -}; - -const PrizeInfo: React.FC<{ - info: Info; - networkId: string; - style?: StyleProp; -}> = ({ info, networkId, style }) => { - const totalPrizeAmount = Long.fromString(info.config.ticket_price.amount).mul( - info.config.max_tickets, - ); - const feePrizeAmount = totalPrizeAmount - .mul(info.config.fee_per10k) - .div(10000); - const winnerPrizeAmount = totalPrizeAmount.sub(feePrizeAmount); - return ( - - - Automated Lottery - - - {prettyPrice( - networkId, - winnerPrizeAmount.toString(), - info.config.ticket_price.denom, - )} - - - in prizes! - - - - ); -}; - -const RakkiLogo: React.FC<{ style?: StyleProp }> = ({ style }) => { - return ( - - - - RAKKi - - - - ); -}; - -const RakkiJap: React.FC = () => { - return ( - - - ラ - - ッ - - キー - - + ); }; - -const rakkiJapTextCStyle: TextStyle = { - textAlign: "center", - fontSize: 51.933, - lineHeight: 62.319 /* 120% */, - letterSpacing: -2.077, - fontWeight: "600", -}; - -const gameBoxLabelCStyle: TextStyle = { - ...fontSemibold12, - color: "#777", - textAlign: "center", -}; - -const sectionLabelCStyle: TextStyle = { - ...fontSemibold28, - textAlign: "center", - marginBottom: 12, -}; diff --git a/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx b/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx new file mode 100644 index 0000000000..4262f79b0b --- /dev/null +++ b/packages/screens/Rakki/components/BuyTickets/BuyTicketsButton.tsx @@ -0,0 +1,53 @@ +import { FC, useState } from "react"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { Box } from "@/components/boxes/Box"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { BuyTicketsModal } from "@/screens/Rakki/components/BuyTickets/BuyTicketsModal"; +import { neutral00, neutralFF, rakkiYellow } from "@/utils/style/colors"; +import { fontSemibold14 } from "@/utils/style/fonts"; + +export const BuyTicketsButton: FC<{ networkId: string; info: Info }> = ({ + networkId, + info, +}) => { + const [isButtonHovered, setButtonHovered] = useState(false); + const [isModalVisible, setModalVisible] = useState(false); + + return ( + + setModalVisible(true)} + onHoverIn={() => setButtonHovered(true)} + onHoverOut={() => setButtonHovered(false)} + > + + + Buy ッ Tickets + + + + + + + ); +}; diff --git a/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx b/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx new file mode 100644 index 0000000000..e516063cd9 --- /dev/null +++ b/packages/screens/Rakki/components/BuyTickets/BuyTicketsModal.tsx @@ -0,0 +1,335 @@ +import { useQueryClient } from "@tanstack/react-query"; +import Long from "long"; +import { Dispatch, FC, SetStateAction, useEffect, useState } from "react"; +import { TextInput, TextStyle, View } from "react-native"; + +import rakkiTicketSVG from "@/assets/icons/rakki-ticket.svg"; +import { BrandText } from "@/components/BrandText"; +import { SVG } from "@/components/SVG"; +import { Box } from "@/components/boxes/Box"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { SecondaryButton } from "@/components/buttons/SecondaryButton"; +import { MainConnectWalletButton } from "@/components/connectWallet/MainConnectWalletButton"; +import { GradientText } from "@/components/gradientText"; +import ModalBase from "@/components/modals/ModalBase"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { Info, RakkiClient } from "@/contracts-clients/rakki"; +import { useBalances } from "@/hooks/useBalances"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { getNetworkFeature, NetworkFeature } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { ModalTicketImage } from "@/screens/Rakki/components/BuyTickets/ModalTicketImage"; +import { prettyPrice } from "@/utils/coins"; +import { + errorColor, + neutral00, + neutral17, + neutral22, + neutral33, + neutral77, + neutralA3, + neutralFF, +} from "@/utils/style/colors"; +import { + fontSemibold13, + fontSemibold14, + fontSemibold16, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { modalMarginPadding } from "@/utils/style/modals"; + +export const BuyTicketsModal: FC<{ + visible: boolean; + setModalVisible: Dispatch>; + info: Info; + networkId: string; +}> = ({ visible, setModalVisible, info, networkId }) => { + const selectedWallet = useSelectedWallet(); + const remainingTickets = info.config.max_tickets - info.current_tickets_count; + const [ticketAmount, setTicketAmount] = useState("1"); + const queryClient = useQueryClient(); + const ticketAmountNumber = Long.fromString(ticketAmount || "0"); + useEffect(() => { + if (remainingTickets > 0 && ticketAmountNumber.gt(remainingTickets)) { + setTicketAmount(remainingTickets.toString()); + } + }, [ticketAmountNumber, remainingTickets]); + const totalPrice = ticketAmountNumber.mul( + Long.fromString(info.config.ticket_price.amount), + ); + const { balances } = useBalances(networkId, selectedWallet?.address); + const ticketDenomBalance = + balances.find((b) => b.denom === info.config.ticket_price.denom)?.amount || + "0"; + const canPay = Long.fromString(ticketDenomBalance).gte(totalPrice); + const canBuy = ticketAmountNumber.gt(0) && canPay; + const { wrapWithFeedback } = useFeedbacks(); + + const prettyTicketPrice = prettyPrice( + networkId, + info.config.ticket_price.amount, + info.config.ticket_price.denom, + ); + const prettyTotalPrice = prettyPrice( + networkId, + totalPrice.toString(), + info.config.ticket_price.denom, + ); + const prettyAvailableBalance = prettyPrice( + networkId, + ticketDenomBalance, + info.config.ticket_price.denom, + ); + + const onPressBuyTickets = wrapWithFeedback(async () => { + if (!selectedWallet?.address) { + throw new Error("No wallet with valid address selected"); + } + const cosmWasmClient = await getKeplrSigningCosmWasmClient(networkId); + const feature = getNetworkFeature(networkId, NetworkFeature.CosmWasmRakki); + if (feature?.type !== NetworkFeature.CosmWasmRakki) { + throw new Error("Rakki not supported on this network"); + } + const rakkiClient = new RakkiClient( + cosmWasmClient, + selectedWallet.address, + feature.contractAddress, + ); + const count = ticketAmountNumber.toNumber(); + const price = { + amount: Long.fromString(info.config.ticket_price.amount) + .multiply(count) + .toString(), + denom: info.config.ticket_price.denom, + }; + await rakkiClient.buyTickets( + { + count, + }, + "auto", + undefined, + [price], + ); + await Promise.all([ + queryClient.invalidateQueries(["rakkiInfo", networkId]), + queryClient.invalidateQueries([ + "balances", + networkId, + selectedWallet.address, + ]), + queryClient.invalidateQueries(["rakkiHistory", networkId]), + ]); + setModalVisible(false); + }); + + return ( + setModalVisible(false)} + > + + + + + + + + + + 1 ticket price{" "} + + {prettyTicketPrice} + + + + + + Number of Lottery Tickets + + + { + if (!newAmount) { + setTicketAmount(newAmount); + return; + } + const newAmountNumber = +newAmount; + if (isNaN(newAmountNumber)) { + return; + } + if (newAmountNumber > remainingTickets) { + return; + } + setTicketAmount(newAmountNumber.toString()); + }} + style={[ + fontSemibold16, + { + paddingLeft: layout.spacing_x2, + paddingRight: layout.spacing_x1_25, + color: neutralFF, + }, + { outlineStyle: "none" } as TextStyle, + ]} + /> + + + Total price + + + {prettyTotalPrice} + + + + + + {!selectedWallet?.address ? ( + + Not connected + + ) : ( + + Available Balance{" "} + + {prettyAvailableBalance} + + + )} + + + + setModalVisible(false)} + /> + + {!selectedWallet?.address ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx b/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx new file mode 100644 index 0000000000..d0b9d8e844 --- /dev/null +++ b/packages/screens/Rakki/components/BuyTickets/ModalTicketImage.tsx @@ -0,0 +1,46 @@ +import { TextStyle, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { TicketImage } from "@/screens/Rakki/components/TicketImage"; +import { neutral67, neutralA3 } from "@/utils/style/colors"; +import { fontSemibold30 } from "@/utils/style/fonts"; + +export const ModalTicketImage = () => { + return ( + + + + + + ラ + + ッ + + キー + + + + + ); +}; + +const japaneseTextCStyle: TextStyle = { + ...fontSemibold30, + textAlign: "center", + letterSpacing: 6, +}; diff --git a/packages/screens/Rakki/components/GameBox.tsx b/packages/screens/Rakki/components/GameBox.tsx new file mode 100644 index 0000000000..4f58165679 --- /dev/null +++ b/packages/screens/Rakki/components/GameBox.tsx @@ -0,0 +1,115 @@ +import Long from "long"; +import { FC } from "react"; +import { StyleProp, View } from "react-native"; + +import { netCurrentPrizeAmount } from "./../utils"; + +import { BrandText } from "@/components/BrandText"; +import { Box, BoxStyle } from "@/components/boxes/Box"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { useRakkiTicketsCountByUser } from "@/hooks/rakki/useRakkiTicketsByUser"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { TicketsAndPrice } from "@/screens/Rakki/components/TicketsAndPrice"; +import { gameBoxLabelCStyle } from "@/screens/Rakki/styles"; +import { prettyPrice } from "@/utils/coins"; +import { neutral22, neutral33, neutralA3 } from "@/utils/style/colors"; +import { fontMedium10, fontSemibold12 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const GameBox: FC<{ + networkId: string; + info: Info; + style?: StyleProp; +}> = ({ networkId, info, style }) => { + const selectedWallet = useSelectedWallet(); + const { ticketsCount: userTicketsCount } = useRakkiTicketsCountByUser( + selectedWallet?.userId, + ); + const userAmount = userTicketsCount + ? Long.fromString(info.config.ticket_price.amount).mul(userTicketsCount) + : 0; + + const prettyCurrentPrizeAmount = prettyPrice( + networkId, + netCurrentPrizeAmount(info), + info.config.ticket_price.denom, + ); + const prettyUserTicketsPriceAmount = prettyPrice( + networkId, + userAmount.toString(), + info.config.ticket_price.denom, + ); + + return ( + + + + Next Draw + + + When the {info.config.max_tickets - info.current_tickets_count}{" "} + remaining tickets will be sold out. + + + + Prize Pot + + + + Your tickets + {userTicketsCount !== null ? ( + + ) : ( + + Not connected + + )} + + + ); +}; diff --git a/packages/screens/Rakki/components/Help.tsx b/packages/screens/Rakki/components/Help.tsx new file mode 100644 index 0000000000..ecc52a8d57 --- /dev/null +++ b/packages/screens/Rakki/components/Help.tsx @@ -0,0 +1,125 @@ +import { FC } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { grossMaxPrizeAmount, netMaxPrizeAmount } from "../utils"; + +import { BrandText } from "@/components/BrandText"; +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { GridList } from "@/components/layout/GridList"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { gameBoxLabelCStyle } from "@/screens/Rakki/styles"; +import { prettyPrice } from "@/utils/coins"; +import { neutral33, neutral77 } from "@/utils/style/colors"; +import { + fontMedium10, + fontSemibold12, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +interface HelpBoxDefinition { + title: string; + description: string; +} + +export const Help: FC<{ + info: Info; + networkId: string; + style?: StyleProp; +}> = ({ info, style, networkId }) => { + const prettyTicketPrice = prettyPrice( + networkId, + info.config.ticket_price.amount, + info.config.ticket_price.denom, + ); + const prettyNetMaxPrize = prettyPrice( + networkId, + netMaxPrizeAmount(info), + info.config.ticket_price.denom, + ); + const prettyMaxPrize = prettyPrice( + networkId, + grossMaxPrizeAmount(info), + info.config.ticket_price.denom, + ); + const feePercent = (info.config.fee_per10k / 10000) * 100; + + const helpBoxes: HelpBoxDefinition[] = [ + { + title: "Buy Tickets", + description: `Prices are ${prettyTicketPrice} per ticket.\nGamblers can buy multiple tickets.`, + }, + { + title: "Wait for the Draw", + description: + "Players just have to wait until the cash prize pool is reached.", + }, + { + title: "Check for Prizes", + description: `Once the cashprize pool is reached, the winner receive the ${prettyNetMaxPrize} transaction directly!`, + }, + ]; + + return ( + + How to Play RAKKi + + {`When the community lottery pool reaches the ${prettyMaxPrize} amount, only one will be the winner!\nSimple!`} + + + + minElemWidth={212} + gap={layout.spacing_x1_75} + keyExtractor={(item) => item.title} + noFixedHeight + data={helpBoxes} + renderItem={({ item, index }, width) => { + return ( + + + {item.title} + + Step {index + 1} + + + + {item.description} + + + ); + }} + /> + + *On the total amount, {feePercent}% are sent to a multisig wallet to + buyback and burn $TORI token. + + + + ); +}; diff --git a/packages/screens/Rakki/components/IntroJapText.tsx b/packages/screens/Rakki/components/IntroJapText.tsx new file mode 100644 index 0000000000..66b56903e0 --- /dev/null +++ b/packages/screens/Rakki/components/IntroJapText.tsx @@ -0,0 +1,34 @@ +import { FC } from "react"; +import { TextStyle, View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { neutral67, neutralFF } from "@/utils/style/colors"; + +export const IntroJapText: FC = () => { + return ( + + + ラ + + ッ + + キー + + + ); +}; + +const japaneseTextCStyle: TextStyle = { + textAlign: "center", + fontSize: 51.933, + lineHeight: 62.319 /* 120% */, + letterSpacing: -2.077, + fontWeight: "600", +}; diff --git a/packages/screens/Rakki/components/IntroTicketImageButton.tsx b/packages/screens/Rakki/components/IntroTicketImageButton.tsx new file mode 100644 index 0000000000..619dd19f8b --- /dev/null +++ b/packages/screens/Rakki/components/IntroTicketImageButton.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; +import { View } from "react-native"; + +import { Info } from "@/contracts-clients/rakki"; +import { BuyTicketsButton } from "@/screens/Rakki/components/BuyTickets/BuyTicketsButton"; +import { TicketImage } from "@/screens/Rakki/components/TicketImage"; + +export const IntroTicketImageButton: FC<{ + networkId: string; + info: Info; +}> = ({ networkId, info }) => { + return ( + + + + + + + ); +}; diff --git a/packages/screens/Rakki/components/PrizeInfo.tsx b/packages/screens/Rakki/components/PrizeInfo.tsx new file mode 100644 index 0000000000..7704f73c43 --- /dev/null +++ b/packages/screens/Rakki/components/PrizeInfo.tsx @@ -0,0 +1,61 @@ +import { FC } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { netMaxPrizeAmount } from "../utils"; + +import { BrandText } from "@/components/BrandText"; +import { GradientText } from "@/components/gradientText"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { IntroTicketImageButton } from "@/screens/Rakki/components/IntroTicketImageButton"; +import { prettyPrice } from "@/utils/coins"; +import { + fontSemibold14, + fontSemibold20, + fontSemibold28, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const PrizeInfo: FC<{ + info: Info; + networkId: string; + style?: StyleProp; +}> = ({ info, networkId, style }) => { + const prettyMaxPrizeAmount = prettyPrice( + networkId, + netMaxPrizeAmount(info), + info.config.ticket_price.denom, + ); + + return ( + + + Automated Lottery + + + {prettyMaxPrizeAmount} + + + in prizes! + + + + + ); +}; diff --git a/packages/screens/Rakki/components/RakkiHistory.tsx b/packages/screens/Rakki/components/RakkiHistory.tsx new file mode 100644 index 0000000000..a7972c5638 --- /dev/null +++ b/packages/screens/Rakki/components/RakkiHistory.tsx @@ -0,0 +1,130 @@ +import moment from "moment"; +import { FC } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { Box } from "@/components/boxes/Box"; +import { UserAvatarWithFrame } from "@/components/images/AvatarWithFrame"; +import { Username } from "@/components/user/Username"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { useRakkiHistory } from "@/hooks/rakki/useRakkiHistory"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; +import { BuyTicketsButton } from "@/screens/Rakki/components/BuyTickets/BuyTicketsButton"; +import { gameBoxLabelCStyle, sectionLabelCStyle } from "@/screens/Rakki/styles"; +import { joinElements } from "@/utils/react"; +import { neutral22, neutral33, neutral77 } from "@/utils/style/colors"; +import { fontMedium10, fontSemibold12 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const RakkiHistory: FC<{ + style?: StyleProp; + networkId: string; + info: Info; +}> = ({ style, networkId, info }) => { + const { width } = useMaxResolution(); + const isSmallScreen = width < 400; + const { rakkiHistory } = useRakkiHistory(networkId); + + if (!rakkiHistory?.length) { + return null; + } + return ( + + RAKKi Finished Rounds + + + + Rounds + + + {rakkiHistory.length} + + + {joinElements( + rakkiHistory.map((historyItem) => { + return ( + + + + + + + Drawn{" "} + {moment(historyItem.date.getTime()).format( + "MMM D, YYYY, h:mm A", + )} + + + ); + }), + , + )} + + + + + + + ); +}; diff --git a/packages/screens/Rakki/components/RakkiLogo.tsx b/packages/screens/Rakki/components/RakkiLogo.tsx new file mode 100644 index 0000000000..2fcc1a6a09 --- /dev/null +++ b/packages/screens/Rakki/components/RakkiLogo.tsx @@ -0,0 +1,25 @@ +import { FC } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { IntroJapText } from "@/screens/Rakki/components/IntroJapText"; + +export const RakkiLogo: FC<{ style?: StyleProp }> = ({ style }) => { + return ( + + + + RAKKi + + + + ); +}; diff --git a/packages/screens/Rakki/components/TicketImage.tsx b/packages/screens/Rakki/components/TicketImage.tsx new file mode 100644 index 0000000000..704540c321 --- /dev/null +++ b/packages/screens/Rakki/components/TicketImage.tsx @@ -0,0 +1,15 @@ +import { FC } from "react"; + +import rakkiTicketImage from "@/assets/logos/rakki-ticket.png"; +import { OptimizedImage } from "@/components/OptimizedImage"; + +export const TicketImage: FC = () => { + return ( + + ); +}; diff --git a/packages/screens/Rakki/components/TicketsAndPrice.tsx b/packages/screens/Rakki/components/TicketsAndPrice.tsx new file mode 100644 index 0000000000..a304e62bfa --- /dev/null +++ b/packages/screens/Rakki/components/TicketsAndPrice.tsx @@ -0,0 +1,36 @@ +import { FC } from "react"; +import { View } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { GradientText } from "@/components/gradientText"; +import { neutralA3 } from "@/utils/style/colors"; +import { fontMedium10, fontSemibold14 } from "@/utils/style/fonts"; + +export const TicketsAndPrice: FC<{ + ticketsCount: number; + price: string; +}> = ({ ticketsCount, price }) => { + return ( + + + ~{price} + + + ({ticketsCount} TICKETS) + + + ); +}; diff --git a/packages/screens/Rakki/components/TicketsRamaining.tsx b/packages/screens/Rakki/components/TicketsRamaining.tsx new file mode 100644 index 0000000000..10fa0b5a42 --- /dev/null +++ b/packages/screens/Rakki/components/TicketsRamaining.tsx @@ -0,0 +1,64 @@ +import { FC } from "react"; +import { StyleProp, View, ViewStyle } from "react-native"; + +import { BrandText } from "@/components/BrandText"; +import { Info } from "@/contracts-clients/rakki/Rakki.types"; +import { sectionLabelCStyle } from "@/screens/Rakki/styles"; +import { primaryColor } from "@/utils/style/colors"; +import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const TicketsRemaining: FC<{ + info: Info; + style?: StyleProp; +}> = ({ info, style }) => { + return ( + + Get your tickets now! + + + {info.config.max_tickets - info.current_tickets_count} + + + tickets + + + remaining + + + + ); +}; diff --git a/packages/screens/Rakki/styles.ts b/packages/screens/Rakki/styles.ts new file mode 100644 index 0000000000..50cde7636b --- /dev/null +++ b/packages/screens/Rakki/styles.ts @@ -0,0 +1,17 @@ +import { TextStyle } from "react-native"; + +import { neutral77 } from "@/utils/style/colors"; +import { fontSemibold12, fontSemibold28 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +export const sectionLabelCStyle: TextStyle = { + ...fontSemibold28, + textAlign: "center", + marginBottom: layout.spacing_x1_5, +}; + +export const gameBoxLabelCStyle: TextStyle = { + ...fontSemibold12, + color: neutral77, + textAlign: "center", +}; diff --git a/packages/screens/Rakki/utils.ts b/packages/screens/Rakki/utils.ts new file mode 100644 index 0000000000..d414ebe7e1 --- /dev/null +++ b/packages/screens/Rakki/utils.ts @@ -0,0 +1,23 @@ +import Long from "long"; + +import { Info } from "../../contracts-clients/rakki/Rakki.types"; + +const grossTicketsPrizeAmount = (info: Info, ticketsCount: number) => + Long.fromString(info.config.ticket_price.amount).mul(ticketsCount); + +const netPrizeAmount = (info: Info, ticketsCount: number) => { + const feePrizeAmount = grossTicketsPrizeAmount(info, ticketsCount) + .mul(info.config.fee_per10k) + .div(10000); + // Net prize amount + return grossTicketsPrizeAmount(info, ticketsCount).sub(feePrizeAmount); +}; + +export const netCurrentPrizeAmount = (info: Info) => + netPrizeAmount(info, info.current_tickets_count).toString(); + +export const netMaxPrizeAmount = (info: Info) => + netPrizeAmount(info, info.config.max_tickets).toString(); + +export const grossMaxPrizeAmount = (info: Info) => + grossTicketsPrizeAmount(info, info.config.max_tickets).toString(); diff --git a/packages/utils/style/colors.ts b/packages/utils/style/colors.ts index 22f480d306..4d10c43fdc 100644 --- a/packages/utils/style/colors.ts +++ b/packages/utils/style/colors.ts @@ -57,9 +57,7 @@ export const dangerColor = "#E44C39"; export const trashBackground = "rgba(244, 111, 118, 0.1)"; export const orangeLight = "#EAA54B"; - -export const rakkiYellow = "#FFD83D"; -export const rakkiYellowLight = "#FFEDAE"; +export const rakkiYellow = "#FFDC5F"; export const gradientColorTurquoise = "#A5FECB"; export const gradientColorLightLavender = "#C3CFE2"; @@ -74,6 +72,8 @@ export const gradientColorPink = "#F46FBF"; export const gradientColorGray = "#676767"; export const gradientColorLightGray = "#B7B7B7"; export const gradientColorLighterGray = "#F5F7FA"; +export const gradientColorRakkiYellow = "#FFD83D"; +export const gradientColorRakkiYellowLight = "#FFEDAE"; export const currencyTORIcolor = primaryColor; export const currencyETHcolor = "#232731";