fix: bug
This commit is contained in:
259
app/__tests__/membership.test.tsx
Normal file
259
app/__tests__/membership.test.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, waitFor, fireEvent } from '@testing-library/react-native'
|
||||||
|
import MembershipScreen from '../membership'
|
||||||
|
|
||||||
|
// Mock react-native-safe-area-context FIRST
|
||||||
|
jest.mock('react-native-safe-area-context', () => ({
|
||||||
|
SafeAreaProvider: ({ children }: any) => children,
|
||||||
|
SafeAreaView: ({ children }: any) => children,
|
||||||
|
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock expo-linear-gradient
|
||||||
|
jest.mock('expo-linear-gradient', () => ({
|
||||||
|
LinearGradient: 'LinearGradient',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock expo-image
|
||||||
|
jest.mock('expo-image', () => ({
|
||||||
|
Image: 'Image',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock expo-status-bar
|
||||||
|
jest.mock('expo-status-bar', () => ({
|
||||||
|
StatusBar: 'StatusBar',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock react-native-svg
|
||||||
|
jest.mock('react-native-svg', () => ({
|
||||||
|
Svg: 'Svg',
|
||||||
|
Path: 'Path',
|
||||||
|
Defs: 'Defs',
|
||||||
|
LinearGradient: 'LinearGradient',
|
||||||
|
Stop: 'Stop',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock react-native-reanimated-carousel
|
||||||
|
jest.mock('react-native-reanimated-carousel', () => {
|
||||||
|
const React = require('react')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ data, renderItem, onSnapToItem }: any) => {
|
||||||
|
return React.createElement(
|
||||||
|
'View',
|
||||||
|
{ testID: 'carousel' },
|
||||||
|
data.map((item: any, index: number) =>
|
||||||
|
React.createElement(
|
||||||
|
'View',
|
||||||
|
{ key: index, testID: `carousel-item-${index}` },
|
||||||
|
renderItem({ item, index })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock icons
|
||||||
|
jest.mock('@/components/icon', () => ({
|
||||||
|
CheckIcon: () => 'CheckIcon',
|
||||||
|
LeftArrowIcon: () => 'LeftArrowIcon',
|
||||||
|
OmitIcon: () => 'OmitIcon',
|
||||||
|
UncheckedIcon: () => 'UncheckedIcon',
|
||||||
|
TermsIcon: () => 'TermsIcon',
|
||||||
|
PrivacyIcon: () => 'PrivacyIcon',
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/icon/checkMark', () => ({
|
||||||
|
CheckMarkIcon: () => 'CheckMarkIcon',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock components
|
||||||
|
jest.mock('@/components/drawer/PointsDrawer', () => 'PointsDrawer')
|
||||||
|
jest.mock('@/components/ui/dropdown', () => {
|
||||||
|
const React = require('react')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ renderTrigger }: any) => {
|
||||||
|
return renderTrigger ? renderTrigger(null, false, jest.fn()) : null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jest.mock('@/components/GradientText', () => 'GradientText')
|
||||||
|
|
||||||
|
// Mock expo-router
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useRouter: jest.fn(() => ({
|
||||||
|
push: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock react-i18next
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock hooks - 默认返回空广告数据
|
||||||
|
const mockLoadActivates = jest.fn()
|
||||||
|
const mockActivatesData = {
|
||||||
|
activities: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-activates', () => ({
|
||||||
|
useActivates: jest.fn(() => ({
|
||||||
|
load: mockLoadActivates,
|
||||||
|
data: mockActivatesData,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-membership', () => ({
|
||||||
|
useMembership: jest.fn(() => ({
|
||||||
|
creditPlans: [
|
||||||
|
{
|
||||||
|
name: 'Plus',
|
||||||
|
amountInCents: 999,
|
||||||
|
popular: false,
|
||||||
|
credits: 100,
|
||||||
|
currency: 'USD',
|
||||||
|
featureList: ['feature1', 'feature2'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
creditBalance: 50,
|
||||||
|
selectedPlanIndex: 0,
|
||||||
|
setSelectedPlanIndex: jest.fn(),
|
||||||
|
isLoadingSubscriptions: false,
|
||||||
|
isStripePricingLoading: false,
|
||||||
|
createSubscription: jest.fn(),
|
||||||
|
upgradeSubscription: jest.fn(),
|
||||||
|
restoreSubscription: jest.fn(),
|
||||||
|
rechargeToken: jest.fn(),
|
||||||
|
activeAuthSubscription: null,
|
||||||
|
hasActiveSubscription: false,
|
||||||
|
stripePricingData: null,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MembershipScreen - Advertisement Carousel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('广告轮播显示', () => {
|
||||||
|
it('当广告数据为空时,应该隐藏轮播图', () => {
|
||||||
|
const { queryByTestId } = render(<MembershipScreen />)
|
||||||
|
|
||||||
|
// 轮播图不应该显示
|
||||||
|
expect(queryByTestId('carousel')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('当广告数据存在时,应该显示轮播图', () => {
|
||||||
|
// 设置有广告数据的mock
|
||||||
|
const { useActivates } = require('@/hooks/use-activates')
|
||||||
|
useActivates.mockReturnValue({
|
||||||
|
load: mockLoadActivates,
|
||||||
|
data: {
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test Ad 1',
|
||||||
|
desc: 'Description 1',
|
||||||
|
coverUrl: 'https://example.com/ad1.jpg',
|
||||||
|
link: '/ad1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Test Ad 2',
|
||||||
|
desc: 'Description 2',
|
||||||
|
coverUrl: 'https://example.com/ad2.jpg',
|
||||||
|
link: '/ad2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByTestId } = render(<MembershipScreen />)
|
||||||
|
|
||||||
|
// 轮播图应该显示
|
||||||
|
expect(getByTestId('carousel')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该在组件挂载时加载广告数据', () => {
|
||||||
|
render(<MembershipScreen />)
|
||||||
|
|
||||||
|
// 应该调用 loadActivates
|
||||||
|
expect(mockLoadActivates).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('广告点击跳转', () => {
|
||||||
|
it('点击广告时应该跳转到正确的链接', () => {
|
||||||
|
const mockPush = jest.fn()
|
||||||
|
const { useRouter } = require('expo-router')
|
||||||
|
useRouter.mockReturnValue({
|
||||||
|
push: mockPush,
|
||||||
|
back: jest.fn(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { useActivates } = require('@/hooks/use-activates')
|
||||||
|
useActivates.mockReturnValue({
|
||||||
|
load: mockLoadActivates,
|
||||||
|
data: {
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Test Ad',
|
||||||
|
desc: 'Description',
|
||||||
|
coverUrl: 'https://example.com/ad.jpg',
|
||||||
|
link: '/test-link',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByTestId } = render(<MembershipScreen />)
|
||||||
|
|
||||||
|
// 点击第一个广告
|
||||||
|
const carouselItem = getByTestId('carousel-item-0')
|
||||||
|
fireEvent.press(carouselItem)
|
||||||
|
|
||||||
|
// 应该跳转到广告链接
|
||||||
|
expect(mockPush).toHaveBeenCalledWith('/test-link')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('广告轮播指示点', () => {
|
||||||
|
it('当有多个广告时,应该显示指示点', () => {
|
||||||
|
const { useActivates } = require('@/hooks/use-activates')
|
||||||
|
useActivates.mockReturnValue({
|
||||||
|
load: mockLoadActivates,
|
||||||
|
data: {
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Ad 1',
|
||||||
|
desc: 'Desc 1',
|
||||||
|
coverUrl: 'https://example.com/ad1.jpg',
|
||||||
|
link: '/ad1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Ad 2',
|
||||||
|
desc: 'Desc 2',
|
||||||
|
coverUrl: 'https://example.com/ad2.jpg',
|
||||||
|
link: '/ad2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getAllByTestId } = render(<MembershipScreen />)
|
||||||
|
|
||||||
|
// 应该有2个指示点
|
||||||
|
const dots = getAllByTestId(/^dot-/)
|
||||||
|
expect(dots).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -13,6 +13,7 @@ configureReanimatedLogger({
|
|||||||
strict: false,
|
strict: false,
|
||||||
})
|
})
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||||
|
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
|
||||||
import { View, ActivityIndicator, AppState, LogBox } from 'react-native'
|
import { View, ActivityIndicator, AppState, LogBox } from 'react-native'
|
||||||
import * as Updates from 'expo-updates'
|
import * as Updates from 'expo-updates'
|
||||||
|
|
||||||
@@ -168,18 +169,20 @@ function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<KeyboardProvider>
|
<BottomSheetModalProvider>
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<KeyboardProvider>
|
||||||
{children}
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
{/* modals */}
|
{children}
|
||||||
|
{/* modals */}
|
||||||
|
|
||||||
{/* 挂载全局方法 */}
|
{/* 挂载全局方法 */}
|
||||||
<ModalPortal ref={(ref) => ((global as any).actionSheet = ref)} />
|
<ModalPortal ref={(ref) => ((global as any).actionSheet = ref)} />
|
||||||
<ModalPortal ref={(ref) => ((global as any).modal = ref)} />
|
<ModalPortal ref={(ref) => ((global as any).modal = ref)} />
|
||||||
<ModalPortal ref={(ref) => ((global as any).loading = ref)} />
|
<ModalPortal ref={(ref) => ((global as any).loading = ref)} />
|
||||||
<ModalPortal ref={(ref) => ((global as any).toast = ref)} />
|
<ModalPortal ref={(ref) => ((global as any).toast = ref)} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</KeyboardProvider>
|
</KeyboardProvider>
|
||||||
|
</BottomSheetModalProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import PointsDrawer from '@/components/drawer/PointsDrawer'
|
|||||||
import Dropdown from '@/components/ui/dropdown'
|
import Dropdown from '@/components/ui/dropdown'
|
||||||
import GradientText from '@/components/GradientText'
|
import GradientText from '@/components/GradientText'
|
||||||
import { useMembership } from '@/hooks/use-membership'
|
import { useMembership } from '@/hooks/use-membership'
|
||||||
|
import { useActivates } from '@/hooks/use-activates'
|
||||||
|
|
||||||
// 使用唯一 id 的 PointsIcon,避免与其他页面的图标 id 冲突
|
// 使用唯一 id 的 PointsIcon,避免与其他页面的图标 id 冲突
|
||||||
const MembershipPointsIcon = () => {
|
const MembershipPointsIcon = () => {
|
||||||
@@ -102,6 +103,9 @@ export default function MembershipScreen() {
|
|||||||
stripePricingData,
|
stripePricingData,
|
||||||
} = useMembership()
|
} = useMembership()
|
||||||
|
|
||||||
|
// 使用 useActivates hook 获取广告数据
|
||||||
|
const { load: loadActivates, data: activatesData } = useActivates()
|
||||||
|
|
||||||
|
|
||||||
// 映射 API 数据到 UI 格式
|
// 映射 API 数据到 UI 格式
|
||||||
const plans: Plan[] = creditPlans.map((plan, index) => ({
|
const plans: Plan[] = creditPlans.map((plan, index) => ({
|
||||||
@@ -131,14 +135,15 @@ export default function MembershipScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 轮播图相关
|
// 轮播图相关 - 使用动态广告数据
|
||||||
const carouselImages = [
|
const activities = activatesData?.activities || []
|
||||||
require('@/assets/images/membership.png'),
|
|
||||||
require('@/assets/images/icon.png'),
|
|
||||||
require('@/assets/images/generate.png'),
|
|
||||||
]
|
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
|
// 加载广告数据
|
||||||
|
useEffect(() => {
|
||||||
|
loadActivates()
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 处理订阅按钮点击
|
// 处理订阅按钮点击
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
if (!agreed || !selectedPlan) return
|
if (!agreed || !selectedPlan) return
|
||||||
@@ -243,43 +248,55 @@ export default function MembershipScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
<Carousel
|
{/* 只在有广告数据时显示轮播图 */}
|
||||||
width={screenWidth}
|
{activities.length > 0 && (
|
||||||
height={screenWidth / 1.1}
|
<>
|
||||||
data={carouselImages}
|
<Carousel
|
||||||
renderItem={({ item }) => (
|
testID="carousel"
|
||||||
<Image
|
width={screenWidth}
|
||||||
source={item}
|
height={screenWidth / 1.1}
|
||||||
style={[styles.membershipImage, { width: screenWidth, height: screenWidth / 1.1 }]}
|
data={activities}
|
||||||
contentFit="cover"
|
renderItem={({ item }) => (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push(item.link as any)}
|
||||||
|
style={{ width: screenWidth, height: screenWidth / 1.1 }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.coverUrl }}
|
||||||
|
style={[styles.membershipImage, { width: screenWidth, height: screenWidth / 1.1 }]}
|
||||||
|
contentFit="cover"
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
autoPlay
|
||||||
|
autoPlayInterval={2000}
|
||||||
|
loop
|
||||||
|
onSnapToItem={(index) => setCurrentImageIndex(index)}
|
||||||
|
enabled={false}
|
||||||
|
windowSize={1}
|
||||||
|
mode="parallax"
|
||||||
/>
|
/>
|
||||||
)}
|
<View style={styles.dotsContainer}>
|
||||||
autoPlay
|
{activities.map((_, index) => (
|
||||||
autoPlayInterval={2000}
|
<View
|
||||||
loop
|
key={index}
|
||||||
onSnapToItem={(index) => setCurrentImageIndex(index)}
|
testID={`dot-${index}`}
|
||||||
enabled={false}
|
style={[
|
||||||
windowSize={1}
|
styles.dot,
|
||||||
mode="parallax"
|
index === currentImageIndex && styles.dotActive,
|
||||||
/>
|
]}
|
||||||
<View style={styles.dotsContainer}>
|
/>
|
||||||
{carouselImages.map((_, index) => (
|
))}
|
||||||
<View
|
</View>
|
||||||
key={index}
|
<LinearGradient
|
||||||
style={[
|
colors={['#090A0B00', '#090A0B']}
|
||||||
styles.dot,
|
start={{ x: 0, y: 0 }}
|
||||||
index === currentImageIndex && styles.dotActive,
|
end={{ x: 0, y: 1 }}
|
||||||
]}
|
style={styles.imageGradient}
|
||||||
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
))}
|
</>
|
||||||
</View>
|
)}
|
||||||
<LinearGradient
|
|
||||||
colors={['#090A0B00', '#090A0B']}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 0, y: 1 }}
|
|
||||||
style={styles.imageGradient}
|
|
||||||
pointerEvents="none"
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 订阅计划标题 */}
|
{/* 订阅计划标题 */}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { Image } from 'expo-image'
|
import { Image } from 'expo-image'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import BottomSheet, { BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'
|
import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'
|
||||||
import { CloseIcon, AvatarUploadIcon } from '@/components/icon'
|
import { CloseIcon, AvatarUploadIcon } from '@/components/icon'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import { type ImagePickerAsset } from 'expo-image-picker'
|
import { type ImagePickerAsset } from 'expo-image-picker'
|
||||||
@@ -37,7 +37,7 @@ export default function EditProfileDrawer({
|
|||||||
onSave,
|
onSave,
|
||||||
}: EditProfileDrawerProps) {
|
}: EditProfileDrawerProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const bottomSheetRef = useRef<BottomSheet>(null)
|
const bottomSheetRef = useRef<BottomSheetModal>(null)
|
||||||
const [name, setName] = useState(initialName)
|
const [name, setName] = useState(initialName)
|
||||||
const [avatar, setAvatar] = useState<string | undefined>(initialAvatar)
|
const [avatar, setAvatar] = useState<string | undefined>(initialAvatar)
|
||||||
const [selectedImage, setSelectedImage] = useState<ImagePickerAsset | null>(null)
|
const [selectedImage, setSelectedImage] = useState<ImagePickerAsset | null>(null)
|
||||||
@@ -45,17 +45,18 @@ export default function EditProfileDrawer({
|
|||||||
|
|
||||||
const { loading, pickImage, updateProfile } = useUpdateProfile()
|
const { loading, pickImage, updateProfile } = useUpdateProfile()
|
||||||
|
|
||||||
const snapPoints = useMemo(() => [280], [])
|
// 增加高度以避免被底部 Tab 栏遮挡,考虑底部安全区域
|
||||||
|
const snapPoints = useMemo(() => [280 + insets.bottom + 60], [insets.bottom])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
bottomSheetRef.current?.expand()
|
bottomSheetRef.current?.present()
|
||||||
// 重置为初始值
|
// 重置为初始值
|
||||||
setName(initialName)
|
setName(initialName)
|
||||||
setAvatar(initialAvatar)
|
setAvatar(initialAvatar)
|
||||||
setSelectedImage(null)
|
setSelectedImage(null)
|
||||||
} else {
|
} else {
|
||||||
bottomSheetRef.current?.close()
|
bottomSheetRef.current?.dismiss()
|
||||||
}
|
}
|
||||||
}, [visible, initialName, initialAvatar])
|
}, [visible, initialName, initialAvatar])
|
||||||
|
|
||||||
@@ -118,9 +119,8 @@ export default function EditProfileDrawer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomSheet
|
<BottomSheetModal
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
index={visible ? 0 : -1}
|
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
onChange={handleSheetChanges}
|
onChange={handleSheetChanges}
|
||||||
enablePanDownToClose={!loading}
|
enablePanDownToClose={!loading}
|
||||||
@@ -205,7 +205,7 @@ export default function EditProfileDrawer({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheet>
|
</BottomSheetModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user