This commit is contained in:
BO KANG 2017-03-09 17:13:43 -08:00
Родитель e9ad1fcd7c
Коммит 24a72e3327
73 изменённых файлов: 5176 добавлений и 947 удалений

5
.gitignore поставляемый
Просмотреть файл

@ -2,6 +2,8 @@
#
.DS_Store
.vscode/
# Xcode
#
build/
@ -51,3 +53,6 @@ android/app/libs
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
js/experiment/
temp.md

Просмотреть файл

@ -1,24 +1,47 @@
# Mobile Center Mobile App
[Microsoft Mobile Center](https://www.visualstudio.com/vs/mobile-center/) is mission control to help mobile developers build mobile apps.
# <a href='https://www.visualstudio.com/vs/mobile-center/'><img src='https://www.visualstudio.com/wp-content/uploads/2016/11/continuous-everything@2x-400x362.png' height='80'></a>
Microsoft Mobile Center Mobile App complement Microsoft Mobile Center portal to let mobile developers track their developed apps' usage from customers in a light and mobile manner.
[Microsoft Mobile Center](https://www.visualstudio.com/vs/mobile-center/) is mission control to help mobile developers build mobile apps.
The key design philosophy of mobile version is to let developers retrieve app usage data from customizable notification mechanism. Developers only want to fetch data or message, view them in the mobile. They can further do the app update through the web portal.
Microsoft Mobile Center Mobile App complements Microsoft Mobile Center portal to let mobile developers track developed apps' usage from customers in a light and mobile manner. The source code is 100% based on **React/React Native/Redux**.
## How We Build It
Microsoft Mobile Center Mobile App is a light-and-thin client side app to access data from [Microsoft Mobile Center REST API End Point](https://docs.mobile.azure.com/api/). The source code is 100% based on *React Native*. We used the Auth0 with JWTs to consume REST APIs. Here it is a good tutorial how to [add authentication to react native app using JWTs](https://github.com/jeffreylees/reactnative-jwts).
## Code Structure
Under **js** folder:
- **actions**: Action and Action Creator
- **sagas**: Saga asyn request (encapsulation of actions)
- **reducers**: State Controllers
- **utils**: REST API Calls
- **store**: Redux Store
- **containers**: [Container Components](https://github.com/reactjs/redux/blob/master/docs/basics/UsageWithReact.md)
- **pages**: [Presentational Components](https://github.com/reactjs/redux/blob/master/docs/basics/UsageWithReact.md)
- **components**: React Components, use [react-storybook](https://github.com/storybooks/react-storybook) to polish
## Working Units -- Sync with Backlog
- **login**: User can login via Github/ MSA / Mobile Center Account.
- **apps**: User can view all apps they have access too and navigate to each app they have access to.
- **user**: User Information
- **notification**: notification of apps (build, distribution, crash, analytics)
- **app**: User can view an app information
- **app.distribution.view**: User can view all distribution groups for an app.
- **app.distribution.addemail**: User can add new user emails to a distribution group.
- **app.distribution.group**: User can create/ delete distribution group.
## TODO List
- Continuous Integration with Fastlane and Bitrise, [Article](http://blog.thebakery.io/continuous-integration-for-react-native-applications-with-fastlane-and-bitrise-ios-version/)
Search for the **TODO** tag in the code base to check for incomplete tasks.
- Code-Push
- Continuous Integration with Fastlane and Bitrise, [Article](http://blog.thebakery.io/continuous-integration-for-react-native-applications-with-fastlane-and-bitrise-ios-version/)
- Mobile Center React Native SDK to track app usage by developers, [mobile-center-sdk-react-native](https://github.com/Microsoft/mobile-center-sdk-react-native)
- Push Notification (per app configuration),[react-native-push-notification](https://github.com/zo0r/react-native-push-notification)
- Redux Immutable State, [redux-immutable](https://github.com/gajus/redux-immutable)
- Code Review Service, [Code Review](https://codeclimate.com/dashboard)
- Reselect : efficient rendering computation.
## Application Architecture
## Third Party Tools and APIs
_We would like to thank and appreciate the efforts that the below library authors have made. We build upon your shoulders._
@ -29,9 +52,10 @@ _We would like to thank and appreciate the efforts that the below library author
- [Jest](https://facebook.github.io/jest/) for testing [React Native](https://github.com/facebook/react-native) components and UT
- [Eslint](https://github.com/eslint/eslint) is a tool for identifying and reporting on patterns found in reading application code
- [redux-devtools](https://github.com/gaearon/redux-devtools) DevTools for Redux with hot reloading, action replay, and customizable UI
- [F8App](https://github.com/fbsamples/f8app) A good reference to build a mobile app from scratch
## Development Workflow
## Build
TODO, check with a new setup in a machine.
<!--### Step One
@ -62,4 +86,14 @@ npm test
# Contributing
There are several ways to contribute to the current app:
1. **Full stack contribution**: If you want to implement one [func sync with backlog](#Working Units -- Sync with Backlog), you need to implement actions, sagas, reducers, api calls, container and presentational containers. __Please make sure to write the saga and reducer jest tests, all tests should be passed for a PR.__ Here it is [Microsoft Mobile Center REST API End Point](https://docs.mobile.azure.com/api/).
2. **UI contribution**: Design is the most important factor other than implementation, feel free to [react-storybook](https://github.com/storybooks/react-storybook) to share your design idea and refactor the react components.
3. **Performance contribution**: Pick the right tool can greatly accelerate our development life cycle, we believe it. In terms of the development, react community is a fast changing world, we love it. Feel free to tell us what libraries we should use to further improve the dev-cycle or the running performance.
4. **Idea contribution**: Have you used the [Mobile Center Portal](https://mobile.azure.com/)? We believe you did. The key design philosophy of mobile version is to let developers retrieve app usage data from customizable notification mechanism. Developers only want to fetch data or message, view them in the mobile. They can further do the app update through the web portal. Do you agree with it? If not, can you tell us your opinions? If yes, what other special requirements do you think about? Tell us, we will add it in to fullfill your needs.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

Просмотреть файл

@ -38,6 +38,7 @@
967445A6F99543C49C47BCF0 /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D843D873E1AF4A358AD2EB31 /* SimpleLineIcons.ttf */; };
AB7289AC917B4E5DABB33A85 /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 37C6442EE45D4A4D9F95DBD1 /* FontAwesome.ttf */; };
ACD7101537094C4CAC2422F4 /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D32F9E9A5504A528C2EDC7D /* Entypo.ttf */; };
C6F43F438CF14063A6B455E8 /* libRNSearchBar.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 08AD9C2359D24DDF9717B8D9 /* libRNSearchBar.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -286,6 +287,8 @@
D843D873E1AF4A358AD2EB31 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
DCB78B73B64B4F47AEF36680 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; };
E8AACDB6442A404B970AC4B4 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = "<group>"; };
9D305C7C431D4644A9582519 /* RNSearchBar.xcodeproj */ = {isa = PBXFileReference; name = "RNSearchBar.xcodeproj"; path = "../node_modules/react-native-search-bar/RNSearchBar.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
08AD9C2359D24DDF9717B8D9 /* libRNSearchBar.a */ = {isa = PBXFileReference; name = "libRNSearchBar.a"; path = "libRNSearchBar.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -316,6 +319,7 @@
3F1096DC29134987B0963FA1 /* libCodePush.a in Frameworks */,
406741C90D344A5C98BA37B3 /* libz.tbd in Frameworks */,
6D9F611F827747FBAD25C03F /* libRNVectorIcons.a in Frameworks */,
C6F43F438CF14063A6B455E8 /* libRNSearchBar.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -512,6 +516,7 @@
D10B24DEAE6D4CAB87D7A4E8 /* RNDeviceInfo.xcodeproj */,
7B89936946D847F68C345331 /* RNVectorIcons.xcodeproj */,
623CB73D55124A85B3F6C94B /* CodePush.xcodeproj */,
9D305C7C431D4644A9582519 /* RNSearchBar.xcodeproj */,
);
name = Libraries;
sourceTree = "<group>";
@ -985,6 +990,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MobileCenterReactNativeApp.app/MobileCenterReactNativeApp";
@ -1004,6 +1010,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_NAME = "$(TARGET_NAME)";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MobileCenterReactNativeApp.app/MobileCenterReactNativeApp";

Просмотреть файл

@ -24,32 +24,20 @@
*
*/
import * as types from './types';
import { REQUEST_APPS, RECEIVE_APPS } from './types';
export function requestAppList(isRefreshing, loading, tokenId, isLoadMore, page = 1) {
export function requestAppList(tokenId) {
return {
type: types.REQUEST_APP_LIST,
isRefreshing,
loading,
isLoadMore,
type: REQUEST_APPS,
tokenId,
page,
};
}
export function fetchAppList(isRefreshing, loading, isLoadMore = false) {
export function receiveAppList(tokenId, json) {
return {
type: types.FETCH_APP_LIST,
isRefreshing,
loading,
isLoadMore
type: RECEIVE_APPS,
tokenId,
apps: json.data.children.map(child => child.data),
receivedAt: Date.now()
};
}
export function receiveAppList(appList, tokenId) {
return {
type: types.RECEIVE_APP_LIST,
appList,
tokenId
};
}

23
js/actions/login.js Normal file
Просмотреть файл

@ -0,0 +1,23 @@
import { LOGIN_REQUEST, LOGIN_SUCCESS , LOGIN_ERROR } from './types';
export function loginRequest({ username, password }) {
return {
type: LOGIN_REQUEST,
username,
password
};
}
export function loginSuccess(response) {
return {
type: LOGIN_SUCCESS,
response
};
}
export function loginError(error) {
return {
type: LOGIN_ERROR,
error
};
}

Просмотреть файл

@ -24,51 +24,21 @@
*
*/
// GET /v0.1/api_tokens
export const LOGGED_IN = 'LOGGED_IN';
export const LOGGED_OUT = 'LOGGED_OUT';
// REST APIs
// https://docs.mobile.azure.com/api/#/
// GET /v0.1/apps
export const REQUEST_APP_LIST = 'REQUEST_APP_LIST';
export const FETCH_APP_LIST = 'FETCH_APP_LIST';
export const RECEIVE_APP_LIST = 'RECEIVE_APP_LIST';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_ERROR = 'LOGIN_ERROR';
// GET /v0.1/user
export const LOADED_USER = 'LOADED_USER';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_ERROR = 'LOGOUT_ERROR';
// GET /v0.1/apps/{owner_name}/{app_name}
export const REQUEST_APPS = 'REQUEST_APP_LIST';
export const RECEIVE_APPS = 'RECEIVE_APP_LIST';
export const RECEIVE_APPS_ERROR = 'RECEIVE_APP_LIST_ERROR';
// GET /v0.1/apps/{owner_name}/{app_name}/users
// GET /v0.1/apps/{owner_name}/{app_name}/testers
/*
*
* build action types
*
*/
// GET /v0.1/apps/{owner_name}/{app_name}/distribution_groups
// GET /v0.1/apps/{owner_name}/{app_name}/distribution_groups/{distribution_group_name}
// GET /v0.1/apps/{owner_name}/{app_name}/distribution_groups/{distribution_group_name}/members
/*
*
* distribute action types
*
*/
/*
*
* crash action types
*
*/
/*
*
* analytics action types
*
*/
export const REQUEST_USER = 'REQUEST_USER';
export const FETCH_USER = 'FETCH_USER';
export const RECEIVE_USER = 'RECEIVE_USER';

Просмотреть файл

@ -0,0 +1,3 @@
export const Container = ({ children }) => (
{ children }
);

Просмотреть файл

@ -0,0 +1,25 @@
import React from 'react';
import {StyleSheet, Text, View} from "react-native";
import Button from 'react-native-button';
import { Actions } from 'react-native-router-flux';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
borderWidth: 2,
borderColor: 'red',
},
});
const ManageApp = () => {
return (
<View>
<Text>Manage App</Text>
</View>
);
};
export default ManageApp;

Просмотреть файл

@ -25,34 +25,29 @@
*/
import React from 'react';
import {
ActivityIndicator,
Text,
StyleSheet,
View
} from 'react-native';
import { ActivityIndicator, Text, StyleSheet, View } from 'react-native';
const LoadingView = () => {
<View Style={styles.loading}>
<ActivityIndicator
size="large"
color="#3e9ce9"
/>
<Text style={styles.loadingText}>Loading Data...</Text>
</View>
<View Style={styles.loading}>
<ActivityIndicator
size="large"
color="#3e9ce9"
/>
<Text style={styles.loadingText}>Loading Data...</Text>
</View>;
};
const styles = StyleSheet.create({
loading: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white'
},
loadingText: {
marginTop: 10,
textAlign: 'center'
}
loading: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white'
},
loadingText: {
marginTop: 10,
textAlign: 'center'
}
});
export default LoadingView;
export default LoadingView;

Просмотреть файл

@ -0,0 +1,28 @@
import React, { PropTypes } from 'react';
import Drawer from 'react-native-drawer';
import { DefaultRenderer, Actions } from 'react-native-router-flux';
import ManageApp from './app/manageApp';
const propTypes = {
navigationState: PropTypes.object,
}
class NavigationDrawer extends React.Component {
render() {
const state = this.props.navigationState;
const children = state.children;
return (
<Drawer
type="displace"
content={<ManageApp />}
side="right"
>
<DefaultRenderer navigationState={children[0]} onNavigate={this.props.onNavigate} />
</Drawer>
);
}
}
NavigationDrawer.propTypes = propTypes;
export default NavigationDrawer;

Просмотреть файл

@ -57,4 +57,4 @@ const styles = StyleSheet.create({
title: {
fontSize: 14
}
});
});

1
js/config.js Normal file
Просмотреть файл

@ -0,0 +1 @@
export const TOKENID = '1938077bc136d1b81c65c4d74fc4f593d9c28b02';

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Analytics from '../../pages/app/Analytics';
class AnalyticsContainer extends React.Component {
render() {
return (
<Analytics />
);
}
}
export default connect()(AnalyticsContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Build from '../../pages/app/Build';
class BuildContainer extends React.Component {
render() {
return (
<Build />
);
}
}
export default connect()(BuildContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Crash from '../../pages/app/Crash';
class CrashContainer extends React.Component {
render() {
return (
<Crash />
);
}
}
export default connect()(CrashContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Distribute from '../../pages/app/Distribute';
class DistributeContainer extends React.Component {
render() {
return (
<Distribute />
);
}
}
export default connect()(DistributeContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import ManageApp from '../../../pages/app/Start/ManageApp';
class ManageAppContainer extends React.Component {
render() {
return (
<ManageApp />
);
}
}
export default connect()(ManageAppContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Start from '../../pages/app/Start';
class StartContainer extends React.Component {
render() {
return (
<Start />
);
}
}
export default connect()(StartContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Test from '../../pages/app/Test';
class TestContainer extends React.Component {
render() {
return (
<Test />
);
}
}
export default connect()(TestContainer);

Просмотреть файл

@ -1,60 +1,13 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Apps from '../pages/apps';
import * as appsCreators from '../actions/apps';
// import CodePush from 'react-native-code-push';
import App from '../pages/App';
class AppsContainer extends React.Component{
componentDidMount(){
//TODO codepush
}
render(){
return (
<Apps {...this.props}/>
);
}
class AppContainer extends React.Component {
render() {
return (
<App />
);
}
}
const mapStateToProps = (state) => {
const { app } = state;
return {
app
};
};
const mapDispatchToProps = (dispatch) => {
const appsActions = bindActionCreators(appsCreators, dispatch);
return {
appsActions
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AppsContainer);
export default connect()(AppContainer);

Просмотреть файл

@ -24,18 +24,16 @@
*
*/
/* global expect, test action */
import React from 'react';
import { connect } from 'react-redux';
import Apps from '../pages/Apps';
import * as types from '../types';
import {receiveAppList} from '../apps';
import TOKENID from '../../utils/RequestUtil';
class AppsContainer extends React.Component {
render() {
return (
<Apps />
);
}
}
describe('test apps action', () => {
it('receiveAppList', () => {
expect(receiveAppList('hockey', TOKENID)).toEqual({
type: types.RECEIVE_APP_LIST,
appList: 'hockey',
tokenId: TOKENID
})
})
});
export default connect()(AppsContainer);

Просмотреть файл

@ -0,0 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import Login from '../pages/Login';
class LoginContainer extends React.Component {
render() {
return (
<Login />
);
}
}
export default connect()(LoginContainer);

Просмотреть файл

@ -25,18 +25,138 @@
*/
import React from 'react';
import { StyleSheet, Navigator, View, Text } from 'react-native';
import { Router, Scene, ActionConst } from 'react-native-router-flux';
import { StyleSheet, Navigator, Image } from 'react-native';
import { Router, Scene, ActionConst, Actions } from 'react-native-router-flux';
import { connect } from 'react-redux';
import User from '../pages/User';
import Notification from '../pages/Notification';
import AppsContainer from './appsContainer';
import AppContainer from './appContainer';
import StartContainer from './app/StartContainer';
import BuildContainer from './app/BuildContainer';
import TestContainer from './app/TestContainer';
import DistributeContainer from './app/DistributeContainer';
import CrashContainer from './app/CrashContainer';
import AnalyticsContainer from './app/AnalyticsContainer';
import About from '../pages/about';
import Notification from '../pages/notification';
import User from '../pages/user';
import AppsContainer from './appContainer';
import Drawer from 'react-native-drawer';
import Login from './loginContainer';
import TabIcon from '../components/tabicon';
import Splash from '../pages/Splash';
import NavigationDrawer from '../components/navigationDrawer';
import ManageApp from '../components/app/manageApp';
const RouterWithRedux = connect()(Router);
// const appBuildImg = require('../img/app-build.png');
// const appTestImg = require('../img/app-test.png');
// const appDistributeImg = require('../img/app-distribute.png');
// const appCrashImg = require('../img/app-crashes.png');
// const appAnalyticsImg = require('../img/app-analytics.png');
class MobileCenter extends React.Component {
render() {
return (
<RouterWithRedux
titleStyle={styles.navBarTitle}
getSceneStyle={getSceneStyle}
navigationBarStyle={styles.navBar}
>
<Scene key="root">
<Scene key="splash" component={Splash} hideNavBar hideTabBar />
<Scene key="login" component={Login} hideNavBar hideTabBar />
<Scene key="tabbar" tabs pressOpacity={0.8} type={ActionConst.REPLACE} initial>
<Scene
key="apps"
component={AppsContainer}
title="Apps"
icon={TabIcon}
iconName="md-apps"
/>
<Scene
key="notification"
component={Notification}
title="Notification"
icon={TabIcon}
iconName="md-notifications"
/>
<Scene
key="user"
component={User}
title="My Info"
icon={TabIcon}
iconName="md-person"
/>
</Scene>
<Scene key="tabbar2">
<Scene
key="main" tabs
tabBarStyle={tabstyles.tabBarStyle}
tabBarSelectedItemStyle={tabstyles.tabBarSelectedItemStyle}
>
<Scene
key="start"
component={StartContainer}
title="Start"
icon={TabIcon}
iconName="md-bulb"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
<Scene
key="build"
component={BuildContainer}
title="Build"
icon={TabIcon}
iconName="md-play"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
<Scene
key="test"
component={TestContainer}
title="Test"
icon={TabIcon}
iconName="ios-checkmark-circle-outline"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
<Scene
key="distribute"
component={DistributeContainer}
title="Distribute"
icon={TabIcon}
iconName="ios-git-branch"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
<Scene
key="crash"
component={CrashContainer}
title="Crash"
icon={TabIcon}
iconName="ios-warning-outline"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
<Scene
key="analytics"
component={AnalyticsContainer}
title="Analytics"
icon={TabIcon}
iconName="ios-stats-outline"
onLeft={() => Actions.pop()}
leftTitle="Back"
/>
</Scene>
</Scene>
</Scene>
</RouterWithRedux>
);
}
}
const getSceneStyle = (props, computedProps) => {
const style = {
flex: 1,
@ -54,51 +174,6 @@ const getSceneStyle = (props, computedProps) => {
return style;
};
class MobileCenter extends React.Component{
render(){
return (
<RouterWithRedux
titleStyle={styles.navBarTitle}
getSceneStyle={getSceneStyle}
navigationBarStyle={styles.navBar}
>
<Scene key='root'>
<Scene key='tabbar' tabs pressOpacity={0.8} type={ActionConst.REPLACE}>
<Scene
key="apps"
component={AppsContainer}
title="Apps"
icon={TabIcon}
iconName="md-apps"
/>
<Scene
key="notification"
component={Notification}
title="Notification"
icon={TabIcon}
iconName="md-notifications"
/>
<Scene
key="user"
component={User}
title="My Info"
icon={TabIcon}
iconName="md-person"
/>
<Scene
key="about"
component={About}
title="About"
icon={TabIcon}
iconName="md-information-circle"
/>
</Scene>
</Scene>
</RouterWithRedux>
)
}
}
const styles = StyleSheet.create({
navBar: {
backgroundColor: '#3e9ce9'
@ -109,4 +184,23 @@ const styles = StyleSheet.create({
}
});
export default MobileCenter;
const drawerStyles = {
drawer: { shadowColor: '#000000', shadowOpacity: 0.8, shadowRadius: 3 },
main: { paddingLeft: 3 },
};
const tabstyles = StyleSheet.create({
container: { flex: 1,
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
},
tabBarStyle: {
backgroundColor: '#eee',
},
tabBarSelectedItemStyle: {
backgroundColor: '#ddd',
},
});
export default MobileCenter;

Просмотреть файл

@ -1,34 +0,0 @@
import {addTodo} from '../todo';
test('adds 1 + 2 to equal 3', () => {
const sum = require('../sum');
expect(sum(1, 2)).toBe(3);
});
const myBeverage = {
delicious: true,
sour: false,
};
describe('my beverage', () => {
it('is delicious', () => {
expect(myBeverage.delicious).toBeTruthy();
});
it('is not sour', () => {
expect(myBeverage.sour).toBeFalsy();
});
});
describe('actions', () => {
it('should create an action to add a todo', () => {
const text = "Go to the Vault"
const id = 1
const expectedAction = {
type: 'ADD_TODO',
text: "Go to the Vault",
id: 1
}
expect(addTodo(text, id)).toEqual(expectedAction)
})
})

Просмотреть файл

@ -1 +0,0 @@
module.exports = (a, b) => a + b;

Просмотреть файл

@ -1,7 +0,0 @@
export const addTodo = (text, id) => {
return {
id: id,
type: 'ADD_TODO',
text
}
}

Двоичные данные
js/img/app-analytics.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 215 B

Двоичные данные
js/img/app-build.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 277 B

Двоичные данные
js/img/app-crashes.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 419 B

Двоичные данные
js/img/app-distribute.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 445 B

Двоичные данные
js/img/app-test.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 519 B

Двоичные данные
js/img/splash.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 38 KiB

Просмотреть файл

Просмотреть файл

@ -1,220 +0,0 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
// import {Actions, Scene, Router} from 'react-native-router-flux';
var React = require('react');
var ReactNative = require('react-native');
var t = require('tcomb-form-native');
var {
AppRegistry,
AsyncStorage,
StyleSheet,
Text,
View,
TouchableHighlight,
AlertIOS,
} = ReactNative;
global.Buffer = global.Buffer || require('buffer').Buffer;
var STORAGE_KEY = 'id_token';
var Form = t.form.Form;
var Person = t.struct({
username: t.String,
password: t.String
});
const options = {};
let myApiUrl = "http://localhost:3001";
let quotePath = "/api/protected/random-quote";
let userPath = "/users";
let userCreatePath = "/sessions/create";
let mc_api_url = "https://api.mobile.azure.com";
let get_user = "/v0.1/user";
let get_apps = "/v0.1/apps";
let get_tokens = "/v0.1/api_tokens";
let userName = "buptkang@gmail.com";
let userPassword = "Kb@241307684";
let storedSessionTokens = "2eb6d0e2250779ad71acde8f383158b48aa0b4b6";
var MobileCenter = React.createClass({
async _onValueChange(item, selectedValue) {
try {
await AsyncStorage.setItem(item, selectedValue);
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
},
async _getUserApps() {
var DEMO_TOKEN = await AsyncStorage.getItem(STORAGE_KEY);
fetch(`${mc_api_url}${get_apps}`, {
method: "GET",
headers: {
'Accept': 'application/json',
'X-API-Token': storedSessionTokens
}
})
.then((response) => response.json())
.then((responseData) => {
responseData.map(app => AlertIOS.alert("Your Apps:",app.display_name));
//AlertIOS.alert("Your Apps:", responseData.map(app=>))
})
.done();
},
async _userLogout() {
try {
await AsyncStorage.removeItem(STORAGE_KEY);
AlertIOS.alert("Logout Success!")
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
},
_userSignup() {
var value = this.refs.form.getValue();
if (value) { // if validation fails, value will be null
fetch(`${myApiUrl}${userPath}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: value.username,
password: value.password,
})
})
.then((response) => response.json())
.then((responseData) => {
this._onValueChange(STORAGE_KEY, responseData.id_token),
AlertIOS.alert(
"Signup Success!",
"Click the button to get a Chuck Norris quote!"
)
})
.done();
}
},
_userLogin() {
let concatStr = userName + ':' + userPassword;
let hashStr = new Buffer(concatStr).toString('base64');
//TODO: check null;
//AlertIOS.alert(hashStr);
// var value = this.refs.form.getValue();
// if (value) { // if validation fails, value will be null
//fetch(`${myApiUrl}${userCreatePath}`, {
fetch(`${mc_api_url}${get_tokens}`, {
method: "GET",
headers: {
'Accept': 'application/json',
'Authorization': 'Basic ' + hashStr
}
})
.then((response) => response.json())
.then((responseData) => {
AlertIOS.alert(
"Login Success!",
//responseData[0].id
),
this._onValueChange(STORAGE_KEY, responseData[0].id)
})
.done();
// }
},
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<Text style={styles.title}>Mobile Center RN Experiment</Text>
</View>
<View style={styles.row}>
<Form
ref="form"
type={Person}
options={options}
/>
</View>
<View style={styles.row}>
<TouchableHighlight style={styles.button} onPress={this._userLogin} underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Login</Text>
</TouchableHighlight>
<TouchableHighlight style={styles.button} onPress={this._userLogout} underlayColor='#99d9f4'>
<Text style={styles.buttonText}>Logout</Text>
</TouchableHighlight>
</View>
<View style={styles.row}>
<TouchableHighlight onPress={this._getUserApps} style={styles.button}>
<Text style={styles.buttonText}>Get your apps!</Text>
</TouchableHighlight>
</View>
</View>
);
}
});
var styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
title: {
fontSize: 30,
alignSelf: 'center',
marginBottom: 30
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
button: {
height: 36,
backgroundColor: '#48BBEC',
borderColor: '#48BBEC',
borderWidth: 1,
borderRadius: 8,
marginBottom: 10,
alignSelf: 'stretch',
justifyContent: 'center'
},
});
module.exports = MobileCenter;

Просмотреть файл

Просмотреть файл

@ -24,133 +24,82 @@
*
*/
import React, {PropTypes} from 'react';
import React, { propTypes } from 'react';
import { StyleSheet,
ListView,
RefreshControl,
ScrollView,
Text,
TouchableOpacity,
InteractionManager,
ActivityIndicator,
RecyclerViewBackedScrollView,
Image,
View,
DeviceEventEmitter,
Platform,
AlertIOS} from 'react-native';
ListView,
View,
Platform,
Text,
TouchableOpacity,
Alert,
Image,
Button
} from 'react-native';
import { Actions } from 'react-native-router-flux';
import LoadingView from '../components/loading';
import { toastShort } from '../utils/ToastUtil';
import Storage from '../utils/StorageUtil';
import { TOKENID } from '../utils/RequestUtil';
const propTypes = {
appsActions: PropTypes.object,
app: PropTypes.object.isRequired
};
const pages = [];
let loadMoreTime = 0;
let currentLoadMoreTypeId;
data = [
const mockData = [
{
"display_name": "Calculator-Test",
"owner": {
"display_name": "Bo Kang",
},
appName: 'Calculator-Test',
abbrName: 'C',
userName: 'buptkang'
},
{
"display_name": "Moody",
"owner": {
"display_name": "Bo Kang",
},
},
appName: 'Moody',
abbrName: 'Moody',
userName: 'buptkang'
}
];
export default class Apps extends React.Component{
export default class Apps extends React.Component {
constructor(props){
constructor(props) {
super(props);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
dataSource: ds.cloneWithRows(data),
tokenId: TOKENID
dataSource: ds.cloneWithRows(mockData),
};
this.renderItem = this.renderItem.bind(this);
this.onPressApp = this.onPressApp.bind(this);
}
componentDidMount(){
const {appsActions} = this.props;
DeviceEventEmitter.addListener('loadApps', (tokenId) => {
appsActions.requestAppList(false, true, tokenId);
pages.push(1);
this.setState({tokenId});
});
// InteractionManager.runAfterInteractions(
// Storage.get('getTokenId')
// .then((tokenIds) => {
// if(!tokenIds){
// return;
// }
// //TODO
// }
// );
}
componentWillReceiveProps(nextProps){
const {app} = this.props;
if (app.isLoadMore && !nextProps.app.isLoadMore && !nextProps.app.isRefreshing){
if (nextProps.app.noMore) {
toastShort('no more data');
}
}
}
componentWillUnmount(){
DeviceEventEmitter.removeAllListeners('loadApps');
}
onRefresh(tokenId){
const { appsActions } = this.props;
appsActions.requestAppList(true, false, tokenId);
onPressApp(app) {
Actions.tabbar2();
}
onPress(app){
const { routes } = this.context;
//TODO
}
renderItem(app){
return (
<View style={styles.containerItem}>
<View style={styles.itemRightContent} >
<Text style={styles.title}>
{app.display_name}
</Text>
<View style={styles.itemRightBottom} >
<Text style={styles.userName} >
{app.owner.display_name}
renderApp(app) {
return (
<TouchableOpacity onPress={() => this.onPressApp(app)}>
<View style={styles.containerItem}>
<Image style={styles.itemImg}>
<Text>
{app.abbrName}
</Text>
</Image>
<View style={styles.itemRightContent}>
<Text style={styles.title}>
{app.appName}
</Text>
<View style={styles.itemRightBottom}>
<Text style={styles.userName}>
{app.userName}
</Text>
</View>
</View>
</View>
);
</View>
</TouchableOpacity>
);
}
render(){
const { app } = this.props;
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderItem}
style={styles.listView}
/>
<View style={styles.container}>
<ListView
initialListSize={1}
dataSource={this.state.dataSource}
renderRow={row => this.renderApp(row)}
/>
</View>
);
}
}
}
const styles = StyleSheet.create({
@ -267,4 +216,4 @@ const styles = StyleSheet.create({
}
});
Apps.propTypes = propTypes;
Apps.propTypes = propTypes;

93
js/pages/Login.js Normal file
Просмотреть файл

@ -0,0 +1,93 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React, { PropTypes } from 'react';
import { StyleSheet, Text,
View, TextInput, TouchableHighlight, Alert } from 'react-native';
import { Actions } from 'react-native-router-flux';
export default class Login extends React.Component {
constructor(props) {
super(props);
this.state = { username: 'default', password: 'default' };
}
onPress() {
Actions.tabbar();
}
render() {
// const {login, dispatch} = this.props;
return (
<View style={styles.container} >
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={username => this.setState({ username })}
value={this.state.username}
/>
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={password => this.setState({ password })}
value={this.state.password}
/>
<TouchableHighlight
style={styles.button}
onPress={this.onPress} underlayColor="#99d9f4"
>
<Text style={styles.buttonText}>Submit</Text>
</TouchableHighlight>
</View>
);
}
}
let styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
title: {
fontSize: 30,
alignSelf: 'center',
marginBottom: 30
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
button: {
height: 36,
backgroundColor: '#48BBEC',
borderColor: '#48BBEC',
borderWidth: 1,
borderRadius: 8,
marginBottom: 10,
alignSelf: 'stretch',
justifyContent: 'center'
}
});

Просмотреть файл

@ -25,19 +25,16 @@
*/
import React from 'react';
import { StyleSheet, Image, Text, TextInput, Linking, View} from 'react-native';
import { Text, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { Actions } from 'react-native-router-flux';
import Icon from 'react-native-vector-icons/Ionicons';
import Button from '../components/button';
// TODO: Add Notification functions
export default class User extends React.Component{
render(){
return (
<View>
<Text> TODO: Render User Information.</Text>
</View>
);
}
}
export default class Notification extends React.Component {
render() {
return (
<View>
<Text> Push Notification </Text>
</View>
);
}
}

49
js/pages/Splash.js Normal file
Просмотреть файл

@ -0,0 +1,49 @@
import React from 'react';
import {
Dimensions,
Animated
} from 'react-native';
import { Actions } from 'react-native-router-flux';
import store from 'react-native-simple-store';
const maxHeight = Dimensions.get('window').height;
const maxWidth = Dimensions.get('window').width;
const splashImg = require('../img/splash.png');
export default class Splash extends React.Component {
constructor(props) {
super(props);
this.state = {
bounceValue: new Animated.Value(0.4)
};
}
componentDidMount() {
Animated.timing(
this.state.bounceValue, { toValue: 2.0, duration: 1000 }
).start();
this.timer = setTimeout(() => {
store.get('isInit')
.then((isInit) => {
if (!isInit) {
Actions.login();
}
});
}, 1000);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
return (
<Animated.Image
style={{ width: maxWidth,
height: maxHeight,
transform: [{ scale: this.state.bounceValue }] }}
source={splashImg}
/>
);
}
}

Просмотреть файл

@ -25,19 +25,16 @@
*/
import React from 'react';
import { StyleSheet, Image, Text, TextInput, Linking, View} from 'react-native';
import { Text, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { Actions } from 'react-native-router-flux';
import Icon from 'react-native-vector-icons/Ionicons';
import Button from '../components/button';
// TODO: User Information
export default class Notification extends React.Component{
render(){
return (
<View>
<Text> TODO: Push Notification </Text>
</View>
);
}
}
export default class User extends React.Component {
render() {
return (
<View>
<Text> Render User Information. </Text>
</View>
);
}
}

38
js/pages/app/Analytics.js Normal file
Просмотреть файл

@ -0,0 +1,38 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { Text, View } from 'react-native';
export default class Analytics extends React.Component {
render() {
return (
<View>
<Text> Analytics </Text>
</View>
);
}
}

152
js/pages/app/Build.js Normal file
Просмотреть файл

@ -0,0 +1,152 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { Text, View, ListView, StyleSheet, Platform, TextInput } from 'react-native';
import SearchBar from 'react-native-search-bar';
import Button from 'react-native-button';
import Drawer from 'react-native-drawer';
import ManageApp from '../../containers/app/Start/ManageAppContainer';
export default class Build extends React.Component {
constructor() {
super();
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
this.state = {
dataSource: ds.cloneWithRows(['Branches ', ' Status', ' Last Commit', ' Last build']),
};
}
renderRow(rowData) {
return <Text>{rowData}</Text>;
}
renderHeader(rowHeader) {
return <Text> { rowHeader } </Text>;
}
renderSectionHeader(sectionData, sectionID) {
return (
<View style={styles.section}>
<Text style={styles.text}>
{sectionData}
</Text>
</View>
);
}
render() {
return (
<Drawer
ref={c => this.drawer = c}
type="overlay"
openDrawerOffset={0.2}
panCloseMask={0.2}
content={<ManageApp />}
side="right"
tapToClose
negotiatePan
>
<View style={styles.container}>
<SearchBar
ref="searchBar"
placeholder="Search"
/>
<ListView
style={styles.listview}
dataSource={this.state.dataSource}
renderRow={this.renderRow}
renderSectionHeader={this.renderSectionHeader}
/>
<Button
style={{ fontSize: 20, color: 'green' }}
containerStyle={{ padding: 10, height: 45, overflow: 'hidden', borderRadius: 4, backgroundColor: 'orange' }}
styleDisabled={{ color: 'red' }}
onPress={() => alert('TODO: link to webview')}
>
Open on GitHub
</Button>
</View>
</Drawer>
);
}
}
var styles = StyleSheet.create({
listview: {
backgroundColor: '#B0C4DE',
},
header: {
height: 40,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#3B5998',
flexDirection: 'row',
},
text: {
color: 'white',
paddingHorizontal: 8,
},
rowText: {
color: '#888888',
},
thumbText: {
fontSize: 20,
color: '#888888',
},
buttonContents: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 5,
marginVertical: 3,
padding: 5,
backgroundColor: '#EAEAEA',
borderRadius: 3,
paddingVertical: 10,
},
img: {
width: 64,
height: 64,
marginHorizontal: 10,
backgroundColor: 'transparent',
},
section: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
padding: 6,
backgroundColor: '#5890ff',
},
});

38
js/pages/app/Crash.js Normal file
Просмотреть файл

@ -0,0 +1,38 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { Text, View } from 'react-native';
export default class Crash extends React.Component {
render() {
return (
<View>
<Text> Crash </Text>
</View>
);
}
}

Просмотреть файл

@ -25,79 +25,68 @@
*/
import React from 'react';
import { StyleSheet, Image, Text, TextInput, Linking, View} from 'react-native';
import { Text, View, StyleSheet, ScrollView, Image } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { Actions } from 'react-native-router-flux';
import Icon from 'react-native-vector-icons/Ionicons';
import Button from '../components/button';
import Button from 'react-native-button';
import {
Card,
CardImage,
CardTitle,
CardContent,
CardAction
} from 'react-native-card-view';
export default class Distribute extends React.Component {
class About extends React.Component{
render(){
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.center}>
_renderTitle (title) {
return (
<View style={{flex: 1, alignItems: 'center', marginTop: 20}}>
<Text style={{fontSize: 10}}>{title}</Text>
</View>
)
}
<Text style={styles.title}>
React Native Mobile Center
</Text>
</View>
render() {
return (
<ScrollView>
<View style={styles.container}>
<Card styles={{card: {width: 300, height: 100 }}}>
<CardTitle>
<Text style={styles.title}>Collaborators</Text>
</CardTitle>
<CardContent>
<Text>Who do you collaborate with?</Text>
</CardContent>
</Card>
<Card styles={{card: {width: 300, height: 100 }}}>
<CardTitle>
<Text style={styles.title}>Alpha Testers</Text>
</CardTitle>
<CardContent>
<Text>With whom?</Text>
</CardContent>
</Card>
</View>
</View>
);
}
</ScrollView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
},
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: 10
},
center: {
flex: 1,
alignItems: 'center'
},
logo: {
width: 110,
height: 110,
marginTop: 50
},
version: {
fontSize: 16,
textAlign: 'center',
color: '#aaaaaa',
marginTop: 5
marginTop: 60,
marginBottom: 60
},
title: {
fontSize: 28,
textAlign: 'center',
color: '#313131',
marginTop: 10
fontSize: 16,
backgroundColor: 'transparent'
},
subtitle: {
fontSize: 18,
textAlign: 'center',
color: '#4e4e4e'
button: {
marginRight: 10
},
disclaimerContent: {
flexDirection: 'column'
},
disclaimer: {
fontSize: 14,
textAlign: 'center'
},
bottomContainer: {
alignItems: 'center'
card: {
width: 300
}
});
export default About;

66
js/pages/app/Start.js Normal file
Просмотреть файл

@ -0,0 +1,66 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { Text, View } from 'react-native';
import Button from 'react-native-button';
import Drawer from 'react-native-drawer';
import ManageApp from '../../containers/app/Start/ManageAppContainer';
export default class Start extends React.Component {
render() {
return (
<Drawer
ref={c => this.drawer = c}
type="overlay"
openDrawerOffset={0.2}
panCloseMask={0.2}
content={<ManageApp />}
side="right"
tapToClose
negotiatePan
>
<View>
<Button
style={{ fontSize: 20, color: 'green'}}
containerStyle={{ padding: 10, height: 45, overflow: 'hidden', borderRadius: 4, backgroundColor: 'orange'}}
styleDisabled={{ color: 'red' }}
onPress={() => this.drawer.open()}
>
Manage App
</Button>
<Text> Add Mobile Centers SDK to your app. </Text>
<Text> 1. Integrate using CocoaPods </Text>
<Text> 2. Start the SDK </Text>
<Text> 3. Explore Data </Text>
</View>
</Drawer>
);
}
}

Просмотреть файл

@ -24,24 +24,26 @@
*
*/
/* global expect */
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import app from '../apps';
import { receiveAppList } from '../../actions/apps';
import * as types from '../../actions/types';
import { TOKENID } from '../../utils/RequestUtil';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
borderWidth: 2,
borderColor: 'red',
},
});
describe('app reducer test', () => {
it(`should handle RECEIVE_APP_LIST`, () => {
expect(
app({}, receiveAppList(['hokeyapp', 'test'], TOKENID))
).toEqual(
{
isRefreshing: false,
isLoadMore: false,
noMore: false,
appList: ['hokeyapp', 'test']
}
)
})
});
export default class ManageApp extends React.Component {
render() {
return (
<View style={styles.container}>
<Text> Wow, managemy app </Text>
</View>
);
}
}

38
js/pages/app/Test.js Normal file
Просмотреть файл

@ -0,0 +1,38 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import React from 'react';
import { Text, View } from 'react-native';
export default class Test extends React.Component {
render() {
return (
<View>
<Text> Mobile Center Test includes a free trial for UI testing. </Text>
</View>
);
}
}

Просмотреть файл

@ -0,0 +1,79 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import apps from '../apps';
import {REQUEST_APPS, RECEIVE_APPS, RECEIVE_APPS_ERROR} from '../../actions/types';
describe('Reducers/ apps', () => {
function getInitState() {
return {
status: 'init'
};
}
let state = {};
beforeEach(() => {
state = getInitState();
});
it('should handle initial state', () => {
expect(
apps(state, {})
).toEqual(
{ status: 'init' }
);
});
it('should handle request apps', () => {
expect(
apps(state, {
type: REQUEST_APPS
})
).toEqual({
status: 'isFetching'
});
});
it('should handle request apps success', () => {
expect(apps(state, {
type: RECEIVE_APPS,
response: {
apps: ['BOA', 'Chase'],
receivedAt: '12314'
}
})).toEqual({
status: 'receiveapps',
apps: ['BOA', 'Chase'],
lastUpdated: '12314'
});
});
it('should handle request apps error', () => {
expect(apps(state, {
type: RECEIVE_APPS_ERROR,
error: 'error message'
})).toEqual({
status: 'error',
error: 'error message'
});
});
});

Просмотреть файл

@ -24,53 +24,61 @@
*
*/
import { put, call } from 'redux-saga/effects';
import { requestAppList } from '../apps';
import { request, TOKENID } from '../../utils/RequestUtil';
import { fetchAppList, receiveAppList } from '../../actions/apps';
import login from '../login';
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_ERROR } from '../../actions/types';
/* global expect */
describe('apps saga tests', () => {
const {
isRefreshing,
loading,
tokenId,
isLoadMore,
page
} = {
isRefreshing: false,
loading: false,
tokenId: request.TOKENID,
isLoadMore: false,
page: 1
};
const generator = requestAppList(
isRefreshing,
loading,
tokenId,
isLoadMore,
page
);
const mockArticleList = {
showapi_res_body: {
pagebean: {
contentlist: [
],
},
},
};
const step = input => generator.next(input).value;
describe('Reducers/ login', () => {
it(`should put(fetchAppList(${isRefreshing}, ${loading}, ${isLoadMore}))`, () => {
const next = step();
// console.log(next);
const tt = put(fetchAppList(isRefreshing, loading, isLoadMore));
// console.log(tt);
// expect(next).toEqual();
});
function getInitState() {
return {
status: 'init'
};
}
it('just for fun', () => {
const tt = 1;
expect(tt).toEqual(1);
});
});
let state = {};
beforeEach(() => {
state = getInitState();
});
it('should handle initial state', () => {
expect(
login(state, {})
).toEqual(
{ status: 'init' }
);
});
it('should handle request login', () => {
expect(
login(state, {
type: LOGIN_REQUEST
})
).toEqual({
status: 'loading'
});
});
it('should handle login success', () => {
expect(login(state, {
type: LOGIN_SUCCESS,
response: {
username: 'denny',
token: '12314'
}
})).toEqual({
status: 'logined',
username: 'denny',
token: '12314'
});
});
it('should handle login error', () => {
expect(login(state, {
type: LOGIN_ERROR,
error: 'error message'
})).toEqual({
status: 'error',
error: 'error message'
});
});
});

Просмотреть файл

@ -24,32 +24,28 @@
*
*/
import * as types from '../actions/types';
import { REQUEST_APPS, RECEIVE_APPS, RECEIVE_APPS_ERROR } from '../actions/types';
const initialState = {
isRefreshing: false,
loading: false,
isLoadMore: false,
noMore: false,
appList: []
};
export default function app(state = initialState, action){
switch(action.type){
case types.FETCH_APP_LIST:
return Object.assign({}, state, {
isRefreshing: action.isRefreshing,
loading: action.loading,
isLoadMore: action.isLoadMore
});
case types.RECEIVE_APP_LIST:
return Object.assign({}, state, {
isRefreshing: false,
isLoadMore: false,
noMore: action.appList.length == 0,
appList: action.appList
});
default:
return state;
}
}
export default function apps(state = {
status: 'init',
}, action) {
switch (action.type) {
case REQUEST_APPS:
return Object.assign({}, state, {
status: 'isFetching',
});
case RECEIVE_APPS:
return Object.assign({}, state, {
status: 'receiveapps',
apps: action.response.apps,
lastUpdated: action.response.receivedAt
});
case RECEIVE_APPS_ERROR:
return {
status: 'error',
error: action.error
};
default:
return state;
}
}

Просмотреть файл

@ -24,13 +24,15 @@
*
*/
import {combineReducers} from 'redux';
import app from './apps'
import routes from './routes'
import { combineReducers } from 'redux';
import app from './apps';
import routes from './routes';
import login from './login';
const rootReducer = combineReducers({
app,
routes
app,
routes,
login
});
export default rootReducer;

25
js/reducers/login.js Normal file
Просмотреть файл

@ -0,0 +1,25 @@
import { LOGIN_REQUEST, LOGIN_SUCCESS , LOGIN_ERROR } from '../actions/types';
export default function login(state = {
status: 'init'
}, action) {
switch (action.type) {
case LOGIN_REQUEST:
return {
status: 'loading'
};
case LOGIN_SUCCESS:
return {
status: 'logined',
username: action.response.username,
token: action.response.token
};
case LOGIN_ERROR:
return {
status: 'error',
error: action.error
};
default:
return state;
}
}

Просмотреть файл

@ -24,19 +24,19 @@
*
*/
import {ActionConst} from 'react-native-router-flux';
import { ActionConst } from 'react-native-router-flux';
const initialState = {
scene: {},
scene: {},
};
export default function reducer(state = initialState, action = {}){
switch(action.type){
case ActionConst.FOCUS:
return Object.assign({}, state, {
scene: action.scene
});
default:
return state;
}
}
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case ActionConst.FOCUS:
return Object.assign({}, state, {
scene: action.scene
});
default:
return state;
}
}

Просмотреть файл

@ -0,0 +1,82 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import { takeEvery, call, put, fork } from 'redux-saga/effects';
import { REQUEST_APPS, RECEIVE_APPS, RECEIVE_APPS_ERROR } from '../../actions/types';
import { fetchApps, fetchAppsFlow, watchFetchAppList } from '../apps';
import { appsAPI } from '../../utils/RequestUtil';
describe('Sagas/ apps', () => {
describe('watchFetchAppList', () => {
const generator = watchFetchAppList();
it('should take every fetch app request', () => {
const expected = takeEvery(REQUEST_APPS, fetchAppsFlow);
const actual = generator.next().value;
expect(expected.name).toEqual(actual.name);
});
});
describe('fetchAppsFlow', () => {
const generator = fetchAppsFlow({ tokenId: '1938077bc136d1b81c65c4d74fc4f593d9c28b02' });
it('should fork a fetchApps test', () => {
const expected = fork(fetchApps, { tokenId: '1938077bc136d1b81c65c4d74fc4f593d9c28b02' });
const actual = generator.next().value;
expect(expected).toEqual(actual);
});
});
describe('fetchApps', () => {
const generator = fetchApps({ tokenId: '1938077bc136d1b81c65c4d74fc4f593d9c28b02' });
it('should call appsAPI', () => {
const expected = call(appsAPI,
{ tokenId: '1938077bc136d1b81c65c4d74fc4f593d9c28b02' }
);
const actual = generator.next().value;
expect(expected).toEqual(actual);
});
it('should handle fetchApps success', () => {
const getResponse = () => ({ token: 'faketoken' });
const expected = put({
type: RECEIVE_APPS,
response: { token: 'faketoken' }
});
const actual = generator.next(getResponse()).value;
expect(expected).toEqual(actual);
});
it('should handle fetchApps error', () => {
const error = 'error message';
const expected = put({
type: RECEIVE_APPS_ERROR,
error: 'error message'
});
const actual = generator.throw(error).value;
expect(expected).toEqual(actual);
});
});
});

Просмотреть файл

@ -0,0 +1,98 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import { takeEvery, call, put, fork } from 'redux-saga/effects';
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_ERROR } from '../../actions/types';
import { watchRequestLogin, loginFlow, authorize } from '../login';
import { loginAPI } from '../../utils/RequestUtil';
describe('Sagas/ login', () => {
describe('watchRequestLogin', () => {
const generator = watchRequestLogin();
it('should take every login request', () => {
const expected = takeEvery(LOGIN_REQUEST, loginFlow);
const actual = generator.next().value;
expect(expected.name).toEqual(actual.name);
});
});
describe('loginFlow', () => {
const generator = loginFlow({
type: LOGIN_REQUEST,
username: 'buptkang@gmail.com',
password: 'Kb@241307684'
});
it('should fork a authorize test', () => {
const expected = fork(authorize, {
username: 'buptkang@gmail.com',
password: 'Kb@241307684'
});
const actual = generator.next().value;
expect(expected).toEqual(actual);
});
});
describe('Authorize', () => {
const generator = authorize({
username: 'buptkang@gmail.com',
password: 'Kb@241307684'
});
it('should call loginAPI', () => {
const expected = call(loginAPI, {
username: 'buptkang@gmail.com',
password: 'Kb@241307684'
});
const actual = generator.next().value;
expect(expected).toEqual(actual);
});
it('should handle login success', () => {
const getResponse = () => ({
username: 'denny',
token: 'fake token'
});
const expected = put({
type: LOGIN_SUCCESS,
response: {
username: 'denny',
token: 'fake token'
}
});
const actual = generator.next(getResponse()).value;
expect(expected).toEqual(actual);
});
it('should handle login error', () => {
const error = 'error message';
const expected = put({
type: LOGIN_ERROR,
error: 'error message'
});
const actual = generator.throw(error).value;
expect(expected).toEqual(actual);
});
});
});

Просмотреть файл

@ -24,35 +24,34 @@
*
*/
import {put, take, call, fork} from 'redux-saga/effects';
import { takeEvery, put, call, fork } from 'redux-saga/effects';
import { REQUEST_APPS, RECEIVE_APPS, RECEIVE_APPS_ERROR } from '../actions/types';
import { toastShort } from '../utils/ToastUtil';
import { appsAPI } from '../utils/RequestUtil';
import * as types from '../actions/types';
import {fetchAppList, receiveAppList} from '../actions/apps';
import {toastShort} from '../utils/ToastUtil';
import {request} from '../utils/RequestUtil';
const get_apps = "/v0.1/apps";
export function* requestAppList(isRefreshing, loading, tokenId, isLoadMore, page){
try{
yield put(fetchAppList(isRefreshing, loading, isLoadMore));
const appList = yield call(request, get_apps, 'get');
yield put(receiveAppList(appList,tokenId));
}catch(error){
yield put(receiveAppList([], tokenId));
toastShort('Network Error, Please Retry!!!');
}
export function* watchFetchAppList() {
yield takeEvery(REQUEST_APPS, fetchAppsFlow);
}
export function* watchRequestAppList(){
while(true){
const {
isRefreshing,
loading,
tokenId,
isLoadMore,
page
} = yield take(types.REQUEST_APP_LIST);
yield fork(requestAppList, isRefreshing, loading, tokenId, isLoadMore, page);
}
}
export function* fetchAppsFlow(action) {
yield fork(fetchApps,
{ tokenId: action.tokenId });
}
export function* fetchApps({ tokenId }) {
try {
const response = yield call(appsAPI, {
tokenId
});
yield put({
type: RECEIVE_APPS,
response
});
} catch (error) {
yield put({
type: RECEIVE_APPS_ERROR,
error
});
toastShort('Network Error, Please Retry!!!');
}
}

Просмотреть файл

@ -23,12 +23,14 @@
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import { fork } from 'redux-saga/effects';
import { watchRequestAppList } from './apps';
import { watchFetchAppList } from './apps';
import { watchRequestLogin } from './login';
export default function* rootSaga(){
yield [
fork(watchRequestAppList)
];
}
yield [
fork(watchRequestLogin),
fork(watchFetchAppList)
];
}

59
js/sagas/login.js Normal file
Просмотреть файл

@ -0,0 +1,59 @@
/**
* Microsoft Mobile Center App
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF
* ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
import { } from 'redux-saga';
import { takeEvery, call, put, fork } from 'redux-saga/effects';
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_ERROR } from '../actions/types';
import { loginAPI } from '../utils/RequestUtil';
export function* watchRequestLogin() {
yield takeEvery(LOGIN_REQUEST, loginFlow);
}
export function* loginFlow(action) {
yield fork(authorize,
{username: action.username, password: action.password });
}
export function* authorize({username, password}) {
try {
const response = yield call(loginAPI, {
username,
password
});
yield put({
type: LOGIN_SUCCESS,
response
});
} catch (error) {
yield put({
type: LOGIN_ERROR,
error
});
}
}

Просмотреть файл

@ -31,14 +31,12 @@ import rootSaga from './sagas/index';
import MobileCenter from './containers/mobilecenter';
const store = configureStore();
//run root saga
store.runSaga(rootSaga);
const setup = () => (
<Provider store={store}>
<MobileCenter/>
</Provider>
<Provider store={store}>
<MobileCenter />
</Provider>
);
export default setup;

Просмотреть файл

@ -24,28 +24,29 @@
*
*/
import {createStore, applyMiddleware} from 'redux';
import createSagaMiddleware, {END} from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware, { END } from 'redux-saga';
import rootReducer from '../reducers/index';
const middlewares = [];
const createLogger = require('redux-logger');
//configure saga middleware
// configure saga middleware
const sagaMiddleware = createSagaMiddleware();
middlewares.push(sagaMiddleware);
if(process.env.NODE_ENV === 'development'){
const logger = createLogger();
middlewares.push(logger);
if (process.env.NODE_ENV === 'development'){
const logger = createLogger();
middlewares.push(logger);
}
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore);
export default function configureStore(initialState) {
const store = createStoreWithMiddleware(rootReducer, initialState);
// install saga run
store.runSaga = sagaMiddleware.run;
store.close = () => store.dispatch(END);
return store;
}
const store = createStoreWithMiddleware(rootReducer, initialState);
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
// install saga run
store.runSaga = sagaMiddleware.run;
store.close = () => store.dispatch(END);
return store;
}

Просмотреть файл

@ -24,38 +24,41 @@
*
*/
const HOST = "https://api.mobile.azure.com";
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
export const TOKENID = "2eb6d0e2250779ad71acde8f383158b48aa0b4b6";
function parseJSON(response) {
return response.json();
}
export const request = (url, method, body) => {
let isOk;
return new Promise((resolve, reject) => {
fetch(HOST + url, {
method,
headers:{
'Accept': 'application/json',
'X-API-Token': TOKENID
},
body
})
.then((response) => {
if(response.ok){
isOk = true;
}else{
isOk = false;
}
return response.json();
})
.then((responseData) => {
if(isOk){
resolve(responseData);
} else {
reject(responseData);
}
})
.catch((error) => {
reject(error);
});
});
};
function fetchAPI(url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON);
}
export function loginAPI({ username, password }) {
return fetchAPI('https://api.mobile.azure.com/v0.1/api_tokens', {
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ username, password })
});
}
export function appsAPI({ tokenID }) {
return fetchAPI('https://api.mobile.azure.com/v0.1/apps', {
headers: {
'Content-Type': 'applicatin/json',
'X-API-Token': tokenID
},
method: 'GET'
});
}

9
jsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,9 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
]
}

Просмотреть файл

@ -5,27 +5,38 @@
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"lint": "eslint ."
"lint": "eslint .",
"storybook": "storybook start -p 7007"
},
"dependencies": {
"buffer": "^5.0.2",
"moment": "^2.17.1",
"react": "~15.4.0-rc.4",
"react-native": "0.40.0",
"react-native-button": "^1.8.2",
"react-native-card-view": "0.0.3",
"react-native-code-push": "^1.17.0-beta",
"react-native-css": "^1.2.47",
"react-native-device-info": "^0.9.9",
"react-native-drawer": "^2.3.0",
"react-native-push-notification": "^2.2.1",
"react-native-router-flux": "^3.37.0",
"react-native-scrollable-tab-view": "^0.7.0",
"react-native-search-bar": "^3.0.0",
"react-native-searchbar": "^1.8.0",
"react-native-send-intent": "^1.0.14",
"react-native-simple-store": "^1.1.0",
"react-native-storage": "^0.1.5",
"react-native-vector-icons": "^4.0.0",
"react-redux": "^5.0.2",
"react-toolbox": "^2.0.0-beta.6",
"redux": "^3.6.0",
"redux-logger": "^2.7.4",
"redux-persist": "^4.0.1",
"redux-saga": "^0.14.2",
"redux-thunk": "^2.1.0",
"reselect": "^2.5.4"
"reselect": "^2.5.4",
"tcomb-form-native": "^0.6.5"
},
"devDependencies": {
"babel-jest": "18.0.0",
@ -37,7 +48,8 @@
"eslint-plugin-react": "^6.9.0",
"jest": "18.1.0",
"react-test-renderer": "~15.4.0-rc.4",
"redux-devtools": "^3.3.2"
"redux-devtools": "^3.3.2",
"@kadira/react-native-storybook": "^2.0.0"
},
"jest": {
"preset": "react-native"

1
storybook/addons.js Normal file
Просмотреть файл

@ -0,0 +1 @@
import '@kadira/react-native-storybook/addons';

Просмотреть файл

@ -0,0 +1,10 @@
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@kadira/react-native-storybook';
// import stories
configure(() => {
require('./stories');
}, module);
const StorybookUI = getStorybookUI({port: 7007, host: 'localhost'});
AppRegistry.registerComponent('MobileCenterReactNativeApp', () => StorybookUI);

10
storybook/index.ios.js Normal file
Просмотреть файл

@ -0,0 +1,10 @@
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@kadira/react-native-storybook';
// import stories
configure(() => {
require('./stories');
}, module);
const StorybookUI = getStorybookUI({port: 7007, host: 'localhost'});
AppRegistry.registerComponent('MobileCenterReactNativeApp', () => StorybookUI);

Просмотреть файл

@ -0,0 +1,10 @@
import React from 'react';
import { TouchableNativeFeedback } from 'react-native';
export default function Button(props) {
return (
<TouchableNativeFeedback onPress={props.onPress || Function()}>
{props.children}
</TouchableNativeFeedback>
);
}

Просмотреть файл

@ -0,0 +1,10 @@
import React from 'react';
import { TouchableHighlight } from 'react-native';
export default function Button(props) {
return (
<TouchableHighlight onPress={props.onPress || Function()}>
{props.children}
</TouchableHighlight>
);
}

Просмотреть файл

@ -0,0 +1,11 @@
import React from 'react';
import { View } from 'react-native';
import style from './style';
export default function CenterView(props) {
return (
<View style={style.main}>
{props.children}
</View>
);
}

Просмотреть файл

@ -0,0 +1,8 @@
export default {
main: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
};

Просмотреть файл

@ -0,0 +1,36 @@
import React from 'react';
import { View, Text } from 'react-native';
export default class Welcome extends React.Component {
styles = {
wrapper: {
flex: 1,
padding: 24,
justifyContent: 'center',
},
header: {
fontSize: 18,
marginBottom: 18,
},
content: {
fontSize: 12,
marginBottom: 10,
lineHeight: 18,
},
}
showApp(e) {
e.preventDefault();
if(this.props.showApp) this.props.showApp();
}
render() {
return (
<View style={this.styles.wrapper}>
<Text style={this.styles.header}>Welcome to React Native Storybook</Text>
<Text style={this.styles.content}>This is a UI Component development environment for your React Native app. Here you can display and interact with your UI components as stories. A story is a single state of one or more UI components. You can have as many stories as you want. In other words a story is like a visual test case.</Text>
<Text style={this.styles.content}>We have added some stories inside the "storybook/stories" directory for examples. Try editing the "storybook/stories/Welcome.js" file to edit this message.</Text>
</View>
);
}
}

Просмотреть файл

@ -0,0 +1,27 @@
import React from 'react';
import { Text } from 'react-native';
import { storiesOf, action, linkTo } from '@kadira/react-native-storybook';
import Button from './Button';
import CenterView from './CenterView';
import Welcome from './Welcome';
storiesOf('Welcome', module)
.add('to Storybook', () => (
<Welcome showApp={linkTo('Button')}/>
));
storiesOf('Button', module)
.addDecorator(getStory => (
<CenterView>{getStory()}</CenterView>
))
.add('with text', () => (
<Button onPress={action('clicked-text')}>
<Text>Hello Button</Text>
</Button>
))
.add('with some emoji', () => (
<Button onPress={action('clicked-emoji')}>
<Text>😀 😎 👍 💯</Text>
</Button>
));

3505
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу