Browse Source

FEATURE | Add purchases

pull/3/head
Vlad Narizhnyi 11 months ago
parent
commit
0ff31c39ce
  1. 2
      src/i18n/interfaces/index.ts
  2. 2
      src/i18n/interfaces/page-titles.interface.ts
  3. 6
      src/i18n/interfaces/purchases.interface.ts
  4. 2
      src/i18n/locales/en/index.ts
  5. 2
      src/i18n/locales/en/page-title.translation.ts
  6. 9
      src/i18n/locales/en/purchases.translation.ts
  7. 2
      src/i18n/locales/ua/index.ts
  8. 2
      src/i18n/locales/ua/page-title.translation.ts
  9. 11
      src/i18n/locales/ua/purchases.translation.ts
  10. 11
      src/module/common/components/header/header.component.tsx
  11. 2
      src/module/common/typing/enums/storage-key.enum.ts
  12. 3
      src/module/common/widgets/alert/alert-widget.component.tsx
  13. 17
      src/module/game/screens/game.screen.tsx
  14. 1
      src/module/game/screens/questions.screen.tsx
  15. 9
      src/module/packages/screens/packages-list.screen.tsx
  16. 5
      src/module/root/screens/on-boarding.screen.tsx
  17. 22
      src/module/settings/atoms/purchases.atom.tsx
  18. 11
      src/module/settings/screens/privacy-policy.tsx
  19. 101
      src/module/settings/screens/purchases.screen.tsx
  20. 8
      src/module/settings/screens/settings.screen.tsx
  21. 8
      src/module/settings/screens/write-to-us.screen.tsx
  22. 66
      src/module/settings/services/purchases.service.ts

2
src/i18n/interfaces/index.ts

@ -3,6 +3,7 @@ import { Common } from './common.interface' @@ -3,6 +3,7 @@ import { Common } from './common.interface'
import { CustomPack } from './custom-pack.interface'
import { OnBoardingLocale } from './on-boarding.types.interface'
import { PageTitles } from './page-titles.interface'
import { PurchasesTranslate } from './purchases.interface'
import { SettingLocale } from './settings.types.interface'
export interface MainLocaleModule {
@ -12,4 +13,5 @@ export interface MainLocaleModule { @@ -12,4 +13,5 @@ export interface MainLocaleModule {
customPack: CustomPack
pageTitles: PageTitles
common: Common
purchases: PurchasesTranslate
}

2
src/i18n/interfaces/page-titles.interface.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
export interface PageTitles {
setting: string,
settings: string,
privacy: string,
terms: string,
}

6
src/i18n/interfaces/purchases.interface.ts

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
export interface PurchasesTranslate {
alertSuccess: string
descSuccess: string
alertError: string
descError: string
}

2
src/i18n/locales/en/index.ts

@ -5,6 +5,7 @@ import { buttonsTranslation } from './onBoardingButton.translation' @@ -5,6 +5,7 @@ import { buttonsTranslation } from './onBoardingButton.translation'
import { customPack } from './custom-pack.translation'
import { pageTitles } from './page-title.translation'
import { common } from './common.translation'
import { purchases } from './purchases.translation'
export const en: MainLocaleModule = {
settingTranslation,
@ -12,5 +13,6 @@ export const en: MainLocaleModule = { @@ -12,5 +13,6 @@ export const en: MainLocaleModule = {
buttonsTranslation,
customPack,
pageTitles,
purchases,
common,
}

2
src/i18n/locales/en/page-title.translation.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
export const pageTitles = {
setting: 'Setting',
settings: 'Settings',
purchases: 'Purchases',
privacy: 'Privacy Policy',
terms: 'Terms and conditions',

9
src/i18n/locales/en/purchases.translation.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import { PurchasesTranslate } from '~i18n/interfaces/purchases.interface'
export const purchases: PurchasesTranslate = {
alertSuccess: 'Ready!',
descSuccess: 'Now play with your friends! Enjoy the game 😊',
alertError: 'Ops, purchase failed 😢',
descError:
'There was an error processing your purchase. Please try again later.',
}

2
src/i18n/locales/ua/index.ts

@ -5,6 +5,7 @@ import { buttonsTranslation } from './onBoardingButton.translation' @@ -5,6 +5,7 @@ import { buttonsTranslation } from './onBoardingButton.translation'
import { customPack } from './custom-pack.translation'
import { pageTitles } from './page-title.translation'
import { common } from './common.translation'
import { purchases } from './purchases.translation'
export const ua: MainLocaleModule = {
stepTranslation: onBoardingTranslationUa,
@ -12,5 +13,6 @@ export const ua: MainLocaleModule = { @@ -12,5 +13,6 @@ export const ua: MainLocaleModule = {
buttonsTranslation,
customPack,
pageTitles,
purchases,
common,
}

2
src/i18n/locales/ua/page-title.translation.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
export const pageTitles = {
purchases: 'Покупки',
setting: 'Налаштування',
settings: 'Налаштування',
privacy: 'Політика \n конфіденційності',
terms: 'Правила та умови',
writeToUs: 'Напишіть нам',

11
src/i18n/locales/ua/purchases.translation.ts

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
import { PurchasesTranslate } from "~i18n/interfaces/purchases.interface";
export const purchases: PurchasesTranslate = {
alertSuccess: 'Ура! Готово!',
descSuccess: 'Тепер зіграйте з друзями! Насолоджуйтеся грою 😊',
alertError: 'Упс, щось не так 😢',
descError:
'Виникла помилка обробки вашої покупки. Будь ласка, спробуйте пізніше.',
}

11
src/module/common/components/header/header.component.tsx

@ -5,6 +5,7 @@ import { Icon } from '../icon' @@ -5,6 +5,7 @@ import { Icon } from '../icon'
import { $size } from '../../helpers'
import { Txt } from '../txt'
import { Font } from '../../typing'
import { useNav } from '~module/common/hooks'
interface IProps {
onPressLeft?: () => any
@ -16,19 +17,25 @@ interface IProps { @@ -16,19 +17,25 @@ interface IProps {
}
export const Header: FC<IProps> = ({
onPressLeft,
leftIcon,
leftIcon = 'arrow',
rightIcon,
title,
gamer,
onPressRight,
}) => {
const nav = useNav()
const goBack = () => {
nav.goBack()
}
return (
<View style={styles.header}>
<View style={styles.button}>
{leftIcon && (
<TouchableOpacity
style={styles.button}
onPress={onPressLeft}>
onPress={onPressLeft || goBack}>
<Icon
name={leftIcon}
size={$size(24)}

2
src/module/common/typing/enums/storage-key.enum.ts

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
export enum StorageKey {
OnBoarding = 'ONBOARDING_END',
Language = 'LANG_SELECTED',
Purchases = 'Purchases',
Products = 'Products',
}

3
src/module/common/widgets/alert/alert-widget.component.tsx

@ -65,10 +65,11 @@ export const AlertWidget = () => { @@ -65,10 +65,11 @@ export const AlertWidget = () => {
return (
<ModalComponent
coverScreen={false}
backdropOpacity={0.8}
useNativeDriverForBackdrop={true}
backdropTransitionOutTiming={400}
hideModalContentWhileAnimating={true}
backdropColor={'#787878'}
backdropColor={'black'}
animationIn="pulse"
isVisible={Boolean(isVisible)}
onBackdropPress={close}

17
src/module/game/screens/game.screen.tsx

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { StyleSheet, TouchableOpacity, View } from 'react-native'
import {
$size,
ButtonPrimary,
ButtonWithIcon,
colors,
Header,
RouteKey,
@ -21,10 +20,6 @@ export const GameScreen: FC = () => { @@ -21,10 +20,6 @@ export const GameScreen: FC = () => {
const { params }: any = useRoute()
const packageName = params.packageName
const goBack = () => {
nav.navigate(RouteKey.Packages)
}
const randomGame = () => {
const isQuestions = Math.random() < 0.5
nav.navigate(RouteKey.Questions, { isQuestions, packageName })
@ -39,15 +34,7 @@ export const GameScreen: FC = () => { @@ -39,15 +34,7 @@ export const GameScreen: FC = () => {
}
return (
<ScreenLayout
headerComponent={
<Header
leftIcon="arrow"
onPressLeft={() => goBack()}
title={packageName}
gamer
/>
}>
<ScreenLayout headerComponent={<Header title={packageName} gamer />}>
<Txt mod="xxl" style={styles.playerName}>
Player
</Txt>

1
src/module/game/screens/questions.screen.tsx

@ -40,7 +40,6 @@ export const QuestionsScreen: React.FC = () => { @@ -40,7 +40,6 @@ export const QuestionsScreen: React.FC = () => {
<ScreenLayout
headerComponent={
<Header
leftIcon="arrow"
gamer
onPressLeft={() => goBack()}
title={packageName}

9
src/module/packages/screens/packages-list.screen.tsx

@ -9,7 +9,6 @@ import { CustomPackage, PackagesPageSeparator } from '../atoms' @@ -9,7 +9,6 @@ import { CustomPackage, PackagesPageSeparator } from '../atoms'
export const PackagesListScreen: FC = () => {
const nav = useNav()
const { i18n } = useTranslation()
const lang = i18n.language
return (
<ScreenLayout
@ -23,8 +22,8 @@ export const PackagesListScreen: FC = () => { @@ -23,8 +22,8 @@ export const PackagesListScreen: FC = () => {
<View style={styles.container}>
{packageListConfig.map((item: any, index) => (
<PackageItem
packageName={item.title[lang]}
description={item.description[lang]}
packageName={item.title[i18n.language]}
description={item.description[i18n.language]}
questions={item.questions}
actions={item.actions}
image={item.image}
@ -39,5 +38,7 @@ export const PackagesListScreen: FC = () => { @@ -39,5 +38,7 @@ export const PackagesListScreen: FC = () => {
}
const styles = StyleSheet.create({
container: {},
container: {
marginBottom: 30,
},
})

5
src/module/root/screens/on-boarding.screen.tsx

@ -38,10 +38,7 @@ export const OnboardingScreen: FC = () => { @@ -38,10 +38,7 @@ export const OnboardingScreen: FC = () => {
const isLastBlock = onBoardingConfig.length - 1 === currentIndex
return (
<ScreenLayout
headerComponent={
<Header leftIcon="arrow" onPressLeft={() => goBack()} />
}>
<ScreenLayout headerComponent={<Header />}>
<View style={styles.container}>
<View style={{ alignItems: 'center' }}>
<Picture />

22
src/module/settings/atoms/purchases.atom.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { StyleSheet, View } from 'react-native'
import { $size, Icon, Txt, colors } from '../../common'
import { $size, ButtonWithIcon, Icon, Txt, colors } from '../../common'
import { TouchableOpacity } from 'react-native-gesture-handler'
interface IProps {
@ -8,6 +8,7 @@ interface IProps { @@ -8,6 +8,7 @@ interface IProps {
price: string
iconName: string
hasDiscount: boolean
isPurchased: boolean
onPress: () => void
}
@ -16,6 +17,7 @@ export const PurchaseAtom: FC<IProps> = ({ @@ -16,6 +17,7 @@ export const PurchaseAtom: FC<IProps> = ({
price,
hasDiscount,
iconName,
isPurchased,
onPress,
}) => {
const renderDiscountAtom = () => {
@ -35,9 +37,17 @@ export const PurchaseAtom: FC<IProps> = ({ @@ -35,9 +37,17 @@ export const PurchaseAtom: FC<IProps> = ({
</Txt>
{hasDiscount && renderDiscountAtom()}
</View>
<Txt mod="lg" style={styles.price}>
{price + ' $'}
</Txt>
{isPurchased ? (
<ButtonWithIcon
styleBtn={styles.iconPlay}
iconName="play"
onPress={() => null}
/>
) : (
<Txt mod="lg" style={styles.price}>
{price + ' $'}
</Txt>
)}
</TouchableOpacity>
)
}
@ -71,4 +81,8 @@ const styles = StyleSheet.create({ @@ -71,4 +81,8 @@ const styles = StyleSheet.create({
lineHeight: $size(28),
fontWeight: '900',
},
iconPlay: {
width: $size(60),
height: '100%'
},
})

11
src/module/settings/screens/privacy-policy.tsx

@ -1,22 +1,15 @@ @@ -1,22 +1,15 @@
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import { $size, colors, Header, ScreenLayout, Txt, useNav } from '../../common'
import { $size, colors, Header, ScreenLayout, Txt } from '../../common'
import { privacyConfig } from '../config'
export const PrivacyPolicyScreen: FC = () => {
const { t, i18n } = useTranslation()
const nav = useNav()
return (
<ScreenLayout
headerComponent={
<Header
leftIcon="arrow"
title={t('pageTitles.privacy')}
onPressLeft={() => nav.goBack()}
/>
}>
headerComponent={<Header title={t('pageTitles.privacy')} />}>
<View style={styles.container}>
<Txt mod="xl" style={styles.description}>
{privacyConfig[i18n.language]}

101
src/module/settings/screens/purchases.screen.tsx

@ -1,17 +1,21 @@ @@ -1,17 +1,21 @@
import React, { FC, useEffect, useState } from 'react'
import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ActivityIndicator,
Alert,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {
$size,
appEvents,
colors,
Font,
Header,
Icon,
ModalComponent,
ProductsEnum,
RouteKey,
ScreenLayout,
Txt,
useNav,
@ -19,7 +23,6 @@ import { @@ -19,7 +23,6 @@ import {
import { purchasesService } from '../services'
import { PurchaseAtom } from '../atoms'
import { useIAP } from 'react-native-iap'
export const PurchasesScreen: FC = () => {
const { t } = useTranslation()
@ -30,38 +33,39 @@ export const PurchasesScreen: FC = () => { @@ -30,38 +33,39 @@ export const PurchasesScreen: FC = () => {
try {
setLoading(true)
await purchasesService.purchaseProduct(productId)
appEvents.emit('alert', {
title: t('purchases.alertSuccess'),
subtitle: t('purchases.descSuccess'),
})
} catch (error) {
appEvents.emit('alert', {
title: t('purchases.alertError'),
subtitle: t('purchases.descError'),
})
} finally {
setLoading(false)
}
}
const handlePurchaseSuccess = (purchaseResult: string) => {
console.log('Purchase successful:', purchaseResult)
Alert.alert('Purchase Successful', 'Thank you for your purchase!')
}
const handlePurchaseError = (purchaseError: any) => {
console.error('Purchase failed:', purchaseError)
Alert.alert(
'Purchase Failed',
'There was an error processing your purchase.',
)
}
if (isLoading) {
return <ActivityIndicator size={50} color={'red'} />
}
return (
<ScreenLayout
headerComponent={
<Header
leftIcon="arrow"
title={t('pageTitles.purchases')}
onPressLeft={() => nav.goBack()}
/>
}>
headerComponent={<Header title={t('pageTitles.purchases')} />}>
{isLoading && (
<ModalComponent
onClose={() => null}
isVisible={isLoading}
style={styles.modal}>
<View style={styles.body}>
<Txt
mod="xl"
font={Font.Roboto700}
style={{ marginBottom: 10 }}>
Loading...
</Txt>
<ActivityIndicator color={colors.textPrimary} />
</View>
</ModalComponent>
)}
<>
{purchasesService.products.map(it => {
return (
@ -71,21 +75,22 @@ export const PurchasesScreen: FC = () => { @@ -71,21 +75,22 @@ export const PurchasesScreen: FC = () => {
price={it.price}
hasDiscount={it.productId === ProductsEnum.All}
iconName={it.icon}
onPress={() => purchaseProduct(it.productId)}
isPurchased={it.isPurchased}
onPress={() =>
it.isPurchased
? nav.navigate(RouteKey.Packages)
: purchaseProduct(it.productId)
}
/>
)
})}
<TouchableOpacity style={styles.row}>
<Icon
name="restore"
size={$size(24)}
color={colors.purple}
/>
<Txt mod="lg" color={colors.purple}>
Restore purchases
</Txt>
</TouchableOpacity>
</>
<TouchableOpacity style={styles.row}>
<Icon name="restore" size={$size(24)} color={colors.purple} />
<Txt mod="lg" color={colors.purple}>
Restore purchases
</Txt>
</TouchableOpacity>
</ScreenLayout>
)
}
@ -105,4 +110,24 @@ const styles = StyleSheet.create({ @@ -105,4 +110,24 @@ const styles = StyleSheet.create({
alignItems: 'center',
columnGap: 8,
},
modal: {
position: 'absolute',
top: 0,
bottom: $size(100),
left: 0,
right: 0,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
body: {
width: '90%',
backgroundColor: colors.primaryColor,
height: $size(100),
justifyContent: 'center',
alignItems: 'center',
borderColor: colors.lightPurple,
borderRadius: 12,
borderWidth: 1,
},
})

8
src/module/settings/screens/settings.screen.tsx

@ -80,13 +80,7 @@ export const SettingsScreen: FC = () => { @@ -80,13 +80,7 @@ export const SettingsScreen: FC = () => {
return (
<ScreenLayout
headerComponent={
<Header
leftIcon="arrow"
title={t('pageTitles.setting')}
onPressLeft={() => nav.goBack()}
/>
}>
headerComponent={<Header title={t('pageTitles.settings')} />}>
<>
{settingsConfig.map((item, index) => (
<SettingsItem

8
src/module/settings/screens/write-to-us.screen.tsx

@ -34,13 +34,7 @@ export const WriteToUsScreen: FC = () => { @@ -34,13 +34,7 @@ export const WriteToUsScreen: FC = () => {
return (
<ScreenLayout
bottomSafeArea
headerComponent={
<Header
leftIcon="arrow"
title={t('pageTitles.writeToUs')}
onPressLeft={() => nav.goBack()}
/>
}>
headerComponent={<Header title={t('pageTitles.writeToUs')} />}>
<FormTextControll
label={t('settingTranslation.label')}
value={form.values.message}

66
src/module/settings/services/purchases.service.ts

@ -4,27 +4,36 @@ import { @@ -4,27 +4,36 @@ import {
Product,
requestPurchase,
purchaseUpdatedListener,
getPurchaseHistory,
finishTransaction,
} from 'react-native-iap'
import { ProductsEnum } from '../../common'
import { ProductsEnum, StorageKey, appEvents } from '../../common'
import { purchasesConfig } from '../config'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Alert } from 'react-native'
const ID_PRODUCTS = ['ALL', 'Crz', 'un18']
const ID_PRODUCTS = [ProductsEnum.All, ProductsEnum.Crazy, ProductsEnum.Under18]
interface ProductItem {
productId: ProductsEnum
price: string
name: string
icon: string
isPurchased: boolean
}
export class PurchasesService {
public products: ProductItem[] = []
public purchasedProducts: ProductsEnum[]
public init() {
this.initializeIAP()
this.loadProducts()
this.getPurchasedProducts()
}
public async getProducts() {
const res = await AsyncStorage.getItem(StorageKey.Products)
return JSON.parse(res)
}
private async initializeIAP() {
@ -36,49 +45,76 @@ export class PurchasesService { @@ -36,49 +45,76 @@ export class PurchasesService {
}
}
private async loadProducts() {
public async loadProducts() {
try {
const products: Product[] = await getProducts({ skus: ID_PRODUCTS })
this.products = this.transformProductsData(products)
const purchaseHistory = await getPurchaseHistory()
const productss = this.transformProductsData(products)
console.log('products', this.products)
console.log('purchaseHistory', purchaseHistory)
this.products = productss
await this.saveProductInStore(productss)
} catch (error) {
console.error('Error loading products:', error)
throw error
}
}
private async saveProductInStore(products: ProductItem[]) {
await AsyncStorage.setItem(
StorageKey.Products,
JSON.stringify(products),
)
}
public async purchaseProduct(productId: ProductsEnum) {
try {
const purchase = await requestPurchase({ sku: productId })
await requestPurchase({
sku: productId,
})
this.purchaseListener()
console.log('Purchase successful:', purchase)
await this.savePurchase(productId)
await purchasesService.loadProducts()
} catch (error) {
console.error('Purchase error:', error)
throw error
}
}
public async savePurchase(productId: ProductsEnum) {
const newProductsId = [...this.purchasedProducts, productId]
const newProducts = JSON.stringify(newProductsId)
this.purchasedProducts = newProductsId
await AsyncStorage.setItem(StorageKey.Purchases, newProducts)
}
protected async getPurchasedProducts() {
const response = await AsyncStorage.getItem(StorageKey.Purchases)
this.purchasedProducts = response ? JSON.parse(response) : []
}
private purchaseListener() {
purchaseUpdatedListener(purchase => {
finishTransaction({ purchase })
console.log('purchaseListener', purchase)
})
}
private transformProductsData = (products: Product[]) => {
console.log('transformProductsData', this.purchasedProducts)
return products
.map(product => {
const isPurchased = this.purchasedProducts.some(
it => product.productId === it,
)
return {
...purchasesConfig[product.productId],
productId: product.productId,
price: product.price,
isPurchased,
}
})
.sort()
.sort((a, b) => a.productId.localeCompare(b.productId))
}
}

Loading…
Cancel
Save