Browse Source
Co-authored-by: Vlad <vlad960706@gmail.com> Reviewed-on: #3 Co-authored-by: Vlad Narizhnyi <vlad960706@gmail.com> Co-committed-by: Vlad Narizhnyi <vlad960706@gmail.com>pull/4/head
93 changed files with 28304 additions and 26295 deletions
@ -1,3 +1,27 @@ |
|||||||
module.exports = { |
module.exports = { |
||||||
presets: ['module:metro-react-native-babel-preset'], |
presets: ['module:metro-react-native-babel-preset'], |
||||||
}; |
plugins: [ |
||||||
|
[ |
||||||
|
require.resolve('babel-plugin-module-resolver'), |
||||||
|
{ |
||||||
|
root: ['./'], |
||||||
|
alias: { |
||||||
|
/** |
||||||
|
* Regular expression is used to match all files inside `./src` directory and map each `.src/folder/[..]` to `~folder/[..]` path |
||||||
|
*/ |
||||||
|
'^~(.+)': './src/\\1', |
||||||
|
}, |
||||||
|
extensions: [ |
||||||
|
'.ios.js', |
||||||
|
'.android.js', |
||||||
|
'.js', |
||||||
|
'.jsx', |
||||||
|
'.json', |
||||||
|
'.tsx', |
||||||
|
'.ts', |
||||||
|
'.native.js', |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
], |
||||||
|
} |
||||||
|
@ -1,33 +1,10 @@ |
|||||||
/** |
module.exports = { |
||||||
* Metro configuration for React Native |
transformer: { |
||||||
* https://github.com/facebook/react-native
|
getTransformOptions: async () => ({ |
||||||
* |
transform: { |
||||||
* @format |
experimentalImportSupport: false, |
||||||
*/ |
inlineRequires: true, |
||||||
|
}, |
||||||
//module.exports = {
|
}), |
||||||
// transformer: {
|
}, |
||||||
// getTransformOptions: async () => ({
|
} |
||||||
// transform: {
|
|
||||||
// experimentalImportSupport: false,
|
|
||||||
// inlineRequires: true,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
//};
|
|
||||||
const { getDefaultConfig } = require('metro-config'); |
|
||||||
|
|
||||||
module.exports = (async() => { |
|
||||||
const { |
|
||||||
resolver: { sourceExts, assetExts }, |
|
||||||
} = await getDefaultConfig(); |
|
||||||
return { |
|
||||||
transformer: { |
|
||||||
babelTransformerPath: require.resolve('react-native-svg-transformer'), |
|
||||||
}, |
|
||||||
resolver: { |
|
||||||
assetExts: assetExts.filter(ext => ext !== 'svg'), |
|
||||||
sourceExts: [...sourceExts, 'svg'], |
|
||||||
}, |
|
||||||
}; |
|
||||||
})(); |
|
||||||
|
@ -1,84 +1,89 @@ |
|||||||
{ |
{ |
||||||
"rnpm": { |
"rnpm": { |
||||||
"assets": [ |
"assets": [ |
||||||
"./resources/fonts/" |
"./resources/fonts/" |
||||||
] |
] |
||||||
}, |
}, |
||||||
"name": "truth", |
"name": "truth", |
||||||
"version": "0.0.1", |
"version": "0.0.1", |
||||||
"private": true, |
"private": true, |
||||||
"scripts": { |
"scripts": { |
||||||
"android": "react-native run-android", |
"android": "react-native run-android", |
||||||
"ios": "react-native run-ios", |
"ios": "react-native run-ios", |
||||||
"start": "react-native start", |
"start": "react-native start", |
||||||
"test": "jest", |
"test": "jest", |
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx" |
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", |
||||||
}, |
"pod": "cd ./ios && pod install && cd ../" |
||||||
"dependencies": { |
}, |
||||||
"@react-native-async-storage/async-storage": "^1.17.11", |
"dependencies": { |
||||||
"@react-native-firebase/app": "^17.0.0", |
"@react-native-async-storage/async-storage": "^1.17.11", |
||||||
"@react-native-firebase/firestore": "^17.0.0", |
"@react-native-firebase/app": "^17.0.0", |
||||||
"@react-navigation/native": "^6.0.11", |
"@react-native-firebase/firestore": "^17.0.0", |
||||||
"@react-navigation/native-stack": "^6.7.0", |
"@react-navigation/native": "^6.0.11", |
||||||
"@reduxjs/toolkit": "^1.9.2", |
"@react-navigation/native-stack": "^6.7.0", |
||||||
"i18next": "^21.8.14", |
"@reduxjs/toolkit": "^1.9.2", |
||||||
"i18next-react-native-async-storage": "^1.0.4", |
"@shopify/flash-list": "^1.6.3", |
||||||
"jet-tools": "^1.3.0", |
"babel-plugin-module-resolver": "^5.0.0", |
||||||
"link": "^1.5.1", |
"i18next": "^21.8.14", |
||||||
"lodash": "^4.17.21", |
"i18next-react-native-async-storage": "^1.0.4", |
||||||
"react": "18.2.0", |
"jet-tools": "^1.3.0", |
||||||
"react-i18next": "^11.18.1", |
"link": "^1.5.1", |
||||||
"react-native": "0.72.7", |
"lodash": "^4.17.21", |
||||||
"react-native-actions-sheet": "*", |
"react": "18.2.0", |
||||||
"react-native-animated-loader": "^1.0.0", |
"react-i18next": "^11.18.1", |
||||||
"react-native-gesture-handler": "^2.5.0", |
"react-native": "0.72.7", |
||||||
"react-native-icomoon": "^0.1.1", |
"react-native-actions-sheet": "*", |
||||||
"react-native-keyboard-aware-scroll-view": "^0.9.5", |
"react-native-animated-loader": "^1.0.0", |
||||||
"react-native-modal": "^13.0.1", |
"react-native-gesture-handler": "^2.5.0", |
||||||
"react-native-safe-area-context": "^4.3.1", |
"react-native-iap": "^12.11.0", |
||||||
"react-native-screens": "^3.27.0", |
"react-native-icomoon": "^0.1.1", |
||||||
"react-native-splash-screen": "^3.3.0", |
"react-native-keyboard-aware-scroll-view": "^0.9.5", |
||||||
"react-native-svg": "^12.5.1", |
"react-native-modal": "^13.0.1", |
||||||
"react-native-svg-transformer": "^1.0.0", |
"react-native-safe-area-context": "^4.3.1", |
||||||
"react-native-vector-icons": "^9.2.0", |
"react-native-screens": "^3.27.0", |
||||||
"react-redux": "^8.0.5" |
"react-native-splash-screen": "^3.3.0", |
||||||
}, |
"react-native-svg": "^12.5.1", |
||||||
"devDependencies": { |
"react-native-svg-transformer": "^1.0.0", |
||||||
"@babel/core": "^7.20.0", |
"react-native-vector-icons": "^9.2.0", |
||||||
"@babel/preset-env": "^7.20.0", |
"react-redux": "^8.0.5", |
||||||
"@babel/runtime": "^7.20.0", |
"validate.js": "^0.13.1" |
||||||
"@react-native/eslint-config": "^0.72.2", |
}, |
||||||
"@react-native/metro-config": "^0.72.11", |
"devDependencies": { |
||||||
"@tsconfig/react-native": "^3.0.0", |
"@babel/core": "^7.20.0", |
||||||
"@types/lodash": "^4.14.201", |
"@babel/preset-env": "^7.20.0", |
||||||
"@types/react": "^18.0.24", |
"@babel/runtime": "^7.20.0", |
||||||
"@types/react-native": "^0.66.15", |
"@react-native/eslint-config": "^0.72.2", |
||||||
"@types/react-native-vector-icons": "^6.4.12", |
"@react-native/metro-config": "^0.72.11", |
||||||
"@types/react-redux": "^7.1.25", |
"@tsconfig/react-native": "^3.0.0", |
||||||
"@types/react-test-renderer": "^18.0.0", |
"@types/lodash": "^4.14.201", |
||||||
"@typescript-eslint/eslint-plugin": "^5.7.0", |
"@types/react": "^18.0.24", |
||||||
"@typescript-eslint/parser": "^5.7.0", |
"@types/react-native": "^0.66.15", |
||||||
"babel-jest": "^29.2.1", |
"@types/react-native-vector-icons": "^6.4.12", |
||||||
"eslint": "^8.19.0", |
"@types/react-redux": "^7.1.25", |
||||||
"jest": "^29.2.1", |
"@types/react-test-renderer": "^18.0.0", |
||||||
"metro-react-native-babel-preset": "0.76.8", |
"@typescript-eslint/eslint-plugin": "^5.7.0", |
||||||
"prettier": "^2.4.1", |
"@typescript-eslint/parser": "^5.7.0", |
||||||
"react-test-renderer": "18.2.0", |
"babel-jest": "^29.2.1", |
||||||
"reactotron-react-native": "^5.0.3", |
"eslint": "^8.19.0", |
||||||
"typescript": "4.8.4" |
"jest": "^29.2.1", |
||||||
}, |
"metro-react-native-babel-preset": "0.76.8", |
||||||
"resolutions": { |
"prettier": "^2.4.1", |
||||||
"@types/react": "^17" |
"react-test-renderer": "18.2.0", |
||||||
}, |
"reactotron-react-native": "^5.0.3", |
||||||
"jest": { |
"typescript": "4.8.4" |
||||||
"preset": "react-native", |
}, |
||||||
"moduleFileExtensions": [ |
"resolutions": { |
||||||
"ts", |
"@types/react": "^17" |
||||||
"tsx", |
}, |
||||||
"js", |
"jest": { |
||||||
"jsx", |
"preset": "react-native", |
||||||
"json", |
"moduleFileExtensions": [ |
||||||
"node" |
"ts", |
||||||
] |
"tsx", |
||||||
} |
"js", |
||||||
|
"jsx", |
||||||
|
"json", |
||||||
|
"node" |
||||||
|
] |
||||||
|
} |
||||||
} |
} |
||||||
|
Before Width: | Height: | Size: 893 B |
Before Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 511 B |
@ -0,0 +1,8 @@ |
|||||||
|
interface Validation { |
||||||
|
isRequire: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface Common { |
||||||
|
validate: Validation |
||||||
|
shareMessage: string |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import { Buttons } from './buttons.interface' |
||||||
|
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 { |
||||||
|
stepTranslation: OnBoardingLocale.OnboardingSteps |
||||||
|
settingTranslation: SettingLocale.Core |
||||||
|
buttonsTranslation: Buttons |
||||||
|
customPack: CustomPack |
||||||
|
pageTitles: PageTitles |
||||||
|
common: Common |
||||||
|
purchases: PurchasesTranslate |
||||||
|
} |
@ -1,5 +1,5 @@ |
|||||||
export interface PageTitles { |
export interface PageTitles { |
||||||
setting: string, |
settings: string, |
||||||
privacy: string, |
privacy: string, |
||||||
terms: string, |
terms: string, |
||||||
} |
} |
@ -0,0 +1,6 @@ |
|||||||
|
export interface PurchasesTranslate { |
||||||
|
alertSuccess: string |
||||||
|
descSuccess: string |
||||||
|
alertError: string |
||||||
|
descError: string |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
export namespace SettingLocale { |
||||||
|
export interface Core { |
||||||
|
purchases: string |
||||||
|
language: string |
||||||
|
notifications: string |
||||||
|
write: string |
||||||
|
rate: string |
||||||
|
share: string |
||||||
|
policy: string |
||||||
|
label: string |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import { Common } from '../../interfaces/common.interface' |
||||||
|
|
||||||
|
const Validation = { |
||||||
|
isRequire: 'Field is require', |
||||||
|
length: 'At least 5 characters', |
||||||
|
} |
||||||
|
|
||||||
|
export const common: Common = { |
||||||
|
validate: Validation, |
||||||
|
shareMessage: 'Share this app with your friends', |
||||||
|
} |
@ -1,4 +1,5 @@ |
|||||||
export const customPack = { |
export const customPack = { |
||||||
title: 'Create custom pack', |
title: 'Create custom pack', |
||||||
description: 'Create your own custom pack with questions and task. It all depends on your imagination!', |
description: |
||||||
} |
'Create your own custom pack with questions and task. It all depends on your imagination!', |
||||||
|
} |
||||||
|
@ -1,13 +1,18 @@ |
|||||||
import {MainLocaleModule} from '../../types'; |
import { MainLocaleModule } from '../../interfaces' |
||||||
import {settingTranslation} from './settings.translation'; |
import { settingTranslation } from './settings.translation' |
||||||
import {onBoardingTranslation} from './steps.translation'; |
import { onBoardingTranslation } from './steps.translation' |
||||||
import {buttonsTranslation} from './onBoardingButton.translation'; |
import { buttonsTranslation } from './onBoardingButton.translation' |
||||||
import {customPack} from './custom-pack.translation'; |
import { customPack } from './custom-pack.translation' |
||||||
import {pageTitles} from './page-title.translation'; |
import { pageTitles } from './page-title.translation' |
||||||
|
import { common } from './common.translation' |
||||||
|
import { purchases } from './purchases.translation' |
||||||
|
|
||||||
export const en: MainLocaleModule = { |
export const en: MainLocaleModule = { |
||||||
settingTranslation, |
settingTranslation, |
||||||
stepTranslation: onBoardingTranslation, |
stepTranslation: onBoardingTranslation, |
||||||
buttonsTranslation, |
buttonsTranslation, |
||||||
customPack, |
customPack, |
||||||
pageTitles, |
pageTitles, |
||||||
}; |
purchases, |
||||||
|
common, |
||||||
|
} |
||||||
|
@ -1,5 +1,7 @@ |
|||||||
export const pageTitles = { |
export const pageTitles = { |
||||||
setting: 'Setting', |
settings: 'Settings', |
||||||
privacy: 'Privacy Policy', |
purchases: 'Purchases', |
||||||
terms: 'Terms and conditions', |
privacy: 'Privacy Policy', |
||||||
} |
terms: 'Terms and conditions', |
||||||
|
writeToUs: 'Write to us', |
||||||
|
} |
||||||
|
@ -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.', |
||||||
|
} |
@ -1,11 +1,12 @@ |
|||||||
import {SettingLocale} from '../../types/settings.types'; |
import { SettingLocale } from '../../interfaces/settings.types.interface' |
||||||
|
|
||||||
export const settingTranslation: SettingLocale.Core = { |
export const settingTranslation: SettingLocale.Core = { |
||||||
purchases: 'Purchases!', |
purchases: 'Purchases!', |
||||||
language: 'Language', |
language: 'Language', |
||||||
write: 'Write to us', |
notifications: 'Notifications', |
||||||
rate: 'Rate us', |
write: 'Write to us', |
||||||
share: 'Share app', |
rate: 'Rate us', |
||||||
policy: 'Privacy policy', |
share: 'Share app', |
||||||
term: 'Terms and conditions', |
policy: 'Privacy policy', |
||||||
information: 'Information', |
label: 'What can we do to help?', |
||||||
}; |
} |
||||||
|
@ -1,19 +1,18 @@ |
|||||||
import {OnBoardingLocale} from '../../types/on-boarding.types'; |
import { OnBoardingLocale } from '../../interfaces/on-boarding.types.interface' |
||||||
export const onBoardingTranslation: OnBoardingLocale.OnboardingSteps = { |
export const onBoardingTranslation: OnBoardingLocale.OnboardingSteps = { |
||||||
step1: { |
step1: { |
||||||
title: 'Welcome!', |
title: 'Welcome!', |
||||||
description: |
description: |
||||||
'Thank you for downloading. Now you are \n in the best game for the company \n or to play with your loved one', |
'Thank you for downloading. Now you are \n in the best game for the company \n or to play with your loved one', |
||||||
}, |
}, |
||||||
step2: { |
step2: { |
||||||
title: 'Relax and enjoy \n the game!', |
title: 'Relax and enjoy \n the game!', |
||||||
description: |
description: |
||||||
'This game features 5 levels of "spiciness",\n some of which are divided into games for a \n couple or a company. All you have to do is \n add players and you can start playing.\n P.S. You can always create your own \n questions and tasks.', |
'This game features 5 levels of "spiciness",\n some of which are divided into games for a \n couple or a company. All you have to do is \n add players and you can start playing.\n P.S. You can always create your own \n questions and tasks.', |
||||||
}, |
}, |
||||||
step3: { |
step3: { |
||||||
title: 'Premium version!', |
title: 'Premium version!', |
||||||
description: |
description: |
||||||
'Provides unlimited access to Hard and \n Extreme packages. Enjoy intriguing questions \n and exciting action.', |
'Provides unlimited access to Hard and \n Extreme packages. Enjoy intriguing questions \n and exciting action.', |
||||||
}, |
}, |
||||||
}; |
} |
||||||
|
|
||||||
|
@ -1,14 +1,14 @@ |
|||||||
import {MainLocaleModule} from '../../types'; |
import { MainLocaleModule } from '../../interfaces' |
||||||
import {settingTranslation} from './settings.translation'; |
import { settingTranslation } from './settings.translation' |
||||||
import {onBoardingTranslation} from './steps.translation'; |
import { onBoardingTranslation } from './steps.translation' |
||||||
import { buttonsTranslation } from './onBoardingButton.translation'; |
import { buttonsTranslation } from './onBoardingButton.translation' |
||||||
import {customPack} from './custom-pack.translation'; |
import { customPack } from './custom-pack.translation' |
||||||
import {pageTitles} from './page-title.translation'; |
import { pageTitles } from './page-title.translation' |
||||||
|
|
||||||
export const hi: MainLocaleModule = { |
export const hi: MainLocaleModule = { |
||||||
settingTranslation, |
settingTranslation, |
||||||
stepTranslation: onBoardingTranslation, |
stepTranslation: onBoardingTranslation, |
||||||
buttonsTranslation, |
buttonsTranslation, |
||||||
customPack, |
customPack, |
||||||
pageTitles, |
pageTitles, |
||||||
}; |
} |
||||||
|
@ -0,0 +1,11 @@ |
|||||||
|
import { Common } from '../../interfaces/common.interface' |
||||||
|
|
||||||
|
const Validation = { |
||||||
|
isRequire: 'Це поле обовʼязкове', |
||||||
|
length: 'Має бути мінімум 5 символів', |
||||||
|
} |
||||||
|
|
||||||
|
export const common: Common = { |
||||||
|
validate: Validation, |
||||||
|
shareMessage: 'Поділіться цим додатком зі своїми друзями', |
||||||
|
} |
@ -1,15 +1,18 @@ |
|||||||
import {MainLocaleModule} from '../../types'; |
import { MainLocaleModule } from '../../interfaces' |
||||||
import {settingTranslation} from './settings.translation'; |
import { settingTranslation } from './settings.translation' |
||||||
import {onBoardingTranslationUa} from './step.translation'; |
import { onBoardingTranslationUa } from './step.translation' |
||||||
import { buttonsTranslation } from './onBoardingButton.translation'; |
import { buttonsTranslation } from './onBoardingButton.translation' |
||||||
import {customPack} from './custom-pack.translation'; |
import { customPack } from './custom-pack.translation' |
||||||
import {pageTitles} from './page-title.translation'; |
import { pageTitles } from './page-title.translation' |
||||||
|
import { common } from './common.translation' |
||||||
|
import { purchases } from './purchases.translation' |
||||||
|
|
||||||
export const ua: MainLocaleModule = { |
export const ua: MainLocaleModule = { |
||||||
stepTranslation: onBoardingTranslationUa, |
stepTranslation: onBoardingTranslationUa, |
||||||
settingTranslation: settingTranslation, |
settingTranslation, |
||||||
buttonsTranslation, |
buttonsTranslation, |
||||||
customPack, |
customPack, |
||||||
pageTitles |
pageTitles, |
||||||
}; |
purchases, |
||||||
|
common, |
||||||
|
} |
||||||
|
@ -1,5 +1,7 @@ |
|||||||
export const pageTitles = { |
export const pageTitles = { |
||||||
setting: 'Налаштування', |
purchases: 'Покупки', |
||||||
privacy: 'Політика \n конфіденційності', |
settings: 'Налаштування', |
||||||
terms: 'Правила та умови', |
privacy: 'Політика \n конфіденційності', |
||||||
} |
terms: 'Правила та умови', |
||||||
|
writeToUs: 'Напишіть нам', |
||||||
|
} |
||||||
|
@ -0,0 +1,11 @@ |
|||||||
|
import { PurchasesTranslate } from "~i18n/interfaces/purchases.interface"; |
||||||
|
|
||||||
|
export const purchases: PurchasesTranslate = { |
||||||
|
alertSuccess: 'Ура! Готово!', |
||||||
|
descSuccess: 'Тепер зіграйте з друзями! Насолоджуйтеся грою 😊', |
||||||
|
alertError: 'Упс, щось не так 😢', |
||||||
|
descError: |
||||||
|
'Виникла помилка обробки вашої покупки. Будь ласка, спробуйте пізніше.', |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -1,11 +1,14 @@ |
|||||||
import {SettingLocale} from '../../types/settings.types'; |
import { SettingLocale } from '../../interfaces/settings.types.interface' |
||||||
|
|
||||||
export const settingTranslation: SettingLocale.Core = { |
export const settingTranslation: SettingLocale.Core = { |
||||||
purchases: 'Магазин!', |
purchases: 'Магазин', |
||||||
language: 'Мова', |
language: 'Мова', |
||||||
write: 'Напишіть нам', |
notifications: 'Сповіщення', |
||||||
rate: 'Оцініть нас', |
write: 'Напишіть нам', |
||||||
share: 'Поділитися програмою', |
rate: 'Оцініть нас', |
||||||
policy: 'Політика конфіденційності', |
share: 'Поділитися програмою', |
||||||
term: 'Правила та умови', |
policy: 'Політика конфіденційності', |
||||||
information: 'Інформація', |
term: 'Правила та умови', |
||||||
}; |
information: 'Інформація', |
||||||
|
label: 'Чим ми можемо допомогти', |
||||||
|
} |
||||||
|
@ -1,12 +0,0 @@ |
|||||||
import { CustomPack, PageTitles } from '../../module/common' |
|
||||||
import { Buttons } from './buttons.types' |
|
||||||
import { OnBoardingLocale } from './on-boarding.types' |
|
||||||
import { SettingLocale } from './settings.types' |
|
||||||
|
|
||||||
export interface MainLocaleModule { |
|
||||||
stepTranslation: OnBoardingLocale.OnboardingSteps |
|
||||||
settingTranslation: SettingLocale.Core |
|
||||||
buttonsTranslation: Buttons |
|
||||||
customPack: CustomPack |
|
||||||
pageTitles: PageTitles |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
export namespace SettingLocale { |
|
||||||
export interface Core { |
|
||||||
purchases: string; |
|
||||||
language: string; |
|
||||||
write: string; |
|
||||||
rate: string; |
|
||||||
share: string; |
|
||||||
policy: string; |
|
||||||
term: string; |
|
||||||
information: string; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,60 @@ |
|||||||
|
import React, { FC, PropsWithChildren } from 'react' |
||||||
|
import { StyleSheet, View, ViewStyle } from 'react-native' |
||||||
|
import { Txt } from '../txt' |
||||||
|
import { $size } from '../../helpers' |
||||||
|
import { colors } from '../../colors' |
||||||
|
|
||||||
|
interface FormControllWrapProps { |
||||||
|
label?: string |
||||||
|
error?: string |
||||||
|
style?: ViewStyle |
||||||
|
rightLabel?: string |
||||||
|
} |
||||||
|
|
||||||
|
export const FormControllWrap: FC<PropsWithChildren<FormControllWrapProps>> = ({ |
||||||
|
label, |
||||||
|
error, |
||||||
|
children, |
||||||
|
style, |
||||||
|
rightLabel, |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<View style={[styles.container, style]}> |
||||||
|
<View style={styles.labelWrap}> |
||||||
|
{label ? ( |
||||||
|
<Txt mod="lg" style={styles.label}> |
||||||
|
{label} |
||||||
|
</Txt> |
||||||
|
) : null} |
||||||
|
{rightLabel ? ( |
||||||
|
<Txt style={styles.label}>{rightLabel}</Txt> |
||||||
|
) : null} |
||||||
|
</View> |
||||||
|
|
||||||
|
{children} |
||||||
|
|
||||||
|
{error ? <Txt style={styles.error}>{error}</Txt> : null} |
||||||
|
</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
container: { |
||||||
|
marginBottom: $size(12), |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
labelWrap: { |
||||||
|
flexDirection: 'row', |
||||||
|
justifyContent: 'space-between', |
||||||
|
marginBottom: $size(5), |
||||||
|
}, |
||||||
|
label: { |
||||||
|
color: colors.secondaryText, |
||||||
|
marginBottom: $size(8), |
||||||
|
}, |
||||||
|
error: { |
||||||
|
color: colors.red, |
||||||
|
fontSize: $size(13), |
||||||
|
marginTop: $size(5), |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1,117 @@ |
|||||||
|
import React, { FC } from 'react' |
||||||
|
import { |
||||||
|
StyleSheet, |
||||||
|
TextInput, |
||||||
|
TextInputProps, |
||||||
|
View, |
||||||
|
ViewStyle, |
||||||
|
} from 'react-native' |
||||||
|
import { Txt } from '../txt' |
||||||
|
import { FormControllWrap } from './form-controll-wrap.component' |
||||||
|
import { Font } from '../../typing' |
||||||
|
import { $size } from '../../helpers' |
||||||
|
import { colors } from '../../colors' |
||||||
|
|
||||||
|
interface FormTextControllProps { |
||||||
|
value: string |
||||||
|
onChange: (value: string) => void |
||||||
|
|
||||||
|
inputProps?: Omit<TextInputProps, 'value' | 'onChange'> |
||||||
|
|
||||||
|
label?: string |
||||||
|
error?: string |
||||||
|
|
||||||
|
postfix?: string |
||||||
|
subtext?: string |
||||||
|
|
||||||
|
inputStyle?: ViewStyle |
||||||
|
style?: ViewStyle |
||||||
|
} |
||||||
|
|
||||||
|
export const FormTextControll: FC<FormTextControllProps> = ({ |
||||||
|
value, |
||||||
|
onChange, |
||||||
|
|
||||||
|
inputProps = {}, |
||||||
|
|
||||||
|
label, |
||||||
|
error, |
||||||
|
|
||||||
|
postfix, |
||||||
|
subtext, |
||||||
|
|
||||||
|
inputStyle, |
||||||
|
style, |
||||||
|
}) => { |
||||||
|
const renderPostfix = () => { |
||||||
|
if (postfix) |
||||||
|
return ( |
||||||
|
<View style={styles.rightComponent}> |
||||||
|
<Txt style={styles.postfix}>{postfix}</Txt> |
||||||
|
</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<FormControllWrap label={label} error={error} style={style}> |
||||||
|
<View style={styles.inputContainer}> |
||||||
|
<TextInput |
||||||
|
style={[ |
||||||
|
styles.input, |
||||||
|
error ? styles.inputActive : null, |
||||||
|
inputStyle, |
||||||
|
]} |
||||||
|
value={value} |
||||||
|
onChangeText={onChange} |
||||||
|
placeholderTextColor="#A0A3BD" |
||||||
|
{...inputProps} |
||||||
|
/> |
||||||
|
{renderPostfix()} |
||||||
|
</View> |
||||||
|
{subtext ? <Txt style={styles.subtext}>{subtext}</Txt> : null} |
||||||
|
</FormControllWrap> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
containerActive: { |
||||||
|
borderColor: '#FB5450', |
||||||
|
}, |
||||||
|
inputContainer: { |
||||||
|
position: 'relative', |
||||||
|
paddingRight: 1, |
||||||
|
}, |
||||||
|
input: { |
||||||
|
borderColor: colors.secondaryText, |
||||||
|
borderWidth: 1, |
||||||
|
height: $size(50), |
||||||
|
paddingHorizontal: $size(18), |
||||||
|
borderRadius: 16, |
||||||
|
color: colors.textPrimary, |
||||||
|
fontFamily: Font.Roboto400, |
||||||
|
fontSize: $size(15), |
||||||
|
lineHeight: $size(20), |
||||||
|
}, |
||||||
|
inputActive: { |
||||||
|
borderColor: '#FB5450', |
||||||
|
}, |
||||||
|
rightComponent: { |
||||||
|
position: 'absolute', |
||||||
|
right: 0, |
||||||
|
top: 0, |
||||||
|
height: 56, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
paddingRight: 20, |
||||||
|
// width: 100,
|
||||||
|
}, |
||||||
|
postfix: { |
||||||
|
color: '#9693AC', |
||||||
|
fontSize: $size(15), |
||||||
|
}, |
||||||
|
subtext: { |
||||||
|
color: '#808080', |
||||||
|
fontSize: $size(12), |
||||||
|
marginBottom: 4, |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1,3 @@ |
|||||||
|
export * from './form-controll-wrap.component'; |
||||||
|
export * from './form-text-controll.component'; |
||||||
|
|
@ -1,7 +1,7 @@ |
|||||||
export * from './icon'; |
export * from './icon' |
||||||
export * from './buttons'; |
export * from './buttons' |
||||||
export * from './header'; |
export * from './header' |
||||||
export * from './layout'; |
export * from './layout' |
||||||
export * from './modal'; |
export * from './modal' |
||||||
export * from './txt'; |
export * from './txt' |
||||||
|
export * from './form' |
||||||
|
@ -0,0 +1,67 @@ |
|||||||
|
import _ from 'lodash' |
||||||
|
import React, { useState } from 'react' |
||||||
|
|
||||||
|
export interface IUseFormState { |
||||||
|
[key: string]: string | number | boolean | any |
||||||
|
} |
||||||
|
type IValidateMethod<T> = (data: T) => FormErrors<T> | null |
||||||
|
|
||||||
|
export type FormErrors<T> = Partial<Record<keyof T, string>> |
||||||
|
|
||||||
|
export interface IForm<T> { |
||||||
|
values: T |
||||||
|
setForm: (form: T) => void |
||||||
|
errors: FormErrors<T> |
||||||
|
setFormField: (key: keyof T, value: any) => any |
||||||
|
setFormError: (key: keyof T, error: string) => void |
||||||
|
setFormErrors: (errors: Record<keyof T, string>) => void |
||||||
|
onSubmit: (callback: Function) => Function |
||||||
|
hasErrors: Boolean |
||||||
|
} |
||||||
|
|
||||||
|
export const useForm = <T extends IUseFormState>( |
||||||
|
initValue: Partial<T>, |
||||||
|
validateMethod: IValidateMethod<T>, |
||||||
|
): IForm<T> => { |
||||||
|
const [values, setForm] = useState(initValue as T) |
||||||
|
const [errors, setErrors] = useState<FormErrors<T>>({}) |
||||||
|
|
||||||
|
const setFormError = (f: keyof T, e: any) => { |
||||||
|
setErrors(oldErrors => { |
||||||
|
return { ...oldErrors, [f]: e } |
||||||
|
}) |
||||||
|
} |
||||||
|
const setFormField = (f: keyof T, v: any) => { |
||||||
|
setForm(oldForm => { |
||||||
|
return { ...oldForm, [f]: v } |
||||||
|
}) |
||||||
|
setFormError(f, null) |
||||||
|
} |
||||||
|
|
||||||
|
const validate = () => { |
||||||
|
const _errors = validateMethod(values) |
||||||
|
if (_errors) { |
||||||
|
setErrors(_errors) |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onSubmit = (callback: Function): any => { |
||||||
|
if (validate && validate()) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
callback() |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
values, |
||||||
|
setForm, |
||||||
|
errors, |
||||||
|
setFormField, |
||||||
|
setFormError, |
||||||
|
setFormErrors: setErrors, |
||||||
|
onSubmit, |
||||||
|
hasErrors: !_.isEmpty(_.omitBy(errors, _.isNil)), |
||||||
|
} |
||||||
|
} |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 53 KiB |
@ -1 +1,2 @@ |
|||||||
export * from './global-container.tool' |
export * from './global-container.tool' |
||||||
|
export * from './validate.tool' |
||||||
|
@ -0,0 +1,67 @@ |
|||||||
|
import _ from 'lodash' |
||||||
|
import _validate from 'validate.js' |
||||||
|
|
||||||
|
export const prepareValidatorResult = <T extends Record<string, any>>( |
||||||
|
result: T, |
||||||
|
): Record<keyof T, string> | null => { |
||||||
|
if (_.isEmpty(result)) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
_.each(result, (it, key, arr: any) => { |
||||||
|
arr[key] = it[0] |
||||||
|
}) |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
const presenceCost = { |
||||||
|
allowEmpty: false, |
||||||
|
message: '^Field is required', |
||||||
|
messageStatic: '^Field is required', |
||||||
|
} |
||||||
|
|
||||||
|
const validate = <T extends Record<string, any>>( |
||||||
|
values: T, |
||||||
|
constraints: any, |
||||||
|
) => { |
||||||
|
const result = _validate(values, constraints) |
||||||
|
return prepareValidatorResult<T>(result) |
||||||
|
} |
||||||
|
|
||||||
|
_validate.validators.array = ( |
||||||
|
arrayItems: any[], |
||||||
|
options: { length: number; message: string; key?: string }, |
||||||
|
) => { |
||||||
|
if (_.isEmpty(arrayItems)) { |
||||||
|
return presenceCost.message |
||||||
|
} |
||||||
|
if (arrayItems.length < options.length) { |
||||||
|
return options.message |
||||||
|
} |
||||||
|
|
||||||
|
if (options.key) { |
||||||
|
if (!arrayItems[0][options.key]) { |
||||||
|
return options.message |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
_validate.validators.characters = ( |
||||||
|
value: string, |
||||||
|
options: { |
||||||
|
message: string |
||||||
|
}, |
||||||
|
) => { |
||||||
|
if (String(value).search(/[^a-zA-Zа-яА-я0-9а-яієїйьЇІ"'.)(, -]+/) !== -1) { |
||||||
|
return options.message |
||||||
|
? options.message |
||||||
|
: '^common.validations.characters' |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
export { validate, presenceCost } |
@ -0,0 +1,5 @@ |
|||||||
|
export enum ProductsEnum { |
||||||
|
All = 'ALL', |
||||||
|
Under18 = 'un18', |
||||||
|
Crazy = 'Crz', |
||||||
|
} |
@ -1,10 +1,16 @@ |
|||||||
export enum RouteKey { |
export enum RouteKey { |
||||||
Onboarding = 'Onboarding', |
Onboarding = 'Onboarding', |
||||||
LanguageSelect = 'LanguageSelect', |
LanguageSelect = 'LanguageSelect', |
||||||
Setting = 'Setting', |
SettingsGroup = 'SettingsGroup', |
||||||
Game = 'Game', |
Game = 'Game', |
||||||
Loading = 'Loading', |
Loading = 'Loading', |
||||||
Package = 'Package', |
Packages = 'Packages', |
||||||
Questions = 'Questions', |
Questions = 'Questions', |
||||||
|
} |
||||||
|
|
||||||
|
export enum SettingsKey { |
||||||
|
Settings = 'Settings', |
||||||
PrivacyPolicy = 'PrivacyPolicy', |
PrivacyPolicy = 'PrivacyPolicy', |
||||||
|
Purchases = 'Purchases', |
||||||
|
WriteToUs = 'WriteToUs' |
||||||
} |
} |
||||||
|
@ -1,4 +1,6 @@ |
|||||||
export enum StorageKey { |
export enum StorageKey { |
||||||
OnBoarding = 'ONBOARDING_END', |
OnBoarding = 'ONBOARDING_END', |
||||||
Language = 'LANG_SELECTED', |
Language = 'LANG_SELECTED', |
||||||
|
Purchases = 'Purchases', |
||||||
|
Products = 'Products', |
||||||
} |
} |
||||||
|
@ -1,5 +1,3 @@ |
|||||||
export * from './custom-pack' |
|
||||||
export * from './dare' |
export * from './dare' |
||||||
export * from './game-item' |
export * from './game-item' |
||||||
export * from './page-titles' |
|
||||||
export * from './truth' |
export * from './truth' |
||||||
|
@ -1 +1,2 @@ |
|||||||
export * from './bottom-sheet-view.atom' |
export * from './bottom-sheet-view.atom' |
||||||
|
export * from './action-sheet-button.atom' |
||||||
|
@ -0,0 +1 @@ |
|||||||
|
export * from './sheets' |
@ -1,2 +1,3 @@ |
|||||||
export * from './alert' |
export * from './alert' |
||||||
export * from './alert-confirm' |
export * from './alert-confirm' |
||||||
|
export * from './bottom-sheet' |
||||||
|
@ -1 +1,3 @@ |
|||||||
export * from './selected-language-in-settings.atom'; |
export * from './selected-language-in-settings.atom' |
||||||
|
export * from './switch-notifications.atom' |
||||||
|
export * from './purchases.atom' |
||||||
|
@ -0,0 +1,88 @@ |
|||||||
|
import React, { FC } from 'react' |
||||||
|
import { StyleSheet, View } from 'react-native' |
||||||
|
import { $size, ButtonWithIcon, Icon, Txt, colors } from '../../common' |
||||||
|
import { TouchableOpacity } from 'react-native-gesture-handler' |
||||||
|
|
||||||
|
interface IProps { |
||||||
|
title: string |
||||||
|
price: string |
||||||
|
iconName: string |
||||||
|
hasDiscount: boolean |
||||||
|
isPurchased: boolean |
||||||
|
onPress: () => void |
||||||
|
} |
||||||
|
|
||||||
|
export const PurchaseAtom: FC<IProps> = ({ |
||||||
|
title, |
||||||
|
price, |
||||||
|
hasDiscount, |
||||||
|
iconName, |
||||||
|
isPurchased, |
||||||
|
onPress, |
||||||
|
}) => { |
||||||
|
const renderDiscountAtom = () => { |
||||||
|
return ( |
||||||
|
<View style={styles.discount}> |
||||||
|
<Txt style={styles.discountTxt}>-30%</Txt> |
||||||
|
</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<TouchableOpacity style={styles.container} onPress={onPress}> |
||||||
|
<View style={styles.row}> |
||||||
|
<Icon name={iconName} size={$size(24)} color={colors.purple} /> |
||||||
|
<Txt mod="lg" color={colors.purple}> |
||||||
|
{title} |
||||||
|
</Txt> |
||||||
|
{hasDiscount && renderDiscountAtom()} |
||||||
|
</View> |
||||||
|
{isPurchased ? ( |
||||||
|
<ButtonWithIcon |
||||||
|
styleBtn={styles.iconPlay} |
||||||
|
iconName="play" |
||||||
|
onPress={() => null} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<Txt mod="lg" style={styles.price}> |
||||||
|
{price + ' $'} |
||||||
|
</Txt> |
||||||
|
)} |
||||||
|
</TouchableOpacity> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
container: { |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'space-between', |
||||||
|
height: $size(28), |
||||||
|
marginBottom: $size(24), |
||||||
|
}, |
||||||
|
row: { |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: 'center', |
||||||
|
columnGap: 8, |
||||||
|
}, |
||||||
|
price: { |
||||||
|
fontWeight: '600', |
||||||
|
color: colors.purple, |
||||||
|
}, |
||||||
|
discount: { |
||||||
|
width: $size(49), |
||||||
|
borderRadius: 40, |
||||||
|
backgroundColor: colors.red, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
}, |
||||||
|
discountTxt: { |
||||||
|
fontSize: $size(14), |
||||||
|
lineHeight: $size(28), |
||||||
|
fontWeight: '900', |
||||||
|
}, |
||||||
|
iconPlay: { |
||||||
|
width: $size(60), |
||||||
|
height: '100%' |
||||||
|
}, |
||||||
|
}) |
@ -1,25 +1,21 @@ |
|||||||
import React from 'react'; |
import React from 'react' |
||||||
import {StyleSheet, Text, View} from 'react-native'; |
import { StyleSheet } from 'react-native' |
||||||
import {useTranslation} from 'react-i18next'; |
import { useTranslation } from 'react-i18next' |
||||||
|
import { $size, Txt, colors } from '../../common' |
||||||
|
|
||||||
export const SelectedLanguage = () => { |
export const SelectedLanguage = () => { |
||||||
const {t, i18n} = useTranslation(); |
const { i18n } = useTranslation() |
||||||
return ( |
|
||||||
<View style={styles.curentLang}> |
console.log(i18n.language) |
||||||
<Text style={styles.text}>{i18n.language}</Text> |
|
||||||
</View> |
return <Txt style={styles.text}>{i18n.language}</Txt> |
||||||
); |
} |
||||||
}; |
|
||||||
const styles = StyleSheet.create({ |
const styles = StyleSheet.create({ |
||||||
curentLang: { |
text: { |
||||||
flex: 0, |
color: colors.purple, |
||||||
alignSelf: 'center', |
fontWeight: '600', |
||||||
}, |
fontSize: $size(18), |
||||||
text: { |
lineHeight: $size(28), |
||||||
color: '#A798FF', |
textTransform: 'uppercase', |
||||||
fontWeight: '600', |
}, |
||||||
fontSize: 18, |
}) |
||||||
lineHeight: 28, |
|
||||||
textTransform: 'uppercase', |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
@ -0,0 +1,22 @@ |
|||||||
|
import React, { useState } from 'react' |
||||||
|
import { Switch } from 'react-native' |
||||||
|
import { $size, colors } from '../../common' |
||||||
|
|
||||||
|
export const SwitchNotificationsAtom = () => { |
||||||
|
const [isEnabled, setIsEnabled] = useState(false) |
||||||
|
|
||||||
|
const toggleSwitch = () => { |
||||||
|
setIsEnabled(previousState => !previousState) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Switch |
||||||
|
trackColor={{ true: colors.purple }} |
||||||
|
thumbColor={isEnabled ? colors.turquoise : colors.darkPurple} |
||||||
|
ios_backgroundColor={colors.purple} |
||||||
|
onValueChange={toggleSwitch} |
||||||
|
style={{ width: $size(51) }} |
||||||
|
value={isEnabled} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
@ -1,2 +1,3 @@ |
|||||||
export * from './settings.config' |
export * from './settings.config' |
||||||
export * from './privacy-text.config' |
export * from './privacy-text.config' |
||||||
|
export * from './purchases.config' |
||||||
|
@ -0,0 +1,16 @@ |
|||||||
|
import { ProductsEnum } from '../../common' |
||||||
|
|
||||||
|
export const purchasesConfig: any = { |
||||||
|
[ProductsEnum.Under18]: { |
||||||
|
name: 'Open package "Under 18"', |
||||||
|
icon: 'ghost', |
||||||
|
}, |
||||||
|
[ProductsEnum.Crazy]: { |
||||||
|
name: 'Open package "Crazy"', |
||||||
|
icon: 'crazy', |
||||||
|
}, |
||||||
|
[ProductsEnum.All]: { |
||||||
|
name: 'Open all packages', |
||||||
|
icon: 'all_packages', |
||||||
|
}, |
||||||
|
} |
@ -1,2 +1,4 @@ |
|||||||
export * from './privacy-policy' |
export * from './privacy-policy' |
||||||
export * from './settings.screen' |
export * from './settings.screen' |
||||||
|
export * from './purchases.screen' |
||||||
|
export * from './write-to-us.screen' |
||||||
|
@ -0,0 +1,133 @@ |
|||||||
|
import React, { FC, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import { |
||||||
|
ActivityIndicator, |
||||||
|
StyleSheet, |
||||||
|
TouchableOpacity, |
||||||
|
View, |
||||||
|
} from 'react-native' |
||||||
|
import { |
||||||
|
$size, |
||||||
|
appEvents, |
||||||
|
colors, |
||||||
|
Font, |
||||||
|
Header, |
||||||
|
Icon, |
||||||
|
ModalComponent, |
||||||
|
ProductsEnum, |
||||||
|
RouteKey, |
||||||
|
ScreenLayout, |
||||||
|
Txt, |
||||||
|
useNav, |
||||||
|
} from '../../common' |
||||||
|
|
||||||
|
import { purchasesService } from '../services' |
||||||
|
import { PurchaseAtom } from '../atoms' |
||||||
|
|
||||||
|
export const PurchasesScreen: FC = () => { |
||||||
|
const { t } = useTranslation() |
||||||
|
const nav = useNav() |
||||||
|
const [isLoading, setLoading] = useState(false) |
||||||
|
|
||||||
|
const purchaseProduct = async (productId: ProductsEnum) => { |
||||||
|
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) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<ScreenLayout |
||||||
|
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 ( |
||||||
|
<PurchaseAtom |
||||||
|
key={it.productId} |
||||||
|
title={it.name} |
||||||
|
price={it.price} |
||||||
|
hasDiscount={it.productId === ProductsEnum.All} |
||||||
|
iconName={it.icon} |
||||||
|
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> |
||||||
|
</ScreenLayout> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.darkPurple, |
||||||
|
borderRadius: 20, |
||||||
|
padding: 20, |
||||||
|
}, |
||||||
|
description: { |
||||||
|
color: colors.purple, |
||||||
|
lineHeight: $size(30), |
||||||
|
}, |
||||||
|
row: { |
||||||
|
flexDirection: 'row', |
||||||
|
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, |
||||||
|
}, |
||||||
|
}) |
@ -1,33 +1,99 @@ |
|||||||
import React, { FC } from 'react' |
import React, { FC } from 'react' |
||||||
import { useTranslation } from 'react-i18next' |
import { useTranslation } from 'react-i18next' |
||||||
import { View } from 'react-native' |
import { Share } from 'react-native' |
||||||
import { SettingsItem } from '../components/settings-item.component' |
import { SettingsItem } from '../components/settings-item.component' |
||||||
import { settingsConfig } from '../config/settings.config' |
import { settingsConfig } from '../config/settings.config' |
||||||
import { Header, colors, ScreenLayout, useNav } from '../../common' |
import { |
||||||
|
EngSvg, |
||||||
|
Header, |
||||||
|
Language, |
||||||
|
ScreenLayout, |
||||||
|
SettingsKey, |
||||||
|
UaSvg, |
||||||
|
useNav, |
||||||
|
} from '../../common' |
||||||
|
import { SheetManager } from 'react-native-actions-sheet' |
||||||
|
|
||||||
export const SettingsScreen: FC = () => { |
export const SettingsScreen: FC = () => { |
||||||
|
const { t, i18n } = useTranslation() |
||||||
const nav = useNav() |
const nav = useNav() |
||||||
const { t } = useTranslation() |
|
||||||
|
const configActions = [ |
||||||
|
{ |
||||||
|
icon: () => <EngSvg />, |
||||||
|
onPress: () => onChangeLanguage(Language.EN), |
||||||
|
label: 'English', |
||||||
|
}, |
||||||
|
{ |
||||||
|
icon: () => <UaSvg />, |
||||||
|
onPress: () => onChangeLanguage(Language.UA), |
||||||
|
label: 'Українська', |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
const shareApp = async () => { |
||||||
|
try { |
||||||
|
const shareOptions = { |
||||||
|
message: 'Share this cool app with your friends', |
||||||
|
} |
||||||
|
|
||||||
|
await Share.share(shareOptions) |
||||||
|
} catch (error) { |
||||||
|
console.log(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onChangeLanguage = (language: Language) => { |
||||||
|
SheetManager.hide('bottom-sheet') |
||||||
|
return i18n.changeLanguage(language) |
||||||
|
} |
||||||
|
|
||||||
|
const openBottomSheetForChangeLanguage = () => { |
||||||
|
SheetManager.show('bottom-sheet', { |
||||||
|
payload: configActions, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const onPressSettingItem = (key: string) => { |
||||||
|
switch (key) { |
||||||
|
case 'purchases': |
||||||
|
nav.navigate(SettingsKey.Purchases) |
||||||
|
break |
||||||
|
case 'lang': |
||||||
|
openBottomSheetForChangeLanguage() |
||||||
|
break |
||||||
|
case 'notification': |
||||||
|
break |
||||||
|
case 'message': |
||||||
|
nav.navigate(SettingsKey.WriteToUs) |
||||||
|
break |
||||||
|
case 'privacy-policy': |
||||||
|
nav.navigate(SettingsKey.PrivacyPolicy) |
||||||
|
break |
||||||
|
case 'rate': |
||||||
|
break |
||||||
|
case 'share': |
||||||
|
shareApp() |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
return ( |
return ( |
||||||
<ScreenLayout |
<ScreenLayout |
||||||
headerComponent={ |
headerComponent={<Header title={t('pageTitles.settings')} />}> |
||||||
<Header |
<> |
||||||
leftIcon="arrow" |
|
||||||
title={t('pageTitles.setting')} |
|
||||||
onPressLeft={() => nav.goBack()} |
|
||||||
/> |
|
||||||
}> |
|
||||||
<View> |
|
||||||
{settingsConfig.map((item, index) => ( |
{settingsConfig.map((item, index) => ( |
||||||
<SettingsItem |
<SettingsItem |
||||||
key={index} |
key={index} |
||||||
title={item.title} |
title={t(item.title)} |
||||||
iconName={item.image} |
iconName={item.image} |
||||||
component={item.component} |
component={item.component} |
||||||
|
onPressSettingItem={() => |
||||||
|
onPressSettingItem(item.image) |
||||||
|
} |
||||||
/> |
/> |
||||||
))} |
))} |
||||||
</View> |
</> |
||||||
</ScreenLayout> |
</ScreenLayout> |
||||||
) |
) |
||||||
} |
} |
||||||
|
@ -0,0 +1,61 @@ |
|||||||
|
import React, { FC } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import { StyleSheet } from 'react-native' |
||||||
|
import { |
||||||
|
$size, |
||||||
|
appEvents, |
||||||
|
ButtonPrimary, |
||||||
|
colors, |
||||||
|
FormTextControll, |
||||||
|
Header, |
||||||
|
ScreenLayout, |
||||||
|
useForm, |
||||||
|
useNav, |
||||||
|
} from '../../common' |
||||||
|
import { writeToUsValidator } from '../validator' |
||||||
|
|
||||||
|
interface WriteUsForm { |
||||||
|
message: string |
||||||
|
} |
||||||
|
|
||||||
|
export const WriteToUsScreen: FC = () => { |
||||||
|
const { t } = useTranslation() |
||||||
|
const nav = useNav() |
||||||
|
const form = useForm<WriteUsForm>({}, writeToUsValidator) |
||||||
|
|
||||||
|
const onSendText = () => { |
||||||
|
appEvents.emit('alert', { |
||||||
|
title: 'Aga, thanks', |
||||||
|
subtitle: 'We rozberemosya', |
||||||
|
onClose: () => nav.goBack(), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<ScreenLayout |
||||||
|
bottomSafeArea |
||||||
|
headerComponent={<Header title={t('pageTitles.writeToUs')} />}> |
||||||
|
<FormTextControll |
||||||
|
label={t('settingTranslation.label')} |
||||||
|
value={form.values.message} |
||||||
|
onChange={val => form.setFormField('message', val)} |
||||||
|
inputProps={{ multiline: true }} |
||||||
|
inputStyle={{ height: $size(100) }} |
||||||
|
error={form.errors['message']} |
||||||
|
/> |
||||||
|
<ButtonPrimary |
||||||
|
style={styles.button} |
||||||
|
disabled={!form.values.message} |
||||||
|
onPress={() => form.onSubmit(onSendText)}> |
||||||
|
Send to us |
||||||
|
</ButtonPrimary> |
||||||
|
</ScreenLayout> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
button: { |
||||||
|
marginTop: $size(20), |
||||||
|
backgroundColor: colors.lightPurple, |
||||||
|
}, |
||||||
|
}) |
@ -0,0 +1 @@ |
|||||||
|
export * from './purchases.service' |
@ -0,0 +1,121 @@ |
|||||||
|
import { |
||||||
|
initConnection, |
||||||
|
getProducts, |
||||||
|
Product, |
||||||
|
requestPurchase, |
||||||
|
purchaseUpdatedListener, |
||||||
|
finishTransaction, |
||||||
|
} from 'react-native-iap' |
||||||
|
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 = [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() { |
||||||
|
try { |
||||||
|
await initConnection() |
||||||
|
} catch (error) { |
||||||
|
console.error('Failed to initialize IAP:', error) |
||||||
|
throw error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public async loadProducts() { |
||||||
|
try { |
||||||
|
const products: Product[] = await getProducts({ skus: ID_PRODUCTS }) |
||||||
|
const productss = this.transformProductsData(products) |
||||||
|
|
||||||
|
this.products = productss |
||||||
|
await this.saveProductInStore(productss) |
||||||
|
} catch (error) { |
||||||
|
console.error('Error loading products:', error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async saveProductInStore(products: ProductItem[]) { |
||||||
|
await AsyncStorage.setItem( |
||||||
|
StorageKey.Products, |
||||||
|
JSON.stringify(products), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
public async purchaseProduct(productId: ProductsEnum) { |
||||||
|
try { |
||||||
|
await requestPurchase({ |
||||||
|
sku: productId, |
||||||
|
}) |
||||||
|
this.purchaseListener() |
||||||
|
await this.savePurchase(productId) |
||||||
|
await purchasesService.loadProducts() |
||||||
|
} catch (error) { |
||||||
|
console.error('Purchase error:', 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 }) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
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((a, b) => a.productId.localeCompare(b.productId)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const purchasesService = new PurchasesService() |
@ -0,0 +1 @@ |
|||||||
|
export * from './writeToUs.validator' |
@ -0,0 +1,15 @@ |
|||||||
|
import validate from 'validate.js' |
||||||
|
import { presenceCost } from '../../common' |
||||||
|
|
||||||
|
const constraints = { |
||||||
|
message: { |
||||||
|
presence: presenceCost, |
||||||
|
length: { |
||||||
|
minimum: 5, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export const writeToUsValidator = (data: any) => { |
||||||
|
return validate(data, constraints) |
||||||
|
} |
@ -1,64 +1,23 @@ |
|||||||
|
|
||||||
{ |
{ |
||||||
"compilerOptions": { |
"extends": "@tsconfig/react-native/tsconfig.json", |
||||||
/* Basic Options */ |
"compilerOptions": { |
||||||
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ |
"jsx": "react", |
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ |
"baseUrl": ".", |
||||||
"lib": ["es2017"], /* Specify library files to be included in the compilation. */ |
"paths": { "~*": ["./src/*"] }, |
||||||
"allowJs": true, /* Allow javascript files to be compiled. */ |
"strictNullChecks": false, |
||||||
// "checkJs": true, /* Report errors in .js files. */ |
"esModuleInterop": true |
||||||
"jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ |
// "jsx": "react-native" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, |
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */ |
}, |
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */ |
"include": [ |
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */ |
"src", |
||||||
// "outDir": "./", /* Redirect output structure to the directory. */ |
".eslintrc.js", |
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ |
"react-native.config.js", |
||||||
// "removeComments": true, /* Do not emit comments to output. */ |
"metro.config.js", |
||||||
"noEmit": true, /* Do not emit outputs. */ |
"index.js", |
||||||
// "incremental": true, /* Enable incremental compilation */ |
"babel.config.js", |
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ |
"jest.config.js" |
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ |
], |
||||||
"isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ |
|
||||||
|
|
||||||
/* Strict Type-Checking Options */ |
|
||||||
"strict": true, /* Enable all strict type-checking options. */ |
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ |
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */ |
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ |
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ |
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ |
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ |
|
||||||
|
|
||||||
/* Additional Checks */ |
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */ |
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */ |
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ |
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ |
|
||||||
|
|
||||||
/* Module Resolution Options */ |
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ |
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ |
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ |
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ |
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */ |
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */ |
|
||||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ |
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ |
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ |
|
||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */ |
|
||||||
"resolveJsonModule": true /* Allows importing modules with a ‘.json’ extension, which is a common practice in node projects. */ |
|
||||||
|
|
||||||
/* Source Map Options */ |
|
||||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ |
|
||||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ |
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ |
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ |
|
||||||
|
|
||||||
/* Experimental Options */ |
/* Completeness */ |
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ |
"skipLibCheck": true |
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ |
|
||||||
}, |
|
||||||
"exclude": [ |
|
||||||
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js" |
|
||||||
] |
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue