completion

This commit is contained in:
2025-08-06 13:49:11 +08:00
commit c2d7317897
684 changed files with 92987 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
[android]
target = Google Inc.:Google APIs:23
[maven_repositories]
central = https://repo1.maven.org/maven2

View File

@@ -0,0 +1,13 @@
# ------------------------------------------------------------------------------
# IGNORE ALL:
# ------------------------------------------------------------------------------
*
# ------------------------------------------------------------------------------
# EXCEPT THE FOLLOWING:
# ------------------------------------------------------------------------------
# DIRECTORIES
# FILES
!package.json

View File

@@ -0,0 +1,35 @@
# For more information about the properties used in
# this file, please see the EditorConfig documentation:
# http://editorconfig.org/
root = true
[*]
# Applied to the following file types:
# - js
# - coffee
# - opts
# - Makefile
# - md
# - Dockerfile
# - xml
# - opts
# - dot files
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
indent_size = 2
indent_style = space
[{*.yml,*.yaml,*.java,*.kt,*.kts,*.py,*.sql,*.gradle}]
# Applied to the following file types:
# - java
# - kotlin
# - python
# - sql
# - yaml (http://www.yaml.org/spec/1.2/spec.html#id2777534)
indent_style = space

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-prod-
debug=false
ENV=prod
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-alpha-
debug=true
ENV=alpha
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-beta-
debug=true
ENV=beta
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-dev-
debug=true
ENV=dev
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-qa-
debug=true
ENV=qa
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,5 @@
APP_ID=com.ioneres.maagap
BUCKET_PREFIX=maagap-rnd-
debug=true
ENV=rnd
MOBILE_API_URL=https://87kprj6t9i.execute-api.ap-southeast-1.amazonaws.com/staging

View File

@@ -0,0 +1,10 @@
.vscode
android
coverage
docker
e2e
fastlane
ios
scripts
node_modules
babel.config.js

View File

@@ -0,0 +1,34 @@
module.exports = {
root: true,
// extends: '@react-native-community',
"env": {
"browser": true
},
"extends": "airbnb",
"globals": {
"__DEV__": true
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"ecmaFeatures": { "legacyDecorators": true }
},
"rules": {
"react/jsx-filename-extension": ["error", { extensions: [".js", ".jsx"] }],
"indent": [2, "tab", { "SwitchCase": 1, "VariableDeclarator": 1 }],
"no-tabs": 0,
"max-len": [2, { "code": 120, "tabWidth": 1, "ignoreComments": true, "ignoreTrailingComments": true, "ignoreUrls": true, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true }],
"arrow-parens": 0,
"react/jsx-indent": [2, "tab"],
"react/jsx-indent-props": [2, "tab"],
"react/forbid-prop-types": 0,
"react/prefer-stateless-function": 0,
"import/prefer-default-export": 0,
"quotes": [2, "single", "avoid-escape"],
},
"settings": {
"import/resolver": {
"babel-module": {}
}
}
};

View File

@@ -0,0 +1,75 @@
[ignore]
; We fork some components by platform
.*/*[.]android.js
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore polyfills
node_modules/react-native/Libraries/polyfills/.*
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/LoadingView.js
[untyped]
.*/node_modules/@react-native-community/cli/.*/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
[options]
emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
munge_underscores=true
module.name_mapper='^react-native$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation'
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
sketchy-number=warn
untyped-type-import=warn
nonstrict-import=warn
deprecated-type=warn
unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
nonstrict-import
sketchy-null
unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import
[version]
^0.105.0

View File

@@ -0,0 +1 @@
*.pbxproj -text

112
ioneapps-maagapp-ee31119a522d/.gitignore vendored Normal file
View File

@@ -0,0 +1,112 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
# node.js
#
node_modules
npm-debug.log
yarn-error.log
# VS Code
#
.vscode/
.settings
# BUCK
buck-out/
\.buckd/
*.keystore
# VS Code
#
.vscode/
.settings
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/README.md
coverage/
*.mobileprovision
*.dSYM.zip
ios/Nixplay/main.jsbundle
ios/Nixplay/main.jsbundle.meta
.vscode/launch.json
android/app/src/main/assets/crashlytics-build.properties
android/app/src/main/res/values/com_crashlytics_export_strings.xml
third-party/
# cocoapods
ios/Pods
android/app/fabric.properties
reactNativeQueue.realm*
# Google services.json
# android/app/google-services.json
# Java class files
*.class
android/app/build/*
android/app/bin
android/app/.classpath
android/app/.project
android/.project
# Language files
language.csv
#Jenkins rvm
rvm.env
*.cer
fastlane/metadata/
src/config/revision.json
GoogleService-Info.plist
device-udid-export.txt
firebase-debug.log
android/app/.classpath
android/app/rnd/release/app-rnd-release.apk
android/app/beneficiary/*
android/app/rnd/*
android/app/rndBE/*
android/app/rndSW/*
android/app/prodSW/*

View File

@@ -0,0 +1 @@
10

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,61 @@
Changelog 2019-06-05 10:46:50
- Customise android transition config
- upgrade react-native-webview
Changelog 2019-05-28 11:24:32
- Added svg transform
- migrate ios svg library to podfile
- added metro config
Changelog 2019-05-24 15:21:03:
- Changed NixShareModule, handle dynamic android content provider uri
- Changed react-native-nixplay-core
- Changed react-native-creedon-imagepicker
- Added shareExtension reducer
- Added shareExtension saga
- Added shareExtension reducer unit test
- Added shareExtension saga unit test
- Changed ShareExtPreview ui/ux
- Changed ShareExtPlaylist ui/ux
- Changed Prompt Natification after upload completed
- Show warning messagewhen user selected contents exist max
Changelog 2019-05-03 18:45:39:
- react-natvie async-storage → @react-native-community/async-storage 1.2.4
- migration is not support 1.2.x to 1.3.x , we stick to 1.2.x now
Change logs 2019-04-26 16:50:16:
upgrade rn 59
- default node verion 10.x
- react-native-languages → react-native-localize
- react-natvie async-storage → @react-native-community/async-storage
- reat-native slider → @react-native-community_slider
- react-natvie netinfo → @react-native-community/netinfo
- react-native-enhanced-webview → react-native-webview
- upgrade babel babel/core": "^7.4.0"
- .babelrc → babel.config.js
- upgrade jest
- toThrow(error.message) → toThrow(error)
- .toThrow(error.error.message) → toMatch(error.error.message)
- android project.supportLibVersion "27.1.1" → "28.0.0"
- upgrade react-navigation
- added react-native-gesture-handler
- Added export const Root = createAppContainer(RootStack);
- headerSetting.navigationOptions → headerSettings.defaultNavigationOptions
- upfrade react-native firebase
- com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true
- Android Share Extension
- changed Theme.Share.Transparent → default "AppTheme"
- CustomMainReactPackage.java
- spec.getType() → ("http".equals(NetworkingModule.class) || "https".equals(NetworkingModule.class))
- Added CustomNetworkModule.java
- gradle-4.6 → gradle-5.1.1
- upgrade react-native-intercom
- depreceated postinstall.sh
- src/components/reduxForm/renderer.js Slider default valude = 0
- webViewComponent.js using react-native-webview
- src/locale/i18n.js upgrade for RNLocalize
- react-native-video 4.3.1 → 4.4.0(customized)
- Textinput multi charaters input method fixed
- update redux-saga
- __tests__ migrate delay to redux-saga/effect

View File

@@ -0,0 +1,8 @@
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods"
gem 'rb-readline'
gem 'pry'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

View File

@@ -0,0 +1,227 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.1)
activesupport (4.2.11.1)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.1)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
babosa (1.0.3)
claide (1.0.3)
cocoapods (1.8.3)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.8.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.11.1, < 2.0)
cocoapods-core (1.8.3)
activesupport (>= 4.0.2, < 6)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.2.2)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.1.0)
coderay (1.1.2)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
concurrent-ruby (1.1.5)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
escape (0.0.4)
excon (0.67.0)
faraday (0.17.0)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.134.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.8.1, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-firebase_app_distribution (0.1.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
google-api-client (~> 0.23)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
method_source (0.9.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mini_magick (4.9.5)
minitest (5.12.2)
molinillo (0.6.6)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
nap (1.1.0)
naturally (2.2.0)
netrc (0.11.0)
os (1.0.1)
plist (3.5.0)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (2.0.5)
rb-readline (0.5.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
ruby-macho (1.4.0)
rubyzip (1.3.0)
security (0.1.3)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.6)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
tty-cursor (0.7.0)
tty-screen (0.7.0)
tty-spinner (0.9.1)
tty-cursor (~> 0.7)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
word_wrap (1.0.0)
xcodeproj (1.13.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
cocoapods
fastlane
fastlane-plugin-firebase_app_distribution
pry
rb-readline
BUNDLED WITH
2.0.2

View File

@@ -0,0 +1,83 @@
## Localization Rules
1. Files is separated by categories. __DO NOT__ just stuff everything in `common`. See below.
2. Include punctuations in the json. Duplicates are ok if necessary. For example:
```
{
"Friends": "Friends",
"friends": "friends",
"signOut?": "Sign out?",
"signout": "Sign out"
}
```
3. When creating files in `en`, create the same files in the other language also, even if its an empty object.
__DO NOT__ give an empty string, empty string does not fallback to `en`
4. __DO NOT__ have more that 1 first level key in the json file, because it is used in the script for importing the texts as filename.
5. __DO NOT__ fill other language files with english. Just leave the field blank, so that the translators know which one to translate.
6. Check if your text available or not before you add new text.
7. The key of your text should be related to the content, not to the component using it. This will help preventing duplicates.
8. __BE CAREFUL__ when changing the existing key, check for all occurrence of previous key.
### IMPORTANT
__NEVER__ do something like this:
```
i18n.t('common.friend').toLowerCase().concat('s')
```
Firstly, its the wrong category, should be `friends`.
Secondly, not all plural ends with `s` or `es`. For example, friend and friends:
- German: `Freund` and `Freunde`
- Chinese Simplified: `朋友` and `朋友们`
Thirdly, `toLowerCase` is tolerable, but better not, because same as the reason above, you cannot guarantee all language follow the same rules.
### Categories:
1. Common:
anything that you cannot guess the category when you read it
2. Users:
- first / last name
- email
- username
- password
- country
- accounts
- tnc / privacy
3. Contents:
- photo
- video
- item
- caption
- upload
- download
4. Albums
5. Playlists
6. Frames
7. Friends
#### Exception:
- Frame Settings
## Updating languages copy
`babel-node` is a command that comes with `@babel/node`
```
npm i -g @babel/core @babel/node
```
### Exporting
Output folder will be the folder where you run the command
```
babel-node src/locale/export-to-csv.js
```
### Importing
```
babel-node src/locale/import-from-csv.js ./path/to/csv/lang.csv ./path/to/locale/folder
# example: babel-node src/locale/import-from-csv.js language.csv src/locale/lang/
```

View File

@@ -0,0 +1,411 @@
# MAAGApp Mobile App
## 1. Setup
### 1.1 Compatibility
To run this mobile app, we need to have node `8.x.x` installed.
To install and manage multiple versions of node, we can use [nvm](https://github.com/creationix/nvm)
We also need to have installed Ruby version 2.3.X to use Gem.
### 1.2 Install
Install [cocoapod](https://guides.cocoapods.org/using/getting-started.html)
Clone the repository and then install all dependencies by running the following command
```bash
nvm use
npm install
```
### 1.3 Run
To run on iOS simulator
pre install pod
```bash
npm run pod
```
or
```bash
cd ios
pod install
cd ../
npm run ios
```
#### Run specific scheme
```bash
npm run ios -- --scheme "Nixplay" --device "e32c0db8ed7e5ac2be743dcada78b57ac944fd6e"
```
or
```bash
npm run ios:rnd -- --simulator "iPhone X"
```
or
```bash
npm run ios:rnd -- --device "iPhone X"
```
```bash
npm run ios:qa -- --device "iPhone X"
```
To run on Android emulator
```bash
npm run android
```
To run the unit tests
```bash
npm test
```
To run the unit tests in watch mode
```bash
npm run test:watch
```
## 2. Deploying Alpha build (We will use this until we release to PROD)
### 2.1 Increment build number
Alpha build is built using the develop branch with only the build number set to `git rev-list --count develop`. To create a new alpha build simply run the following command to update build number and commit it:
Update package.json version
e.g
"version": "3.0.1"
1. run `jq -r '.version' package.json` to echo
1. `npm run create:release:branch` create release branch with version name
1. `npm run prerelease` bumped version code when release to qa
2. `git flow release finish` to close release branch
### 2.2 Release an iOS build
After running the prerelease script, we can release iOS build by running the following command:
```bash
npm run ios:release:alpha
```
### 2.3 Release an Android build
After running the prerelease script, we can release Android build by running the following command:
```bash
npm run android:release:alpha
```
## 3. Deploying to QA
### 3.1 Before you begin
#### 3.1.A Setup prpvisioning profile
You need to install Fabric app to your mac. You need a account with https://fabric.io/, ask `Kumar` / `James` to send an invite so an account on Fabric can be created. After creating an account you can download and install the Fabric app.
You need 2 passwords to successfully deploy the app to crashlytics 1) the passcode for decrypting developer certificates to use with match and 2) the password for **mobile@nix-digital.com** apple developer account. Again ask `Kumar` / `James` for these.
You also need to install Fastlane CLI. Run the following command to do so:
```bash
gem install fastlane -NV
```
#### 3.1.B sync certificat
If certificate is outdate/not be synced run the following commnad to sync up from repo
`fastlane ios certificates --env=<env>`
e.g.
```bash
fastlane ios certificates --env=rnd
```
for specific type of cert
`fastlane ios read_dev_cert --env=<env>`
`fastlane ios read_adhoc_cert --env=<env>`
`fastlane ios read_appstore_cert --env=<env>`
### 3.2 Create release branch
You can either use gitflow or simply use git to create a new branch:
```bash
git flow release start <version>
```
or
```bash
git checkout -b release/<version>
```
### 3.3 Bump build number
Bump build number and run other prerelease scripts like npm install etc
```bash
npm run prerelease
```
### 3.3.1 Bump version number
```bash
npm --no-git-tag-version version [major|minor|patch] patch -m "Upgrade to version %s"
```
example output
``` bash
npm --no-git-tag-version version patch -m "Upgrade to version %s"
v0.1.2
[RNV] Versioning Android...
[RNV] Android updated
[RNV] Versioning iOS...
[RNV] iOS updated
[RNV] Amending...
[RNV] Adjusting Git tag...
[RNV] Done
```
### 3.4 Release to QA
Release iOS version to QA:
```bash
npm run ios:release:qa
```
Release Android version to QA:
```bash
npm run android:release:qa
```
#### 3.4.1 Extra command
Release to Alpha:
```bash
npm run ios:release:alpha
npm run android:release:alpha
```
<!-- ```
npm run ios:release:beta
npm run android:release:beta
npm run ios:release:qa
npm run android:release:qa
``` -->
### 3.5 Merge the release branch back to Develop and Master
## 4. Workflow
Always create a new git branch to work on a user story. After a user story is completed then commit the code and push your changes. Login to https://git.nixplay.ninja/ and create a **Merge Request**, make sure to assign it to someone. The assignee should review the code and discuss if any issues with the committer and if no issues then merge it back to the develop branch.
> Note: Please never use **--no-verify** when pushing your changes to any branch including feature branches.
### 4.1 Create a new branch
You can either create a new branch using `git` or use `gitflow` if you have that set up
To create and switch to a new branch you can:
```bash
git checkout -b <branch name>
```
Work on a user story and make sure to add unit tests to cover your changes and to fix any broken unit tests due to the changes made.
### 4.2 Commit your changes and create merge request
Continue to commit changes regularly but make sure all tests and eslint issues is fixed before pushing. If you think a user story is done then go to https://git.nixplay.ninja/ and create a new merge request and assign it to someone to merge.
> Note: Do not commit directly to master or develop branch unless the changes is minimal. This means changing this README file is okay to commit directly to develop branch or changing the padding of a component. Any changes that requires more changes than that should be committed to feature branch.
### 4.3 Merge back from develop branch often
When we are working on a feature branch we may diverge from the develop branch. To ensure what we are doing is as upto date as possible and also to avoid merge conflicts, we should merge develop branch to our feature branch as often as possible.
### 5 Update reducerVersion when modifying the index reducer
Whenever we modify `src/reducers/index.js` we should also make sure we update the reducerVersion in `src/config/ReduxPersist.js` to prevent the app from crashing.
## Troubleshooting
iOS error `No bundle url present`, can try:
- https://www.andrewcbancroft.com/2017/04/22/solving-react-natives-no-bundle-url-present-error/
iOS error related to bundle url, you can also try:
- Kill the existing bundler process and then run the bundler
```bash
watchman watch-del-all
yarn cache clean
npm start -- --reset-cache
```
iOS error `Entry, :CFBundleIdentifier, Does Not Exist`, can try:
- Run
```bash
npm run build:ios
```
Android stuck on splash screen, can try:
- Uninstall the app, then run
```bash
npm run android
```
If you see a version mismatch error message then try running the following command:
```bash
npm run ios:reset
```
Android build failed
```
Could not find manifest-merger.jar ...
```
run `rm -rf $HOME/.gradle/caches`
run `./android/gradlew cleanBuildCache -p ./android`
## Update app icon with single command
``` bash
https://blog.bam.tech/developper-news/change-your-react-native-app-icons-in-a-single-command-line
```
# Testing Subscription
Library used: [react-native-iap](https://github.com/dooboolab/react-native-iap)
Subscription flow:
![Subscription Flow](https://miro.medium.com/max/1400/1*LuTwkpUazCzJugMaYBZm2Q.png)
Reference: https://medium.com/dooboolab/react-native-iap-v3-1259e0b0c017
### Creating subscription item for iOS
`Apple Store Connect` > `My Apps` > Select an App > `Features` > `In-App Purchases` > Create your item
__IMPORTANT:__ Your item need to be `Ready to Submit` in RND and QA for it to purchasable.
__IMPORTANT 2:__ Product ID is unique and is for all apps. Even if you delete the item, you still cannot create with the same product ID even in another env.
### Creating subscription item for Android
`Google Play Console` > Select an App > `Store presence` > `In-app products` > `Subscriptions` > Create your item
__NOTE:__ Once you activate the subscription item, there is no way to deactivate or delete.
### Setting up sandbox on iOS
Create an email address under:
`Apple Store Connect` > `Users and Access` > `Testers`
__Note:__ Cannot create with email that is used for existing appleID, you can create using a non-existing email address.
There will be no purchase related email sent for sandbox purchase.
### Setting up sandbox on Android
1. Make sure the email address you register for testing is the primary account in your phone.
That means the account is the first ever account you use in your phone.
If not, remove all gmail account, then login with that email.
Or, create another user profile in your phone (if the function is enabled), and use the email address registered as a user
2. Add email address in `Google Play Console > Settings > Account Details > License Testing`
3. Add email address in one of the list in `Google Play Console > Manage email lists`
4. Open this url with the email account you register:
- RND: `https://play.google.com/apps/testing/com.creedon.Nixplay.rnd`
- QA: `https://play.google.com/apps/testing/com.creedon.Nixplay.qa`
__Extra:__ The enabled countries might matters. Go to ` App (RND / QA) > Store presence > Pricing & distribution > Countries`, and enable the country.
### Subscription renewal timing
There is no way to manually unsubscribe in iOS sandbox, need to wait.
For Android, can unsubsribe in playstore.
Here are the info for the timings:
- https://help.apple.com/app-store-connect/#/dev7e89e149d
- https://developer.android.com/google/play/billing/billing_testing#testing-renewals
# Trouble Shooting
## xcrun: error: unable to find utility "instruments", not a developer
- if you have the following error message
``` sh
Found Xcode project TestProject.xcodeproj
xcrun: error: unable to find utility "instruments", not a developer
tool or in PATH
Command failed: xcrun instruments -s
xcrun: error: unable to find utility "instruments", not a developer
tool or in PATH
```
```
sudo xcode-select -s /Applications/Xcode.app
xcode-select --install
```
## Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
- if you have the following error
``` sh
Fetching json 1.8.1
Installing json 1.8.1 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
current directory: /usr/local/rvm/gems/ruby-2.3.4/gems/json-1.8.1/ext/json/ext/generator
/usr/local/rvm/rubies/ruby-2.3.4/bin/ruby -r ./siteconf20180608-11801-sqv5ra.rb extconf.rb
creating Makefile
current directory: /usr/local/rvm/gems/ruby-2.3.4/gems/json-1.8.1/ext/json/ext/generator
make "DESTDIR=" clean
current directory: /usr/local/rvm/gems/ruby-2.3.4/gems/json-1.8.1/ext/json/ext/generator
make "DESTDIR="
compiling generator.c
In file included from generator.c:1:
./../fbuffer/fbuffer.h:175:47: error: too few arguments provided to function-like macro invocation
VALUE result = rb_str_new(FBUFFER_PAIR(fb));
^
/usr/local/rvm/rubies/ruby-2.3.4/include/ruby-2.3.0/ruby/intern.h:798:9: note: macro 'rb_str_new' defined here
#define rb_str_new(str, len) __extension__ ( \
^
In file included from generator.c:1:
./../fbuffer/fbuffer.h:175:11: warning: incompatible pointer to integer conversion initializing 'VALUE' (aka 'unsigned
long') with an expression of type 'VALUE (const char *, long)' (aka 'unsigned long (const char *, long)')
[-Wint-conversion]
VALUE result = rb_str_new(FBUFFER_PAIR(fb));
^ ~~~~~~~~~~
1 warning and 1 error generated.
make: *** [generator.o] Error 1
make failed, exit code 2
Gem files will remain installed in /usr/local/rvm/gems/ruby-2.3.4/gems/json-1.8.1 for inspection.
Results logged to /usr/local/rvm/gems/ruby-2.3.4/extensions/x86_64-darwin-16/2.3.0/json-1.8.1/gem_make.out
An error occurred while installing json (1.8.1), and Bundler cannot continue.
Make sure that `gem install json -v '1.8.1' --source 'https://rubygems.org/'` succeeds before bundling.
```
run the following command to update gem file
```
https://github.com/flori/json/issues/229
```
## 'config.h' file not found
Copy and paste the following commands
```
cd ./node_modules/react-native
./scripts/ios-install-third-party.sh
cd third-party/glog-0.3.4
./configure
cd ../../../..
```
## clean the cache
```bash
npm run clean
```
you can run any one of the command individually
```
watchman watch-del-all
rm -rf node_modules
npm cache clean
rm -rf android/build/
rm -rf ./android/app/build/
rm -rf ./android/.gradle/
rm -rf ./android/.idea/
rm -rf ./ios/build
rm -rf ~/Library/Developer/Xcode/DerivedData && npm install
```
## Android subscription `item not found` in RND and QA
Check the `Setting up sandbox on Android` section above

View File

@@ -0,0 +1,8 @@
/* global jest: true */
jest.mock('@react-native-community/async-storage', () => ({
getItem: () => Promise.resolve(),
setItem: () => Promise.resolve(),
mergeItem: () => Promise.resolve(),
removeItem: () => Promise.resolve(),
}));

View File

@@ -0,0 +1,11 @@
/* global jest: true */
const geolocation = {
setRNConfiguration: jest.fn(() => {}),
getCurrentPosition: jest.fn(() => {}),
watchPosition: jest.fn(() => {}),
clearWatch: jest.fn(() => {}),
stopObserving: jest.fn(() => {}),
};
export default geolocation;

View File

@@ -0,0 +1,35 @@
/* global jest: false */
import { NativeModules } from 'react-native';
jest.mock('@react-native-community/google-signin', () => {
const mockGoogleSignin = require.requireActual('@react-native-community/google-signin');
mockGoogleSignin.GoogleSignin.hasPlayServices = () => Promise.resolve(true);
mockGoogleSignin.GoogleSignin.configure = () => Promise.resolve();
mockGoogleSignin.GoogleSignin.currentUserAsync = () => Promise.resolve({
name: 'name',
email: 'test@example.com',
// .... other user data
});
// ... and other functions you want to mock
return mockGoogleSignin;
});
NativeModules.RNGoogleSignin = {
BUTTON_SIZE_ICON: 0,
BUTTON_SIZE_STANDARD: 0,
BUTTON_SIZE_WIDE: 0,
BUTTON_COLOR_AUTO: 0,
BUTTON_COLOR_LIGHT: 0,
BUTTON_COLOR_DARK: 0,
SIGN_IN_CANCELLED: '0',
IN_PROGRESS: '1',
PLAY_SERVICES_NOT_AVAILABLE: '2',
SIGN_IN_REQUIRED: '3',
configure: jest.fn(),
currentUserAsync: jest.fn(),
};
export { NativeModules };

View File

@@ -0,0 +1,7 @@
/* global jest: true */
jest.mock('@react-native-community/netinfo', () => ({
getConnectionInfo: () => Promise.resolve(),
addEventListener: jest.fn(),
isConnected: jest.fn(),
}));

View File

@@ -0,0 +1,5 @@
/* global jest: true */
jest.mock('@react-native-community/push-notification-ios', () => ({
}));

View File

@@ -0,0 +1,9 @@
// __mocks__/react-native-config.js
export default {
FOO_BAR: 'baz',
FILE_SIZE_LIMIT: 20000000,
MAX_WIDTH: 1820,
MAX_HEIGHT: 1820,
DEFAULT_VIDEO_LENGTH: 15,
DEFAULT_MIN_VIDEO_LENGTH: 3,
};

View File

@@ -0,0 +1,16 @@
/* global jest: false */
import { NativeModules } from 'react-native';
NativeModules.RNCreedonImagepicker = {
assets: jest.fn(),
launchImageLibrary: jest.fn(),
cleanupTempFiles: jest.fn(),
addListener: jest.fn(),
};
const mockRNCreedonImagepicker = jest.genMockFromModule('react-native-creedon-imagepicker');
mockRNCreedonImagepicker.assets = () => Promise.resolve();
mockRNCreedonImagepicker.launchImageLibrary = () => Promise.resolve();
mockRNCreedonImagepicker.cleanupTempFiles = () => Promise.resolve();
module.exports = mockRNCreedonImagepicker;

View File

@@ -0,0 +1,12 @@
/* global jest: true */
const deviceInfo = {
getVersion: () => '1.0.0',
getBuildNumber: () => '100',
getUniqueID: () => '0',
getDeviceId: () => '0',
getModel: () => 'model',
getIPAddress: jest.fn(),
// add more methods as needed
};
export default deviceInfo;

View File

@@ -0,0 +1,14 @@
/* global jest: true */
const firebase = {
messaging: jest.fn(() => ({
hasPermission: jest.fn(() => new Promise(resolve => resolve(true))),
})),
analytics: jest.fn(() => ({
logEvent: jest.fn(),
setUserId: jest.fn(),
setUserProperties: jest.fn(),
})),
};
export default firebase;

View File

@@ -0,0 +1,44 @@
/* global jest: true */
jest.mock('react-native-fs', () => Promise.resolve({
mkdir: jest.fn(),
moveFile: jest.fn(),
copyFile: jest.fn(),
pathForBundle: jest.fn(),
pathForGroup: jest.fn(),
getFSInfo: jest.fn(),
getAllExternalFilesDirs: jest.fn(),
unlink: jest.fn(),
exists: jest.fn(),
stopDownload: jest.fn(),
resumeDownload: jest.fn(),
isResumable: jest.fn(),
stopUpload: jest.fn(),
completeHandlerIOS: jest.fn(),
readDir: jest.fn(),
readDirAssets: jest.fn(),
existsAssets: jest.fn(),
readdir: jest.fn(),
setReadable: jest.fn(),
stat: jest.fn(),
readFile: jest.fn(),
read: jest.fn(),
readFileAssets: jest.fn(),
hash: jest.fn(),
copyFileAssets: jest.fn(),
copyFileAssetsIOS: jest.fn(),
copyAssetsVideoIOS: jest.fn(),
writeFile: jest.fn(),
appendFile: jest.fn(),
write: jest.fn(),
downloadFile: jest.fn(),
uploadFiles: jest.fn(),
touch: jest.fn(),
MainBundlePath: jest.fn(),
CachesDirectoryPath: jest.fn(),
DocumentDirectoryPath: jest.fn(),
ExternalDirectoryPath: jest.fn(),
ExternalStorageDirectoryPath: jest.fn(),
TemporaryDirectoryPath: jest.fn(),
LibraryDirectoryPath: jest.fn(),
PicturesDirectoryPath: jest.fn(),
}));

View File

@@ -0,0 +1,14 @@
/* global jest: false */
jest.mock('NativeModules', () => ({
UIManager: {
RCTView: () => {},
},
RNGestureHandlerModule: {
attachGestureHandler: jest.fn(),
createGestureHandler: jest.fn(),
dropGestureHandler: jest.fn(),
updateGestureHandler: jest.fn(),
State: {},
Directions: {},
},
}));

View File

@@ -0,0 +1,35 @@
/* global jest: false */
import { NativeModules } from 'react-native';
jest.mock('@react-native-community/google-signin', () => {
const mockGoogleSignin = require.requireActual('@react-native-community/google-signin');
mockGoogleSignin.GoogleSignin.hasPlayServices = () => Promise.resolve(true);
mockGoogleSignin.GoogleSignin.configure = () => Promise.resolve();
mockGoogleSignin.GoogleSignin.currentUserAsync = () => Promise.resolve({
name: 'name',
email: 'test@example.com',
// .... other user data
});
// ... and other functions you want to mock
return mockGoogleSignin;
});
NativeModules.RNGoogleSignin = {
BUTTON_SIZE_ICON: 0,
BUTTON_SIZE_STANDARD: 0,
BUTTON_SIZE_WIDE: 0,
BUTTON_COLOR_AUTO: 0,
BUTTON_COLOR_LIGHT: 0,
BUTTON_COLOR_DARK: 0,
SIGN_IN_CANCELLED: '0',
IN_PROGRESS: '1',
PLAY_SERVICES_NOT_AVAILABLE: '2',
SIGN_IN_REQUIRED: '3',
configure: jest.fn(),
currentUserAsync: jest.fn(),
};
export { NativeModules };

View File

@@ -0,0 +1,15 @@
/* global jest: false */
jest.mock('react-native-haptic-feedback', () => ({
trigger: jest.fn(),
}));
// import { NativeModules } from 'react-native';
// NativeModules.RNReactNativeHapticFeedback = {
// trigger: jest.fn(),
// };
// const ReactNativeHapticFeedback = jest.genMockFromModule('react-native-haptic-feedback');
// module.exports = ReactNativeHapticFeedback;

View File

@@ -0,0 +1,5 @@
/* global jest: false */
export const setGenericPassword = () => jest.fn();
export const getGenericPassword = () => jest.fn(Promise.resolve(''));
export const resetGenericPassword = () => jest.fn();

View File

@@ -0,0 +1,60 @@
/* global jest: true */
// https://github.com/react-native-community/react-native-localize/blob/master/README.md#how-to-test-your-code
const getLocales = () => [
// you can choose / add the locales you want
{
countryCode: 'US',
languageTag: 'en',
languageCode: 'en',
isRTL: false,
},
{
countryCode: 'DE',
languageTag: 'de',
languageCode: 'de',
isRTL: false,
},
{
countryCode: 'JA',
languageTag: 'ja',
languageCode: 'ja',
isRTL: false,
},
];
// use a provided translation, or return undefined to test your fallback
const findBestAvailableLanguage = () => ({
languageTag: 'en',
isRTL: false,
});
const getNumberFormatSettings = () => ({
decimalSeparator: '.',
groupingSeparator: ',',
});
const getCalendar = () => 'gregorian'; // or 'japanese', 'buddhist'
const getCountry = () => 'US'; // the country code you want
const getCurrencies = () => ['USD', 'EUR']; // can be empty array
const getTemperatureUnit = () => 'celsius'; // or 'fahrenheit'
const getTimeZone = () => 'Europe/Paris'; // the timezone you want
const uses24HourClock = () => true;
const usesMetricSystem = () => true;
const addEventListener = jest.fn();
const removeEventListener = jest.fn();
export {
findBestAvailableLanguage,
getLocales,
getNumberFormatSettings,
getCalendar,
getCountry,
getCurrencies,
getTemperatureUnit,
getTimeZone,
uses24HourClock,
usesMetricSystem,
addEventListener,
removeEventListener,
};

View File

@@ -0,0 +1,10 @@
/* global jest: true */
const NixplayCore = jest.genMockFromModule('react-native-nixplay-core');
NixplayCore.setUploadSessionIdentifier = () => Promise.resolve();
NixplayCore.setApiURL = () => Promise.resolve();
NixplayCore.setBucketPrefix = () => Promise.resolve();
NixplayCore.setFileSizeLimit = () => Promise.resolve();
NixplayCore.startUpload = () => Promise.resolve();
export default NixplayCore;

View File

@@ -0,0 +1,13 @@
/* global jest: true */
const PushNotification = jest.genMockFromModule('react-native-push-notification');
PushNotification.configure = jest.fn();
PushNotification.onRegister = jest.fn();
PushNotification.onNotification = jest.fn();
PushNotification.addEventListener = jest.fn();
PushNotification.requestPermissions = jest.fn();
PushNotification.cancelAllLocalNotifications = jest.fn();
PushNotification.setApplicationIconBadgeNumber = jest.fn();
export default PushNotification;

View File

@@ -0,0 +1,3 @@
const NativeEventEmitter = {};
export default NativeEventEmitter;

View File

@@ -0,0 +1,14 @@
/* global jest: true */
const SafeArea = {};
SafeArea.getSafeAreaInsetsForRootView = () => ({
safeAreaInsets: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});
SafeArea.addEventListener = jest.fn();
export default SafeArea;

View File

@@ -0,0 +1,7 @@
/* global jest: true */
const ShareExtension = {
data: jest.fn(() => Promise.resolve({})),
close: jest.fn(() => Promise.resolve(true)),
};
export default ShareExtension;

View File

@@ -0,0 +1,8 @@
/* global jest: true */
const FilesystemStorage = {
getItem: jest.fn(() => Promise.resolve()),
setItem: jest.fn(() => Promise.resolve()),
};
export default FilesystemStorage;

View File

@@ -0,0 +1,159 @@
/* global describe: true, test:true, expect:true */
import { albums as albumsReducer, initialState } from '~/reducers/albums';
import _ from 'lodash';
import {
GET_ALBUMS,
CREATE_ALBUM,
GET_ALBUMS_SUCCESS,
GET_ALBUMS_ERROR,
CREATE_ALBUM_ERROR,
GET_ALBUM_CONTENT,
GET_ALBUM_CONTENT_SUCCESS,
GET_ALBUM_CONTENT_ERROR,
CREATE_ALBUM_SUCCESS,
LOGOUT_SUCCESS,
} from '~/store/actionTypes';
import {
DEFAULT_ALBUM_NAME,
DEFAULT_ALBUM_TYPE,
} from '~/store/contentTypes';
describe('albums reducer', () => {
test('CASE 1: should return initial state', () => {
expect(albumsReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with loading when type is GET_ALBUMS or CREATE_ALBUM', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
expect(albumsReducer(initialState, { type: GET_ALBUMS })).toEqual(expectedState);
expect(albumsReducer(initialState, { type: CREATE_ALBUM })).toEqual(expectedState);
});
test('CASE 3: should return albums with meta property when type is GET_ALBUMS_SUCCESS', () => {
const albums = [{ id: 1 }, { id: 2 }];
const stateResult = albumsReducer(undefined, { type: GET_ALBUMS_SUCCESS, payload: { albums } });
expect(stateResult).toHaveProperty('albums');
expect(stateResult.albums).toHaveLength(albums.length);
stateResult.albums.forEach(album => {
expect(album).toHaveProperty('meta');
});
});
test('CASE 5: should return correct state when type is GET_ALBUMS_ERROR or CREATE_ALBUM_ERROR', () => {
const albumErr = new Error('getAlbumsError');
const expectedState = { ...initialState, meta: { error: albumErr, loading: false } };
expect(albumsReducer(initialState, {
type: GET_ALBUMS_ERROR,
payload: { error: albumErr },
})).toEqual(expectedState);
expect(albumsReducer(initialState, {
type: CREATE_ALBUM_ERROR,
payload: { error: albumErr },
})).toEqual(expectedState);
});
test('CASE 6: should return state with album loading to TRUE when type is GET_ALBUM_CONTENT', () => {
const albums = [{ id: 1 }, { id: 2 }];
const payload = {
albumId: 1,
};
const originalState = { ...initialState, albums, meta: { error: '' } };
const stateResult = albumsReducer({ ...originalState, albums, meta: { error: '' } }, {
type: GET_ALBUM_CONTENT,
payload,
});
const expectedStateAlbums = [
{ id: 1, meta: { error: '', loading: true } },
{ id: 2 },
];
expect(stateResult.albums).toEqual(expectedStateAlbums);
});
test('CASE 7: should return state with album content when type is GET_ALBUM_CONTENT_SUCCESS', () => {
const albums = [{ id: 1 }, { id: 2 }];
const content = [
{ id: 5 }, // photo id
{ id: 4 }, // photo id
];
const payload = {
albumId: 1,
content,
};
const originalState = { ...initialState, albums, meta: { error: '' } };
const stateResult = albumsReducer({ ...originalState, albums, meta: { error: '' } }, {
type: GET_ALBUM_CONTENT_SUCCESS,
payload,
});
expect(stateResult.albums[0]).toHaveProperty('content', _.orderBy(content, ['id'], ['desc']));
// expect(stateResult.albums[0]).toHaveProperty('cover');
// expect(stateResult.albums[0]).toHaveProperty('thumbs');
});
test('CASE 8: should return state with album error when type is GET_ALBUM_CONTENT_ERROR', () => {
const albums = [{ id: 1 }, { id: 2 }];
const albumErr = new Error('getAlbumsContentError');
const payload = {
albumId: 1,
error: albumErr,
};
const originalState = { ...initialState, albums, meta: { error: '' } };
const stateResult = albumsReducer({ ...originalState, albums, meta: { error: '' } }, {
type: GET_ALBUM_CONTENT_ERROR,
payload,
});
const expectedStateAlbums = [
{ id: 1, meta: { error: albumErr, loading: false } },
{ id: 2 },
];
expect(stateResult.albums).toEqual(expectedStateAlbums);
});
test('CASE 9: should add album on CREATE_ALBUM_SUCCESS with meta', () => {
const albums = [{ id: 2 }, { id: 3 }];
const payload = {
album: { id: 1 },
};
const originalState = { ...initialState, albums, meta: { error: '' } };
const stateResult = albumsReducer({ albums, ...originalState, meta: { error: '' } }, {
type: CREATE_ALBUM_SUCCESS,
payload,
});
const expectedStateAlbums = [
{ id: 1, meta: { error: '', loading: false } },
{ id: 2 },
{ id: 3 },
];
expect(stateResult.albums).toEqual(expectedStateAlbums);
});
test('CASE 10: should empty the state on LOGOUT_SUCCESS', () => {
const existingState = [{ id: 1 }, { id: 2 }];
expect(albumsReducer(existingState, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 11: should return albums with defaultAlbumId property when type is GET_ALBUMS_SUCCESS', () => {
const albums = [{ id: 1, name: DEFAULT_ALBUM_NAME, type: DEFAULT_ALBUM_TYPE }, { id: 2 }];
const stateResult = albumsReducer(undefined, { type: GET_ALBUMS_SUCCESS, payload: { albums } });
expect(stateResult).toHaveProperty('albums');
expect(stateResult).toHaveProperty('defaultAlbumId');
expect(stateResult.albums).toHaveLength(albums.length);
expect(stateResult.defaultAlbumId).toEqual(1);
stateResult.albums.forEach(album => {
expect(album).toHaveProperty('meta');
});
});
test('CASE 12: should return albums with defaultAlbumId 0 property when type is GET_ALBUMS_SUCCESS', () => {
const albums = [{ id: 1 }, { id: 2 }];
const stateResult = albumsReducer(undefined, { type: GET_ALBUMS_SUCCESS, payload: { albums } });
expect(stateResult).toHaveProperty('albums');
expect(stateResult).toHaveProperty('defaultAlbumId');
expect(stateResult.albums).toHaveLength(albums.length);
expect(stateResult.defaultAlbumId).toEqual(0);
stateResult.albums.forEach(album => {
expect(album).toHaveProperty('meta');
});
});
});

View File

@@ -0,0 +1,71 @@
/* global describe: true, test:true, expect:true */
import { app } from '~/reducers/app';
import {
CHANGE_APP_ROOT,
UPDATE_APP_STATE,
UPDATE_SHOW_NOTIFICATION,
ACCEPT_TNC_LICENSE,
ACCEPT_TNC_LICENSE_SUCCESS,
ACCEPT_TNC_LICENSE_ERROR,
} from '~/store/actionTypes';
describe('app reducer', () => {
test('CASE 1: should return initial state', () => {
const expectedState = {
root: 'splash',
sharedPlaylistOnboardingTutorialState: 0,
location: 'United States',
appStatus: 'active',
hasLoggedInOnce: false,
showNotification: false,
fromSplashScreen: false,
ssid: '',
config: {},
};
expect(app(undefined, { type: 'Default' }))
.toEqual(expectedState);
});
test('CASE 2: should return the correct state when actionType is `CHANGE_APP_ROOT` and payload is valid root', () => {
const expectedState = { root: 'home' };
expect(app({}, { type: CHANGE_APP_ROOT, payload: { root: 'home' } }))
.toEqual(expectedState);
});
test('CASE 3: should return the initial state when actionType is `CHANGE_APP_ROOT` and payload is not valid root', () => {
const expectedState = {};
expect(app({}, { type: CHANGE_APP_ROOT, payload: { root: 'something else' } }))
.toEqual(expectedState);
});
test('CASE 8: default should return the appStatus active', () => {
const expectedState = { appStatus: 'active' };
expect(app({ appStatus: 'active' }, { type: UPDATE_APP_STATE, payload: 'active' }))
.toEqual(expectedState);
});
test('CASE 9: default should return the showNotification false', () => {
const expectedState = { showNotification: false };
expect(app({ showNotification: false }, { type: UPDATE_SHOW_NOTIFICATION, payload: false }))
.toEqual(expectedState);
});
test('CASE 10: default should return the state ACCEPT_TNC_LICENSE', () => {
const expectedState = { config: { user_accepted_tnc: true } };
expect(app({ config: { user_accepted_tnc: true } }, { type: ACCEPT_TNC_LICENSE, payload: false }))
.toEqual(expectedState);
});
test('CASE 11: default should return the state ACCEPT_TNC_LICENSE_SUCCESS', () => {
const expectedState = { config: { user_accepted_tnc: true } };
expect(app({ config: { user_accepted_tnc: true } }, { type: ACCEPT_TNC_LICENSE_SUCCESS, payload: false }))
.toEqual(expectedState);
});
test('CASE 12: default should return the config error', () => {
const expectedState = { config: {} };
expect(app({ config: {} }, { type: ACCEPT_TNC_LICENSE_ERROR, payload: false }))
.toEqual(expectedState);
});
});

View File

@@ -0,0 +1,122 @@
/* global describe: true, test:true, expect:true */
import {
avatar as avatarReducer,
initialState,
} from '~/reducers/avatar';
import {
GET_AVATAR_UPLOADPOLICY,
GET_AVATAR_UPLOADPOLICY_SUCCESS,
GET_AVATAR_UPLOADPOLICY_ERROR,
GET_AVATAR,
GET_AVATAR_SUCCESS,
GET_AVATAR_ERROR,
ACTIVATE_AVATAR,
ACTIVATE_AVATAR_SUCCESS,
ACTIVATE_AVATAR_ERROR,
DELETE_AVATAR,
DELETE_AVATAR_SUCCESS,
DELETE_AVATAR_ERROR,
} from '~/store/actionTypes';
describe('avatar reducer', () => {
test('CASE 1: should return initial state', () => {
expect(avatarReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_AVATAR', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3: should return state with meta set to loading when type is ACTIVATE_AVATAR', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = avatarReducer(initialState, {
type: ACTIVATE_AVATAR, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 4: should return state with meta set to loading when type is GET_AVATAR_UPLOADPOLICY', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR_UPLOADPOLICY, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 5: should return state with meta set to loading when type is DELETE_AVATAR', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = avatarReducer(initialState, {
type: DELETE_AVATAR, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6: should return state with error when type is GET_AVATAR_ERROR', () => {
const error = new Error('getAvatarError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6: should return state with error when type is ACTIVATE_AVATAR_ERROR', () => {
const error = new Error('activateAvatarError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = avatarReducer(initialState, {
type: ACTIVATE_AVATAR_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 8: should return state with error when type is GET_AVATAR_UPLOADPOLICY_ERROR', () => {
const error = new Error('getUploadPolicyAvatarError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR_UPLOADPOLICY_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 9: should return state with error when type is DELETE_AVATAR_ERROR', () => {
const error = new Error('deleteAvatarError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = avatarReducer(initialState, {
type: DELETE_AVATAR_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 10: should return state with meta set to loading when type is GET_AVATAR_SUCCESS', () => {
const payload = { uri: undefined };
const meta = { loading: false, error: '' };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR_SUCCESS, payload,
});
expect(stateResult.source).toEqual(payload);
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 11: should return state with meta set to loading when type is ACTIVATE_AVATAR_SUCCESS', () => {
const payload = { uri: undefined };
const meta = { loading: false, error: '' };
const stateResult = avatarReducer(initialState, {
type: ACTIVATE_AVATAR_SUCCESS, payload,
});
expect(stateResult.source).toEqual(payload);
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 12: should return state with meta set to loading when type is GET_AVATAR_UPLOADPOLICY_SUCCESS', () => {
const payload = { };
const meta = { loading: false, error: '' };
const stateResult = avatarReducer(initialState, {
type: GET_AVATAR_UPLOADPOLICY_SUCCESS, payload,
});
expect(stateResult.uploadPolicy).toEqual(payload);
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 13: should return state with meta set to loading when type is DELETE_AVATAR_SUCCESS', () => {
const payload = { };
const meta = { loading: false, error: '' };
const stateResult = avatarReducer(initialState, {
type: DELETE_AVATAR_SUCCESS, payload,
});
expect(stateResult).toEqual(initialState);
expect(stateResult).toHaveProperty('meta', meta);
});
});

View File

@@ -0,0 +1,74 @@
/* global describe: true, test:true, expect:true */
import { framePairing as framePairingReducer, initialState } from '~/reducers/framePairing';
import {
FRAME_DISCOVER_START,
FRAME_DISCOVER_STARTED_SUCCESS,
FRAME_DISCOVER_ERROR,
FRAME_DISCOVER_SUCCESS,
} from '~/store/actionTypes';
describe('framePairing reducer', () => {
test('CASE 1: should return initial state', () => {
expect(framePairingReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with loading when type is FRAME_DISCOVER_START or FRAME_DISCOVER_STARTED_SUCCESS', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
expect(framePairingReducer(initialState, {
type: FRAME_DISCOVER_START,
})).toEqual(expectedState);
expect(framePairingReducer(initialState, {
type: FRAME_DISCOVER_STARTED_SUCCESS,
})).toEqual(expectedState);
});
test('CASE 3: should return frames with meta property when type is FRAME_DISCOVER_SUCCESS', () => {
const services = [{
txt: {
serialNumber: '',
model: 1,
},
},
{
txt: {
serialNumber: '',
model: 2,
},
}];
const specs = {
1: {
name: '',
colors: [1, 2],
photo: [
'',
'',
],
},
2: {
name: '',
colors: [1, 2],
photo: [
'',
'',
],
},
};
const stateResult = framePairingReducer(undefined, {
type: FRAME_DISCOVER_SUCCESS,
payload: { services, specs },
});
expect(stateResult).toHaveProperty('frames');
expect(stateResult.frames).toHaveLength(services.length);
expect(stateResult).toHaveProperty('meta');
});
test('CASE 5: should return correct state when type is FRAME_DISCOVER_ERROR', () => {
const err = new Error('discoverFrameErr');
const expectedState = { ...initialState, meta: { error: err, loading: false } };
expect(framePairingReducer(initialState, {
type: FRAME_DISCOVER_ERROR,
payload: { error: err },
})).toEqual(expectedState);
});
});

View File

@@ -0,0 +1,438 @@
/* global describe: true, test:true, expect:true */
import { frames as framesReducer, initialState } from '~/reducers/frames';
import Factory from 'helper/factories/api';
import {
GET_FRAMES,
GET_FRAMES_SUCCESS,
GET_FRAMES_ERROR,
LOGOUT_SUCCESS,
GET_FRAME_STATE_SUCCESS,
GET_FRAME_STATE_ERROR,
SET_CURRENT_FRAME_PLAYLIST,
GET_FRAMES_STATUS_SUCCESS,
SET_FRAME_POWER_STATE_SUCCESS,
SET_CURRENT_FRAME,
UPDATE_FRAME_PLAYLIST_ASSIGNMENT,
GET_FRAME_STATE,
UNPAIR_FRAME,
UNPAIR_FRAME_SUCCESS,
UNPAIR_FRAME_ERROR,
} from '~/store/actionTypes';
import { FRAME_ORIENTATION, FRAME_POWER } from '~/store/frameConstants';
describe('frames reducer', () => {
test('CASE 1: should return initial state', () => {
expect(framesReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_FRAMES', () => {
const frames = [{ id: 1 }, { id: 2 }];
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = framesReducer(initialState, {
type: GET_FRAMES, payload: { frames },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3a: should return with removed frame with existing frameState when type is GET_FRAMES_SUCCESS', () => {
const frames = [{
id: 1,
}];
const currentState = {
framePreventPlaylistAssignChange: {},
frames: ['1ea790afd4c67c77', 'U6XYMRIEWBJ82RLL'].map((f, i) => (
{ id: i, ...Factory.build('FrameState', { frameId: f }) })),
};
const meta = { loading: false, error: '' };
const specs = { 'W08A-01': { colors: [], photo: {} } };
const stateResult = framesReducer(currentState, {
type: GET_FRAMES_SUCCESS, payload: { frames, specs },
});
expect(stateResult.frames[0]).toHaveProperty('meta', meta);
expect(stateResult.frames[0]).toHaveProperty('orientation');
expect(stateResult.frames[0]).toHaveProperty('power');
expect(stateResult.frames).toHaveLength(frames.length);
});
test('CASE 3b: should return with added frame with existing frameState and needUpdate as true when type is GET_FRAMES_SUCCESS', () => {
const frames = [
{ id: 1, softwareVersion: '5.0.0' },
{ id: 2 },
{ id: 3 },
];
const currentState = {
framePreventPlaylistAssignChange: {},
frames: ['1ea790afd4c67c77', 'U6XYMRIEWBJ82RLL'].map((f, i) => (
{ id: i, softwareVersion: '5.0.0', ...Factory.build('FrameState', { frameId: f }) })),
};
const meta = { loading: false, error: '' };
const specs = { 'W08A-01': { colors: [], photo: {}, minVersionSupported: '6.0.0' } };
const stateResult = framesReducer(currentState, {
type: GET_FRAMES_SUCCESS, payload: { frames, specs },
});
expect(stateResult.frames[0]).toHaveProperty('meta', meta);
expect(stateResult.frames[0]).toHaveProperty('orientation');
expect(stateResult.frames[0]).toHaveProperty('power');
expect(stateResult.frames[0]).toHaveProperty('needUpdate', true);
expect(stateResult.frames).toHaveLength(frames.length);
});
test('CASE 3c: should return with added frame with existing frameState and needUpdate set to false when type is GET_FRAMES_SUCCESS and checkEnabled is false', () => {
const frames = [
{ id: 1, softwareVersion: '5.0.0' },
{ id: 2 },
{ id: 3 },
];
const currentState = {
framePreventPlaylistAssignChange: {},
frames: ['1ea790afd4c67c77', 'U6XYMRIEWBJ82RLL'].map((f, i) => (
{ id: i, softwareVersion: '5.0.0', ...Factory.build('FrameState', { frameId: f }) })),
};
const meta = { loading: false, error: '' };
const specs = { 'W08A-01': { colors: [], photo: {}, minVersionSupported: '6.0.0' } };
const stateResult = framesReducer(currentState, {
type: GET_FRAMES_SUCCESS, payload: { frames, specs, checkEnabled: false },
});
expect(stateResult.frames[0]).toHaveProperty('meta', meta);
expect(stateResult.frames[0]).toHaveProperty('orientation');
expect(stateResult.frames[0]).toHaveProperty('power');
expect(stateResult.frames[0]).toHaveProperty('needUpdate', false);
expect(stateResult.frames).toHaveLength(frames.length);
});
test('CASE 5: should return state with error and loading = `false` when type is GET_FRAMES_ERROR', () => {
const error = new Error('getFramesError');
const expectedState = {
...initialState,
meta:
{ ...initialState.meta, error, loading: false },
};
const stateResult = framesReducer(initialState, {
type: GET_FRAMES_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6: should empty the state on LOGOUT_SUCCESS', () => {
const existingState = [{ id: 1 }, { id: 2 }];
expect(framesReducer(existingState, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 7a: should return state with frameState on GET_FRAME_STATE_SUCCESS', () => {
const playlistId = 321;
const existingState = { framePreventPlaylistPlayingChange: {}, framePreventPowerChange: {}, frames: [{ id: 1, frameId: '1ea790afd4c67c77', playlists: [{ id: playlistId }] }, { id: 2 }] };
const expectedState = {
framePreventPlaylistPlayingChange: {},
framePreventPowerChange: {},
frames: [
{
id: 1,
frameId: '1ea790afd4c67c77',
orientation: FRAME_ORIENTATION.LANDSCAPE,
slideshow: playlistId,
playlists: [{ id: playlistId }],
power: 'on',
},
{ id: 2 },
],
meta: { stateLoading: false, error: '' },
};
const frameState = Factory.build('FrameState', { frameId: existingState.frames[0].frameId, slideshow: { value: playlistId } });
expect(framesReducer(existingState, { type: GET_FRAME_STATE_SUCCESS, payload: { frameState } }))
.toEqual(expectedState);
});
test('CASE 7b: should return state with slideshow=`-1` on GET_FRAME_STATE_SUCCESS when playlist is no longer assigned', () => {
const playlistId = 321;
const existingState = { framePreventPlaylistPlayingChange: {}, framePreventPowerChange: {}, frames: [{ id: 1, frameId: '1ea790afd4c67c77', playlists: [] }, { id: 2 }] };
const expectedState = {
framePreventPlaylistPlayingChange: {},
framePreventPowerChange: {},
frames: [
{
id: 1,
frameId: '1ea790afd4c67c77',
orientation: FRAME_ORIENTATION.LANDSCAPE,
slideshow: -1,
playlists: [],
power: 'on',
},
{ id: 2 },
],
meta: { stateLoading: false, error: '' },
};
const frameState = Factory.build('FrameState', { frameId: existingState.frames[0].frameId, slideshow: { value: playlistId } });
expect(framesReducer(existingState, { type: GET_FRAME_STATE_SUCCESS, payload: { frameState } }))
.toEqual(expectedState);
});
test('CASE 7c: should return state with slideshow=`-1` on GET_FRAME_STATE_SUCCESS when frameState.slideshow is `null`', () => {
const playlistId = 'null';
const existingState = { framePreventPlaylistPlayingChange: {}, framePreventPowerChange: {}, frames: [{ id: 1, frameId: '1ea790afd4c67c77', playlists: [] }, { id: 2 }] };
const expectedState = {
framePreventPlaylistPlayingChange: {},
framePreventPowerChange: {},
frames: [
{
id: 1,
frameId: '1ea790afd4c67c77',
orientation: FRAME_ORIENTATION.LANDSCAPE,
slideshow: -1,
playlists: [],
power: 'on',
},
{ id: 2 },
],
meta: { stateLoading: false, error: '' },
};
const frameState = Factory.build('FrameState', { frameId: existingState.frames[0].frameId, slideshow: { value: playlistId } });
expect(framesReducer(existingState, { type: GET_FRAME_STATE_SUCCESS, payload: { frameState } }))
.toEqual(expectedState);
});
test('CASE 8: should return state with corrent frame playlist on SET_CURRENT_FRAME_PLAYLIST', () => {
const existingState = {
frames: [
{
id: 1,
frameId: '1ea790afd4c67c77',
orientation: FRAME_ORIENTATION.LANDSCAPE,
slideshow: 123,
power: 'on',
},
{ id: 2 },
],
};
const payload = {
framePk: 1,
playlistId: 234,
};
expect(framesReducer(existingState, { type: SET_CURRENT_FRAME_PLAYLIST, payload }).frames[0])
.toHaveProperty('slideshow', 234);
});
test('CASE 9: should return state with correct frameState on GET_FRAMES_STATUS_SUCCESS', () => {
const framesStatus = {
frames: [
{ lastSeen: null, lastConnected: null, framePk: 17544 },
{ lastSeen: 1535338881548, lastConnected: 1535338819374, framePk: 18122 },
{
connected: true, lastSeen: 1539065938946, lastConnected: 1539065965182, framePk: 18429,
},
{
connected: true, lastSeen: 1539066031822, lastConnected: 1539066063638, framePk: 18438,
},
],
};
const currentState = {
frames: [17544, 18122, 18429, 18438, 18433].map((f) => (
{ id: f, ...Factory.build('FrameState') })),
};
const payload = { framesStatus };
const newState = framesReducer(currentState, { type: GET_FRAMES_STATUS_SUCCESS, payload });
expect(newState.frames)
.toEqual(expect.arrayContaining(framesStatus.frames.map(() =>
expect.objectContaining({ online: expect.any(Boolean) }))));
});
test('CASE 10: should return state with frameState on SET_FRAME_POWER_STATE_SUCCESS', () => {
const emptyObject = {};
const currentState = {
framePreventPowerChange: { 1: true },
};
const payload = { framePk: 1, powerState: FRAME_POWER.OFF };
const newState = framesReducer(currentState, { type: SET_FRAME_POWER_STATE_SUCCESS, payload });
expect(newState)
.toHaveProperty('framePreventPowerChange', emptyObject);
});
test('CASE 11: should return currentFrameId set to the payload.id when action is SET_CURRENT_FRAME', () => {
const currentState = {
frames: [
{ id: 1 },
{ id: 2 },
],
currentFrameId: 2,
};
const payload = { id: 1 };
const newState = framesReducer(currentState, { type: SET_CURRENT_FRAME, payload });
expect(newState)
.toHaveProperty('currentFrameId', 1);
});
test('CASE 12: should return state with loading = `false` on GET_FRAME_STATE_ERROR', () => {
const currentState = {
framePreventPlaylistAssignChange: {},
framePreventPlaylistPlayingChange: {},
framePreventPowerChange: {},
frames: [],
currentFrameId: 0,
meta: {
loading: true,
stateLoading: false,
statusLoading: false,
error: '',
},
};
const newState = framesReducer(initialState, { type: GET_FRAME_STATE_ERROR });
expect(newState)
.toEqual(currentState);
});
test('CASE 13a: should return state with updated playlists when operation = `add` on UPDATE_FRAME_PLAYLIST_ASSIGNMENT', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { loading: false, error: '' },
};
const playlistIds = [103];
const payload = { frameIds, playlistIds, operation: 'add' };
const newState = framesReducer(
currentState,
{ type: UPDATE_FRAME_PLAYLIST_ASSIGNMENT, payload },
);
expect(newState.frames)
.toEqual(expect.arrayContaining([
expect.objectContaining({
playlists: [
{ id: expect.any(Number) },
{ id: expect.any(Number) },
{ id: expect.any(Number) },
],
})]));
});
test('CASE 13b: should return state with removed playlists when operation = `remove` on UPDATE_FRAME_PLAYLIST_ASSIGNMENT', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { loading: false, error: '' },
};
const playlistIds = [currentState.frames[0].playlists[0].id];
const payload = { frameIds, playlistIds, operation: 'remove' };
const newState = framesReducer(
currentState,
{ type: UPDATE_FRAME_PLAYLIST_ASSIGNMENT, payload },
);
expect(newState.frames)
.toEqual(expect.arrayContaining([
expect.objectContaining({
playlists: [
{ id: expect.any(Number) },
],
})]));
});
test('CASE 13c: should return state with new playlists when operation is not specified on UPDATE_FRAME_PLAYLIST_ASSIGNMENT', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { loading: false, error: '' },
};
const playlistIds = [111];
const payload = { frameIds, playlistIds };
const newState = framesReducer(
currentState,
{ type: UPDATE_FRAME_PLAYLIST_ASSIGNMENT, payload },
);
expect(newState.frames)
.toEqual(expect.arrayContaining([
expect.objectContaining({
playlists: [
{ id: playlistIds[0] },
],
})]));
});
test('CASE 14: should return state loading = `false` when action is GET_FRAME_STATE', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { error: '' },
};
const newState = framesReducer(
currentState,
{ type: GET_FRAME_STATE },
);
expect(newState.meta)
.toEqual({ stateLoading: true, error: '' });
});
test('CASE 15: should return state loading = `true` when action is UNPAIR_FRAME', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { error: '' },
};
const newState = framesReducer(
currentState,
{ type: UNPAIR_FRAME },
);
expect(newState.meta)
.toEqual({ loading: true, error: '' });
});
test('CASE 16: should return state loading = `false` when action is UNPAIR_FRAME_SUCCESS', () => {
const frameIds = [17544, 18122, 18429, 18438, 18433];
const specs = { 'W08A-01': { colors: [], photo: {} } };
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
specs,
currentFrameId: 0,
meta: { error: '', loading: false },
};
const newState = framesReducer(
currentState,
{ type: UNPAIR_FRAME_SUCCESS, payload: { framePk: 0 } },
);
expect(newState.meta)
.toEqual({ loading: false, error: '' });
});
test('CASE 17: should return state loading = `false` error message when action is UNPAIR_FRAME_ERROR', () => {
const error = 'unpair frame error';
const frameIds = [17544, 18122, 18429, 18438, 18433];
const currentState = {
frames: frameIds.map((f) => (
Factory.build('Frame', { id: f })
)),
currentFrameId: 0,
meta: { error: '' },
};
const newState = framesReducer(
currentState,
{ type: UNPAIR_FRAME_ERROR, payload: { error } },
);
expect(newState.meta)
.toEqual({ loading: false, error });
});
});

View File

@@ -0,0 +1,83 @@
/* global describe: true, test:true, expect:true */
import { initialState, friendActivities } from '~/reducers/friendActivities';
import {
GET_COMMENTS_SUCCESS,
DELETE_ACTIVITY_PHOTO,
DELETE_ACTIVITY_PHOTO_ERROR,
DELETE_ACTIVITY_PHOTO_SUCCESS,
} from '~/store/actionTypes';
import Factory from 'helper/factories/api';
describe('friendActivities reducer', () => {
test('CASE 1: should return initial state', () => {
expect(friendActivities(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2a: should return with comments when comment is related to the friend and actionType is GET_COMMENTS_SUCCESS', () => {
const friend = { username: 'friend@mynixplay.com' };
const originalState = {
friend,
nextTimestamp: 0,
activities: [],
};
const comments = Factory.buildList('Comment', 2, { friend: friend.username });
const payload = { comments };
const newState = friendActivities(originalState, { type: GET_COMMENTS_SUCCESS, payload });
expect(newState.mediaViewer.comments)
.toHaveLength(2);
});
test('CASE 2b: should return without comments when comment is not related to the friend and actionType is GET_COMMENTS_SUCCESS', () => {
const friend = { username: 'friend@mynixplay.com' };
const originalState = {
friend,
nextTimestamp: 0,
activities: [],
};
const comments = Factory.buildList('Comment', 2, { friend: 'other@mynixplay.com' });
const payload = { comments };
const newState = friendActivities(originalState, { type: GET_COMMENTS_SUCCESS, payload });
expect(newState.mediaViewer.comments)
.toHaveLength(0);
});
test('CASE 3: friendActivities should update meta loading state when type is DELETE_ACTIVITY_PHOTO', (s3Key = '', albumId = '', timestamp = '', friend = '') => {
const existingState = { ...initialState, meta: { loading: true, error: '' } };
const stateResult = friendActivities(existingState, {
type: DELETE_ACTIVITY_PHOTO,
payload: {
s3Key,
albumId,
timestamp,
friend,
},
});
expect(stateResult).toEqual(existingState);
});
test('CASE 4: friendActivities should update meta loading state when type is DELETE_ACTIVITY_PHOTO_ERROR', () => {
const existingState = { ...initialState, actions: { submitting: false, error: '' } };
const stateResult = friendActivities(existingState, {
type: DELETE_ACTIVITY_PHOTO_ERROR,
payload: {
error: '',
},
});
expect(stateResult).toEqual(existingState);
});
test('CASE 5: friendActivities should update meta loading state when type is DELETE_ACTIVITY_PHOTO_SUCCESS', (s3Key = '', albumId = '', timestamp = '', friend = '') => {
const existingState = { ...initialState, meta: { loading: false, error: '' } };
const stateResult = friendActivities(existingState, {
type: DELETE_ACTIVITY_PHOTO_SUCCESS,
payload: {
s3Key,
albumId,
timestamp,
friend,
},
});
expect(stateResult).toEqual(existingState);
});
});

View File

@@ -0,0 +1,187 @@
/* global describe: true, test:true, expect:true */
import {
friends as friendsReducer,
initialState,
initAddFriend,
addFriend as addFriendReducer,
} from '~/reducers/friends';
import {
GET_FRIENDS,
GET_FRIENDS_SUCCESS,
GET_FRIENDS_ERROR,
UPDATE_SEEN_TIMESTAMP_SUCCESS,
LOGOUT_SUCCESS,
UPDATE_ADD_FRIEND,
ACCEPT_FRIEND_INVITE_SUCCESS,
ACCEPT_FRIEND_INVITE_ERROR,
REJECT_FRIEND_INVITE_SUCCESS,
REJECT_FRIEND_INVITE_ERROR,
RESET_ADD_FRIEND_ERROR,
RESET_ADD_FRIEND_PLAYLIST,
} from '~/store/actionTypes';
describe('friends reducer', () => {
test('CASE 1: should return initial state', () => {
expect(friendsReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_FRIENDS', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = friendsReducer(initialState, {
type: GET_FRIENDS, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3: should return state with meta set to loading when type is GET_FRIENDS_SUCCESS', () => {
const friends = [{ username: '1', activity: { timestamp: 0 } }, { username: '2', activity: { timestamp: 0 } }];
const invites = [{ requester: '1' }, { requester: '2' }];
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, {
type: GET_FRIENDS_SUCCESS, payload: { friends: { friends, invites } },
});
expect(stateResult.friends).toEqual([]);
expect(stateResult.invites[0]).toHaveProperty('meta', meta);
});
test('CASE 4: should return state with error when type is GET_FRIENDS_ERROR', () => {
const error = new Error('getFriendsError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = friendsReducer(initialState, {
type: GET_FRIENDS_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 5: should return state when type is UPDATE_SEEN_TIMESTAMP_SUCCESS', () => {
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, { type: UPDATE_SEEN_TIMESTAMP_SUCCESS });
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 6: should return state with loading = false when type is ACCEPT_FRIEND_INVITE_SUCCESS', () => {
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, { type: ACCEPT_FRIEND_INVITE_SUCCESS });
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 7: should return state with loading = false when type is ACCEPT_FRIEND_INVITE_ERROR', () => {
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, { type: ACCEPT_FRIEND_INVITE_ERROR, payload: { error: '' } });
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 8: should return state with loading = false when type is REJECT_FRIEND_INVITE_SUCCESS', () => {
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, { type: REJECT_FRIEND_INVITE_SUCCESS });
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 9: should return state with loading = false when type is REJECT_FRIEND_INVITE_ERROR', () => {
const meta = { loading: false, error: '' };
const stateResult = friendsReducer(initialState, { type: REJECT_FRIEND_INVITE_ERROR, payload: { error: '' } });
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 10: should empty the state on LOGOUT_SUCCESS', () => {
const existingState = {};
expect(friendsReducer(existingState, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 11: addFriend should return initial state', () => {
expect(addFriendReducer(undefined, { type: 'Default' }))
.toEqual(initAddFriend);
});
test('CASE 12: addFriend should return state when type is UPDATE_ADD_FRIEND', () => {
const stateResult = friendsReducer(initAddFriend, {
type: UPDATE_ADD_FRIEND,
payload: {
lookupKey: '',
friendName: '',
playlistId: -1,
hasError: '',
isNewPlaylist: 0,
popCount: 2,
},
});
expect(stateResult).toEqual(initAddFriend);
});
test('CASE 13: addFriend should update error state when type is UPDATE_ADD_FRIEND', (hasError = '') => {
const {
lookupKey,
friendName,
playlistId,
isNewPlaylist,
} = initAddFriend;
const stateResult = friendsReducer(initAddFriend, {
type: RESET_ADD_FRIEND_ERROR,
payload: {
lookupKey,
friendName,
playlistId,
hasError,
isNewPlaylist,
},
});
expect(stateResult).toEqual(initAddFriend);
});
test('CASE 14: addFriend should update error state when type is RESET_ADD_FRIEND_ERROR', (hasError = '') => {
const stateResult = friendsReducer(initAddFriend, {
type: RESET_ADD_FRIEND_ERROR,
payload: { hasError },
});
expect(stateResult).toEqual(initAddFriend);
});
test('CASE 15: addFriend should update error state when type is RESET_ADD_FRIEND_PLAYLIST', (isNewPlaylist = 0) => {
const stateResult = friendsReducer(initAddFriend, {
type: RESET_ADD_FRIEND_PLAYLIST,
payload: { isNewPlaylist },
});
expect(stateResult).toEqual(initAddFriend);
});
test('CASE 16: Badge should be true when a new friend invite has been added to state GET_FRIENDS_SUCCESS', () => {
const existingState = { ...initialState, invites: [{ requester: '1' }, { requester: '2' }] };
const friends = {
invites: [{ requester: '1' }, { requester: '3' }],
friends: [{ username: '1', activity: { timestamp: 0 } }],
};
const stateResult = friendsReducer(existingState, {
type: GET_FRIENDS_SUCCESS,
payload: { friends },
});
expect(stateResult).toHaveProperty('isBadged', true);
});
test('CASE 17: Badge should be true when a new friend invite GET_FRIENDS_SUCCESS', () => {
const existingState = { ...initialState, invites: [{ requester: '1' }, { requester: '2' }] };
const friends = {
invites: [{ requester: '1' }],
friends: [{ username: '1', activity: { timestamp: 0 } }],
};
const stateResult = friendsReducer(existingState, {
type: GET_FRIENDS_SUCCESS,
payload: { friends },
});
expect(stateResult).toHaveProperty('isBadged', false);
});
test('CASE 18: Badge should be true when a friend gets a new activity', () => {
const existingState = { ...initialState, invites: [], friends: [{ username: '1', activity: { timestamp: 1 } }] };
const friends = {
invites: [],
friends: [{ username: '1', activity: { timestamp: 2 } }],
};
const stateResult = friendsReducer(existingState, {
type: GET_FRIENDS_SUCCESS,
payload: { friends },
});
expect(stateResult).toHaveProperty('isBadged', false);
});
});

View File

@@ -0,0 +1,37 @@
/* global describe: true, test:true, expect:true */
import { fujifilmCategories as fujifilmCategoriesReducer, initialState } from '~/reducers/fujifilmCategories';
import {
GET_FUJIFILM_CHILD_CATEGORY_PROPDUCTS_SUCCESS,
} from '~/store/actionTypes';
describe('fujifilmCategories reducer', () => {
test('CASE 1: should return initial state', () => {
expect(fujifilmCategoriesReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return correct state when type is GET_FUJIFILM_CHILD_CATEGORY_PROPDUCTS_SUCCESS', () => {
const categoryId = 1;
const products = [{
ProductCode: '1',
}, {
ProductCode: '2',
}];
const serviceType = 'MailOrder';
const childCategoryProducts = {
1: [{
ProductCode: '1',
}, {
ProductCode: '2',
}],
};
const stateResult = fujifilmCategoriesReducer(initialState, {
type: GET_FUJIFILM_CHILD_CATEGORY_PROPDUCTS_SUCCESS,
payload: { categoryId, products, serviceType },
});
expect(stateResult).toHaveProperty('childCategoryProducts');
expect(stateResult.childCategoryProducts).toEqual(childCategoryProducts);
expect(stateResult).toHaveProperty('meta');
});
});

View File

@@ -0,0 +1,335 @@
/* global describe: true, test:true, expect:true */
import { google as googleReducer, initialState } from '~/reducers/google';
import {
SET_GOOGLE_INFO,
GET_GOOGLE_TOKEN,
GET_GOOGLE_TOKEN_SUCCESS,
LOGOUT_SUCCESS,
LOGOUT_FROM_GOOGLE_SUCCESS,
GET_GOOGLE_TOKEN_ERROR,
GET_GOOGLE_ALBUMS,
GET_GOOGLE_CATEGORIES,
GET_GOOGLE_ALBUMS_ERROR,
GET_GOOGLE_CATEGORIES_ERROR,
GET_GOOGLE_ALBUMS_SUCCESS,
GET_GOOGLE_CATEGORIES_SUCCESS,
GET_DYNAMIC_PLAYLISTS_ERROR,
GET_DYNAMIC_PLAYLISTS_SUCCESS,
GET_GOOGLE_ALBUM_CONTENT_SUCCESS,
} from '~/store/actionTypes';
import Factory from 'helper/factories/google';
describe('google reducer', () => {
test('CASE 1: should return initial state', () => {
expect(googleReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with Google info set when type is SET_GOOGLE_INFO', () => {
const googleInfo = {
email: 'user@gmail.com',
photo: 'https://lh4.googleusercontent.com/photo.jpg',
idToken: '{idToken}',
id: '{id}',
familyName: '{lastName}',
serverAuthCode: '{serverAuthCode}',
accessToken: '{accessToken}',
givenName: '{firstName}',
accessTokenExpirationDate: 3260.7057960033417,
name: '{firstName} {lastName}',
};
const expectedState = { ...initialState, auth: googleInfo };
const stateResult = googleReducer(initialState, {
type: SET_GOOGLE_INFO, payload: googleInfo,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3: should return state with meta set when type is GET_GOOGLE_TOKEN', () => {
const expectedState = initialState;
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_TOKEN,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 4: should return state with Google set when type is GET_GOOGLE_TOKEN_SUCCESS', () => {
const googleInfo = {
email: 'user@gmail.com',
photo: 'https://lh4.googleusercontent.com/photo.jpg',
idToken: '{idToken}',
id: '{id}',
familyName: '{lastName}',
serverAuthCode: '{serverAuthCode}',
accessToken: '{accessToken}',
givenName: '{firstName}',
accessTokenExpirationDate: 3260.7057960033417,
name: '{firstName} {lastName}',
};
const googleToken = { googleId: googleInfo.email, valid: true };
const expectedState = {
...initialState,
auth: googleInfo,
};
const stateResult = googleReducer({ ...initialState, auth: googleInfo }, {
type: GET_GOOGLE_TOKEN_SUCCESS,
payload: { googleToken },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 5: should return state with Google unset when type is GET_GOOGLE_TOKEN_SUCCESS and server Token is invalid', () => {
const googleInfo = {
email: 'user@gmail.com',
photo: 'https://lh4.googleusercontent.com/photo.jpg',
idToken: '{idToken}',
id: '{id}',
familyName: '{lastName}',
serverAuthCode: '{serverAuthCode}',
accessToken: '{accessToken}',
givenName: '{firstName}',
accessTokenExpirationDate: 3260.7057960033417,
name: '{firstName} {lastName}',
};
const googleToken = { googleId: googleInfo.email, valid: false };
const expectedState = {
...initialState,
auth: {},
};
const stateResult = googleReducer({ ...initialState, auth: googleInfo }, {
type: GET_GOOGLE_TOKEN_SUCCESS,
payload: { googleToken },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6: should return state with Google unset when type is GET_GOOGLE_TOKEN_SUCCESS and server Token is valid but googleId does not match the email', () => {
const googleInfo = {
email: 'user@gmail.com',
photo: 'https://lh4.googleusercontent.com/photo.jpg',
idToken: '{idToken}',
id: '{id}',
familyName: '{lastName}',
serverAuthCode: '{serverAuthCode}',
accessToken: '{accessToken}',
givenName: '{firstName}',
accessTokenExpirationDate: 3260.7057960033417,
name: '{firstName} {lastName}',
};
const googleToken = { googleId: 'other@gmail.com', valid: true };
const expectedState = {
...initialState,
auth: {},
};
const stateResult = googleReducer({ ...initialState, auth: googleInfo }, {
type: GET_GOOGLE_TOKEN_SUCCESS,
payload: { googleToken },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 7: should return state with meta loading set to false when actionType is GET_GOOGLE_TOKEN_ERROR', () => {
const expectedState = initialState;
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_TOKEN_ERROR,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 7: should return initialState when actionType is LOGOUT_FROM_GOOGLE_SUCCESS', () => {
const stateResult = googleReducer(initialState, {
type: LOGOUT_FROM_GOOGLE_SUCCESS,
});
expect(stateResult).toEqual(initialState);
});
test('CASE 8: should return initialState when actionType is LOGOUT_SUCCESS', () => {
const stateResult = googleReducer(initialState, {
type: LOGOUT_SUCCESS,
});
expect(stateResult).toEqual(initialState);
});
test('CASE 9: should return loading for albums set to true when actionType is GET_GOOGLE_ALBUMS', () => {
const expectedState = {
...initialState,
meta: { loading: { ...initialState.meta.loading, albums: true } },
};
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_ALBUMS,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 10: should return loading for categories set to true when actionType is GET_GOOGLE_CATEGORIES', () => {
const expectedState = {
...initialState,
meta: { loading: { ...initialState.meta.loading, categories: true } },
};
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_CATEGORIES,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 11: should return loading for categories set to false when actionType is GET_GOOGLE_ALBUMS_ERROR', () => {
const expectedState = {
...initialState,
meta: { loading: { ...initialState.meta.loading, categories: false } },
};
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_ALBUMS_ERROR,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 12: should return loading for categories set to false when actionType is GET_GOOGLE_CATEGORIES_ERROR', () => {
const expectedState = {
...initialState,
meta: { loading: { ...initialState.meta.loading, categories: false } },
};
const stateResult = googleReducer(initialState, {
type: GET_GOOGLE_CATEGORIES_ERROR,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 12: should return albums with alreadySynced set correctly when actionType is GET_GOOGLE_ALBUMS_SUCCESS', () => {
const googleId = 'user@gmail.com';
const playlists = Factory.buildList('DynamicPlaylist', 4, { googleId });
const albums = Factory.buildList('GoogleAlbum', 2);
const prevState = {
auth: { email: googleId },
categories: [],
albums: [],
allPhotos: {},
playlists,
meta: { loading: { albums: false, categories: false } },
};
const expectedState = {
...prevState,
meta: { loading: { ...initialState.meta.loading, categories: false } },
albums: [
{ title: 'Album 1', id: 'album1', alreadySynced: true },
{ title: 'Album 2', id: 'album2', alreadySynced: true },
],
allPhotos: {},
};
const stateResult = googleReducer(prevState, {
type: GET_GOOGLE_ALBUMS_SUCCESS,
payload: albums,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 13: should return categories with alreadySynced set correctly when actionType is GET_GOOGLE_CATEGORIES_SUCCESS', () => {
const googleId = 'user@gmail.com';
const playlists = Factory.buildList('DynamicPlaylist', 4, { googleId });
const categories = [
{ id: 'ANIMAL' },
{ id: 'PEOPLE' },
{ id: 'LANDSCAPE' },
];
const prevState = {
auth: { email: googleId },
categories: [],
albums: [],
playlists,
meta: { loading: { albums: false, categories: false } },
};
const expectedState = {
...prevState,
meta: { loading: { ...initialState.meta.loading, categories: false } },
categories: [
{ id: 'ANIMAL', alreadySynced: true },
{ id: 'PEOPLE', alreadySynced: true },
{ id: 'LANDSCAPE' },
],
allPhotos: { alreadySynced: false },
};
const stateResult = googleReducer(prevState, {
type: GET_GOOGLE_CATEGORIES_SUCCESS,
payload: { categories, allPhotos: {} },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 14: should return alreadySynced set correctly when actionType is GET_DYNAMIC_PLAYLISTS_SUCCESS', () => {
const googleId = 'user@gmail.com';
const playlists = Factory.buildList('DynamicPlaylist', 4, { googleId });
const albums = Factory.buildList('GoogleAlbum', 2);
const categories = [
{ id: 'ANIMAL' },
{ id: 'PEOPLE' },
{ id: 'LANDSCAPE' },
];
const prevState = {
auth: { email: googleId },
categories,
albums,
playlists: [],
meta: { loading: { albums: false, categories: false } },
allPhotos: {},
};
const expectedState = {
...prevState,
meta: { loading: { albums: false, categories: false } },
categories: [
{ id: 'ANIMAL', alreadySynced: true },
{ id: 'PEOPLE', alreadySynced: true },
{ id: 'LANDSCAPE' },
],
albums,
playlists,
allPhotos: { alreadySynced: false },
};
const stateResult = googleReducer(prevState, {
type: GET_DYNAMIC_PLAYLISTS_SUCCESS,
payload: { google: playlists },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 15: should return categories with alreadySynced set correctly when actionType is GET_DYNAMIC_PLAYLISTS_ERROR', () => {
const stateResult = googleReducer(initialState, {
type: GET_DYNAMIC_PLAYLISTS_ERROR,
});
expect(stateResult).toEqual(initialState);
});
test('CASE 16: should return album with mediaItems set correctly when actionType is GET_GOOGLE_ALBUM_CONTENT_SUCCESS', () => {
const googleId = 'user@gmail.com';
const playlists = Factory.buildList('DynamicPlaylist', 4, { googleId });
const albums = Factory.buildList('GoogleAlbum', 1);
const prevState = {
auth: { email: googleId },
albums,
playlists,
meta: { loading: { albums: false, categories: false } },
};
const payload = {
title: albums[0].title,
id: albums[0].id,
mediaItems: [
{ id: 'AGj1epUmh4wiG3g2bKOKDoclVoRTsE-_STHFhWdUdhvR142wa0wx4-y-rb9mYq2363Cr0_Gid19iJw4' },
{ id: 'AGj1epWNpu29AtPtEyX3ICRDLOcxyUxhCt45UG1C4swn1me84xFxculsQDCJaclsC3mBeX54MBM7HL8' },
{ id: 'AGj1epWAvBQE-WStq6wzHkv-4oW28J154cd5SU4KDCUTL-XB9hJdxyT7GCaOLett4qVyRXFFmYHpYJ4' },
],
};
const expectedState = {
...prevState,
meta: { loading: { albums: false, categories: false } },
albums: [payload],
playlists,
};
const stateResult = googleReducer(prevState, {
type: GET_GOOGLE_ALBUM_CONTENT_SUCCESS, payload,
});
expect(stateResult).toEqual(expectedState);
});
});

View File

@@ -0,0 +1,17 @@
/* global describe: true, test:true, expect:true */
import { locale } from '~/reducers/locale';
import { CHANGE_LOCALE } from '~/store/actionTypes';
describe('locale reducer', () => {
test('CASE 1: should return initial state', () => {
const expectedState = { locale: 'en' };
expect(locale(undefined, { type: 'Default' }))
.toEqual(expectedState);
});
test('CASE 2: should return correct state when action is `CHANGE_LOCALE`', () => {
const expectedState = { locale: 'de' };
expect(locale(undefined, { type: CHANGE_LOCALE, payload: { locale: 'de' } }))
.toEqual(expectedState);
});
});

View File

@@ -0,0 +1,121 @@
/* global describe: true, test:true, expect:true */
import { photoPrint as photoPrintReducer, initialState } from '~/reducers/photoPrint';
import {
PLACE_PHOTO_PRINT_ORDER,
PLACE_PHOTO_PRINT_ORDER_SUCCESS,
PLACE_PHOTO_PRINT_ORDER_ERROR,
SET_PHOTOS_TO_QUEUE,
SET_PHOTOS_TO_QUEUE_SUCCESS,
SET_PHOTOS_TO_QUEUE_ERROR,
REMOVE_PHOTO_FROM_QUEUE,
REMOVE_PHOTO_FROM_QUEUE_SUCCESS,
REMOVE_PHOTO_FROM_QUEUE_ERROR,
} from '~/store/actionTypes';
describe('photoPrint reducer', () => {
test('CASE 1: should return initial state', () => {
expect(photoPrintReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with loading when type is PLACE_PHOTO_PRINT_ORDER, SET_PHOTOS_TO_QUEUE or REMOVE_PHOTO_FROM_QUEUE', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
expect(photoPrintReducer(initialState, { type: PLACE_PHOTO_PRINT_ORDER })).toEqual(expectedState);
expect(photoPrintReducer(initialState, { type: SET_PHOTOS_TO_QUEUE })).toEqual(expectedState);
expect(photoPrintReducer(initialState, { type: REMOVE_PHOTO_FROM_QUEUE })).toEqual(expectedState);
});
test('CASE 4: should return correct state when type is PLACE_PHOTO_PRINT_ORDER_SUCCESS', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: false }, orderNumbers: ['1234567'] };
expect(photoPrintReducer(
initialState,
{
type: PLACE_PHOTO_PRINT_ORDER_SUCCESS,
payload: {
orderNumber: '1234567',
},
},
)).toEqual(expectedState);
});
test('CASE 5: should return correct state when type is REMOVE_PHOTO_FROM_QUEUE_SUCCESS', () => {
const images = ['image1', 'image2', 'image3'];
const expectedState = {
...initialState,
images,
count: 3,
meta: {
...initialState.meta,
loading: false,
},
};
expect(photoPrintReducer(
initialState,
{
type: REMOVE_PHOTO_FROM_QUEUE_SUCCESS,
payload: {
images,
},
},
)).toEqual(expectedState);
});
test('CASE 6: should return correct state when type is SET_PHOTOS_TO_QUEUE_SUCCESS', () => {
const images = ['image1', 'image2', 'image3'];
const expectedState = {
...initialState,
images,
count:
3,
meta:
{
...initialState.meta,
loading:
false,
},
};
expect(photoPrintReducer(
initialState,
{
type: SET_PHOTOS_TO_QUEUE_SUCCESS,
payload: {
images,
},
},
)).toEqual(expectedState);
});
test('CASE 7: should return error state when type is PLACE_PHOTO_PRINT_ORDER_ERROR', () => {
const error = new Error('Error');
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: false, error } };
expect(photoPrintReducer(
initialState,
{
type: PLACE_PHOTO_PRINT_ORDER_ERROR,
payload: { error },
},
)).toEqual(expectedState);
});
test('CASE 8: should return error state when type is SET_PHOTOS_TO_QUEUE_ERROR', () => {
const error = new Error('Error');
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: false, error } };
expect(photoPrintReducer(
initialState,
{
type: SET_PHOTOS_TO_QUEUE_ERROR,
payload: { error },
},
)).toEqual(expectedState);
});
test('CASE 9: should return error state when type is REMOVE_PHOTO_FROM_QUEUE_ERROR', () => {
const error = new Error('Error');
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: false, error } };
expect(photoPrintReducer(
initialState,
{
type: REMOVE_PHOTO_FROM_QUEUE_ERROR,
payload: { error },
},
)).toEqual(expectedState);
});
});

View File

@@ -0,0 +1,339 @@
/* global describe: true, test:true, expect:true */
import { playlists as playlistsReducer, initialState } from '~/reducers/playlists';
import {
GET_PLAYLISTS,
GET_PLAYLISTS_SUCCESS,
GET_PLAYLISTS_ERROR,
GET_PLAYLIST_CONTENT,
GET_PLAYLIST_CONTENT_SUCCESS,
GET_PLAYLIST_CONTENT_ERROR,
LOGOUT_SUCCESS,
CREATE_DYNAMIC_PLAYLIST_SUCCESS,
GET_DYNAMIC_PLAYLISTS_SUCCESS,
UPLOAD_ITEM_STARTED,
UPLOAD_ITEMS_SUCCESS,
UPLOAD_ITEMS_ERROR,
PLAYLIST_PROCESSING_STARTED,
PLAYLIST_PROCESSING_DONE,
UPLOAD_SUCCESS,
UPLOAD_ALL_CANCELLED,
} from '~/store/actionTypes';
import Factory from 'helper/factories/api';
describe('playlists reducer', () => {
test('CASE 1: should return initial state', () => {
expect(playlistsReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_PLAYLISTS', () => {
const playlists = [{ id: 1 }, { id: 2 }];
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: false } };
const stateResult = playlistsReducer(initialState, {
type: GET_PLAYLISTS, payload: { playlists },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3a: should return state with playlist content merged with dynamic playlist when type is GET_PLAYLISTS_SUCCESS', () => {
const playlists = Factory.buildList('Playlist', 6);
const googleId = 'someone@gmail.com';
const dynamicPlaylists = playlists.map(playlist => Factory.build('GoogleDynamicPlaylist', { playlistId: playlist.id, googleId }));
const statePlaylists = playlists.map((item) => ({ ...item, slides: [{ url: 'url' }] }));
const originalState = {
...initialState, playlists: statePlaylists, dynamicPlaylists,
};
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLISTS_SUCCESS, payload: { playlists },
});
expect(stateResult.playlists).toEqual(expect.arrayContaining([expect.objectContaining({
name: expect.any(String),
googleId: expect.any(String),
status: expect.any(String),
slides: expect.any(Array),
dynamicPlaylistName: expect.any(String),
lastSynced: expect.any(String),
})]));
});
test('CASE 3b: should return state with playlist content merged with dynamic playlist when type is GET_PLAYLISTS_SUCCESS', () => {
const playlists = Factory.buildList('Playlist', 1);
const googleId = 'someone@gmail.com';
const dynamicPlaylists = playlists.map(playlist => Factory.build('GoogleDynamicPlaylist', { playlistId: playlist.id, googleId }));
const originalState = { ...initialState, dynamicPlaylists };
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLISTS_SUCCESS, payload: { playlists },
});
expect(stateResult.playlists).toEqual(expect.arrayContaining([expect.objectContaining({
name: expect.any(String),
googleId: expect.any(String),
status: expect.any(String),
dynamicPlaylistName: expect.any(String),
lastSynced: expect.any(String),
googleCollectionType: expect.any(String),
})]));
});
test('CASE 3c: should return state with playlist content merged with dynamic playlist when type is GET_PLAYLISTS_SUCCESS', () => {
const playlists = Factory.buildList('Playlist', 1);
const googleId = 'someone@gmail.com';
const dynamicPlaylists = playlists.map(playlist => Factory.build('GoogleDynamicPlaylist', { playlistId: playlist.id, googleId }));
const statePlaylists = playlists.map((item) => ({ ...item, slides: [{ url: 'url' }] }));
const originalState = {
...initialState, playlists: statePlaylists, dynamicPlaylists,
};
const count = Math.floor((Math.random() * 500) + 1);
const thumbs = [0, 1, 2].map(item => ({ src: `https://loremflickr.com/1024/768/cats?asdf=${item}`, rotation: 0, orientation: 1 }));
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLISTS_SUCCESS, payload: { playlists: [{ ...playlists[0], count, thumbs }] },
});
// The count and thumbs properties should be overridden by the new playlists data
expect(stateResult.playlists[0].count).toEqual(count);
expect(stateResult.playlists[0].thumbs).toEqual(thumbs);
});
test('CASE 4: should return state with error when type is GET_PLAYLISTS_ERROR', () => {
const error = new Error('getPlaylistError');
const expectedState = { ...initialState, meta: { ...initialState.meta, error } };
const stateResult = playlistsReducer(initialState, {
type: GET_PLAYLISTS_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 5: should return state with meta set to loading when type is GET_PLAYLIST_CONTENT', () => {
const playlists = [
{ id: 1 },
{ id: 2 },
];
const originalState = { ...initialState, playlists };
const payload = { playlistId: 1 };
const expectedState = {
...initialState,
playlists: [{
id: 1,
meta: { error: '', loading: true },
},
{ id: 2 }],
meta: { error: '', loading: false, uploading: false },
};
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLIST_CONTENT, payload,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6a: should return state with playlist content set when type is GET_PLAYLIST_CONTENT_SUCCESS', () => {
const playlists = [
{ id: 1 },
];
const originalState = { ...initialState, playlists };
const payload = {
playlistId: 1,
playlist: { name: 'PL one', slides: [] },
offset: 0,
};
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLIST_CONTENT_SUCCESS, payload,
});
expect(stateResult.playlists[0]).toHaveProperty('name', payload.playlist.name);
expect(stateResult.playlists[0]).toHaveProperty('slides');
// expect(stateResult.playlists[0]).toHaveProperty('thumbs');
});
test('CASE 6b: GET_PLAYLIST_CONTENT_SUCCESS should combine playlist slides if offset is > 0', () => {
const initialSlides = ['a', 'b', 'c'];
const nextSlides = ['d', 'e', 'f'];
const playlists = [
{
id: 1, slides: initialSlides, pictureCount: 6, offset: 3,
},
];
const originalState = { ...initialState, playlists };
const payload = {
playlistId: 1,
playlist: {
name: 'PL one',
slides: nextSlides,
offset: 5,
pictureCount: 6,
},
startOffset: 3,
endOffset: 6,
};
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLIST_CONTENT_SUCCESS, payload,
});
expect(stateResult.playlists[0]).toHaveProperty('slides', [...initialSlides, ...nextSlides]);
});
test('CASE 7: should return state error when type is GET_PLAYLIST_CONTENT_ERROR', () => {
const playlists = [
{ id: 1, meta: { error: '', loading: true } },
{ id: 2, meta: { error: '', loading: false } },
];
const originalState = { ...initialState, playlists };
// console.log({ originalState: JSON.stringify(originalState) });
const error = new Error('getPlaylistError');
const payload = {
playlistId: 1,
error,
};
const expectedState = {
...initialState,
playlists: [{
id: 1,
meta: { error, loading: false },
},
{ id: 2, meta: { error: '', loading: false } }],
meta: { error: '', loading: false, uploading: false },
};
const stateResult = playlistsReducer(originalState, {
type: GET_PLAYLIST_CONTENT_ERROR, payload,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 9: should empty the state on LOGOUT_SUCCESS', () => {
const existingState = [{ id: 1 }, { id: 2 }];
expect(playlistsReducer(existingState, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 10: should current state on CREATE_DYNAMIC_PLAYLIST_SUCCESS', () => {
const existingState = [{ id: 1 }, { id: 2 }];
expect(playlistsReducer(existingState, { type: CREATE_DYNAMIC_PLAYLIST_SUCCESS }))
.toEqual(existingState);
});
test('CASE 11: should return state with dynamicPlaylist set when type is GET_DYNAMIC_PLAYLISTS_SUCCESS', () => {
const playlists = Factory.buildList('Playlist', 6);
const googleId = 'someone@gmail.com';
const dynamicPlaylists = playlists.map(playlist => Factory.build('GoogleDynamicPlaylist', { playlistId: playlist.id, googleId }));
const originalState = { ...initialState, playlists };
const stateResult = playlistsReducer(originalState, {
type: GET_DYNAMIC_PLAYLISTS_SUCCESS, payload: { google: dynamicPlaylists },
});
expect(stateResult.playlists).toEqual(expect.arrayContaining([expect.objectContaining({
name: expect.any(String),
googleId: expect.any(String),
status: expect.any(String),
dynamicPlaylistName: expect.any(String),
lastSynced: expect.any(String),
})]));
expect(stateResult.dynamicPlaylists).toEqual(dynamicPlaylists);
});
test('CASE 12: should return state with upload state set when type is UPLOAD_ITEM_STARTED', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, uploading: true } };
const stateResult = playlistsReducer(initialState, {
type: UPLOAD_ITEM_STARTED,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 13: should return state with upload state set when type is UPLOAD_ITEMS_SUCCESS or UPLOAD_ITEMS_ERROR', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, uploading: false } };
const stateResult = playlistsReducer(initialState, {
type: UPLOAD_ITEMS_SUCCESS,
});
expect(stateResult).toEqual(expectedState);
const expectedState2 = { ...initialState, meta: { ...initialState.meta, uploading: false } };
const stateResult2 = playlistsReducer(initialState, {
type: UPLOAD_ITEMS_ERROR,
});
expect(stateResult2).toEqual(expectedState2);
});
test('CASE 14: should return state with trackerId set when type is PLAYLIST_PROCESSING_STARTED', () => {
const playlists = [
{ id: 1, meta: { error: '', loading: true } },
{ id: 2, meta: { error: '', loading: false } },
{ id: 3, meta: { error: '', loading: true } },
];
const originalState = { ...initialState, playlists };
const playlistsWithTracker = [
{ id: 1, trackerId: 'a74d2e06f17e2893017431375b416b76', meta: { error: '', loading: true } },
{ id: 2, meta: { error: '', loading: false } },
{ id: 3, trackerId: '5b0e992791ed43ca79928eed54ca0b4f', meta: { error: '', loading: true } },
];
const expectedState = { ...originalState, playlists: playlistsWithTracker };
const stateResult = playlistsReducer(originalState, {
type: PLAYLIST_PROCESSING_STARTED,
payload: {
data: [
{ receiver: '1', trackerId: 'a74d2e06f17e2893017431375b416b76' },
{ receiver: '3', trackerId: '5b0e992791ed43ca79928eed54ca0b4f' },
],
},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 15: should return state without trackerId set when type is PLAYLIST_PROCESSING_DONE', () => {
const playlists = [
{ id: 1, trackerId: 'a74d2e06f17e2893017431375b416b76', meta: { error: '', loading: true } },
{ id: 2, meta: { error: '', loading: false } },
{ id: 3, trackerId: '5b0e992791ed43ca79928eed54ca0b4f', meta: { error: '', loading: true } },
];
const originalState = { ...initialState, playlists };
const playlistsWithTracker = [
{ id: 1, trackerId: 'a74d2e06f17e2893017431375b416b76', meta: { error: '', loading: true } },
{ id: 2, meta: { error: '', loading: false } },
{ id: 3, meta: { error: '', loading: true } },
];
const expectedState = { ...originalState, playlists: playlistsWithTracker };
const stateResult = playlistsReducer(originalState, {
type: PLAYLIST_PROCESSING_DONE,
payload: {
playlistId: 3, trackerId: '5b0e992791ed43ca79928eed54ca0b4f',
},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 16: should return state with meta.uploading set to false when type is UPLOAD_SUCCESS', () => {
const playlists = [
{ id: 1, meta: { error: '', uploading: true } },
{ id: 2, meta: { error: '', uploading: false } },
{ id: 3, meta: { error: '', uploading: true } },
];
const originalState = { ...initialState, playlists };
const stateResult = playlistsReducer(originalState, {
type: UPLOAD_SUCCESS,
});
stateResult.playlists.forEach((item) => {
expect(item.meta).toHaveProperty('uploading', false);
});
});
test('CASE 17: should return state with no trackerId and meta.uploading set to false when type is UPLOAD_ITEMS_ERROR or UPLOAD_ALL_CANCELLED', () => {
const playlists = [
{ id: 1, trackerId: 'a74d2e06f17e2893017431375b416b76', meta: { error: '', uploading: true } },
{ id: 2, meta: { error: '', uploading: false } },
{ id: 3, meta: { error: '', uploading: true } },
];
const originalState = { ...initialState, playlists };
const stateResult = playlistsReducer(originalState, {
type: UPLOAD_ALL_CANCELLED,
});
stateResult.playlists.forEach((item) => {
expect(item.meta).toHaveProperty('uploading', false);
expect(item).not.toHaveProperty('trackerId');
});
});
});

View File

@@ -0,0 +1,41 @@
/* global describe: true, test:true, expect:true */
import { receivers as receiversReducer, initialState } from '~/reducers/receivers';
import {
GET_RECEIVERS_TOKEN,
GET_RECEIVERS_TOKEN_SUCCESS,
GET_RECEIVERS_TOKEN_ERROR,
} from '~/store/actionTypes';
describe('receivers reducer', () => {
test('CASE 1: should return initial state', () => {
expect(receiversReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_RECEIVERS_TOKEN', () => {
const receiversToken = { meta: { error: '', loading: true }, receiversToken: '' };
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
const stateResult = receiversReducer(initialState, {
type: GET_RECEIVERS_TOKEN, payload: receiversToken,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3: should return state with meta set to loading when type is GET_RECEIVERS_TOKEN_SUCCESS', () => {
const receiversToken = { meta: { error: '', loading: false }, receiversToken: { meta: { error: '', loading: false } } };
const expectedState = { ...initialState, receiversToken: { meta: { error: '', loading: false } } };
const stateResult = receiversReducer(initialState, {
type: GET_RECEIVERS_TOKEN_SUCCESS, payload: receiversToken,
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 4: should return state with error when type is GET_RECEIVERS_TOKEN_ERROR', () => {
const error = { meta: { error: '', loading: false }, receiversToken: '' };
const expectedState = { ...initialState };
const stateResult = receiversReducer(initialState, {
type: GET_RECEIVERS_TOKEN_ERROR, payload: error,
});
expect(stateResult).toEqual(expectedState);
});
});

View File

@@ -0,0 +1,34 @@
/* global describe: true, test:true, expect:true */
import { settings as settingsReducer, initialState } from '~/reducers/settings';
import {
GET_SETTINGS_SUCCESS,
SET_SETTINGS_SUCCESS,
LOGOUT_SUCCESS,
} from '~/store/actionTypes';
describe('settings reducer', () => {
test('CASE 1: should return initial state', () => {
expect(settingsReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should empty the state on LOGOUT_SUCCESS', () => {
expect(settingsReducer({ languageCode: 'en' }, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 3: should return settings on GET_SETTINGS_SUCCESS', () => {
const settings = { autoAcceptFriend: true, defaultPlaylistId: 123 };
const expectedState = { ...initialState, ...settings };
expect(settingsReducer(undefined, { type: GET_SETTINGS_SUCCESS, payload: { settings } }))
.toEqual(expectedState);
});
test('CASE 3: should return settings on SET_SETTINGS_SUCCESS', () => {
const settings = { defaultPlaylistId: 123 };
const existingState = { autoAcceptFriend: false, defaultPlaylistId: -1 };
const expectedState = { ...existingState, ...settings };
expect(settingsReducer(existingState, { type: SET_SETTINGS_SUCCESS, payload: { settings } }))
.toEqual(expectedState);
});
});

View File

@@ -0,0 +1,140 @@
/* global describe: true, test:true, expect:true */
import {
shareExtension as shareExtReducer,
initialState,
} from '~/reducers/shareExtension';
import {
SET_SHARE_EXT,
DEFAULT_SHARE_EXT,
LOGOUT_SUCCESS,
GET_SHARE_EXT_DATA,
GET_SHARE_EXT_DATA_SUCCESS,
GET_SHARE_EXT_DATA_ERROR,
} from '~/store/actionTypes';
describe('shareExtension reducer', () => {
test('CASE 1: should return initial state', () => {
expect(shareExtReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should empty the state on LOGOUT_SUCCESS', () => {
expect(shareExtReducer({}, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
test('CASE 3: should return share extension on DEFAULT_SHARE_EXT', () => {
const expectedState = { ...initialState };
expect(shareExtReducer(undefined, { type: DEFAULT_SHARE_EXT, payload: { } }))
.toEqual(expectedState);
});
test('CASE 4: should return share extension on SET_SHARE_EXT', () => {
const albumId = 123;
const shareExtension = { albumId };
const expectedState = { ...initialState, ...shareExtension };
expect(shareExtReducer(initialState, { type: SET_SHARE_EXT, payload: { albumId } }))
.toEqual(expectedState);
});
test('CASE 5: should return share extension on GET_SHARE_EXT_DATA', () => {
const shareExtension = { images: [], meta: { loading: true, error: '' } };
const expectedState = { ...initialState, ...shareExtension };
expect(shareExtReducer(initialState, { type: GET_SHARE_EXT_DATA, payload: { props1: '', props2: [] } }))
.toEqual(expectedState);
});
test('CASE 6: should return share extension on GET_SHARE_EXT_DATA_SUCCESS', () => {
const images = [
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
type: 'jpg',
fileSize: '27209639',
caption: '',
mime: 'jpg',
fileName: 'IMG_8246.JPG',
id: 'id1',
},
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
type: 'jpg',
fileSize: '54096296',
caption: '',
mime: 'jpg',
fileName: 'IMG_8244.JPG',
id: 'id2',
}];
const shareExtension = { images, meta: { loading: false, error: '' } };
const expectedState = { ...initialState, ...shareExtension };
expect(shareExtReducer(initialState, { type: GET_SHARE_EXT_DATA_SUCCESS, payload: { images } }))
.toEqual(expectedState);
});
test('CASE 7: should return share extension on GET_SHARE_EXT_DATA_ERROR', () => {
const shareExtension = { images: [], meta: { loading: false, error: 'error' } };
const expectedState = { ...initialState, ...shareExtension };
expect(shareExtReducer(initialState, { type: GET_SHARE_EXT_DATA_ERROR, payload: { error: 'error' } }))
.toEqual(expectedState);
});
test('CASE 8: should return update caption share extension on GET_SHARE_EXT_DATA_SUCCESS SET_SHARE_EXT', () => {
const images = [
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
type: 'jpg',
fileSize: '27209639',
caption: '',
mime: 'jpg',
fileName: 'IMG_8246.JPG',
id: 'id1',
},
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
type: 'jpg',
fileSize: '54096296',
caption: '',
mime: 'jpg',
fileName: 'IMG_8244.JPG',
id: 'id2',
}];
const shareExtension = { images, meta: { loading: false, error: '' } };
const expectedState = { ...initialState, ...shareExtension };
expect(shareExtReducer(initialState, { type: GET_SHARE_EXT_DATA_SUCCESS, payload: { images } }))
.toEqual(expectedState);
const images2 = [
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8246.JPG',
type: 'jpg',
fileSize: '27209639',
caption: 'caption1',
mime: 'jpg',
fileName: 'IMG_8246.JPG',
id: 'id1',
},
{
meta: { loading: false, error: '' },
fullFilePath: '/data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
uri: 'file:///data/user/0/com.creedon.Nixplay.qa/cache/IMG_8244.JPG',
type: 'jpg',
fileSize: '54096296',
caption: 'caption2',
mime: 'jpg',
fileName: 'IMG_8244.JPG',
id: 'id2',
}];
const expectedState2 = { ...initialState, ...shareExtension, images: images2 };
expect(shareExtReducer(initialState, { type: SET_SHARE_EXT, payload: { images: images2 } }))
.toEqual(expectedState2);
});
});

View File

@@ -0,0 +1,223 @@
/* global describe: true, test:true, expect:true */
/* eslint-disable max-len */
import { upload as uploadReducer, initialState } from '~/reducers/upload';
import {
UPLOAD_ITEM_STARTED,
UPLOAD_SUCCESS,
UPLOAD_ITEMS,
UPLOAD_ITEMS_STATE_CHANGED,
UPLOAD_ITEMS_ERROR,
} from '~/store/actionTypes';
import {
UPLOAD_STATUS_COMPLETED,
UPLOAD_STATUS_UPLOADING,
UPLOAD_STATUS_ERROR,
} from '~/lib/uploaders/constants';
describe('Upload reducer', () => {
test('CASE 1: should return initial state', () => {
expect(uploadReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return new state with items when type is UPLOAD_ITEMS', () => {
const items = [{ uri: '1.jpg' }, { uri: '2.jpg' }];
const stateResult = uploadReducer(undefined, { type: UPLOAD_ITEMS, payload: { items } });
expect(stateResult).toHaveProperty('items');
expect(stateResult.items).toHaveLength(items.length);
stateResult.items.forEach(item => {
expect(item).toHaveProperty('meta');
});
});
test('CASE 3: should return state with loading when type is UPLOAD_ITEM_STARTED', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true } };
expect(uploadReducer(initialState, { type: UPLOAD_ITEM_STARTED })).toEqual(expectedState);
});
test('CASE 4: should return empty items with meta property when type is UPLOAD_SUCCESS', () => {
const expectedState = {
items: [],
meta: {
error: '',
status: UPLOAD_STATUS_COMPLETED,
loading: false,
elapsed: 0,
},
receivers: [],
};
expect(uploadReducer(initialState, { type: UPLOAD_SUCCESS })).toEqual(expectedState);
});
test('CASE 5: should return items with meta property when type is UPLOAD_ITEMS_STATE_CHANGED', () => {
// add pending items
const items = [{ fullFilePath: '1.jpg' }, { fullFilePath: '2.jpg' }];
const item = { fullFilePath: '1.jpg', progress: 1 };
const pendingItemState = uploadReducer(initialState, { type: UPLOAD_ITEMS, payload: { items } });
const stateResult = uploadReducer(
pendingItemState,
{
type: UPLOAD_ITEMS_STATE_CHANGED,
payload: {
item,
elapsed: 1,
},
},
);
const expectedState = {
items: [{
fullFilePath: '1.jpg',
meta: {
progress: 1,
error: '',
status: UPLOAD_STATUS_UPLOADING,
loading: true,
},
}, {
fullFilePath: '2.jpg',
meta: {
progress: 0.0,
error: '',
status: UPLOAD_STATUS_UPLOADING,
loading: true,
},
}],
meta: {
error: '',
status: UPLOAD_STATUS_UPLOADING,
loading: true,
elapsed: 1,
},
};
stateResult.items.forEach(i => {
expect(i).toHaveProperty('meta');
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 6: should return correct state when type is UPLOAD_ITEMS_ERROR', () => {
const items = [{ fullFilePath: '1.jpg' }, { fullFilePath: '2.jpg' }];
const item = { fullFilePath: '1.jpg', error: 'error' };
const pendingItemState = uploadReducer(initialState, { type: UPLOAD_ITEMS, payload: { items } });
const stateResult = uploadReducer(
pendingItemState,
{
type: UPLOAD_ITEMS_ERROR,
payload: {
item,
error: item.error,
},
},
);
const expectedState = {
items: [],
meta: {
error: 'error',
status: UPLOAD_STATUS_ERROR,
loading: false,
elapsed: 0,
},
};
expect(stateResult).toEqual(expectedState);
});
// test('CASE 6: should return state with album loading to TRUE when type is GET_ALBUM_CONTENT', () => {
// const albums = [{ id: 1 }, { id: 2 }];
// const payload = {
// albumId: 1,
// };
// const originalState = { ...initialState, albums, meta: { error: '' } };
// const stateResult = uploadReducer({ ...originalState, albums, meta: { error: '' } }, {
// type: GET_ALBUM_CONTENT,
// payload,
// });
// const expectedState = {
// albums: [{
// id: 1,
// meta: { error: '', loading: true },
// },
// { id: 2 }],
// meta: { error: '' },
// };
// expect(stateResult).toEqual(expectedState);
// });
// test('CASE 7: should return state with album content when type is GET_ALBUM_CONTENT_SUCCESS', () => {
// const albums = [{ id: 1 }, { id: 2 }];
// const content = {
// name: 'album name',
// photos: [
// { id: 4 },
// { id: 5 },
// ],
// };
// const payload = {
// albumId: 1,
// content,
// };
// const originalState = { ...initialState, albums, meta: { error: '' } };
// const stateResult = uploadReducer({ ...originalState, albums, meta: { error: '' } }, {
// type: GET_ALBUM_CONTENT_SUCCESS,
// payload,
// });
// const expectedState = {
// albums: [{
// id: 1,
// content,
// meta: { error: '', loading: false },
// },
// { id: 2 }],
// meta: { error: '' },
// };
// expect(stateResult).toEqual(expectedState);
// });
// test('CASE 8: should return state with album error when type is GET_ALBUM_CONTENT_ERROR', () => {
// const albums = [{ id: 1 }, { id: 2 }];
// const albumErr = new Error('getAlbumsContentError');
// const payload = {
// albumId: 1,
// error: albumErr,
// };
// const originalState = { ...initialState, albums, meta: { error: '' } };
// const stateResult = uploadReducer({ ...originalState, albums, meta: { error: '' } }, {
// type: GET_ALBUM_CONTENT_ERROR,
// payload,
// });
// const expectedState = {
// albums: [{
// id: 1,
// meta: { error: albumErr, loading: false },
// },
// { id: 2 }],
// meta: { error: '' },
// };
// expect(stateResult).toEqual(expectedState);
// });
// test('CASE 9: should return state with album error when type is CREATE_ALBUM_SUCCESS', () => {
// const albums = [{ id: 1 }, { id: 2 }];
// const payload = {
// album: { id: 3 },
// };
// const originalState = { ...initialState, albums, meta: { error: '' } };
// const stateResult = uploadReducer({ ...originalState, albums, meta: { error: '' } }, {
// type: CREATE_ALBUM_SUCCESS,
// payload,
// });
// const expectedState = {
// albums: [
// { id: 1 },
// { id: 2 },
// { id: 3, meta: { error: '', loading: false } },
// ],
// meta: { error: '', loading: false },
// };
// expect(stateResult).toEqual(expectedState);
// });
// test('CASE 10: should empty the state on LOGOUT_SUCCESS', () => {
// const existingState = [{ id: 1 }, { id: 2 }];
// expect(uploadReducer(existingState, { type: LOGOUT_SUCCESS }))
// .toEqual(initialState);
// });
});

View File

@@ -0,0 +1,73 @@
/* global describe: true, test:true, expect:true */
import { user } from '~/reducers/user';
import {
SUBMIT_LOGIN_SUCCESS,
LOGOUT_SUCCESS,
} from '~/store/actionTypes';
describe('user reducer', () => {
test('CASE 1: should return initial state', () => {
const expectedState = {
checkUserStorage: false,
showLanguageChangedDialog: false,
enableLanguageDiscrepancyPopup: false,
displayUploadDialogue: true,
showStorageFull: false,
showLanguageDiscrepancy: false,
meta: { loading: false, error: '' },
isFullStoragePopup: false,
todo: {
avatar: {
avatarChanged: true,
tutorialDismissed: false,
},
},
};
expect(user(undefined, {}))
.toEqual(expectedState);
});
test('CASE 2: should return correct state when actionType is `SUBMIT_LOGIN_SUCCESS`', () => {
const expectedState = {
token: 'token',
username: 'username',
email: 'email@email.com',
firstName: 'first',
lastName: 'last',
checkUserStorage: true,
meta: { loading: false, error: '' },
isFullStoragePopup: false,
todo: {
avatar: {
avatarChanged: true,
tutorialDismissed: false,
},
},
};
expect(user({}, { type: SUBMIT_LOGIN_SUCCESS, payload: { user: expectedState } }))
.toEqual(expectedState);
});
test('CASE 3: should return correct state when actionType is `LOGOUT_SUCCESS`', () => {
const enableLanguageDiscrepancyPopup = false;
const expectedState = {
checkUserStorage: false,
displayUploadDialogue: true,
showLanguageChangedDialog: false,
enableLanguageDiscrepancyPopup,
showStorageFull: false,
showLanguageDiscrepancy: false,
meta: { loading: false, error: '' },
isFullStoragePopup: false,
todo: {
avatar: {
avatarChanged: true,
tutorialDismissed: false,
},
},
};
expect(user({ enableLanguageDiscrepancyPopup }, { type: LOGOUT_SUCCESS, payload: { user: expectedState } }))
.toEqual(expectedState);
});
});

View File

@@ -0,0 +1,62 @@
/* global describe: true, test:true, expect:true */
import {
userActivities as userActivitiesReducer,
initialState,
} from '~/reducers/userActivities';
import {
GET_USER_ACTIVITIES,
GET_USER_ACTIVITIES_SUCCESS,
GET_USER_ACTIVITIES_ERROR,
SET_SHOWN_MEDIA_STATUS,
LOGOUT_SUCCESS,
} from '~/store/actionTypes';
describe('user activities reducer', () => {
test('CASE 1: should return initial state', () => {
expect(userActivitiesReducer(undefined, { type: 'Default' }))
.toEqual(initialState);
});
test('CASE 2: should return state with meta set to loading when type is GET_USER_ACTIVITIES', () => {
const expectedState = { ...initialState, meta: { ...initialState.meta, loading: true, error: '' } };
const stateResult = userActivitiesReducer(initialState, {
type: GET_USER_ACTIVITIES, payload: {},
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 3: should return state with meta set to loading when type is GET_USER_ACTIVITIES_SUCCESS', () => {
const activities = [{ mediaExchange: {} }];
const meta = { loading: false, error: '' };
const stateResult = userActivitiesReducer(initialState, {
type: GET_USER_ACTIVITIES_SUCCESS, payload: { activities: { activities } },
});
expect(stateResult).toHaveProperty('meta', meta);
});
test('CASE 4: should return state with error when type is GET_USER_ACTIVITIES_ERROR', () => {
const error = new Error('getFriendsError');
const expectedState = {
...initialState,
meta: { ...initialState.meta, error, loading: false },
};
const stateResult = userActivitiesReducer(initialState, {
type: GET_USER_ACTIVITIES_ERROR, payload: { error },
});
expect(stateResult).toEqual(expectedState);
});
test('CASE 5: should update isShown state when type is SET_SHOWN_MEDIA_STATUS', (isShown = false) => {
const stateResult = userActivitiesReducer(initialState, {
type: SET_SHOWN_MEDIA_STATUS,
payload: { isShown },
});
expect(stateResult).toEqual(initialState);
});
test('CASE 6: should empty the state on LOGOUT_SUCCESS', () => {
const existingState = {};
expect(userActivitiesReducer(existingState, { type: LOGOUT_SUCCESS }))
.toEqual(initialState);
});
});

View File

@@ -0,0 +1,17 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
global.fetch = require('jest-fetch-mock');
// https://github.com/oblador/react-native-vector-icons/issues/433#issuecomment-354663885
const { NativeModules } = require('react-native');
const mockIcon = require('~/images/general/logo-color.png');
NativeModules.RNVectorIconsManager = {
getImageForFont: function getImageForFont(fontFamily, glyph, fontSize, color, callback) {
return callback(null, mockIcon);
},
};

View File

@@ -0,0 +1,55 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
create_aar_targets(glob(["libs/*.aar"]))
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "com.creedon.Nixplay",
)
android_resource(
name = "res",
package = "com.creedon.Nixplay",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)

View File

@@ -0,0 +1,346 @@
project.ext.envConfigFiles = [
dev: ".env.dev",
rnd: ".env.rnd",
qa: ".env.qa",
alpha: ".env.alpha",
beta: ".env.beta",
prod: ".env",
]
apply plugin: "com.android.application"
apply plugin: "com.google.firebase.firebase-perf"
apply plugin: "io.fabric"
//apply plugin: 'com.appsee.appsee-plugin'
// apply plugin: 'io.fabric'
repositories {
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://maven.google.com' }
}
import com.android.build.OutputFile
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation
* entryFile: "index.android.js",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
entryFile: "index.js",
enableHermes: false, // clean and rebuild if changing
// This change is to support the merged_assets update using gradle 3.2.0 which is a Firebase Dependency
// https://github.com/Microsoft/react-native-code-push/issues/1427
// TODO: SHOULD BE REMOVED AFTER THE 57.8 UPDATE!!!
// jsBundleDirQaDebug: "$buildDir/intermediates/merged_assets/qaDebug/mergeQaDebugAssets/out",
// jsBundleDirQaRelease: "$buildDir/intermediates/merged_assets/qaRelease/mergeQaReleaseAssets/out",
// jsBundleDirRndDebug: "$buildDir/intermediates/merged_assets/rndDebug/mergeRndDebugAssets/out",
// jsBundleDirRndRelease: "$buildDir/intermediates/merged_assets/rndRelease/mergeRndReleaseAssets/out",
// jsBundleDirProdDebug: "$buildDir/intermediates/merged_assets/prodDebug/mergeProdDebugAssets/out",
// jsBundleDirProdRelease: "$buildDir/intermediates/merged_assets/prodRelease/mergeProdReleaseAssets/out",
// jsBundleDirAlphaDebug: "$buildDir/intermediates/merged_assets/alphaDebug/mergeAlphaDebugAssets/out",
// jsBundleDirAlphaRelease: "$buildDir/intermediates/merged_assets/alphaRelease/mergeAlphaReleaseAssets/out",
// jsBundleDirBetaDebug: "$buildDir/intermediates/merged_assets/betaDebug/mergeBetaDebugAssets/out",
// jsBundleDirBetaRelease: "$buildDir/intermediates/merged_assets/betaRelease/mergBetaReleaseAssets/out",
]
def enableHermes = project.ext.react.get("enableHermes", false);
def jscFlavor = 'org.webkit:android-jsc:+'
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
def supportLibVersion = project.hasProperty('supportLibVersion') ? project.supportLibVersion : "28.0.0"
android {
signingConfigs {
debug {
storeFile file('/Users/nixplay/Documents/Opensource/ssskeystore/sssmobile_keystore.jks')
storePassword 'sssmobile123'
keyPassword 'sssmobile123'
keyAlias = 'sssmobile'
}
}
compileSdkVersion rootProject.ext.compileSdkVersion
// buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.ioneres.maagap" +
""
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 19
versionName "0.1.15"
multiDexEnabled true
renderscriptTargetApi 23
renderscriptSupportModeEnabled true
missingDimensionStrategy 'react-native-camera', 'general'
ndk {
abiFilters "armeabi-v7a","arm64-v8a","x86","x86_64"
}
resValue "string", "build_config_package", "com.ioneres.maagap"
addManifestPlaceholders([NOTIFICATION_CHANNEL_NAME: "MAAGApp(SW) "])
addManifestPlaceholders([NOTIFICATION_CHANNEL_DESCRIPTION: "MAAGApp(SW) Notification"])
}
packagingOptions{
doNotStrip '*/mips/*.so'
doNotStrip '*/mips64/*.so'
exclude 'META-INF/DEPENDENCIES.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INFTICE.txt'
exclude 'META-INFTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/DEPEND' +
'' +
'ENCIES'
exclude 'META-INFtice.txt'
exclude 'META-INFcense.txt'
exclude 'META-INF/dependencies.txt'
exclude 'META-INF/LGPL2.1'
exclude 'META-INF/MANIFEST.MF'
exclude 'META-INF/maven/com.google.protobuf/protobuf-java/pom.xml'
exclude 'META-INF/maven/com.google.protobuf/protobuf-java/pom.properties'
}
flavorDimensions "release"
productFlavors {
rndSW {
dimension = "release"
addManifestPlaceholders([APP_NAME: "MAAGApp(SW-D)"])
addManifestPlaceholders([APP_ID: "com.ioneres.maagap"])
addManifestPlaceholders([APP_ICON:"@mipmap/ic_launcher_rnd"])
}
prodSW {
dimension = "release"
addManifestPlaceholders([APP_NAME: "MAAGApp(SW)"])
addManifestPlaceholders([APP_ID: "com.ioneres.maagapp.sw"])
addManifestPlaceholders([APP_ICON:"@mipmap/ic_launcher_rnd"])
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
zipAlignEnabled true
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
dexOptions {
jumboMode true
javaMaxHeapSize "4g"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
aaptOptions.cruncherEnabled = false
dataBinding {
enabled = true
}
}
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
// maven { url 'https://maven.google.com' }
}
dependencies {
// These docs use an open ended version so that our plugin
// can be updated quickly in response to Android tooling updates
// We recommend changing it to the latest version from our changelog:
// https://docs.fabric.io/android/changelog.html#fabric-gradle-plugin
// classpath 'io.fabric.tools:gradle:1.22.2'
// classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.1'
}
}
dependencies {
// implementation project(':react-native-spring-scrollview')
implementation project(':@react-native-community_cameraroll')
// implementation project(':@react-native-community_art')
implementation project(':react-native-config')
implementation project(':@react-native-community_geolocation')
implementation project(':react-native-geocoder')
implementation project(':@react-native-community_viewpager')
// implementation project(':react-native-photo-view-ex')
implementation project(':react-native-detect-navbar-android')
implementation project(':@react-native-community_blur')
implementation project(':@react-native-community_netinfo')
implementation project(':react-native-localize')
implementation project(':@react-native-community_async-storage')
implementation project(':@react-native-community_slider')
implementation project(':react-native-webview')
implementation project(':react-native-default-preference')
implementation project(':react-native-firebase')
implementation project(':react-native-app-settings')
implementation project(':react-native-svg')
// implementation project(':react-native-network-info')
implementation project(':react-native-splash-screen')
implementation project(':react-native-haptic-feedback')
implementation project(':react-native-push-notification')
// implementation project(':react-native-google-signin')
implementation project(':react-native-keychain')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-view-overflow')
implementation project(':rn-fetch-blob')
implementation project(':react-native-image-resizer')
implementation project(':react-native-fs')
implementation project(':react-native-creedon-imagepicker')
implementation project(':react-native-device-info')
implementation project(':react-native-fabric')
implementation project(':react-native-vector-icons')
implementation project(':react-native-permissions')
implementation project(':react-native-mixpanel')
implementation project(':react-native-randombytes')
implementation project(':react-native-bluetooth-escpos-printer')
implementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') {
transitive = true
}
implementation fileTree(dir: "libs", include: ["*.jar"])
// implementation "com.android.support:support-compat:${rootProject.ext.supportLibVersion}"
// implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
// implementation 'com.android.support:multidex:1.0.3'
implementation 'com.android.support:multidex:1.0.3'
// implementation "com.android.support:design:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "com.facebook.fresco:animated-gif:1.3.0"
implementation "com.facebook.fresco:animated-base-support:1.3.0"
// implementation "com.android.support.constraint:constraint-layout:1.1.3"
// implementation "com.android.support:exifinterface:${rootProject.ext.supportLibVersion}"
implementation 'androidx.exifinterface:exifinterface:1.0.0'
// implementation 'io.intercom.android:intercom-sdk-base:5.1.5'
implementation "com.google.firebase:firebase-core:16.0.8"
implementation "com.google.firebase:firebase-perf:16.2.5"
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'
// conflict with react-native-codepush dependencies need to skip version checking, should remove it in the future
com.google.gms.googleservices.GoogleServicesPlugin.config.disableVersionCheck = true
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

View File

@@ -0,0 +1,20 @@
"""Helper definitions to glob .aar and .jar targets"""
def create_aar_targets(aarfiles):
for aarfile in aarfiles:
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
lib_deps.append(":" + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
def create_jar_targets(jarfiles):
for jarfile in jarfiles:
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
lib_deps.append(":" + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)

View File

@@ -0,0 +1,127 @@
{
"project_info": {
"project_number": "570052590774",
"firebase_url": "https://maagap-fd046.firebaseio.com",
"project_id": "maagap-fd046",
"storage_bucket": "maagap-fd046.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:570052590774:android:2c6f0b3dc2562ebe447a17",
"android_client_info": {
"package_name": "com.ioneres.maagap"
}
},
"oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA0Qmx57KvXiND_6KQPGfzhlgY7brCtrzc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:570052590774:android:25c0f3d85975d938447a17",
"android_client_info": {
"package_name": "com.ioneres.maagap.beneficiary"
}
},
"oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA0Qmx57KvXiND_6KQPGfzhlgY7brCtrzc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:570052590774:android:5142e1392f1aa782447a17",
"android_client_info": {
"package_name": "com.ioneres.maagapp.beneficiary"
}
},
"oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA0Qmx57KvXiND_6KQPGfzhlgY7brCtrzc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:570052590774:android:c50fd580d03f5739447a17",
"android_client_info": {
"package_name": "com.ioneres.maagapp.sw"
}
},
"oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyA0Qmx57KvXiND_6KQPGfzhlgY7brCtrzc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "570052590774-ktmpt3jo0b7aq4hc3sg790qkck9m9098.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -0,0 +1,108 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# config
-keep class com.creedon.Nixplay.BuildConfig { *; }
-keep class com.bumptech.** {*;}
-keepclassmembers class com.bumptech.** {*;}
-keep class com.bumptech.glide.integration.okhttp.OkHttpGlideModule
# fast image
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}
-keep class com.facebook.imagepipeline.animated.factory.AnimatedFactoryImpl { public AnimatedFactoryImpl(com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory, com.facebook.imagepipeline.core.ExecutorSupplier); }
#appsee
-keep class com.appsee.** { *; }
-dontwarn com.appsee.**
-keep class android.support.** { *; }
-keep interface android.support.** { *; }
-keep class androidx.** { *; }
-keep interface androidx.** { *; }
-keepattributes SourceFile,LineNumberTable
#fujifilm-spa-sdk
-keepclassmembers enum * { *; }
-keepclassmembers class com.fujifilmssd.FujifilmSPASDKActivity.** {*;}
-keep public class com.fujifilmssd.FujifilmSPASDKActivity.**
-keepnames class io.card.payment.** {*;}
-keep public class io.card.payment.** {*;}
-dontwarn io.card.payment.CardIOActivity
-dontwarn io.card.payment.CreditCard
#iap
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

View File

@@ -0,0 +1,107 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.ioneres.maagap">
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.INTERNET"/>
<uses-permission
android:required="true"
android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission
android:required="true"
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission
android:required="true"
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.CAMERA"/>
<uses-permission
android:required="true"
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.location"/>
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
<!--<uses-permission android:name="android.permission.WAKE_LOCK" />-->
<!--<permission-->
<!--android:name="${applicationId}.permission.C2D_MESSAGE"-->
<!--android:protectionLevel="signature" />-->
<!--<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />-->
<!-- < Only if you're using GCM or localNotificationSchedule() > -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Require OpenGL ES >= 2.0. -->
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<application
android:name=".MainApplication"
android:icon="${APP_ICON}"
android:label="${APP_NAME}"
android:largeHeap="true"
android:theme="@style/AppTheme"
android:allowBackup="false"
tools:replace="android:label, android:allowBackup">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="${APP_NAME}"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http" android:host="www.maagap.com" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths_public"
tools:replace="android:resource"/>
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
{
"app_secret": "7c35b0d3-f6d4-40ff-92eb-a143d36ea885"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,43 @@
package com.ioneres.maagap;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import org.devio.rn.splashscreen.SplashScreen;
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this); // here
super.onCreate(savedInstanceState);
// ATTENTION: This was auto-generated to handle app links.
Intent appLinkIntent = getIntent();
String appLinkAction = appLinkIntent.getAction();
Uri appLinkData = appLinkIntent.getData();
}
@Override
protected void onPause() {
SplashScreen.hide(this);
super.onPause();
}
@Override
protected String getMainComponentName() {
return "MAAGApp";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}

View File

@@ -0,0 +1,86 @@
package com.ioneres.maagap;
import androidx.multidex.BuildConfig;
import androidx.multidex.MultiDexApplication;
import android.content.Context;
import com.facebook.react.PackageList;
import java.lang.reflect.InvocationTargetException;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import com.microsoft.codepush.react.CodePush;
import java.util.List;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
public class MainApplication extends MultiDexApplication implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new RNFirebaseAnalyticsPackage());
packages.add(new RNFirebaseCrashlyticsPackage());
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this); // Remove this line if you don't want Flipper enabled
}
/**
* Loads Flipper in React Native templates.
*
* @param context
*/
private static void initializeFlipper(Context context) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper");
aClass.getMethod("initializeFlipper", Context.class).invoke(null, context);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/launch_screen" />
</LinearLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/launch_screen" />
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More