From 17e24472d79b015bfb10c5908cef32a1e62ade1a Mon Sep 17 00:00:00 2001 From: Mike de Geofroy Date: Tue, 26 Aug 2025 02:45:59 +0300 Subject: [PATCH 1/6] Added payment with webview --- client/package.json | 1 + .../screens/main/game/route.screen.tsx | 2 +- .../components/screens/main/shop.screen.tsx | 140 +++++++++++------- client/src/components/ui/button.tsx | 14 +- client/src/components/widgets/modal-card.tsx | 43 +++++- client/src/components/widgets/shop/routes.tsx | 33 +---- client/src/shared/api/hooks/useRoutes.ts | 15 +- client/src/shared/api/hooks/useStyles.ts | 8 +- client/src/shared/api/routes.api.ts | 11 +- client/src/shared/api/styles.api.ts | 7 +- client/src/shared/interfaces/route.ts | 1 + .../src/shared/interfaces/styles/payment.ts | 11 ++ client/yarn.lock | 10 +- 13 files changed, 192 insertions(+), 104 deletions(-) create mode 100644 client/src/shared/interfaces/styles/payment.ts diff --git a/client/package.json b/client/package.json index f51a9b2..d4ac439 100644 --- a/client/package.json +++ b/client/package.json @@ -46,6 +46,7 @@ "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", "react-native-svg": "15.11.2", + "react-native-webview": "13.13.5", "socket.io-client": "2.3.1", "tailwind-merge": "^3.3.0", "tailwindcss": "3", diff --git a/client/src/components/screens/main/game/route.screen.tsx b/client/src/components/screens/main/game/route.screen.tsx index 1e34ce7..0bd1c30 100644 --- a/client/src/components/screens/main/game/route.screen.tsx +++ b/client/src/components/screens/main/game/route.screen.tsx @@ -115,7 +115,7 @@ export const RouteScreen = () => { // Show only free/accessible routes for now (no ownership info available on client) const routes = useMemo(() => { const list = isFetched && data !== undefined ? data : []; - return list.filter((r) => r.priceRoubles === 0); + return list.filter((r) => r.bought === true || r.priceRoubles === 0); }, [isFetched, data]); const [index, setIndex] = useState(0); diff --git a/client/src/components/screens/main/shop.screen.tsx b/client/src/components/screens/main/shop.screen.tsx index 2ff2796..89c9a17 100644 --- a/client/src/components/screens/main/shop.screen.tsx +++ b/client/src/components/screens/main/shop.screen.tsx @@ -12,7 +12,6 @@ import { RoutesWidget } from "@/components/widgets/shop/routes"; import { SafeAreaView } from "react-native-safe-area-context"; import { Header } from "@/components/ui/header"; import { StatusBar } from "expo-status-bar"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useStyles } from "@/shared/api/hooks/useStyles"; import { useAuthStore } from "@/shared/stores/auth.store"; import { setAvatar } from "@/shared/api/styles.api"; @@ -21,6 +20,9 @@ import { Style } from "@/shared/interfaces/styles/styles"; import { useModal } from "@/shared/hooks/useModal"; import { logger } from "@/shared/instances/logger.instance"; import { useRoutes } from "@/shared/api/hooks/useRoutes"; +import { WebView } from "react-native-webview"; +import { buyRoute } from "@/shared/api/routes.api"; +import { Route } from "@/shared/interfaces/route"; export const ShopScreen = () => { const navigation = useNavigation>(); @@ -28,17 +30,20 @@ export const ShopScreen = () => { selectedSection: "Аватарки", }); + const [url, setUrl] = useState(null); + const [selectedSeciton, setSelectedSection] = useState("Аватарки"); const { data: avatars, isFetched: isAvatarsFetched, buyStyle, + updateStyles, } = useStyles({ type: "avatar", }); - const { data, isFetched } = useRoutes(); + const { data, isFetched, updateRoutes } = useRoutes(); const routes = useMemo( () => (isFetched && data !== undefined ? data : []), [isFetched, data] @@ -51,7 +56,7 @@ export const ShopScreen = () => { navigation.goBack(); }; - const handleButton = async (avatar: Style) => { + const handleAvatarButton = async (avatar: Style) => { if (avatar.bought) { try { await setAvatar(avatar.id); @@ -79,66 +84,99 @@ export const ShopScreen = () => { }); } } else { - await buyStyle(avatar.id); + var result = await buyStyle(avatar.id); + setUrl(result.confirmationUrl); } }; + const handleRouteButton = async (r: Route) => { + var result = await buyRoute(r.id); + setUrl(result.confirmationUrl); + }; + return ( - - - - - - - - - + {url !== null ? ( + { + if (state.url.includes("success")) { + setUrl(null); -
+ updateStyles(); + updateRoutes(); - - setModalOpen(false), + }, + ], + }); + } + }} + /> + ) : ( + + + + + + + + + + +
- - {sectionRef.current?.selectedSection === "Аватарки" && - isAvatarsFetched && ( - ({ - avatar: avatar.style.url, - title: avatar.title, - withButton: true, - bought: avatar.bought, - buttonAction: async () => { - await handleButton(avatar); - }, - subtitle: - avatar.priceRoubles == 0 - ? "Бесплатно" - : `${avatar.priceRoubles} ₽`, - disabled: avatar.id === user?.styles?.avatarId, + + + + + {sectionRef.current?.selectedSection === "Аватарки" && + isAvatarsFetched && ( + ({ + avatar: avatar.style.url, + title: avatar.title, + withButton: true, + bought: avatar.bought, + buttonAction: async () => await handleAvatarButton(avatar), + subtitle: + avatar.priceRoubles == 0 + ? "Бесплатно" + : `${avatar.priceRoubles} ₽`, + disabled: avatar.id === user?.styles?.avatarId, + }))} + className="pb-[70px]" + /> + )} + + {sectionRef.current?.selectedSection === "Маршруты" && ( + r.bought == false).map((r) => ({ + route: r, + disabled: false, + buttonAction: async () => await handleRouteButton(r), }))} - className="pb-[70px]" /> )} - - {sectionRef.current?.selectedSection === "Маршруты" && ( - ({ - route: r, - disabled: false, - }))} - /> - )} - - + + + )} ); diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index a9ab741..74c1f73 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -1,6 +1,7 @@ -import { Pressable, Text } from "react-native"; +import { Pressable, StyleProp, Text, ViewStyle } from "react-native"; import { cva, type VariantProps } from "class-variance-authority"; import { twMerge } from "tailwind-merge"; +import React from "react"; const button = { variant: { @@ -39,12 +40,19 @@ interface ButtonProps extends VariantProps { className?: string; onPress?: () => void; text: string; - variant?: "default" | "disabled" | "invisible"; + style?: StyleProp; } -export const Button = ({ className, onPress, text, variant }: ButtonProps) => { +export const Button = ({ + className, + onPress, + text, + variant, + style, +}: ButtonProps) => { return ( diff --git a/client/src/components/widgets/modal-card.tsx b/client/src/components/widgets/modal-card.tsx index 17b4fd2..85b71f2 100644 --- a/client/src/components/widgets/modal-card.tsx +++ b/client/src/components/widgets/modal-card.tsx @@ -1,6 +1,7 @@ -import { View, Text, Pressable } from "react-native"; +import { View, Text, Animated, Easing } from "react-native"; import { BlurView } from "expo-blur"; import { twMerge } from "tailwind-merge"; +import { useEffect, useRef } from "react"; import { Button } from "../ui/button"; interface ModalOption { @@ -16,23 +17,50 @@ export interface ModalCardProps { buttons: [ModalOption] | [ModalOption, ModalOption]; } +const AnimatedBlurView = Animated.createAnimatedComponent(BlurView); + export const ModalCard = ({ className, title, subtitle, buttons, }: ModalCardProps) => { + const blurAnim = useRef(new Animated.Value(0)).current; // for blur intensity + const scaleAnim = useRef(new Animated.Value(0.95)).current; // for modal scale + + useEffect(() => { + Animated.parallel([ + Animated.timing(blurAnim, { + toValue: 80, // final blur intensity + duration: 300, + easing: Easing.out(Easing.ease), + useNativeDriver: false, // must be false for blur intensity + }), + Animated.spring(scaleAnim, { + toValue: 1, + friction: 6, + tension: 80, + useNativeDriver: true, + }), + ]).start(); + }, []); + return ( - - @@ -48,6 +76,7 @@ export const ModalCard = ({ )} + {buttons.map((btn, index) => (