├── .watchmanconfig
├── server
├── .nvmrc
├── package.json
├── follows.js
├── secured.js
├── posts.js
├── server.js
├── auth.js
└── models.js
├── App
├── jsVersion.js
├── Locales
│ ├── en.js
│ ├── base.js
│ ├── en-GB-xtra.js
│ ├── en-GB.js
│ ├── en-US.js
│ └── boot.js
├── Vendor
│ └── react-native-refreshable-listview
│ │ ├── .travis.yml
│ │ ├── index.js
│ │ ├── .npmignore
│ │ ├── .eslintignore
│ │ └── lib
│ │ ├── ListView.js
│ │ ├── delay.js
│ │ ├── createElementFrom.js
│ │ ├── RefreshingIndicator.js
│ │ ├── RefreshableListView.js
│ │ └── ControlledRefreshableListView.js
├── Platform
│ ├── Keychain.ios.js
│ ├── Back.ios.js
│ ├── RefreshableListView.ios.js
│ ├── StatusBar.android.js
│ ├── StatusBar.ios.js
│ ├── Back.android.js
│ ├── Keychain.android.js
│ └── RefreshableListView.android.js
├── Dispatcher.js
├── Root
│ ├── TestComponents.js
│ ├── LoggedIn.js
│ ├── LoggedOut.js
│ ├── Launch.js
│ ├── Launcher.js
│ └── TestRunner.js
├── Lib
│ ├── cssVar.js
│ ├── CSSVarConfig.js
│ └── assignDefined.js
├── Models
│ ├── Follow.js
│ ├── Post.js
│ ├── CurrentUser.js
│ └── Environment.js
├── Constants
│ └── AppConstants.js
├── Actions
│ ├── FollowActions.js
│ ├── AuthActions.js
│ ├── PostActions.js
│ └── AppActions.js
├── Screens
│ ├── LogIn.js
│ ├── SignUp.js
│ ├── Settings.js
│ ├── Loading.js
│ ├── PostList.js
│ ├── FollowList.js
│ └── CreatePost.js
├── Components
│ ├── Text.js
│ ├── TextInput.js
│ ├── SimpleList.js
│ ├── AutoScaleText.js
│ ├── SpinnerLoader.js
│ ├── SimpleListItem.js
│ └── SegmentedControl.js
├── Api
│ ├── FollowService.js
│ ├── AuthService.js
│ ├── Network.js
│ ├── PostService.js
│ └── HTTPClient.js
├── Navigation
│ ├── Navigator.js
│ ├── NavigationHeader.js
│ ├── NavigationTitle.js
│ ├── NavigationBar.js
│ ├── NavigationButton.js
│ ├── Router.js
│ └── Routes.js
├── Mixins
│ ├── DispatcherListener.js
│ ├── NavBarHelper.js
│ ├── NavigationListener.js
│ ├── KeyboardListener.js
│ └── ListHelper.js
├── Extensions
│ ├── AddSpinnerLoader.js
│ └── Extension.js
├── Stores
│ ├── FollowListStore.js
│ ├── EnvironmentStore.js
│ ├── LocalKeyStore.js
│ ├── PostListStore.js
│ ├── DebugStore.js
│ └── CurrentUserStore.js
└── Locale.js
├── android
├── settings.gradle
├── app
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ └── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── sample
│ │ │ │ ├── SampleConstants.java
│ │ │ │ ├── MainActivity.java
│ │ │ │ ├── utils
│ │ │ │ └── LocaleUtils.java
│ │ │ │ ├── MainApplication.java
│ │ │ │ ├── SamplePackage.java
│ │ │ │ ├── TLSSetup.java
│ │ │ │ ├── TestRunnerManager.java
│ │ │ │ └── TLSSocketFactory.java
│ │ │ └── AndroidManifest.xml
│ ├── BUCK
│ └── proguard-rules.pro
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── keystores
│ ├── debug.keystore.properties
│ └── BUCK
├── build.gradle
├── gradle.properties
├── Sample.iml
└── gradlew.bat
├── screenshots
├── follows.png
├── sign_up.png
├── show_post.png
└── create_post.png
├── tasks
├── compiler.js
├── translation.js
├── compile.js
├── translation-en-US.json
├── react-native-stub.js
└── translation-en-GB.json
├── .babelrc
├── .buckconfig
├── ios
├── Debug.xcconfig
├── Staging.xcconfig
├── Podfile
├── Sample
│ ├── Sample.entitlements
│ ├── AppDelegate.h
│ ├── main.m
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── TestRunnerManager.m
│ ├── EnvironmentManager.m
│ ├── Info.plist
│ ├── AppDelegate.m
│ └── Base.lproj
│ │ └── LaunchScreen.xib
├── Sample.xcworkspace
│ └── contents.xcworkspacedata
├── Podfile.lock
├── main.jsbundle
└── Sample.xcodeproj
│ └── xcshareddata
│ └── xcschemes
│ ├── Sample.xcscheme
│ ├── Sample Staging.xcscheme
│ └── Sample Test.xcscheme
├── index.ios.js
├── index.android.js
├── test
├── integration
│ ├── smoke.test.js
│ ├── authentication.test.js
│ ├── posts.test.js
│ └── follows.test.js
└── helpers
│ ├── dispatcher.js
│ ├── packager.js
│ ├── fixtures.js
│ ├── bootstrap.js
│ ├── driver.js
│ └── server.js
├── .gitignore
├── .travis.yml
├── MIT-LICENSE
├── .flowconfig
└── package.json
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/server/.nvmrc:
--------------------------------------------------------------------------------
1 | 4.2.3
--------------------------------------------------------------------------------
/App/jsVersion.js:
--------------------------------------------------------------------------------
1 | export default 3;
2 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'Sample'
2 |
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/App/Locales/en.js:
--------------------------------------------------------------------------------
1 | // base translations that get translated
2 |
3 | export default {
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/RefreshableListView')
2 |
--------------------------------------------------------------------------------
/screenshots/follows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/screenshots/follows.png
--------------------------------------------------------------------------------
/screenshots/sign_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/screenshots/sign_up.png
--------------------------------------------------------------------------------
/tasks/compiler.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 |
3 | module.exports = require('./_compiler').default;
4 |
--------------------------------------------------------------------------------
/App/Platform/Keychain.ios.js:
--------------------------------------------------------------------------------
1 | import * as KeyChain from 'react-native-keychain';
2 |
3 | export default KeyChain;
4 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | **/__tests__/**/*.js
3 | **/__mocks__/**/*.js
4 |
--------------------------------------------------------------------------------
/screenshots/show_post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/screenshots/show_post.png
--------------------------------------------------------------------------------
/App/Dispatcher.js:
--------------------------------------------------------------------------------
1 | import flux from 'flux';
2 | const {Dispatcher} = flux;
3 |
4 | export default new Dispatcher();
5 |
--------------------------------------------------------------------------------
/App/Platform/Back.ios.js:
--------------------------------------------------------------------------------
1 | const Back = {
2 | setNavigator(navigator) {
3 | },
4 | };
5 |
6 | export default Back;
7 |
--------------------------------------------------------------------------------
/screenshots/create_post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/screenshots/create_post.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"],
3 | "plugins": ["syntax-async-functions", "transform-regenerator"]
4 | }
5 |
--------------------------------------------------------------------------------
/App/Locales/base.js:
--------------------------------------------------------------------------------
1 | // these do not get translated, mostly for values or something
2 |
3 | export default {
4 |
5 | };
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sample
3 |
4 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/.eslintignore:
--------------------------------------------------------------------------------
1 | example/
2 | test/
3 | **/__tests__/**/*.js
4 | **/__mocks__/**/*.js
5 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/ListView.js:
--------------------------------------------------------------------------------
1 | var {ListView} = require('react-native')
2 |
3 | module.exports = ListView
4 |
--------------------------------------------------------------------------------
/App/Locales/en-GB-xtra.js:
--------------------------------------------------------------------------------
1 | // Programatic GB translations - do not add directly
2 | // Use npm run translation:backfill
3 |
4 | export default {};
--------------------------------------------------------------------------------
/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/App/Platform/RefreshableListView.ios.js:
--------------------------------------------------------------------------------
1 | import RefreshableListView from '../Vendor/react-native-refreshable-listview';
2 |
3 | export default RefreshableListView;
4 |
--------------------------------------------------------------------------------
/android/keystores/debug.keystore.properties:
--------------------------------------------------------------------------------
1 | key.store=debug.keystore
2 | key.alias=androiddebugkey
3 | key.store.password=android
4 | key.alias.password=android
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ios/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"
2 |
3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) kEnvironment="@\"debug\""
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taskrabbit/ReactNativeSampleApp/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ios/Staging.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Sample/Pods-Sample.staging.xcconfig"
2 |
3 |
4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) kEnvironment="@\"staging\""
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/delay.js:
--------------------------------------------------------------------------------
1 | function delay(time) {
2 | return new Promise((resolve) => setTimeout(resolve, time))
3 | }
4 |
5 | module.exports = delay
6 |
--------------------------------------------------------------------------------
/android/keystores/BUCK:
--------------------------------------------------------------------------------
1 | keystore(
2 | name = 'debug',
3 | store = 'debug.keystore',
4 | properties = 'debug.keystore.properties',
5 | visibility = [
6 | 'PUBLIC',
7 | ],
8 | )
9 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/SampleConstants.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | public class SampleConstants {
4 | public static final String APP_PREFS_NAME = "sample-app-prefs";
5 | }
6 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '8.0'
2 | source 'https://github.com/CocoaPods/Specs.git'
3 |
4 | # inhibit_all_warnings!
5 |
6 | target 'Sample' do
7 | pod 'SimulatorRemoteNotifications', '~> 0.0.3'
8 | end
9 |
--------------------------------------------------------------------------------
/index.ios.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import {
5 | AppRegistry,
6 | } from 'react-native';
7 |
8 | import Root from './App/Root';
9 | AppRegistry.registerComponent('Sample', () => Root);
10 |
--------------------------------------------------------------------------------
/index.android.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import {
5 | AppRegistry,
6 | } from 'react-native';
7 |
8 | import Root from './App/Root';
9 | AppRegistry.registerComponent('Sample', () => Root);
10 |
--------------------------------------------------------------------------------
/App/Platform/StatusBar.android.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | var StatusBar = {
4 | setNetworkActive(active) {
5 | },
6 | setHidden(hidden) {
7 | },
8 | setStyle(style) {
9 | },
10 | };
11 |
12 | export default StatusBar;
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tasks/translation.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register')({
2 | // This will override `node_modules` ignoring - you can alternatively pass
3 | // an array of strings to be explicitly matched or a regex / glob
4 | ignore: /node_modules/,
5 | });
6 | require('./_translation');
7 |
--------------------------------------------------------------------------------
/App/Root/TestComponents.js:
--------------------------------------------------------------------------------
1 | var components = {
2 | 'Components/SimpleListItem': require("../Components/SimpleListItem").default
3 | };
4 |
5 | var TestComponents = {
6 | find(name) {
7 | return components[name];
8 | }
9 | };
10 |
11 | export default TestComponents;
12 |
--------------------------------------------------------------------------------
/tasks/compile.js:
--------------------------------------------------------------------------------
1 | var Compiler = require("./compiler");
2 |
3 | var compiler = new Compiler('ios', process.env.TARGET, console);
4 | compiler.cleanDirectory();
5 | compiler.build();
6 | compiler.zip();
7 |
8 | if (process.env.PHONE) {
9 | compiler.phoneInstall();
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 20 16:55:35 PDT 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 |
--------------------------------------------------------------------------------
/App/Root/LoggedIn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NavigationBar from '../Navigation/NavigationBar';
3 |
4 | var LoggedIn = React.createClass({
5 | mixins: [NavigationBar],
6 |
7 | getDefaultProps: function() {
8 | return {};
9 | },
10 | });
11 |
12 | export default LoggedIn;
13 |
--------------------------------------------------------------------------------
/ios/Sample/Sample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 |
8 |
9 |
--------------------------------------------------------------------------------
/test/integration/smoke.test.js:
--------------------------------------------------------------------------------
1 | import it from '../helpers/appium';
2 |
3 | describe("integration smoke test", () => {
4 | it("should launch the simulator and compute the sum", function* (driver, done) {
5 | var value = 1 + 1;
6 | value.should.equal(2);
7 | done();
8 | });
9 |
10 | });
11 |
--------------------------------------------------------------------------------
/ios/Sample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/helpers/dispatcher.js:
--------------------------------------------------------------------------------
1 |
2 | var Dispatcher = {
3 | login: function() {
4 | return {
5 | token: "fe37a156494cae1f2761ab2845e6cc3446010267",
6 | userProps: {
7 | id: 123,
8 | username: "tester"
9 | }
10 | };
11 | },
12 | };
13 |
14 | module.exports = Dispatcher;
15 |
--------------------------------------------------------------------------------
/App/Root/LoggedOut.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import NavigationBar from '../Navigation/NavigationBar';
4 |
5 | var LoggedOut = React.createClass({
6 | mixins: [NavigationBar],
7 |
8 | getDefaultProps: function() {
9 | return {navBarHidden: true};
10 | },
11 | });
12 |
13 | export default LoggedOut;
14 |
--------------------------------------------------------------------------------
/App/Lib/cssVar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import invariant from 'invariant';
4 | import CSSVarConfig from '../Lib/CSSVarConfig';
5 |
6 | var cssVar = function(/*string*/ key) /*string*/ {
7 | invariant(CSSVarConfig[key], 'invalid css variable ' + key);
8 |
9 | return CSSVarConfig[key];
10 | };
11 |
12 | export default cssVar;
13 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SimulatorRemoteNotifications (0.0.3)
3 |
4 | DEPENDENCIES:
5 | - SimulatorRemoteNotifications (~> 0.0.3)
6 |
7 | SPEC CHECKSUMS:
8 | SimulatorRemoteNotifications: 1610643e0582b2af67bc254b071ace4077e8ef86
9 |
10 | PODFILE CHECKSUM: 1e7bba3fe4e4d12126efc77f3f430a3dcd1d8be9
11 |
12 | COCOAPODS: 1.1.1
13 |
--------------------------------------------------------------------------------
/ios/main.jsbundle:
--------------------------------------------------------------------------------
1 | // Offline JS
2 | // To re-generate the offline bundle, run this from the root of your project:
3 | //
4 | // $ react-native bundle --minify
5 | //
6 | // See http://facebook.github.io/react-native/docs/runningondevice.html for more details.
7 |
8 | throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions');
9 |
--------------------------------------------------------------------------------
/App/Platform/StatusBar.ios.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StatusBar
4 | } from 'react-native';
5 |
6 | var _StatusBar = {
7 | setNetworkActive: function(active) {
8 | StatusBar.setNetworkActivityIndicatorVisible(active);
9 | },
10 |
11 | setHidden: function(hidden) {
12 | StatusBar.setHidden(hidden);
13 | }
14 | };
15 |
16 | export default _StatusBar;
17 |
--------------------------------------------------------------------------------
/App/Models/Follow.js:
--------------------------------------------------------------------------------
1 | import assign from '../Lib/assignDefined';
2 |
3 | var Model = function(options) {
4 | this.data = {};
5 | this.setAttributes(options);
6 | };
7 |
8 | Model.prototype.setAttributes = function(options) {
9 | options = (options || {});
10 | assign(this.data, {
11 | id: options.id,
12 | username: options.username
13 | });
14 | };
15 |
16 | export default Model;
17 |
--------------------------------------------------------------------------------
/App/Models/Post.js:
--------------------------------------------------------------------------------
1 | import assign from '../Lib/assignDefined';
2 |
3 | var Model = function(options) {
4 | this.data = {};
5 | this.setAttributes(options);
6 | };
7 |
8 | Model.prototype.setAttributes = function(options) {
9 | options = (options || {});
10 | assign(this.data, {
11 | id: options.id,
12 | content: options.content,
13 | username: options.username
14 | });
15 | };
16 |
17 | export default Model;
18 |
--------------------------------------------------------------------------------
/App/Root/Launch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | Text,
5 | View,
6 | StyleSheet
7 | } from 'react-native';
8 |
9 |
10 | var Launch = React.createClass({
11 | render: function() {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 | });
19 |
20 | var styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | backgroundColor: '#F5FCFF',
24 | }
25 | });
26 |
27 | export default Launch;
28 |
--------------------------------------------------------------------------------
/App/Constants/AppConstants.js:
--------------------------------------------------------------------------------
1 | import keyMirror from 'keymirror';
2 |
3 | export default keyMirror({
4 | APP_LAUNCHED: null,
5 | LOGIN_USER: null,
6 | LOGOUT_REQUESTED: null,
7 | RELOAD_PATH: null,
8 | NETWORK_ACTIVITY: null,
9 | LAUNCH_ROUTE_PATH: null,
10 | NETWORK_ACTIVITY: null,
11 | NAVBAR_UPDATE: null,
12 | POST_LIST_UPDATED: null,
13 | POST_ADDED: null,
14 | FOLLOW_LIST_UPDATED: null,
15 | TEST_COMPONENT_ROUTE: null,
16 | DEBUG_CURRENT_ROUTE_PATH_KEY: null,
17 | });
18 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node --harmony server.js"
7 | },
8 | "dependencies": {
9 | "koa": "^1.0.0",
10 | "koa-bodyparser": "^2.0.1",
11 | "koa-error": "^1.1.3",
12 | "koa-logger": "^1.3.0",
13 | "koa-passport": "^1.2.0",
14 | "koa-router": "^5.1.2",
15 | "passport": "^0.3.0",
16 | "passport-http-bearer": "^1.0.1",
17 | "password": "^0.1.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/createElementFrom.js:
--------------------------------------------------------------------------------
1 | var React = require('react')
2 | var {
3 | cloneElement,
4 | createElement,
5 | isValidElement,
6 | } = React
7 |
8 |
9 | function createElementFrom(elementOrClass, props) {
10 |
11 | if (isValidElement(elementOrClass)) {
12 | return cloneElement(elementOrClass, props)
13 | } else { // is a component class, not an element
14 | return createElement(elementOrClass, props)
15 | }
16 | }
17 |
18 | module.exports = createElementFrom
19 |
--------------------------------------------------------------------------------
/ios/Sample/AppDelegate.h:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import
11 |
12 | @interface AppDelegate : UIResponder
13 |
14 | @property (nonatomic, strong) UIWindow *window;
15 |
16 | @end
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IJ
26 | #
27 | .idea
28 | .gradle
29 | local.properties
30 |
31 | # node.js
32 | #
33 | node_modules/
34 | npm-debug.log
35 |
36 | # Custom
37 | Pods
38 | /testbuild
39 |
--------------------------------------------------------------------------------
/ios/Sample/main.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import
11 |
12 | #import "AppDelegate.h"
13 |
14 | int main(int argc, char * argv[]) {
15 | @autoreleasepool {
16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tasks/translation-en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "fail": {
3 | "lower": [
4 | "organis",
5 | "neighbour",
6 | "postcode",
7 | "post code",
8 | "postal code",
9 | "takeaway",
10 | "cancelled",
11 | "queue in line",
12 | "revolutionis",
13 | "categoris",
14 | "christmas help",
15 | "hoover",
16 | "lorry",
17 | "behaviour",
18 | "favourite",
19 | "surname",
20 | "sort code",
21 | "www.taskrabbit.co.uk",
22 | "taskrabbit.co.uk",
23 | "£"
24 | ],
25 | "sensitive": [
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/App/Models/CurrentUser.js:
--------------------------------------------------------------------------------
1 | import assign from '../Lib/assignDefined';
2 |
3 | var Model = function(options, token) {
4 | this.data = {};
5 | this.token = token;
6 | this.setAttributes(options);
7 | };
8 |
9 | Model.prototype.setAttributes = function(options) {
10 | options = (options || {});
11 | assign(this.data, {
12 | id: options.id,
13 | username: options.username
14 | });
15 | };
16 |
17 | Model.prototype.getToken = function() {
18 | return this.token;
19 | };
20 |
21 | Model.prototype.isLoggedIn = function() {
22 | return !!this.token;
23 | };
24 |
25 | export default Model;
26 |
--------------------------------------------------------------------------------
/App/Actions/FollowActions.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 | import AppConstants from '../Constants/AppConstants';
3 | import FollowService from '../Api/FollowService';
4 |
5 | var FollowActions = {
6 |
7 | fetchList: function(username, callback) {
8 | FollowService.fetchList(username, function(error, listProps) {
9 | if(callback) callback(error);
10 |
11 | if (!error) {
12 | Dispatcher.dispatch({
13 | actionType: AppConstants.FOLLOW_LIST_UPDATED,
14 | listProps: listProps
15 | });
16 | }
17 | });
18 | }
19 | };
20 |
21 | export default FollowActions;
22 |
--------------------------------------------------------------------------------
/App/Platform/Back.android.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | BackAndroid,
5 | } from 'react-native';
6 |
7 | import AppActions from '../Actions/AppActions';
8 | // import DrawerStore from '../Stores/DrawerStore';
9 |
10 | const Back = {
11 | setNavigator(navigator) {
12 | BackAndroid.addEventListener('hardwareBackPress', () => {
13 | // if (DrawerStore.get().open === true) {
14 | // AppActions.toggleDrawer();
15 | // return true;
16 | // }
17 | if (navigator && navigator.back()) {
18 | return true;
19 | }
20 | return false;
21 | });
22 | },
23 | };
24 |
25 | export default Back;
26 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import android.os.Bundle;
4 |
5 | import com.facebook.react.ReactActivity;
6 |
7 | public class MainActivity extends ReactActivity {
8 |
9 | /**
10 | * Returns the name of the main component registered from JavaScript.
11 | * This is used to schedule rendering of the component.
12 | */
13 | @Override
14 | protected String getMainComponentName() {
15 | return "Sample";
16 | }
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 |
22 | TLSSetup.configure();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Hepburn
2 | language: objective-c
3 | osx_image: xcode7
4 | xcode_sdk: iphonesimulator9.0
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 | - ios/Pods
10 | - ~/.nvm
11 |
12 | before_install:
13 | - export NVM_DIR=~/.nvm
14 | - which nvm || curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.29.0/install.sh | bash
15 | - source ~/.nvm/nvm.sh --install
16 | - nvm install 4.2.3
17 | - brew update
18 | - brew reinstall xctool
19 | - brew reinstall watchman
20 | - npm install
21 | - gem install xcpretty
22 | - gem install cocoapods
23 | - pod install --project-directory=ios
24 |
25 | before_script:
26 | - npm run compile:test
27 |
28 | script:
29 | - npm test
30 |
--------------------------------------------------------------------------------
/App/Screens/LogIn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AuthHelper from '../Mixins/AuthHelper';
4 | import AuthActions from '../Actions/AuthActions';
5 |
6 | var LogIn = React.createClass({
7 | mixins: [AuthHelper],
8 |
9 | getDefaultProps: function() {
10 | return {
11 | authType: 'login'
12 | };
13 | },
14 |
15 | onAuthButton: function() {
16 | var username = this.state.username;
17 | var password = this.state.password;
18 | AuthActions.submitLogin(username, password, function(error) {
19 | if (error) {
20 | // TODO: better errors
21 | alert(error.message);
22 | }
23 | });
24 | // TODO: setState to denote busy
25 | },
26 | });
27 |
28 | export default LogIn;
29 |
--------------------------------------------------------------------------------
/App/Screens/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AuthHelper from '../Mixins/AuthHelper';
4 | import AuthActions from '../Actions/AuthActions';
5 |
6 | var SignUp = React.createClass({
7 | mixins: [AuthHelper],
8 |
9 | getDefaultProps: function() {
10 | return {
11 | authType: 'signup'
12 | };
13 | },
14 |
15 | onAuthButton: function() {
16 | var username = this.state.username;
17 | var password = this.state.password;
18 | AuthActions.submitSignup(username, password, function(error) {
19 | if (error) {
20 | // TODO: better errors
21 | alert(error.message);
22 | }
23 | });
24 | // TODO: setState to denote busy
25 | },
26 | });
27 |
28 | export default SignUp;
29 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.0'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | mavenLocal()
18 | jcenter()
19 | maven {
20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
21 | url "$rootDir/../node_modules/react-native/android"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/utils/LocaleUtils.java:
--------------------------------------------------------------------------------
1 | package com.sample.utils;
2 |
3 | import java.util.Locale;
4 |
5 | /**
6 | * TaskRabbit created on 5/12/14.
7 | */
8 | public class LocaleUtils {
9 |
10 | public static Locale getDefault() {
11 | if (null == Locale.getDefault()) { return Locale.US; }
12 | return Locale.getDefault();
13 | }
14 |
15 | public static String getServerLocale(Locale locale) {
16 | if (null != locale) {
17 | return locale.toString().replace("_", "-");
18 | } else {
19 | return "";
20 | }
21 | }
22 |
23 | public static String getDefaultLocaleForServer() {
24 | Locale locale = getDefault();
25 | return getServerLocale(locale);
26 | }
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/server/follows.js:
--------------------------------------------------------------------------------
1 | var User = require('./models').User;
2 |
3 | var renderFollow = function(follow) {
4 | return {
5 | id: follow.follow_id,
6 | username: follow.username
7 | };
8 | };
9 |
10 | var renderFollows = function(user) {
11 | var out = {
12 | follows: [],
13 | username: user.username
14 | };
15 | var follows = user.getFollows();
16 | for(var i in follows) {
17 | var follow = follows[i];
18 | out.follows.push(renderFollow(follow));
19 | }
20 |
21 | return out;
22 | };
23 |
24 | exports.userFollows = function *() {
25 | var username = this.params.username;
26 | var user = User.findByUsername(username);
27 | if (!user) {
28 | this.throw("No user found", 404);
29 | }
30 | this.status = 200;
31 | this.body = renderFollows(user);
32 | };
33 |
--------------------------------------------------------------------------------
/ios/Sample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/App/Lib/CSSVarConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import {
5 | PixelRatio
6 | } from 'react-native';
7 |
8 | export default {
9 | gray90: '#323A3B',
10 | gray50: '#828A8B',
11 | gray30: '#B4B9B9',
12 | gray20: '#CFD2D3',
13 | gray10: '#EBECEC',
14 | gray5: '#F5F6F6',
15 |
16 | blue50: '#23B4D2',
17 |
18 | // http://iosfonts.com/
19 | fontRegular: "HelveticaNeue",
20 | fontIcon: "HelveticaNeue", // TODO: get an icon font and include
21 |
22 | listLine: {
23 | backgroundColor: 'rgba(0, 0, 0, 0.1)',
24 | height: 1 / PixelRatio.get(), // thinnest possible line
25 | marginLeft: 10,
26 | },
27 | listFullLine: {
28 | backgroundColor: 'rgba(0, 0, 0, 0.1)',
29 | height: 1 / PixelRatio.get(), // thinnest possible line
30 | marginLeft: 0,
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/App/Locales/en-GB.js:
--------------------------------------------------------------------------------
1 | // Known GB Translations - generally matches US ones
2 |
3 | export default {
4 | datetime: {
5 | day_and_time: '%a, %b %-d, %-H:%M',
6 | day_header: '%+A - %B %d',
7 | short_day: '%a %b %-d',
8 | short_time: '%-H:%M',
9 | month_day: '%B %-d',
10 | short_month_day: '%b %-d',
11 | month_day_year: '%e %b %Y',
12 | month_day_year_at_time: '%-d %B %Y at %-H:%M',
13 | short_weekday_name: '%a',
14 | long_day_of_month: '%d',
15 | time_zone: '%Z',
16 | },
17 | number: {
18 | currency: {
19 | format: {
20 | delimiter: ',',
21 | format: '%u%n',
22 | precision: 2,
23 | separator: '.',
24 | significant: false,
25 | strip_insignificant_zeros: false,
26 | unit: '£',
27 | },
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/App/Locales/en-US.js:
--------------------------------------------------------------------------------
1 | // All US Translations, more or less should match the GB one
2 |
3 | export default {
4 | datetime: {
5 | day_and_time: '%a, %b %-d, %-I:%M %p',
6 | day_header: '%+A - %B %d',
7 | short_day: '%a %b %-d',
8 | short_time: '%-I:%M %p',
9 | month_day: '%B %-d',
10 | short_month_day: '%b %-d',
11 | month_day_year: '%b %e, %Y',
12 | month_day_year_at_time: '%B %-d, %Y at %-I:%M %p',
13 | short_weekday_name: '%a',
14 | long_day_of_month: '%d',
15 | time_zone: '%Z',
16 | },
17 | number: {
18 | currency: {
19 | format: {
20 | delimiter: ',',
21 | format: '%u%n',
22 | precision: 2,
23 | separator: '.',
24 | significant: false,
25 | strip_insignificant_zeros: false,
26 | unit: '$',
27 | },
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/App/Components/Text.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ReactNative, {
4 | Text,
5 | StyleSheet,
6 | } from 'react-native';
7 |
8 | import cssVar from '../Lib/cssVar';
9 |
10 | var _Text = React.createClass({
11 | propTypes: Text.propTypes,
12 |
13 | setNativeProps() {
14 | var text = this.refs.text;
15 | text.setNativeProps.apply(text, arguments);
16 | },
17 | render() {
18 | return (
19 |
24 | );
25 | }
26 | })
27 |
28 | var styles = StyleSheet.create({
29 | text: {
30 | fontFamily: cssVar('fontRegular'),
31 | color: cssVar('gray90'),
32 | fontSize: 8 // make it small to know it's not set
33 | }
34 | });
35 |
36 | export default _Text;
37 |
--------------------------------------------------------------------------------
/server/secured.js:
--------------------------------------------------------------------------------
1 | var passport = require("koa-passport");
2 | var BearerStrategy = require('passport-http-bearer').Strategy;
3 | var User = require("./models").User;
4 |
5 | passport.use(new BearerStrategy({},
6 | function(token, done) {
7 | user = User.findByToken(token);
8 | if (user) return done(null, user);
9 | else return done(null, false);
10 | }
11 | ));
12 |
13 | var secured = function *(next) {
14 | var _this = this;
15 | yield passport.authenticate("bearer", { session: false },
16 | function*(err, user, info) {
17 | if (err) {
18 | throw err;
19 | }
20 | else if (!user) {
21 | _this.status = 401;
22 | _this.body = {error: "Please log in"}
23 | }
24 | else {
25 | _this.passport.user = user;
26 | yield next;
27 | }
28 | });
29 | };
30 |
31 | module.exports = secured;
32 |
--------------------------------------------------------------------------------
/App/Actions/AuthActions.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 | import AppConstants from '../Constants/AppConstants';
3 | import AuthService from '../Api/AuthService';
4 |
5 | var AuthActions = {
6 | authCallback: function(callback) {
7 | return function(error, data) {
8 | if(callback) callback(error);
9 |
10 | if (!error) {
11 | Dispatcher.dispatch({
12 | actionType: AppConstants.LOGIN_USER,
13 | userProps: data.userProps,
14 | token: data.token
15 | });
16 | }
17 | };
18 | },
19 |
20 | submitLogin: function(username, password, callback) {
21 | AuthService.login(username, password, this.authCallback(callback));
22 | },
23 |
24 | submitSignup: function(username, password, callback) {
25 | AuthService.signup(username, password, this.authCallback(callback));
26 | }
27 | };
28 |
29 | export default AuthActions;
30 |
--------------------------------------------------------------------------------
/App/Api/FollowService.js:
--------------------------------------------------------------------------------
1 | import client from '../Api/HTTPClient';
2 |
3 | var FolllowService = {
4 | parseFolllow: function(response) {
5 | if (!response) return null;
6 |
7 | return {
8 | id: response.id,
9 | username: response.username
10 | };
11 | },
12 |
13 | parseFolllows: function(response) {
14 | if (!response) return null;
15 |
16 | var out = {follows: []};
17 | for(var i in response.follows) {
18 | out.follows.push(FolllowService.parseFolllow(response.follows[i]));
19 | }
20 | out.username = response.username;
21 | return out;
22 | },
23 |
24 | fetchList: function(username, callback) {
25 | client.get("api/follows/" + username, {}, function(error, response) {
26 | var listProps = FolllowService.parseFolllows(response);
27 | callback(error, listProps);
28 | });
29 | }
30 | };
31 |
32 | export default FolllowService;
33 |
--------------------------------------------------------------------------------
/App/Navigation/Navigator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | NavigationExperimental,
4 | } from 'react-native';
5 |
6 | import AppActions from '../Actions/AppActions';
7 |
8 | const {
9 | StateUtils: NavigationStateUtils,
10 | } = NavigationExperimental;
11 |
12 | var Navigator = function() {
13 | this.stack = null;
14 | };
15 |
16 | Navigator.prototype.setStack = function(stack) {
17 | this.stack = stack;
18 | };
19 |
20 | Navigator.prototype.back = function() {
21 | if (!this.stack) return false;
22 |
23 | var state = this.stack.props.navigationState;
24 | var index = state.index;
25 | if (index === 0) return false;
26 |
27 | var updated = NavigationStateUtils.pop(state);
28 | var routes = updated.routes;
29 | if(routes.length === 0) return false;
30 |
31 | var previous = routes[routes.length-1];
32 | AppActions.launchRoutePath(previous.routePath);
33 | };
34 |
35 | export default Navigator;
36 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | android.useDeprecatedNdk=true
21 |
--------------------------------------------------------------------------------
/App/Actions/PostActions.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 | import AppConstants from '../Constants/AppConstants';
3 | import PostService from '../Api/PostService';
4 |
5 | var PostActions = {
6 |
7 | fetchList: function(username, callback) {
8 | PostService.fetchList(username, function(error, listProps) {
9 | if(callback) callback(error);
10 |
11 | if (!error) {
12 | Dispatcher.dispatch({
13 | actionType: AppConstants.POST_LIST_UPDATED,
14 | listProps: listProps
15 | });
16 | }
17 | });
18 | },
19 |
20 | createPost: function(content, callback) {
21 | PostService.createPost(content, function(error, postProps) {
22 | if(callback) callback(error);
23 |
24 | if (!error) {
25 | Dispatcher.dispatch({
26 | actionType: AppConstants.POST_ADDED,
27 | postProps: postProps
28 | });
29 | }
30 | });
31 | }
32 | };
33 |
34 | export default PostActions;
35 |
--------------------------------------------------------------------------------
/App/Api/AuthService.js:
--------------------------------------------------------------------------------
1 | import client from '../Api/HTTPClient';
2 |
3 | var UserService = {
4 | parseAccount: function(response) {
5 | if (!response) return null;
6 |
7 | var data = {};
8 | data.token = response.token;
9 | data.userProps = {
10 | id: response.id,
11 | username: response.username
12 | };
13 | return data;
14 | },
15 |
16 | accountCallback: function(callback) {
17 | return function(error, response) {
18 | var data = UserService.parseAccount(response);
19 | callback(error, data);
20 | };
21 | },
22 |
23 | signup: function(username, password, callback) {
24 | client.post("api/signup", {username: username, password: password}, UserService.accountCallback(callback));
25 | },
26 |
27 | login: function(username, password, callback) {
28 | client.post("api/login", {username: username, password: password}, UserService.accountCallback(callback));
29 | }
30 | };
31 |
32 | export default UserService;
33 |
--------------------------------------------------------------------------------
/android/Sample.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/App/Mixins/DispatcherListener.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 |
3 | var DispatcherListener = {
4 | unregisterDispatcher: function() {
5 | if(this.dispatchToken) {
6 | Dispatcher.unregister(this.dispatchToken);
7 | this.dispatchToken = null;
8 | }
9 | },
10 |
11 | dispatchedActionCallback: function(action) {
12 | if (this.isMounted()) {
13 | if (action.targetPath) {
14 | if (this.props.currentRoute && this.props.currentRoute.routePath === action.targetPath) {
15 | this.dispatchAction(action);
16 | }
17 | }
18 | else {
19 | this.dispatchAction(action);
20 | }
21 | }
22 | },
23 |
24 | componentDidMount: function() {
25 | this.unregisterDispatcher();
26 |
27 | var self = this;
28 | this.dispatchToken = Dispatcher.register(this.dispatchedActionCallback);
29 | },
30 |
31 | componentWillUnmount: function() {
32 | this.unregisterDispatcher();
33 | },
34 | };
35 |
36 | export default DispatcherListener;
37 |
--------------------------------------------------------------------------------
/test/helpers/packager.js:
--------------------------------------------------------------------------------
1 | // ios/node_modules/react-native/packager/packager.sh --port 9091
2 | // node /Users/brian/taskrabbit/tasker/ios/node_modules/react-native/packager/packager.js --port 9091
3 |
4 | var pwd = process.cwd();
5 | var path = pwd + '/node_modules/react-native/packager/packager.sh'
6 |
7 | var child_process = require('child_process');
8 | var packager = child_process.spawn(path, ['--port', '9091']);
9 |
10 | var packagerReady = false;
11 |
12 | packager.stdout.on('data', function (data) {
13 | if (packagerReady) return;
14 | console.log('PACKAGER: ' + data);
15 | if (data.indexOf('React packager ready.') >=0) {
16 | packagerReady = true;
17 | }
18 | });
19 |
20 | packager.stderr.on('data', function (data) {
21 | console.log('PACKAGER err: ' + data);
22 | });
23 |
24 | packager.on('close', function (code) {
25 | console.log('PACKAGER exited with code: ' + code);
26 | });
27 |
28 |
29 | process.on('exit', function () {
30 | packager.kill();
31 | });
32 |
33 | module.exports = packager;
34 |
--------------------------------------------------------------------------------
/App/Extensions/AddSpinnerLoader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import * as Extension from '../Extensions/Extension';
4 |
5 | import SpinnerLoader from '../Components/SpinnerLoader';
6 |
7 | var AddSpinnerLoader = {
8 | extensionName: 'AddSpinnerLoader',
9 |
10 | optionalParams: {
11 | spinnerContainerStyles: 'styles for the spinner loader',
12 | },
13 |
14 | exports: {
15 | methods: ['start', 'stop']
16 | },
17 |
18 | getInitialState() {
19 | return {
20 | spinning: false,
21 | }
22 | },
23 |
24 | start() {
25 | this.setState({spinning: true})
26 | },
27 |
28 | stop() {
29 | this.setState({spinning: false})
30 | },
31 |
32 | renderExtension() {
33 | return (
34 |
38 | {this.renderComponent()}
39 |
40 | )
41 | }
42 | };
43 |
44 | export default Extension.create(AddSpinnerLoader);
45 |
--------------------------------------------------------------------------------
/App/Platform/Keychain.android.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import LocalKeyStore from '../Stores/LocalKeyStore';
4 |
5 | const KEYCHAIN_STORAGE_KEY = 'KEYCHAIN';
6 |
7 | var KeyChain = {
8 | setGenericPassword(username, password, service) {
9 | return new Promise((resolve) => {
10 | LocalKeyStore.setKey(this.getStorageKey(service), {username, password}, (error) => {
11 | resolve();
12 | });
13 | });
14 | },
15 |
16 | getGenericPassword(service) {
17 | return new Promise((resolve) => {
18 | LocalKeyStore.getKey(this.getStorageKey(service), (error, value) => {
19 | resolve(value);
20 | });
21 | });
22 | },
23 |
24 | resetGenericPassword(service) {
25 | return new Promise((resolve) => {
26 | LocalKeyStore.setKey(this.getStorageKey(service), null, (error) => {
27 | resolve();
28 | });
29 | });
30 | },
31 |
32 | getStorageKey(service) {
33 | return `${KEYCHAIN_STORAGE_KEY}-${service}`;
34 | },
35 |
36 | };
37 |
38 | export default KeyChain;
39 |
--------------------------------------------------------------------------------
/App/Api/Network.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 | import AppConstants from '../Constants/AppConstants';
3 |
4 | var _httpCount = 0; // TODO: immutable?
5 |
6 | var Network = {
7 | onThread: function(callback) {
8 | // TODO: should this be requestAnimationFrame?
9 | global.setTimeout(callback, 0);
10 | },
11 |
12 | started: function() {
13 | this.onThread(function() {
14 | if (_httpCount < 0) _httpCount = 0;
15 | _httpCount++;
16 | if (_httpCount == 1) {
17 | Dispatcher.dispatch({
18 | actionType: AppConstants.NETWORK_ACTIVITY,
19 | isActive: true
20 | });
21 | }
22 | });
23 | },
24 | completed: function() {
25 | this.onThread(function() {
26 | _httpCount--;
27 | if (_httpCount < 0) _httpCount = 0;
28 | if (_httpCount === 0) {
29 | Dispatcher.dispatch({
30 | actionType: AppConstants.NETWORK_ACTIVITY,
31 | isActive: false
32 | });
33 | }
34 | });
35 | }
36 | };
37 |
38 | export default Network;
39 |
--------------------------------------------------------------------------------
/App/Components/TextInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | PixelRatio,
5 | StyleSheet,
6 | TextInput,
7 | } from 'react-native';
8 |
9 | import cssVar from '../Lib/cssVar';
10 |
11 | class _TextInput extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | }
15 |
16 | setNativeProps(...args) {
17 | this.refs.input.setNativeProps(...args);
18 | }
19 |
20 | blur() {
21 | this.refs.input.blur();
22 | }
23 |
24 | focus() {
25 | this.refs.input.focus();
26 | }
27 |
28 | render() {
29 | return (
30 |
35 | );
36 | }
37 | }
38 |
39 | const styles = StyleSheet.create({
40 | input: {
41 | borderWidth: 1 / PixelRatio.get(),
42 | borderColor: cssVar('gray50'),
43 | padding: 10,
44 | fontFamily: cssVar('fontRegular'),
45 | color: cssVar('gray90'),
46 | fontSize: 8, // make it small to know it's not set
47 | },
48 | });
49 |
50 | export default _TextInput;
51 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import android.app.Application;
4 | import android.util.Log;
5 |
6 | import com.facebook.react.ReactApplication;
7 | import com.facebook.react.ReactInstanceManager;
8 | import com.facebook.react.ReactNativeHost;
9 | import com.facebook.react.ReactPackage;
10 | import com.facebook.react.shell.MainReactPackage;
11 |
12 | import java.util.Arrays;
13 | import java.util.List;
14 |
15 | public class MainApplication extends Application implements ReactApplication {
16 |
17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
18 | @Override
19 | protected boolean getUseDeveloperSupport() {
20 | return BuildConfig.DEBUG;
21 | }
22 |
23 | @Override
24 | protected List getPackages() {
25 | return Arrays.asList(
26 | new MainReactPackage(),
27 | new SamplePackage()
28 | );
29 | }
30 | };
31 |
32 | @Override
33 | public ReactNativeHost getReactNativeHost() {
34 | return mReactNativeHost;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 TaskRabbit
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/server/posts.js:
--------------------------------------------------------------------------------
1 | var User = require('./models').User;
2 |
3 | var renderPost = function(post) {
4 | return {
5 | id: post.id,
6 | content: post.content,
7 | username: post.username
8 | };
9 | };
10 |
11 | var renderPosts = function(user) {
12 | var out = {
13 | posts: [],
14 | username: user.username
15 | };
16 | var posts = user.getPosts();
17 | for(var i in posts) {
18 | var post = posts[i];
19 | out.posts.push(renderPost(post));
20 | }
21 |
22 | return out;
23 | };
24 |
25 | exports.userPosts = function *() {
26 | var username = this.params.username;
27 | var user = User.findByUsername(username);
28 | if (!user) {
29 | this.throw("No user found", 404);
30 | }
31 | this.status = 200;
32 | this.body = renderPosts(user);
33 | };
34 |
35 | exports.createPost = function *() {
36 | var user = this.passport.user;
37 | var params = this.request.body || {};
38 | var content = (params.content || "").trim();
39 |
40 | if(!content) this.throw("No content provided", 422);
41 |
42 | var post = user.addPost(content);
43 |
44 | this.status = 201;
45 | this.body = renderPost(post);
46 | };
47 |
--------------------------------------------------------------------------------
/App/Api/PostService.js:
--------------------------------------------------------------------------------
1 | import client from '../Api/HTTPClient';
2 |
3 | var PostService = {
4 | parsePost: function(response) {
5 | if (!response) return null;
6 |
7 | return {
8 | id: response.id,
9 | content: response.content,
10 | username: response.username
11 | };
12 | },
13 |
14 | parsePosts: function(response) {
15 | if (!response) return null;
16 |
17 | var out = {posts: []};
18 | for(var i in response.posts) {
19 | out.posts.push(PostService.parsePost(response.posts[i]));
20 | }
21 | out.username = response.username;
22 | return out;
23 | },
24 |
25 | fetchList: function(username, callback) {
26 | client.get("api/posts/" + username, {}, function(error, response) {
27 | var listProps = PostService.parsePosts(response);
28 | callback(error, listProps);
29 | });
30 | },
31 |
32 | createPost: function(content, callback) {
33 | client.post("api/posts", {content: content}, function(error, response) {
34 | var postProps = PostService.parsePost(response);
35 | callback(error, postProps);
36 | });
37 | },
38 | };
39 |
40 | export default PostService;
41 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/App/Lib/assignDefined.js:
--------------------------------------------------------------------------------
1 | // https://github.com/sindresorhus/object-assign/blob/cad4374651f0d3c869e5f43287e6e99b86b13da0/index.js
2 |
3 | 'use strict';
4 | var propIsEnumerable = Object.prototype.propertyIsEnumerable;
5 |
6 | function ToObject(val) {
7 | if (val == null) {
8 | throw new TypeError('Object.assign cannot be called with null or undefined');
9 | }
10 |
11 | return Object(val);
12 | }
13 |
14 | function ownEnumerableKeys(obj) {
15 | var keys = Object.getOwnPropertyNames(obj);
16 |
17 | if (Object.getOwnPropertySymbols) {
18 | keys = keys.concat(Object.getOwnPropertySymbols(obj));
19 | }
20 |
21 | return keys.filter(function (key) {
22 | // this file adds false if not defined
23 | if (typeof obj[key] === "undefined") return false;
24 |
25 | // otherwise normal thing
26 | return propIsEnumerable.call(obj, key);
27 | });
28 | }
29 |
30 | export default /*Object.assign ||*/ function (target, source) {
31 | var from;
32 | var keys;
33 | var to = ToObject(target);
34 |
35 | for (var s = 1; s < arguments.length; s++) {
36 | from = arguments[s];
37 | keys = ownEnumerableKeys(Object(from));
38 |
39 | for (var i = 0; i < keys.length; i++) {
40 | to[keys[i]] = from[keys[i]];
41 | }
42 | }
43 |
44 | return to;
45 | };
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const koa = require("koa");
4 | const passport = require("koa-passport");
5 | const errorHandler = require("koa-error");
6 | const bodyParser = require("koa-bodyparser");
7 | const logger = require("koa-logger");
8 | const Router = require("koa-router");
9 |
10 | const app = module.exports = koa();
11 | app.use(logger());
12 | app.use(errorHandler());
13 | app.use(bodyParser());
14 | app.use(passport.initialize());
15 |
16 | var router = new Router();
17 |
18 | router.use(function *(next) {
19 | // everything json
20 | this.type = "json";
21 | this.headers['accept'] = "application/json";
22 | yield next;
23 | });
24 |
25 |
26 | var secured = require('./secured')
27 | var auth = require("./auth");
28 | var posts = require("./posts");
29 | var follows = require("./follows");
30 |
31 | router.post("/api/signup", auth.createUser);
32 | router.post("/api/login", auth.loginUser);
33 | router.get ("/api/account", secured, auth.getCurrentUser);
34 | router.post("/api/posts", secured, posts.createPost);
35 | router.get ("/api/posts/:username", posts.userPosts);
36 | router.get ("/api/follows/:username", follows.userFollows);
37 |
38 | app.use(router.routes());
39 | app.listen(3000);
40 | console.log("Server started, listening on port: 3000");
41 |
--------------------------------------------------------------------------------
/ios/Sample/TestRunnerManager.m:
--------------------------------------------------------------------------------
1 | //
2 | // TestRunnerManager.m
3 | // Tasker
4 | //
5 | // Created by Brian Leonard on 8/9/15.
6 | // Copyright (c) 2015 TaskRabbit. All rights reserved.
7 | //
8 |
9 | #import "RCTBridgeModule.h"
10 | #import "RCTBridge.h"
11 |
12 | @interface TestRunnerManager : NSObject
13 |
14 | @end
15 |
16 |
17 | @implementation TestRunnerManager
18 |
19 | RCT_EXPORT_MODULE()
20 |
21 | RCT_EXPORT_METHOD(reset:(RCTResponseSenderBlock)callback)
22 | {
23 | // delete all data in documents directory
24 | NSString *folderPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
25 | NSError *error = nil;
26 | for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:folderPath error:&error]) {
27 | // If you want to skip certain files
28 | if ([file containsString:@".skip"]) {
29 | // but be sure to clear them out if applicable from your js code
30 | continue;
31 | }
32 |
33 | // otherwise, delete
34 | [[NSFileManager defaultManager] removeItemAtPath:[folderPath stringByAppendingPathComponent:file] error:&error];
35 | }
36 |
37 | // reload the app
38 | [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil];
39 |
40 | callback(@[]);
41 | }
42 |
43 | @end
44 |
--------------------------------------------------------------------------------
/App/Models/Environment.js:
--------------------------------------------------------------------------------
1 | import assign from '../Lib/assignDefined';
2 | import jsVersion from '../jsVersion';
3 |
4 | var Model = function(options) {
5 | this.data = {};
6 | this.setAttributes(options);
7 | };
8 |
9 | Model.prototype.setAttributes = function(options) {
10 | options = (options || {});
11 | assign(this.data, {
12 | name: options.name,
13 | simulator: options.simulator,
14 | buildCode: parseInt(options.buildCode),
15 | version: options.version,
16 | locale: options.locale
17 | });
18 | };
19 |
20 | Model.prototype.getApiHost = function() {
21 | switch(this.data.name) {
22 | case 'test':
23 | return 'http://localhost:3001';
24 | case 'debug':
25 | return 'http://localhost:3000';
26 | case 'staging':
27 | return 'https://someday.herokuapp.com';
28 | default:
29 | throw("Unknown Environment.getApiHost: " + this.data.name);
30 | }
31 | };
32 |
33 | Model.prototype.combinedBuildCode = function() {
34 | var ios = this.data.buildCode * 1000000;
35 | return ios + jsVersion;
36 | };
37 |
38 | Model.prototype.displayVersion = function() {
39 | var out = this.data.version;
40 | out += "." + this.data.buildCode;
41 | out += "." + jsVersion;
42 | return out;
43 | };
44 |
45 | Model.prototype.getLocale = function() {
46 | return this.data.locale;
47 | };
48 |
49 | export default Model;
50 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/SamplePackage.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import android.app.Activity;
4 | import com.facebook.react.ReactPackage;
5 | import com.facebook.react.bridge.JavaScriptModule;
6 | import com.facebook.react.bridge.NativeModule;
7 | import com.facebook.react.bridge.ReactApplicationContext;
8 | import com.facebook.react.uimanager.ViewManager;
9 |
10 | import java.util.ArrayList;
11 | import java.util.Collections;
12 | import java.util.List;
13 |
14 | public class SamplePackage implements ReactPackage {
15 | private Activity mActivity;
16 |
17 | //public SamplePackage(Activity activity) {
18 | // mActivity = activity;
19 | //}
20 |
21 | @Override
22 | public List> createJSModules() {
23 | return Collections.emptyList();
24 | }
25 |
26 | @Override
27 | public List createViewManagers(ReactApplicationContext reactApplicationContext) {
28 | return Collections.emptyList();
29 | }
30 |
31 | @Override
32 | public List createNativeModules(
33 | ReactApplicationContext reactContext) {
34 | List modules = new ArrayList<>();
35 |
36 | modules.add(new EnvironmentManager(reactContext));
37 | modules.add(new TestRunnerManager(reactContext));
38 |
39 | return modules;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/App/Mixins/NavBarHelper.js:
--------------------------------------------------------------------------------
1 | // parent must implement getNavBarState()
2 |
3 | import TimerMixin from 'react-timer-mixin';
4 |
5 | import Dispatcher from '../Dispatcher';
6 | import AppConstants from '../Constants/AppConstants';
7 |
8 | var NavBarHelper = {
9 | mixins: [TimerMixin],
10 |
11 | componentDidMount: function() {
12 | this.updateNavTitle();
13 | },
14 |
15 | componentDidUpdate: function() {
16 | this.updateNavTitle();
17 | },
18 |
19 | updateNavTitle: function() {
20 | var updates = this.getNavBarState();
21 | if (!updates) return;
22 |
23 | if(typeof updates.title !== "undefined") {
24 | this.props.currentRoute.title = updates.title;
25 | }
26 | if (typeof updates.navRightDisabled !== "undefined") {
27 | this.props.currentRoute.navRight.disabled = updates.navRightDisabled;
28 | }
29 | if (typeof updates.navRightIcon !== "undefined") {
30 | this.props.currentRoute.navRight.icon = updates.navRightIcon;
31 | }
32 |
33 | // if called during componentDidLoad, nav bar not loaded yet
34 | // requestAnimationFrame to allow it to finish
35 | var route = this.props.currentRoute;
36 | this.requestAnimationFrame(function() {
37 | Dispatcher.dispatch({
38 | actionType: AppConstants.NAVBAR_UPDATE,
39 | route: route
40 | });
41 | });
42 | },
43 | };
44 |
45 | export default NavBarHelper;
46 |
--------------------------------------------------------------------------------
/App/Stores/FollowListStore.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 | import assign from 'object-assign';
3 |
4 | import Follow from '../Models/Follow';
5 | import Dispatcher from '../Dispatcher';
6 | import AppConstants from '../Constants/AppConstants';
7 |
8 | var CHANGE_EVENT = 'change';
9 |
10 | // TODO: Immutable?
11 | var _hash = {};
12 |
13 | function setList(key, list) {
14 | var models = [];
15 | for(var i in list) {
16 | var model = new Follow(list[i]);
17 | models.push(model);
18 | }
19 | _hash[key] = models;
20 | }
21 |
22 | var ModelStore = assign({}, EventEmitter.prototype, {
23 | get: function(key) {
24 | return _hash[key];
25 | },
26 |
27 | emitChange: function(key) {
28 | this.emit(CHANGE_EVENT, key);
29 | },
30 |
31 | addChangeListener: function(callback) {
32 | this.on(CHANGE_EVENT, callback);
33 | },
34 |
35 | removeChangeListener: function(callback) {
36 | this.removeListener(CHANGE_EVENT, callback);
37 | }
38 | });
39 |
40 | // Register callback to handle all updates
41 | Dispatcher.register(function(action) {
42 | switch(action.actionType) {
43 | case AppConstants.FOLLOW_LIST_UPDATED:
44 | setList(action.listProps.username, action.listProps.follows);
45 | ModelStore.emitChange(action.listProps.username);
46 | break;
47 | // TODO: save
48 | default:
49 | // no op
50 | }
51 | });
52 |
53 | export default ModelStore;
--------------------------------------------------------------------------------
/test/helpers/fixtures.js:
--------------------------------------------------------------------------------
1 | // API Responses
2 |
3 | var Fixtures = {
4 | signup: function() {
5 | return {
6 | "id": 123,
7 | "token": "d3f4g5h67j8",
8 | "username": "tester",
9 | };
10 | },
11 |
12 | home: function() {
13 | return {
14 | username: 'tester',
15 | posts: [
16 | { id: 1, content: "post1", username: "tester" },
17 | { id: 2, content: "post2", username: "tester" }
18 | ]
19 | };
20 | },
21 |
22 | myFollows: function() {
23 | return {
24 | username: 'tester',
25 | follows: [
26 | { id: 42, username: "friend" },
27 | { id: 2, username: "follow2" }
28 | ]
29 | };
30 | },
31 |
32 | friend: function() {
33 | return {
34 | username: 'friend',
35 | posts: [
36 | { id: 3, content: "post3", username: "friend" },
37 | { id: 4, content: "post4", username: "friend" }
38 | ]
39 | };
40 | },
41 |
42 | friendFollows: function() {
43 | return {
44 | username: 'friend',
45 | follows: [
46 | { id: 123, username: "tester" },
47 | { id: 4, username: "follow4" }
48 | ]
49 | };
50 | },
51 |
52 | error: function(message, status, key, code) {
53 | return {
54 | status: (status ? status : 422),
55 | body: {
56 | error: message
57 | }
58 |
59 | };
60 | }
61 |
62 | };
63 |
64 | module.exports = Fixtures;
65 |
--------------------------------------------------------------------------------
/App/Screens/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | Text
6 | } from 'react-native';
7 |
8 | import cssVar from '../Lib/cssVar';
9 | import Locale from '../Locale';
10 |
11 | import SimpleList from '../Components/SimpleList';
12 | import AppConstants from '../Constants/AppConstants';
13 |
14 | import EnvironmentStore from '../Stores/EnvironmentStore';
15 |
16 | var i18n = Locale.key('Settings', {
17 | logout: 'Log out',
18 | });
19 |
20 | function getListState() {
21 | var list = [];
22 | list.push({
23 | title: i18n.t('logout'),
24 | actionType: AppConstants.LOGOUT_REQUESTED
25 | });
26 |
27 | return {
28 | items: list
29 | };
30 | };
31 |
32 | var Settings = React.createClass({
33 | getInitialState() {
34 | return getListState();
35 | },
36 |
37 | render: function() {
38 | return (
39 |
40 |
41 |
42 | Version {EnvironmentStore.get().displayVersion()}
43 |
44 |
45 | )
46 | }
47 | });
48 |
49 | var styles = StyleSheet.create({
50 | container: {
51 | flex: 1
52 | },
53 | bottomText: {
54 | padding: 10,
55 | color: cssVar('gray20'),
56 | fontSize: 12
57 | },
58 | });
59 |
60 | export default Settings;
61 |
--------------------------------------------------------------------------------
/App/Screens/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet
5 | } from 'react-native';
6 |
7 | import Text from '../Components/Text';
8 | import Button from '../Components/Button';
9 | import AppActions from '../Actions/AppActions';
10 |
11 | var Loading = React.createClass({
12 | onRetryButton: function() {
13 | AppActions.reloadCurrentPath();
14 | },
15 |
16 | renderError: function() {
17 | return (
18 |
19 |
20 | {this.props.error.message}
21 |
22 |
25 |
26 | );
27 | },
28 |
29 | renderLoading: function() {
30 | return (
31 |
32 |
33 | Loading
34 |
35 |
36 | );
37 | },
38 |
39 | render: function() {
40 | if (this.props.error) {
41 | return this.renderError();
42 | }
43 | else {
44 | return this.renderLoading();
45 | }
46 | }
47 | });
48 |
49 | var styles = StyleSheet.create({
50 | container: {
51 | flex: 1,
52 | justifyContent: 'center',
53 | alignItems: 'center'
54 | },
55 | welcome: {
56 | fontSize: 20,
57 | textAlign: 'center',
58 | margin: 10,
59 | }
60 | });
61 |
62 | export default Loading;
63 |
--------------------------------------------------------------------------------
/App/Components/SimpleList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ListView
4 | } from 'react-native';
5 |
6 | import RefreshableListView from '../Platform/RefreshableListView';
7 |
8 | import SimpleListItem from '../Components/SimpleListItem';
9 |
10 | var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
11 |
12 | var SimpleList = React.createClass({
13 | renderRow: function(item, sectionId, rowId) {
14 | var passAlong = {};
15 | if (this.props.currentRoute) passAlong.currentRoute = this.props.currentRoute;
16 | if (this.props.navigation) passAlong.navigation = this.props.navigation;
17 | if (this.props.nextIcon) passAlong.nextIcon = this.props.nextIcon;
18 | if (this.props.noTap) passAlong.noTap = this.props.noTap;
19 |
20 | if (this.props.getItemProps) {
21 | // swtich it out
22 | item = this.props.getItemProps(item);
23 | }
24 |
25 | return (
26 |
27 | );
28 | },
29 |
30 | render: function() {
31 | var Component = this.props.reloadList ? RefreshableListView : ListView;
32 | return (
33 |
39 | );
40 | }
41 | });
42 |
43 | export default SimpleList;
44 |
--------------------------------------------------------------------------------
/App/Mixins/NavigationListener.js:
--------------------------------------------------------------------------------
1 | var NavigationListener = {
2 | _onDidFocusNavigation: function(event) {
3 | if (event.data.route.routePath === this.props.currentRoute.routePath) {
4 | this._hasNavigationFocus = true;
5 | if(this.onDidFocusNavigation) {
6 | this.onDidFocusNavigation();
7 | }
8 | }
9 | },
10 |
11 | _onWillFocusNavigation: function(event) {
12 | if(this._hasNavigationFocus) {
13 | this._hasNavigationFocus = false;
14 | if(this.onDidUnfocusNavigation) {
15 | this.onDidUnfocusNavigation();
16 | }
17 | }
18 | },
19 |
20 | hasNavigationFocus: function() {
21 | return !!this._hasNavigationFocus;
22 | },
23 |
24 | componentWillMount: function() {
25 | this._hasNavigationFocus = false;
26 | // TODO: this._onDidFocusNavigationSub = this.props.navigator.navigationContext.addListener('didfocus', this._onDidFocusNavigation)
27 | // TODO: this._onWillFocusNavigationSub = this.props.navigator.navigationContext.addListener('willfocus', this._onWillFocusNavigation)
28 | },
29 |
30 | componentWillUnmount: function() {
31 | if(this._onDidFocusNavigationSub) {
32 | this._onDidFocusNavigationSub.remove();
33 | this._onDidFocusNavigationSub = null;
34 | }
35 | if(this._onWillFocusNavigationSub) {
36 | this._onWillFocusNavigationSub.remove();
37 | this._onWillFocusNavigationSub = null;
38 | }
39 | }
40 | };
41 |
42 | export default NavigationListener;
43 |
--------------------------------------------------------------------------------
/App/Locales/boot.js:
--------------------------------------------------------------------------------
1 | import I18n from '../Locales/i18n';
2 |
3 | I18n.translations['base'] = require('../Locales/base.js').default; // defaults, not translated - likely for set values
4 | I18n.translations['en'] = require('../Locales/en.js').default; // shared for english countries, generally loaded through Locale.register
5 |
6 | I18n.translations['en-US'] = require('../Locales/en-US.js').default; // known ones for US
7 |
8 | I18n.translations['en-GB'] = require('../Locales/en-GB.js').default; // known ones for GB
9 | I18n.translations['en-GB-xtra'] = require('../Locales/en-GB-xtra.js').default; // programmatic - ideally empty and in translated
10 |
11 |
12 | I18n.default_locale = 'en-US';
13 | I18n.locale = 'en-US';
14 |
15 | I18n.fallbacks = {
16 | 'en-US': ['en', 'base'],
17 | 'en-GB': ['en-GB-xtra', 'en', 'base'],
18 | };
19 |
20 | const countryIsoCodesToLocale = {
21 | 'US': 'en-US',
22 | 'GB': 'en-GB',
23 | };
24 |
25 | const currenciesToLocale = {
26 | 'USD': 'en-US',
27 | 'GBP': 'en-GB',
28 | };
29 |
30 | I18n.register = function(component, enHash) {
31 | I18n.translations['en'][component] = enHash;
32 | };
33 |
34 | I18n.getLocaleFromCountryIsoCode = (countryIsoCode) => {
35 | return countryIsoCodesToLocale[countryIsoCode] || countryIsoCodesToLocale['US'];
36 | };
37 |
38 | I18n.getLocaleFromCurrency = (countryIsoCode) => {
39 | return currenciesToLocale[countryIsoCode] || currenciesToLocale['USD'];
40 | };
41 |
42 | export default I18n;
43 |
--------------------------------------------------------------------------------
/App/Locale.js:
--------------------------------------------------------------------------------
1 | import I18n from './Locales/boot';
2 |
3 | var Manager = function(object) {
4 | this.key = object;
5 | };
6 |
7 | Manager.prototype.en = function(hash) {
8 | if (I18n.translations['en'][this.key] && !process.env.TEST_UNIT) {
9 | console.warn(`Locale (en) key already exists: ${this.key}`);
10 | }
11 | I18n.translations['en'][this.key] = hash;
12 | return this;
13 | };
14 |
15 | Manager.prototype.t = function(scope, options) {
16 | if (this.key) {
17 | scope = this.key + '.' + scope; // prepend our key
18 | }
19 | return I18n.t(scope, options);
20 | };
21 |
22 | var Locale = {
23 | key: function(object, enHash) {
24 | var manager = new Manager(object);
25 | // common case is passing en
26 | if (enHash) {
27 | return manager.en(enHash);
28 | }
29 | else {
30 | return manager;
31 | }
32 | },
33 |
34 | global: function() {
35 | return new Manager(null);
36 | },
37 |
38 | lib: function() {
39 | return I18n;
40 | },
41 |
42 | formatMoneyWithCountryIsoCode: function(cents, countryIsoCode, options = {}) {
43 | const locale = I18n.getLocaleFromCountryIsoCode(countryIsoCode);
44 | return Locale.lib().l('currency', cents, {locale, ...options});
45 | },
46 |
47 | formatMoneyWithCurrency: function(cents, currency, options = {}) {
48 | const locale = I18n.getLocaleFromCurrency(currency);
49 | return Locale.lib().l('currency', cents, {locale, ...options});
50 | },
51 | };
52 |
53 | export default Locale;
54 |
--------------------------------------------------------------------------------
/ios/Sample/EnvironmentManager.m:
--------------------------------------------------------------------------------
1 | //
2 | // EnvironmentManager.m
3 | // Sample
4 | //
5 |
6 | #import "RCTBridgeModule.h"
7 |
8 | @interface EnvironmentManager : NSObject
9 |
10 | @end
11 |
12 |
13 | @implementation EnvironmentManager
14 |
15 | RCT_EXPORT_MODULE()
16 |
17 | RCT_EXPORT_METHOD(get:(RCTResponseSenderBlock)callback)
18 | {
19 | NSString *locale = [[NSLocale currentLocale] localeIdentifier];
20 | locale = [locale stringByReplacingOccurrencesOfString:@"_" withString:@"-"];
21 |
22 | NSNumber * simulator = @NO;
23 | NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
24 | NSString * buildCode = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
25 |
26 | NSString * envName = kEnvironment;
27 | NSDictionary *passed = [[NSProcessInfo processInfo] environment];
28 | NSString *override = [passed valueForKey:@"SAMPLE_ENV"];
29 | if (override) {
30 | envName = override;
31 | }
32 | #ifdef TEST_ENVIRONMENT
33 | envName = @"test";
34 | #endif
35 | #ifdef STAGING_ENVIRONMENT
36 | envName = @"staging";
37 | #endif
38 |
39 |
40 | #if TARGET_IPHONE_SIMULATOR
41 | simulator = @YES;
42 | #endif
43 |
44 | callback(@[ @{
45 | @"name": envName,
46 | @"buildCode": buildCode,
47 | @"simulator": simulator,
48 | @"version": version,
49 | @"locale": locale
50 | }]);
51 | }
52 |
53 | @end
54 |
--------------------------------------------------------------------------------
/App/Stores/EnvironmentStore.js:
--------------------------------------------------------------------------------
1 | import { NativeModules} from 'react-native';
2 | const EnvironmentManager = NativeModules.EnvironmentManager;
3 |
4 | import {EventEmitter} from 'events';
5 | import assign from 'object-assign';
6 |
7 | import Environment from '../Models/Environment';
8 | import Dispatcher from '../Dispatcher';
9 | import AppConstants from '../Constants/AppConstants';
10 |
11 | var CHANGE_EVENT = 'change';
12 |
13 | // null so we know to initialize on app launch
14 | var _singleton = null;
15 |
16 | function setSingleton(storedProps) {
17 | _singleton = new Environment(storedProps);
18 | }
19 |
20 | var SingletonStore = assign({}, EventEmitter.prototype, {
21 | get: function() {
22 | return _singleton;
23 | },
24 |
25 | emitChange: function() {
26 | this.emit(CHANGE_EVENT);
27 | },
28 |
29 | addChangeListener: function(callback) {
30 | this.on(CHANGE_EVENT, callback);
31 | },
32 |
33 | removeChangeListener: function(callback) {
34 | this.removeListener(CHANGE_EVENT, callback);
35 | }
36 | });
37 |
38 | // Register callback to handle all updates
39 | Dispatcher.register(function(action) {
40 | switch(action.actionType) {
41 | case AppConstants.APP_LAUNCHED:
42 | EnvironmentManager.get(function(attributes) {
43 | console.log("Environment: " + attributes.name);
44 | setSingleton(attributes);
45 | SingletonStore.emitChange();
46 | });
47 | break;
48 | default:
49 | // no op
50 | }
51 | });
52 |
53 | export default SingletonStore;
54 |
--------------------------------------------------------------------------------
/server/auth.js:
--------------------------------------------------------------------------------
1 | var User = require("./models").User;
2 |
3 | var renderAccount = function (user) {
4 | var out = {
5 | id: user.id,
6 | token: user.token,
7 | username: user.username
8 | };
9 | return out;
10 | };
11 |
12 | exports.loginUser = function *() {
13 | var params = this.request.body || {};
14 | var username = (params.username || "").trim();
15 | var password = (params.password || "").trim();
16 |
17 | if(!username) this.throw("No username provided", 422);
18 | if(!password) this.throw("No password provided", 422);
19 |
20 | var user = User.findByUsername(username);
21 | if (user && user.authenticate(password)) {
22 | this.status = 200;
23 | this.body = renderAccount(user);
24 | }
25 | else {
26 | this.status = 422;
27 | this.body = {error: "Username and password not found"}
28 | }
29 | };
30 |
31 | exports.getCurrentUser = function *() {
32 | this.status = 200;
33 | this.body = renderAccount(this.passport.user);
34 | };
35 |
36 | exports.createUser = function *() {
37 | var params = this.request.body || {};
38 | var username = (params.username || "").trim();
39 | var password = (params.password || "").trim();
40 |
41 | if(!username) this.throw("No username provided", 422);
42 | if(!password) this.throw("No password provided", 422);
43 |
44 | var user = User.findByUsername(username);
45 | if(user) this.throw("User already exists", 422);
46 |
47 | user = User.create(params.username, params.password);
48 |
49 | this.status = 201;
50 | this.body = renderAccount(user);
51 | };
52 |
--------------------------------------------------------------------------------
/App/Stores/LocalKeyStore.js:
--------------------------------------------------------------------------------
1 | // wrapper on local storage technique
2 | import React from 'react';
3 | import {
4 | AsyncStorage
5 | } from 'react-native';
6 |
7 | var LocalKeyStore = {
8 | getKey: function(keyName, callback) {
9 | AsyncStorage.getItem(keyName, function(error, value) { // callback: error, value
10 | if (error) {
11 | console.log('Error getting item (' + keyName + ') from local storage! ' + error.message);
12 | if(callback) callback(error, null);
13 | } else {
14 | var json = JSON.parse(value);
15 | if(callback) callback(null, json);
16 | }
17 | });
18 | },
19 |
20 | setKey: function(keyName, value, callback) { // callback: error
21 | if (value) {
22 | var encoded = JSON.stringify(value);
23 | AsyncStorage.setItem(keyName, encoded, function(error) {
24 | if (error) {
25 | console.log('Error setting item (' + keyName + ') to local storage! ' + error.message);
26 | if(callback) callback(error);
27 | } else {
28 | if(callback) callback(null);
29 | }
30 | });
31 | }
32 | else {
33 | // deleting it
34 | // TODO: what if it's not there? does it error
35 | AsyncStorage.removeItem(keyName, function(error) { // callback: error
36 | if (error) {
37 | console.log('Error removing item (' + keyName + ') from local storage! ' + error.message);
38 | if(callback) callback(error);
39 | } else {
40 | if(callback) callback(null);
41 | }
42 | });
43 | }
44 | },
45 | };
46 |
47 | export default LocalKeyStore;
48 |
--------------------------------------------------------------------------------
/App/Screens/PostList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Locale from '../Locale';
4 |
5 | import ListHelper from '../Mixins/ListHelper';
6 |
7 | import PostListStore from '../Stores/PostListStore';
8 | import PostActions from '../Actions/PostActions';
9 |
10 | var i18n = Locale.key('PostList', {
11 | posts: 'Posts',
12 | follows: 'Follows',
13 | });
14 |
15 | var PostList = React.createClass({
16 | mixins: [ListHelper],
17 |
18 | getDefaultProps: function() {
19 | return {
20 | store: PostListStore,
21 | listProps: {
22 | noTap: true
23 | },
24 | segment: {
25 | items: [
26 | {
27 | title: i18n.t('posts'),
28 | replacePath: 'posts',
29 | selected: true
30 | },
31 | {
32 | title: i18n.t('follows'),
33 | replacePath: 'follows'
34 | }
35 | ]
36 | }
37 | };
38 | },
39 |
40 | getListItems: function() {
41 | return PostListStore.get(this.getUsername());
42 | },
43 |
44 | isListChange: function(username) {
45 | return this.getUsername() == username;
46 | },
47 |
48 | getItemProps: function(post) {
49 | return {
50 | key: post.data.id,
51 | title: post.data.content
52 | }
53 | },
54 |
55 | reloadList: function() {
56 | console.log("reloading posts: " + this.getUsername());
57 | PostActions.fetchList(this.getUsername(), function(error) {
58 | // TODO: handle error
59 | if (error) {
60 | console.log(error.message);
61 | }
62 | });
63 | }
64 | });
65 |
66 |
67 |
68 | export default PostList;
69 |
--------------------------------------------------------------------------------
/App/Root/Launcher.js:
--------------------------------------------------------------------------------
1 | import AppConstants from '../Constants/AppConstants';
2 | import Routes from '../Navigation/Routes';
3 |
4 | import StatusBar from '../Platform/StatusBar';
5 |
6 | import {
7 | Linking
8 | } from 'react-native';
9 |
10 | var Launcher = {
11 | launch: function(root, action) {
12 | switch(action.actionType) {
13 | case AppConstants.LAUNCH_ROUTE_PATH:
14 | var routePath = action.routePath;
15 | var loggedIn = root.state && root.state.user.isLoggedIn();
16 | var parsed = Routes.parse(routePath, loggedIn, false);
17 | if (!parsed) {
18 | alert("Unknown route: " + routePath);
19 | }
20 | else {
21 | root.setState({navigationState: parsed});
22 | }
23 | break;
24 | case AppConstants.NETWORK_ACTIVITY:
25 | StatusBar.setNetworkActive(action.isActive);
26 | break;
27 | case AppConstants.OPEN_URL:
28 | var url = action.url;
29 | Linking.openURL(url)
30 | break;
31 | case AppConstants.RELOAD_PATH:
32 | // TODO
33 | //root.setState({loading: true}, function() {
34 | // root.setState({loading: false});
35 | //});
36 | break;
37 | case AppConstants.TEST_COMPONENT_ROUTE:
38 | var TestComponents = require("../Root/TestComponents").default;
39 | action.routeUnderTest.component = TestComponents.find(action.routeUnderTest.component);
40 | rootComponent.setState({routeUnderTest: action.routeUnderTest});
41 | break;
42 | default:
43 | break;
44 | }
45 | }
46 | };
47 |
48 | export default Launcher;
49 |
--------------------------------------------------------------------------------
/tasks/react-native-stub.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import 'babel-core/register';
3 | import 'react-native-mock/mock';
4 | import 'isomorphic-fetch';
5 |
6 | import React from 'react';
7 | import ReactNative from 'react-native';
8 | // MOCKS
9 |
10 | ReactNative.ListView = React.createClass({
11 | statics: {
12 | DataSource: () => {},
13 | },
14 |
15 | render() {
16 | return null;
17 | },
18 | });
19 | ReactNative.Navigator.SceneConfigs = {
20 | HorizontalSwipeJump: {},
21 | };
22 | ReactNative.NativeModules = {
23 | ...ReactNative.NativeModules,
24 | };
25 |
26 | var TextInputState = {};
27 | var NavigatorNavigationBarStyles = {
28 | General: {},
29 | };
30 |
31 | // End MOCKS
32 |
33 | export default function(request, parent, isMain) {
34 | switch (request) {
35 | case 'TextInputState':
36 | return TextInputState;
37 | case 'NavigatorNavigationBarStylesIOS':
38 | return NavigatorNavigationBarStyles;
39 | case 'NavigatorNavigationBarStylesAndroid':
40 | return NavigatorNavigationBarStyles;
41 | case 'ReactNativeART':
42 | return {};
43 | case 'Platform':
44 | return {};
45 | case 'ErrorUtils':
46 | return {};
47 | case 'Portal':
48 | return {};
49 | case 'React':
50 | return {};
51 | case 'parseErrorStack':
52 | return {};
53 | default:
54 | if (React[request]) {
55 | return React[request];
56 | }
57 | if (/react-native-/.test(request)) {
58 | return {};
59 | }
60 | }
61 |
62 | // other issues
63 | if (request.match(/.*\.png/g)) {
64 | return 'png';
65 | }
66 |
67 | return null;
68 | }
69 |
--------------------------------------------------------------------------------
/App/Screens/FollowList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Locale from '../Locale';
4 |
5 | import ListHelper from '../Mixins/ListHelper';
6 |
7 | import FollowListStore from '../Stores/FollowListStore';
8 | import FollowActions from '../Actions/FollowActions';
9 |
10 | var i18n = Locale.key('FollowList', {
11 | posts: 'Posts',
12 | follows: 'Follows',
13 | });
14 |
15 | var FollowList = React.createClass({
16 | mixins: [ListHelper],
17 |
18 | getDefaultProps: function() {
19 | return {
20 | store: FollowListStore,
21 | listProps: {
22 | nextIcon: true
23 | },
24 | segment: {
25 | items: [
26 | {
27 | title: i18n.t('posts'),
28 | replacePath: 'posts'
29 | },
30 | {
31 | title: i18n.t('follows'),
32 | replacePath: 'follows',
33 | selected: true
34 | }
35 | ]
36 | }
37 | };
38 | },
39 |
40 | getListItems: function() {
41 | return FollowListStore.get(this.getUsername());
42 | },
43 |
44 | isListChange: function(username) {
45 | return this.getUsername() == username;
46 | },
47 |
48 | getItemProps: function(follow) {
49 | return {
50 | key: follow.data.id,
51 | title: follow.data.username,
52 | subPath: follow.data.username
53 | }
54 | },
55 |
56 | reloadList: function() {
57 | console.log("reloading follows: " + this.getUsername());
58 | FollowActions.fetchList(this.getUsername(), function(error) {
59 | // TODO: handle error
60 | if (error) {
61 | console.log(error.message);
62 | }
63 | });
64 | }
65 | });
66 |
67 | export default FollowList;
68 |
--------------------------------------------------------------------------------
/ios/Sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UIViewControllerBasedStatusBarAppearance
38 |
39 | NSLocationWhenInUseUsageDescription
40 |
41 | NSAppTransportSecurity
42 |
43 |
44 | NSAllowsArbitraryLoads
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/App/Stores/PostListStore.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 | import assign from 'object-assign';
3 |
4 | import Post from '../Models/Post';
5 | import Dispatcher from '../Dispatcher';
6 | import AppConstants from '../Constants/AppConstants';
7 |
8 | var CHANGE_EVENT = 'change';
9 |
10 | // TODO: Immutable?
11 | var _hash = {};
12 |
13 | function addModel(key, props) {
14 | if(!_hash[key]) _hash[key] = [];
15 | var model = new Post(props);
16 | _hash[key].unshift(model);
17 | }
18 |
19 | function setList(key, list) {
20 | var models = [];
21 | for(var i in list) {
22 | var model = new Post(list[i]);
23 | models.push(model);
24 | }
25 | _hash[key] = models;
26 | }
27 |
28 | var ModelStore = assign({}, EventEmitter.prototype, {
29 | get: function(key) {
30 | return _hash[key];
31 | },
32 |
33 | emitChange: function(key) {
34 | this.emit(CHANGE_EVENT, key);
35 | },
36 |
37 | addChangeListener: function(callback) {
38 | this.on(CHANGE_EVENT, callback);
39 | },
40 |
41 | removeChangeListener: function(callback) {
42 | this.removeListener(CHANGE_EVENT, callback);
43 | }
44 | });
45 |
46 | // Register callback to handle all updates
47 | Dispatcher.register(function(action) {
48 | switch(action.actionType) {
49 | case AppConstants.POST_LIST_UPDATED:
50 | setList(action.listProps.username, action.listProps.posts);
51 | ModelStore.emitChange(action.listProps.username);
52 | break;
53 | case AppConstants.POST_ADDED:
54 | addModel(action.postProps.username, action.postProps);
55 | ModelStore.emitChange(action.postProps.username);
56 | break;
57 | // TODO: save
58 | default:
59 | // no op
60 | }
61 | });
62 |
63 | export default ModelStore;
--------------------------------------------------------------------------------
/android/app/BUCK:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | # To learn about Buck see [Docs](https://buckbuild.com/).
4 | # To run your application with Buck:
5 | # - install Buck
6 | # - `npm start` - to start the packager
7 | # - `cd android`
8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
10 | # - `buck install -r android/app` - compile, install and run application
11 | #
12 |
13 | lib_deps = []
14 | for jarfile in glob(['libs/*.jar']):
15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
16 | lib_deps.append(':' + name)
17 | prebuilt_jar(
18 | name = name,
19 | binary_jar = jarfile,
20 | )
21 |
22 | for aarfile in glob(['libs/*.aar']):
23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
24 | lib_deps.append(':' + name)
25 | android_prebuilt_aar(
26 | name = name,
27 | aar = aarfile,
28 | )
29 |
30 | android_library(
31 | name = 'all-libs',
32 | exported_deps = lib_deps
33 | )
34 |
35 | android_library(
36 | name = 'app-code',
37 | srcs = glob([
38 | 'src/main/java/**/*.java',
39 | ]),
40 | deps = [
41 | ':all-libs',
42 | ':build_config',
43 | ':res',
44 | ],
45 | )
46 |
47 | android_build_config(
48 | name = 'build_config',
49 | package = 'com.sample',
50 | )
51 |
52 | android_resource(
53 | name = 'res',
54 | res = 'src/main/res',
55 | package = 'com.sample',
56 | )
57 |
58 | android_binary(
59 | name = 'app',
60 | package_type = 'debug',
61 | manifest = 'src/main/AndroidManifest.xml',
62 | keystore = '//android/keystores:debug',
63 | deps = [
64 | ':app-code',
65 | ],
66 | )
67 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/TLSSetup.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import android.util.Log;
4 | import com.facebook.react.modules.network.OkHttpClientProvider;
5 | import com.facebook.react.modules.network.ReactCookieJarContainer;
6 | import okhttp3.ConnectionSpec;
7 | import okhttp3.OkHttpClient;
8 | import okhttp3.TlsVersion;
9 |
10 | import javax.net.ssl.*;
11 | import java.util.Arrays;
12 | import java.util.concurrent.TimeUnit;
13 |
14 | public class TLSSetup {
15 |
16 | static String TAG = "TLSSetup";
17 |
18 | public static void configure(){
19 | try {
20 | SSLContext sc = SSLContext.getInstance("TLSv1.1");
21 | sc.init(null, null, null);
22 | ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
23 | .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1)
24 | .build();
25 | // Taken from OkHttpClientProvider.java
26 | // Set no timeout by default
27 | OkHttpClient sClient = new OkHttpClient.Builder()
28 | .connectTimeout(0, TimeUnit.MILLISECONDS)
29 | .readTimeout(0, TimeUnit.MILLISECONDS)
30 | .writeTimeout(0, TimeUnit.MILLISECONDS)
31 | .cookieJar(new ReactCookieJarContainer())
32 | // set sslSocketFactory
33 | .sslSocketFactory(new TLSSocketFactory(sc.getSocketFactory()))
34 | // set connectionSpecs
35 | .connectionSpecs(Arrays.asList(cs, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT))
36 | .build();
37 |
38 | OkHttpClientProvider.replaceOkHttpClient(sClient);
39 | } catch (Exception e) {
40 | Log.e(TAG, e.getMessage());
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/App/Navigation/NavigationHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | NavigationExperimental,
4 | StyleSheet,
5 | } from 'react-native';
6 |
7 | import cssVar from '../Lib/cssVar';
8 |
9 | import NavigationTitle from '../Navigation/NavigationTitle';
10 | import NavigationButton from '../Navigation/NavigationButton';
11 |
12 | const {
13 | Header: NavigationHeader,
14 | } = NavigationExperimental;
15 |
16 | var _NavigationHeader = React.createClass({
17 | renderTitle: function (props) {
18 | var route = props.scene.route;
19 | return ;
20 | },
21 |
22 | renderLeft: function(props) {
23 | var route = props.scene.route;
24 | var index = props.scene.index;
25 | return
26 | },
27 |
28 | renderRight: function(props) {
29 | var route = props.scene.route;
30 | var index = props.scene.index;
31 | return
32 | },
33 |
34 | render: function() {
35 | return (
36 |
43 | );
44 | }
45 | });
46 |
47 | var styles = StyleSheet.create({
48 | appContainer: {
49 | flex: 1
50 | },
51 | navBar: {
52 | backgroundColor: cssVar('blue50'),
53 | },
54 | scene: {
55 | flex: 1,
56 | backgroundColor: cssVar('gray5'),
57 | },
58 | sceneHidden: {
59 | marginTop: 0
60 | },
61 | header: {
62 | backgroundColor: cssVar('blue50'),
63 | borderBottomWidth: 0,
64 | }
65 | });
66 |
67 | export default _NavigationHeader;
68 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/RefreshingIndicator.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropTypes,
3 | isValidElement,
4 | createElement,
5 | } from 'react';
6 |
7 | import {
8 | View,
9 | Text,
10 | ActivityIndicator,
11 | StyleSheet,
12 | } from 'react-native';
13 |
14 | var RefreshingIndicator = React.createClass({
15 | propTypes: {
16 | activityIndicatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
17 | stylesheet: PropTypes.object,
18 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
19 | },
20 | getDefaultProps() {
21 | return {
22 | activityIndicatorComponent: ActivityIndicator,
23 | }
24 | },
25 | renderActivityIndicator(style) {
26 | var activityIndicator = this.props.activityIndicatorComponent
27 |
28 | if (isValidElement(activityIndicator)) {
29 | return activityIndicator
30 | } else { // is a component class, not an element
31 | return createElement(activityIndicator, {style})
32 | }
33 | },
34 | render() {
35 | var styles = Object.assign({}, stylesheet, this.props.stylesheet)
36 |
37 | return (
38 |
39 |
40 |
41 | {this.props.description}
42 |
43 | {this.renderActivityIndicator(styles.activityIndicator)}
44 |
45 |
46 | )
47 | },
48 | })
49 |
50 | var stylesheet = StyleSheet.create({
51 | container: {
52 | flex: 1,
53 | justifyContent: 'space-around',
54 | alignItems: 'center',
55 | },
56 | wrapper: {
57 | height: 60,
58 | marginTop: 10,
59 | },
60 | content: {
61 | marginTop: 10,
62 | height: 60,
63 | },
64 | })
65 |
66 | module.exports = RefreshingIndicator
67 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/TestRunnerManager.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import android.content.SharedPreferences;
4 | import android.os.Build;
5 | import android.preference.PreferenceManager;
6 | import com.facebook.react.bridge.*;
7 | import com.facebook.react.devsupport.DevSupportManager;
8 |
9 | import javax.annotation.Nullable;
10 |
11 | import java.lang.reflect.Field;
12 |
13 | public class TestRunnerManager extends ReactContextBaseJavaModule {
14 | private @Nullable DevSupportManager mDevSupportManager;
15 | private @Nullable SharedPreferences mPreferences;
16 | private @Nullable ReactApplicationContext mReactContext;
17 |
18 | public TestRunnerManager (ReactApplicationContext reactContext) {
19 | super(reactContext);
20 |
21 | mReactContext = reactContext;
22 |
23 | setDevSupportManager();
24 | }
25 |
26 | private void setDevSupportManager() {
27 | try {
28 | Field field = ReactContext.class.getDeclaredField("mNativeModuleCallExceptionHandler");
29 | field.setAccessible(true);
30 | mDevSupportManager = (DevSupportManager) field.get(mReactContext);
31 | } catch (NoSuchFieldException e) {
32 | e.printStackTrace();
33 | } catch (IllegalAccessException e) {
34 | e.printStackTrace();
35 | }
36 | }
37 |
38 | @Override
39 | public String getName() {
40 | return "TestRunnerManager";
41 | }
42 |
43 | @ReactMethod
44 | public void reset(Callback callback) {
45 | if (mDevSupportManager != null) {
46 | UiThreadUtil.runOnUiThread(
47 | new Runnable() {
48 | @Override
49 | public void run() {
50 | mDevSupportManager.handleReloadJS();
51 | }
52 | }
53 | );
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/integration/authentication.test.js:
--------------------------------------------------------------------------------
1 | import it from '../helpers/appium';
2 | import server from '../helpers/server';
3 | import fixtures from '../helpers/fixtures';
4 |
5 | describe("Authentication", () => {
6 | beforeEach(() => {
7 | server.get("/api/posts/tester", fixtures.home());
8 | });
9 |
10 | it("should sign up the user and show dashboard", function* (driver, done) {
11 | server.post("api/signup", fixtures.signup());
12 |
13 | var username = yield driver.elementById('Username');
14 | var password = yield driver.elementById('Password');
15 | var button = yield driver.elementById('Sign up');
16 | yield username.setImmediateValue("tester");
17 | yield password.setImmediateValue("sample");
18 | yield button.click();
19 |
20 | // make sure logged in
21 | yield driver.elementById('Dashboard');
22 | yield driver.elementById('post1');
23 |
24 | done();
25 | });
26 |
27 | it("should log in and out", function* (driver, done) {
28 | server.post("api/login", fixtures.signup());
29 |
30 | yield driver.elementById('Already a user? Login here.').click();
31 |
32 | var username = yield driver.elementById('Username');
33 | var password = yield driver.elementById('Password');
34 | var button = yield driver.elementById('Log in');
35 | yield username.setImmediateValue("tester");
36 | yield password.setImmediateValue("sample");
37 | yield button.click();
38 |
39 | // make sure logged in
40 | yield driver.elementById('Dashboard');
41 | yield driver.elementById('post1');
42 |
43 | // show settings, log out
44 | yield driver.elementByXPath('//UIAApplication[1]/UIAWindow[1]/UIAElement[4]').click(); // "Me"
45 | yield driver.elementById('Settings');
46 | yield driver.elementById('Log out').click();
47 |
48 | // back on signup
49 | yield driver.elementById('Already a user? Login here.');
50 |
51 | done();
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/App/Mixins/KeyboardListener.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | Keyboard,
5 | } from 'react-native';
6 |
7 |
8 | var KeyboardListener = {
9 | getInitialState: function() {
10 | return {
11 | keyboardSpace: 0
12 | };
13 | },
14 |
15 | isKeyboardVisible: function() {
16 | return this.state.keyboardSpace > 0;
17 | },
18 |
19 | updateKeyboardSpace: function(e) {
20 | if (this.isMounted() && e && e.endCoordinates) {
21 | this.setState({keyboardSpace: e.endCoordinates.height});
22 | }
23 | },
24 |
25 | resetKeyboardSpace: function() {
26 | if (this.isMounted()) {
27 | this.setState({keyboardSpace: 0});
28 | }
29 | },
30 |
31 | // onKeyboardHideCallback: function() {
32 | // // TODO handle case when the keyboard never showed up in the first place
33 | // // Might want to use state to check that it did showed up
34 | // if (this.params.onKeyboardHide) {
35 | // this.params.onKeyboardHide.call(this.originalComponent());
36 | // }
37 | // },
38 |
39 |
40 | componentDidMount: function() {
41 | this._keyboardSubscriptions = [];
42 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardWillShow', this.updateKeyboardSpace));
43 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidShow', this.updateKeyboardSpace));
44 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardWillHide', this.resetKeyboardSpace));
45 | this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidHide', this.resetKeyboardSpace));
46 | //if (this.params.onKeyboardHide) {
47 | // this._keyboardSubscriptions.push(Keyboard.addListener('keyboardDidHide', this.onKeyboardHideCallback));
48 | //}
49 | },
50 |
51 | componentWillUnmount: function() {
52 | this._keyboardSubscriptions.forEach((subscription) => {
53 | subscription.remove();
54 | });
55 | },
56 |
57 | };
58 |
59 | export default KeyboardListener;
60 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | # We fork some components by platform.
4 | .*/*.android.js
5 |
6 | # Ignore templates with `@flow` in header
7 | .*/local-cli/generator.*
8 |
9 | # Ignore malformed json
10 | .*/node_modules/y18n/test/.*\.json
11 |
12 | # Ignore the website subdir
13 | /website/.*
14 |
15 | # Ignore BUCK generated dirs
16 | /\.buckd/
17 |
18 | # Ignore unexpected extra @providesModule
19 | .*/node_modules/commoner/test/source/widget/share.js
20 |
21 | # Ignore duplicate module providers
22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root
23 | .*/Libraries/react-native/React.js
24 | .*/Libraries/react-native/ReactNative.js
25 | .*/node_modules/jest-runtime/build/__tests__/.*
26 |
27 | [include]
28 |
29 | [libs]
30 | node_modules/react-native/Libraries/react-native/react-native-interface.js
31 | node_modules/react-native/flow
32 | flow/
33 |
34 | [options]
35 | module.system=haste
36 |
37 | esproposal.class_static_fields=enable
38 | esproposal.class_instance_fields=enable
39 |
40 | experimental.strict_type_args=true
41 |
42 | munge_underscores=true
43 |
44 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
45 | 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\)$' -> 'RelativeImageStub'
46 |
47 | suppress_type=$FlowIssue
48 | suppress_type=$FlowFixMe
49 | suppress_type=$FixMe
50 |
51 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(30\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
52 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(30\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
53 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
54 |
55 | unsafe.enable_getters_and_setters=true
56 |
57 | [version]
58 | ^0.30.0
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ReactNativeSampleApp",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node_modules/react-native/packager/packager.sh",
7 | "test": "npm run mocha-test test/integration",
8 | "mocha-test": "mocha --recursive --compilers js:babel-core/register",
9 | "compile:test": "TARGET=test node tasks/compile.js",
10 | "compile:staging": "TARGET=staging node tasks/compile.js",
11 | "install:staging": "PHONE=true TARGET=staging node tasks/compile.js",
12 | "translation:dump": "ACTION=dump node tasks/translation.js",
13 | "translation:backfill": "ACTION=backfill node tasks/translation.js",
14 | "translation:times": "ACTION=times node tasks/translation.js",
15 | "translation:check": "ACTION=check node tasks/translation.js"
16 | },
17 | "dependencies": {
18 | "events": "1.1.0",
19 | "flux": "2.1.1",
20 | "invariant": "2.2.0",
21 | "is-promise": "2.1.0",
22 | "jstz": "1.0.7",
23 | "keymirror": "0.1.1",
24 | "moment-timezone": "0.4.1",
25 | "object-assign": "4.0.1",
26 | "react": "15.4.0",
27 | "react-native": "0.38.0",
28 | "react-native-keychain": "0.2.6",
29 | "react-native-mock": "0.2.7",
30 | "react-timer-mixin": "0.13.3",
31 | "require-all": "2.0.0",
32 | "superagent": "1.8.3",
33 | "underscore": "1.8.3",
34 | "underscore.string": "3.2.2"
35 | },
36 | "devDependencies": {
37 | "appium": "1.5.1",
38 | "babel-plugin-syntax-async-functions": "^6.5.0",
39 | "babel-plugin-transform-class-properties": "^6.6.0",
40 | "babel-plugin-transform-regenerator": "^6.6.5",
41 | "babel-preset-es2015": "^6.6.0",
42 | "chai": "3.4.0",
43 | "chai-as-promised": "5.1.0",
44 | "colors": "1.1.2",
45 | "ios-deploy": "1.8.2",
46 | "koa": "1.1.2",
47 | "koa-bodyparser": "2.0.1",
48 | "mocha": "2.4.5",
49 | "node-inspector": "0.12.8",
50 | "wd": "0.4.0",
51 | "yiewd": "0.6.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/App/Stores/DebugStore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NativeModules } from 'react-native';
3 | import { EventEmitter } from 'events';
4 |
5 | import Dispatcher from '../Dispatcher';
6 | import AppConstants from '../Constants/AppConstants';
7 | import LocalKeyStore from '../Stores/LocalKeyStore';
8 |
9 | const EnvironmentManager = NativeModules.EnvironmentManager;
10 |
11 | const CHANGE_EVENT = 'change';
12 | const DEBUG_CURRENT_ROUTE_PATH_KEY = AppConstants.DEBUG_CURRENT_ROUTE_PATH_KEY;
13 |
14 | var _values = null;
15 |
16 | function setCurrentRoutePath(routePath) {
17 | _values = _values || {};
18 | _values.currentRoutePath = routePath;
19 | }
20 |
21 | function resetData() {
22 | _values = {};
23 | }
24 |
25 | var SingletonStore = Object.assign({}, EventEmitter.prototype, {
26 | get() {
27 | return _values;
28 | },
29 |
30 | emitChange() {
31 | this.emit(CHANGE_EVENT);
32 | },
33 |
34 | addChangeListener(callback) {
35 | this.on(CHANGE_EVENT, callback);
36 | },
37 |
38 | removeChangeListener(callback) {
39 | this.removeListener(CHANGE_EVENT, callback);
40 | }
41 | });
42 |
43 | Dispatcher.register(function(action) {
44 | switch (action.actionType) {
45 | case AppConstants.APP_LAUNCHED:
46 | LocalKeyStore.getKey(DEBUG_CURRENT_ROUTE_PATH_KEY, (error, routePath) => {
47 | setCurrentRoutePath(routePath);
48 | SingletonStore.emitChange();
49 | });
50 | break;
51 | case AppConstants.LAUNCH_ROUTE_PATH:
52 | if (action.routePath) {
53 | LocalKeyStore.setKey(DEBUG_CURRENT_ROUTE_PATH_KEY, action.routePath);
54 | setCurrentRoutePath(action.routePath);
55 | }
56 | break;
57 | case AppConstants.LOGOUT_REQUESTED:
58 | resetData();
59 | LocalKeyStore.setKey(DEBUG_CURRENT_ROUTE_PATH_KEY, '');
60 | SingletonStore.emitChange();
61 | break;
62 | default:
63 | // no op
64 | }
65 | });
66 |
67 | export default SingletonStore;
68 |
--------------------------------------------------------------------------------
/test/integration/posts.test.js:
--------------------------------------------------------------------------------
1 | import it, {itOnly} from '../helpers/appium';
2 | import server from '../helpers/server';
3 | import fixtures from '../helpers/fixtures';
4 | import bootstrap from '../helpers/bootstrap';
5 |
6 | describe("Posts", () => {
7 | it("should show my posts", function* (driver, done) {
8 | server.get("/api/posts/tester", fixtures.home());
9 |
10 | yield bootstrap().login().nav("dashboard").launch(driver);
11 |
12 | yield driver.elementById('Dashboard');
13 | yield driver.elementById('post1');
14 |
15 | done();
16 | });
17 |
18 | it("should show an empty list", function* (driver, done) {
19 | server.get("/api/posts/tester", { username: "tester", posts: [] });
20 |
21 | yield bootstrap().login().nav("dashboard").launch(driver);
22 |
23 | yield driver.elementById('Dashboard');
24 | yield driver.elementById('No items');
25 |
26 | done();
27 | });
28 |
29 | it("should my friends's posts", function* (driver, done) {
30 | server.get("/api/posts/friend", fixtures.friend());
31 |
32 | yield bootstrap().login().nav("dashboard/follows/friend").launch(driver);
33 |
34 | yield driver.elementById('friend');
35 | yield driver.elementById('post3');
36 |
37 | done();
38 | });
39 |
40 | it("should create a new post", function* (driver, done) {
41 | var list = fixtures.home();
42 | server.get("/api/posts/tester", list);
43 | server.post("/api/posts",
44 | {id: 100, content: 'new post here', username: 'tester'}, // return this content
45 | {content: 'new post here'} // expect this content
46 | );
47 |
48 | yield bootstrap().login().launch(driver);
49 |
50 | yield driver.elementById('+').click(); // new post!
51 |
52 | yield driver.elementById('New Post');
53 |
54 | yield driver.execute("target.frontMostApp().keyboard().typeString('new post here')");
55 |
56 | yield driver.elementById('Submit').click();
57 |
58 | yield driver.elementById('Dashboard');
59 |
60 | yield driver.elementById('new post here'); // it's there!
61 |
62 | done();
63 | });
64 |
65 | });
66 |
--------------------------------------------------------------------------------
/App/Actions/AppActions.js:
--------------------------------------------------------------------------------
1 | import Dispatcher from '../Dispatcher';
2 | import AppConstants from '../Constants/AppConstants';
3 | import assign from 'object-assign';
4 |
5 | var AppActions = {
6 | appLaunched: function() {
7 | Dispatcher.dispatch({
8 | actionType: AppConstants.APP_LAUNCHED
9 | });
10 | },
11 |
12 | reloadCurrentPath: function() {
13 | Dispatcher.dispatch({
14 | actionType: AppConstants.RELOAD_PATH
15 | });
16 | },
17 |
18 | launchRoutePath: function(routePath) {
19 | console.log("Launching! " + routePath);
20 | Dispatcher.dispatch({
21 | actionType: AppConstants.LAUNCH_ROUTE_PATH,
22 | routePath: routePath
23 | });
24 | },
25 |
26 | launchExternalURL: function(url) {
27 | console.log("Launching! " + url);
28 | Dispatcher.dispatch({
29 | actionType: AppConstants.OPEN_URL,
30 | url: url
31 | });
32 | },
33 |
34 | launchItem: function(props) {
35 | if (props.actionType) {
36 | console.log("Action! " + props.actionType);
37 | Dispatcher.dispatch(props);
38 | }
39 | else if (props.routePath) {
40 | console.log(props.routePath);
41 | this.launchRoutePath(props.routePath);
42 | }
43 | else {
44 | console.log("Unknown launchItem");
45 | }
46 | },
47 |
48 | launchRelativeItem: function(currentRoute, item) {
49 | var navItem = assign({}, item); // clone so we can mess with it
50 |
51 | if(!navItem.routePath && navItem.replacePath) {
52 | var pieces = currentRoute.routePath.split("/");
53 | pieces[pieces.length-1] = navItem.replacePath;
54 | navItem.routePath = pieces.join('/');
55 | }
56 | if(!navItem.routePath && navItem.subPath) {
57 | navItem.routePath = currentRoute.routePath + "/" + navItem.subPath;
58 | }
59 | navItem.currentRoute = currentRoute;
60 | this.launchItem(navItem);
61 | },
62 |
63 | launchNavItem: function(currentRoute, item) {
64 | var navItem = assign({}, item); // clone so we can mess with it
65 | navItem.targetPath = currentRoute.routePath;
66 | this.launchRelativeItem(currentRoute, navItem);
67 | },
68 |
69 | };
70 |
71 | export default AppActions;
72 |
--------------------------------------------------------------------------------
/App/Components/AutoScaleText.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import ReactNative, {
4 | NativeModules,
5 | } from 'react-native';
6 |
7 | const UIManager = NativeModules.UIManager;
8 |
9 | import Text from './Text';
10 |
11 | class AutoScaleText extends React.Component {
12 | static propTypes = {
13 | ...Text.propTypes,
14 | maxFontSize: React.PropTypes.number.isRequired,
15 | //maxHeight: React.PropTypes.number.isRequired,
16 | color: React.PropTypes.string,
17 | //style: React.PropTypes.oneOfType([
18 | // React.PropTypes.number,
19 | // React.PropTypes.shape({
20 | // width: React.PropTypes.number,
21 | // }),
22 | //]),
23 | };
24 |
25 | static defaultProps = {
26 | color: 'black',
27 | };
28 |
29 | constructor(props) {
30 | super(props);
31 |
32 | this.state = {
33 | fontSize: props.maxFontSize,
34 | finished: null,
35 | };
36 |
37 | this.visible = true
38 | }
39 |
40 | determineFontSize = () => {
41 | UIManager.measure(ReactNative.findNodeHandle(this.refs.textView), (x, y, w, h, px, py) => {
42 | if (!this.visible) return;
43 |
44 | var tooBig = this.props.maxHeight && h > this.props.maxHeight;
45 | if (!tooBig) {
46 | tooBig = this.props.maxWidth && w > this.props.maxWidth;
47 | }
48 | if (tooBig) {
49 | this.setState({
50 | fontSize: this.state.fontSize - 0.5,
51 | });
52 | this.determineFontSize();
53 | }
54 | else {
55 | this.setState({finished: true});
56 | }
57 | });
58 | };
59 |
60 | componentWillUnmount() {
61 | this.visible = false;
62 | }
63 |
64 | render() {
65 | return (
66 |
79 | {this.props.children}
80 |
81 | );
82 | }
83 | }
84 |
85 | export default AutoScaleText;
86 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/sample/TLSSocketFactory.java:
--------------------------------------------------------------------------------
1 | package com.sample;
2 |
3 | import java.io.IOException;
4 | import java.net.InetAddress;
5 | import java.net.Socket;
6 | import java.net.UnknownHostException;
7 |
8 | import javax.net.ssl.SSLSocket;
9 | import javax.net.ssl.SSLSocketFactory;
10 |
11 | /**
12 | * Taken from https://gist.github.com/mlc/549409f649251897ebef
13 | *
14 | * Enables TLS when creating SSLSockets.
15 | *
16 | * @link https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
17 | * @see SSLSocketFactory
18 | */
19 | class TLSSocketFactory extends SSLSocketFactory {
20 | final SSLSocketFactory delegate;
21 |
22 | public TLSSocketFactory(SSLSocketFactory delegate) {
23 | this.delegate = delegate;
24 | }
25 |
26 | @Override
27 | public String[] getDefaultCipherSuites() {
28 | return delegate.getDefaultCipherSuites();
29 | }
30 |
31 | @Override
32 | public String[] getSupportedCipherSuites() {
33 | return delegate.getSupportedCipherSuites();
34 | }
35 |
36 | @Override
37 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
38 | return patch(delegate.createSocket(s, host, port, autoClose));
39 | }
40 |
41 | @Override
42 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
43 | return patch(delegate.createSocket(host, port));
44 | }
45 |
46 | @Override
47 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
48 | return patch(delegate.createSocket(host, port, localHost, localPort));
49 | }
50 |
51 | @Override
52 | public Socket createSocket(InetAddress host, int port) throws IOException {
53 | return patch(delegate.createSocket(host, port));
54 | }
55 |
56 | @Override
57 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
58 | return patch(delegate.createSocket(address, port, localAddress, localPort));
59 | }
60 |
61 | private Socket patch(Socket s) {
62 | if (s instanceof SSLSocket) {
63 | ((SSLSocket) s).setEnabledProtocols(((SSLSocket) s).getSupportedProtocols());
64 | }
65 | return s;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/App/Platform/RefreshableListView.android.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | ListView,
5 | RefreshControl,
6 | } from 'react-native';
7 |
8 | import isPromise from 'is-promise';
9 |
10 | function delay(time) {
11 | return new Promise((resolve) => setTimeout(resolve, time));
12 | }
13 |
14 | class RefreshableListView extends React.Component {
15 |
16 | static propTypes = {
17 | loadData: React.PropTypes.func.isRequired,
18 | minDisplayTime: React.PropTypes.number,
19 | minBetweenTime: React.PropTypes.number,
20 | androidRefreshable: React.PropTypes.bool,
21 | };
22 |
23 | static defaultProps = {
24 | loadData: (() => {}),
25 | minDisplayTime: 300,
26 | minBetweenTime: 300,
27 | androidRefreshable: true,
28 | };
29 |
30 | constructor(props) {
31 | super(props);
32 |
33 | this.state = {};
34 | }
35 |
36 | handleRefresh = () => {
37 | if (this.willRefresh) return;
38 |
39 | this.willRefresh = true;
40 |
41 | var loadingDataPromise = new Promise((resolve) => {
42 | var loadDataReturnValue = this.props.loadData(resolve);
43 |
44 | if (isPromise(loadDataReturnValue)) {
45 | loadingDataPromise = loadDataReturnValue;
46 | }
47 |
48 | Promise.all([
49 | loadingDataPromise,
50 | new Promise((resolve) => this.setState({isRefreshing: true}, resolve)),
51 | delay(this.props.minDisplayTime),
52 | ])
53 | .then(() => delay(this.props.minBetweenTime))
54 | .then(() => {
55 | this.willRefresh = false;
56 | this.setState({isRefreshing: false});
57 | });
58 | });
59 | };
60 |
61 | render() {
62 | const listViewProps = {
63 | dataSource: this.props.dataSource,
64 | renderRow: this.props.renderRow,
65 | renderFooter: this.props.renderFooter,
66 | style: [this.props.style, {flex: 1}],
67 | renderScrollComponent: this.props.renderScrollComponent,
68 | renderHeader: this.props.renderHeaderWrapper,
69 | };
70 |
71 | const pullToRefreshProps = {
72 | style: [this.props.pullToRefreshStyle, {flex: 1}],
73 | refreshing: this.state.isRefreshing || false,
74 | onRefresh: this.handleRefresh,
75 | enabled: this.props.androidRefreshable,
76 | };
77 |
78 | return (
79 |
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | export default RefreshableListView;
87 |
--------------------------------------------------------------------------------
/App/Screens/CreatePost.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet
5 | } from 'react-native';
6 |
7 | import Locale from '../Locale';
8 |
9 | import TextInput from '../Components/TextInput';
10 | import Button from '../Components/Button';
11 | import PostActions from '../Actions/PostActions';
12 |
13 | import KeyboardListener from '../Mixins/KeyboardListener';
14 |
15 | import AddSpinnerLoader from '../Extensions/AddSpinnerLoader';
16 |
17 | var CreatePost = React.createClass({
18 | mixins: [KeyboardListener],
19 |
20 | getInitialState: function() {
21 | return {
22 | content: ''
23 | };
24 | },
25 |
26 | onSubmitButton: function() {
27 | this.props['AddSpinnerLoader'].start();
28 |
29 | PostActions.createPost(this.state.content, function(error) {
30 | this.props['AddSpinnerLoader'].stop();
31 | if (error) {
32 | // TODO: better error handling
33 | alert(error.message);
34 | }
35 | else {
36 | this.props.navigation.back();
37 | }
38 | }.bind(this));
39 | },
40 |
41 | render: function() {
42 | return (
43 |
44 | this.state.content = event.nativeEvent.text }
53 | />
54 |
55 |
56 |
59 |
60 |
61 |
62 | );
63 | }
64 | });
65 |
66 | var i18n = Locale.key('CreatePost', {
67 | placeholder: 'What do you have to say for yourself?',
68 | submit: 'Submit',
69 | });
70 |
71 | var styles = StyleSheet.create({
72 | flex: {
73 | flex: 1
74 | },
75 | input: {
76 | flex: 1,
77 | fontSize: 16,
78 | backgroundColor: 'white',
79 | padding: 20,
80 | textAlignVertical: 'top'
81 | },
82 | button: {
83 | // width: 150
84 | },
85 | footer: {
86 | padding: 10,
87 | flexDirection: 'row'
88 | }
89 | });
90 |
91 | CreatePost = AddSpinnerLoader(CreatePost);
92 | export default CreatePost;
93 |
--------------------------------------------------------------------------------
/App/Components/SpinnerLoader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StyleSheet,
4 | PanResponder,
5 | PixelRatio,
6 | View,
7 | PropTypes,
8 | ActivityIndicator,
9 | } from 'react-native';
10 |
11 | import cssVar from '../Lib/cssVar';
12 |
13 | const SpinnerLoader = React.createClass({
14 | propTypes: {
15 | spinning: React.PropTypes.bool,
16 | },
17 |
18 | getInitialState() {
19 | return {
20 | width: 0,
21 | height: 0,
22 | };
23 | },
24 |
25 | componentWillMount() {
26 | this._panGesture = PanResponder.create({
27 | onStartShouldSetPanResponder: (evt, gestureState) => { return false },
28 | onStartShouldSetPanResponderCapture: (evt, gestureState) => { return false },
29 | onMoveShouldSetPanResponder: (evt, gestureState) => { return false },
30 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => { return false },
31 | });
32 | },
33 |
34 | overlayStyle() {
35 | return {
36 | top: 0,
37 | left: 0,
38 | height: this.state.height,
39 | width: this.state.width,
40 | backgroundColor: 'transparent',
41 | alignItems: 'center',
42 | justifyContent: 'center',
43 | position: 'absolute',
44 | };
45 | },
46 |
47 | onChildrenLayout(event) {
48 | this.setState({
49 | height: event.nativeEvent.layout.height,
50 | width: event.nativeEvent.layout.width,
51 | });
52 | },
53 |
54 | renderSpinner() {
55 | if (!this.props.spinning || this.state.height === 0) {
56 | return null;
57 | }
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 | );
66 | },
67 |
68 | render() {
69 | return (
70 |
71 | {this.props.children}
72 | {this.renderSpinner()}
73 |
74 | );
75 | },
76 |
77 | });
78 |
79 | var styles = StyleSheet.create({
80 | spinnerContainer: {
81 | width: 60,
82 | height: 60,
83 | backgroundColor: 'transparent',
84 | alignItems: 'center',
85 | justifyContent: 'center',
86 | borderRadius: 60 / PixelRatio.get(),
87 | opacity: 0.70,
88 | },
89 | spinner: {
90 | width: 32,
91 | height: 32,
92 | },
93 | });
94 |
95 | export default SpinnerLoader;
96 |
--------------------------------------------------------------------------------
/App/Navigation/NavigationTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | Dimensions,
6 | Platform,
7 | } from 'react-native';
8 |
9 | const { width } = Dimensions.get('window');
10 |
11 | import cssVar from '../Lib/cssVar';
12 |
13 | import DispatcherListener from '../Mixins/DispatcherListener';
14 | import AppConstants from '../Constants/AppConstants';
15 |
16 | import AutoScaleText from '../Components/AutoScaleText';
17 |
18 | var NavigationTitle = React.createClass({
19 | mixins: [DispatcherListener],
20 |
21 | getInitialState: function() {
22 | return { updatedTitle: null };
23 | },
24 |
25 | dispatchAction: function(action) {
26 | switch(action.actionType) {
27 | case AppConstants.NAVBAR_UPDATE:
28 | if (action.route.routePath == this.props.route.routePath) {
29 | this.setState({updatedTitle: action.route.title});
30 | }
31 | break;
32 | }
33 | },
34 |
35 | getButtonPadding() {
36 | const { route } = this.props;
37 |
38 | // for some reason 70 is the magic number for Android
39 | if (Platform.OS === 'android') {
40 | return 70;
41 | }
42 | // if navRight is a text return 70
43 | if (route.navRight && route.navRight.label) {
44 | return 70;
45 | }
46 | // if navLeft is a text return 70
47 | if (route.navLeft && route.navLeft.label) {
48 | return 70;
49 | }
50 |
51 | if (route.navBack && route.navBack.label) {
52 | return 70;
53 | }
54 |
55 | return 40;
56 | },
57 |
58 | render: function() {
59 | var title = this.state.updatedTitle || this.props.route.title;
60 | return (
61 |
62 |
68 | {title}
69 |
70 |
71 | );
72 | }
73 | });
74 |
75 | const marginVertical = Platform.OS === 'ios' ? 8 : 15;
76 | var styles = StyleSheet.create({
77 | title: {
78 | flex: 1,
79 | flexDirection: 'row',
80 | alignItems: 'center',
81 | marginHorizontal: 16
82 | },
83 |
84 | titleText: {
85 | color: 'white',
86 | fontFamily: cssVar('fontRegular'),
87 | flex: 1,
88 | fontSize: 18,
89 | fontWeight: '500',
90 | textAlign: 'center',
91 | //textAlign: Platform.OS === 'ios' ? 'center' : 'left',
92 | paddingBottom: 6,
93 | }
94 | });
95 |
96 | export default NavigationTitle;
97 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/RefreshableListView.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | var isPromise = require('is-promise')
4 | var delay = require('./delay')
5 | var ListView = require('./ListView')
6 | var RefreshingIndicator = require('./RefreshingIndicator')
7 | var ControlledRefreshableListView = require('./ControlledRefreshableListView')
8 |
9 | const LISTVIEW_REF = 'listview'
10 |
11 | var RefreshableListView = React.createClass({
12 | propTypes: {
13 | loadData: PropTypes.func.isRequired,
14 | minDisplayTime: PropTypes.number,
15 | minBetweenTime: PropTypes.number,
16 | // props passed to child
17 | refreshDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
18 | refreshingIndictatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
19 | minPulldownDistance: PropTypes.number,
20 | renderHeaderWrapper: PropTypes.func,
21 | },
22 | getDefaultProps() {
23 | return {
24 | minDisplayTime: 300,
25 | minBetweenTime: 300,
26 | minPulldownDistance: 40,
27 | refreshingIndictatorComponent: RefreshingIndicator,
28 | }
29 | },
30 | getInitialState() {
31 | return {
32 | isRefreshing: false,
33 | }
34 | },
35 | handleRefresh() {
36 | if (this.willRefresh) return
37 |
38 | this.willRefresh = true
39 |
40 | var loadingDataPromise = new Promise((resolve) => {
41 | var loadDataReturnValue = this.props.loadData(resolve)
42 |
43 | if (isPromise(loadDataReturnValue)) {
44 | loadingDataPromise = loadDataReturnValue
45 | }
46 |
47 | Promise.all([
48 | loadingDataPromise,
49 | new Promise((resolve) => this.setState({isRefreshing: true}, resolve)),
50 | delay(this.props.minDisplayTime),
51 | ])
52 | .then(() => delay(this.props.minBetweenTime))
53 | .then(() => {
54 | this.willRefresh = false
55 | this.setState({isRefreshing: false})
56 | })
57 | })
58 | },
59 | getScrollResponder() {
60 | return this.refs[LISTVIEW_REF].getScrollResponder()
61 | },
62 | setNativeProps(props) {
63 | this.refs[LISTVIEW_REF].setNativeProps(props)
64 | },
65 | render() {
66 | return (
67 |
73 | )
74 | },
75 | })
76 |
77 | RefreshableListView.DataSource = ListView.DataSource
78 | RefreshableListView.RefreshingIndicator = RefreshingIndicator
79 |
80 | module.exports = RefreshableListView
81 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Disabling obfuscation is useful if you collect stack traces from production crashes
20 | # (unless you are using a system that supports de-obfuscate the stack traces).
21 | -dontobfuscate
22 |
23 | # React Native
24 |
25 | # Keep our interfaces so they can be used by other ProGuard rules.
26 | # See http://sourceforge.net/p/proguard/bugs/466/
27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
30 |
31 | # Do not strip any method/class that is annotated with @DoNotStrip
32 | -keep @com.facebook.proguard.annotations.DoNotStrip class *
33 | -keep @com.facebook.common.internal.DoNotStrip class *
34 | -keepclassmembers class * {
35 | @com.facebook.proguard.annotations.DoNotStrip *;
36 | @com.facebook.common.internal.DoNotStrip *;
37 | }
38 |
39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
40 | void set*(***);
41 | *** get*();
42 | }
43 |
44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; }
46 | -keepclassmembers,includedescriptorclasses class * { native ; }
47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; }
48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; }
49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; }
50 |
51 | -dontwarn com.facebook.react.**
52 |
53 | # okhttp
54 |
55 | -keepattributes Signature
56 | -keepattributes *Annotation*
57 | -keep class okhttp3.** { *; }
58 | -keep interface okhttp3.** { *; }
59 | -dontwarn okhttp3.**
60 |
61 | # okio
62 |
63 | -keep class sun.misc.Unsafe { *; }
64 | -dontwarn java.nio.file.*
65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
66 | -dontwarn okio.**
67 |
--------------------------------------------------------------------------------
/App/Extensions/Extension.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import invariant from 'invariant';
3 | import _ from 'underscore';
4 |
5 | const create = ({extensionName, requiredParams = {}, exports = {}, optionalParams, ...BaseLib}) => {
6 | invariant(
7 | extensionName,
8 | 'extensionName required when defining an extension'
9 | );
10 |
11 | const Extension = (Component, params = {}) => {
12 |
13 | const componentName = Component.displayName || Component.name;
14 | const containerName = `${extensionName} (${componentName})`;
15 |
16 | _.pairs(requiredParams).forEach(([key, text]) => {
17 | invariant(
18 | params[key],
19 | `Extension params required ${containerName} ${key}: ${text}`
20 | );
21 | });
22 |
23 | var ComponentContainer = React.createClass(Object.assign(params, {
24 | mixins: [BaseLib],
25 |
26 | displayName: containerName,
27 |
28 | params: params,
29 |
30 | getInitialState() {
31 | return {};
32 | },
33 |
34 | originalComponent() {
35 | if (!this.refs._ExtensionComponent) { return null; }
36 |
37 | if (this.refs._ExtensionComponent.originalComponent) {
38 | return this.refs._ExtensionComponent.originalComponent();
39 | }
40 | return this.refs._ExtensionComponent;
41 | },
42 |
43 | // Implement getExtensionProps if you want to add more behavior passed to the Component
44 | // it will allow accessing in the Extended Component with this.props[ExtensionName]
45 | getExtensionProps() {
46 | return {
47 | [extensionName]: Object.assign({variables: this.getExportedVariables()}, this.getExportedMethods())
48 | }
49 | },
50 |
51 | getExportedVariables() {
52 | var _variables = {};
53 | (exports.variables || []).forEach((variableName) => _variables[variableName] = this.state[variableName]);
54 |
55 | return _variables;
56 | },
57 |
58 | getExportedMethods() {
59 | var _methods = {};
60 | (exports.methods || []).forEach((methodName) => _methods[methodName] = this[methodName]);
61 |
62 | return _methods;
63 | },
64 |
65 | renderComponent() {
66 | return (
67 |
72 | );
73 | },
74 |
75 | render() {
76 | if (this.renderExtension) {
77 | return this.renderExtension();
78 | }
79 |
80 | return this.renderComponent();
81 | },
82 |
83 | }));
84 |
85 | return ComponentContainer;
86 | };
87 |
88 | return Extension;
89 | };
90 |
91 | export {create};
92 |
--------------------------------------------------------------------------------
/App/Components/SimpleListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | TouchableHighlight
6 | } from 'react-native';
7 |
8 | import cssVar from '../Lib/cssVar';
9 |
10 | import Text from '../Components/Text';
11 | import AppActions from '../Actions/AppActions';
12 |
13 | var SimpleListItem = React.createClass({
14 | onSelection: function() {
15 | AppActions.launchRelativeItem(this.props.currentRoute, this.props);
16 | },
17 |
18 | renderTitle: function() {
19 | if (!this.props.title) return null;
20 |
21 | return (
22 |
23 | {this.props.title}
24 |
25 | );
26 | },
27 |
28 | renderSubtitle: function() {
29 | if (!this.props.subtitle) return null;
30 |
31 | return (
32 |
33 | {this.props.subtitle}
34 |
35 | );
36 | },
37 |
38 | renderRightIcon: function() {
39 | if (!this.props.nextIcon) return null;
40 |
41 | // caret-right-semi
42 | return (
43 |
44 | >
45 |
46 | );
47 | },
48 |
49 | renderContent: function() {
50 | return (
51 |
52 |
53 | {this.renderTitle()}
54 | {this.renderSubtitle()}
55 |
56 |
57 | {this.renderRightIcon()}
58 |
59 |
60 | );
61 | },
62 |
63 | render: function() {
64 | if (this.props.noTap) {
65 | return this.renderContent();
66 | }
67 |
68 | return (
69 |
70 |
75 | {this.renderContent()}
76 |
77 |
78 |
79 | );
80 | }
81 | });
82 |
83 | var styles = StyleSheet.create({
84 | touch: {
85 | backgroundColor: 'white'
86 | },
87 | row: {
88 | flexDirection: 'row',
89 | alignItems: 'center',
90 | padding:20
91 | },
92 | title: {
93 | fontSize: 18,
94 | },
95 | subtitle: {
96 | paddingTop: 5,
97 | fontSize: 14,
98 | color: cssVar('gray20'),
99 | },
100 | left: {
101 | flex: 1,
102 | },
103 | right: {
104 |
105 | },
106 | rightIcon: {
107 | fontFamily: cssVar('fontIcon'),
108 | color: cssVar('gray30'),
109 | fontSize: 12,
110 | }
111 | });
112 |
113 | export default SimpleListItem;
114 |
--------------------------------------------------------------------------------
/ios/Sample/AppDelegate.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015-present, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | */
9 |
10 | #import "AppDelegate.h"
11 |
12 | #import "RCTRootView.h"
13 |
14 | @implementation AppDelegate
15 |
16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
17 | {
18 | NSURL *jsCodeLocation;
19 |
20 | /**
21 | * Loading JavaScript code - uncomment the one you want.
22 | *
23 | * OPTION 1
24 | * Load from development server. Start the server from the repository root:
25 | *
26 | * $ npm start
27 | *
28 | * To run on device, change `localhost` to the IP address of your computer
29 | * (you can get this by typing `ifconfig` into the terminal and selecting the
30 | * `inet` value under `en0:`) and make sure your computer and iOS device are
31 | * on the same Wi-Fi network.
32 | */
33 |
34 | // jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
35 |
36 | /**
37 | * OPTION 2
38 | * Load from pre-bundled file on disk. To re-generate the static bundle
39 | * from the root of your project directory, run
40 | *
41 | * $ react-native bundle --minify
42 | *
43 | * see http://facebook.github.io/react-native/docs/runningondevice.html
44 | */
45 |
46 | // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
47 |
48 |
49 | #ifdef TEST_ENVIRONMENT
50 | // different port
51 | jsCodeLocation = [NSURL URLWithString:@"http://localhost:9091/index.ios.bundle?platform=ios&dev=true"];
52 | #else
53 | #if TARGET_IPHONE_SIMULATOR
54 | jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
55 | #else
56 | jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
57 | #endif
58 | #endif
59 |
60 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
61 | moduleName:@"Sample"
62 | initialProperties:nil
63 | launchOptions:launchOptions];
64 |
65 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
66 | UIViewController *rootViewController = [UIViewController new];
67 | rootViewController.view = rootView;
68 | self.window.rootViewController = rootViewController;
69 | [self.window makeKeyAndVisible];
70 | return YES;
71 | }
72 |
73 | @end
74 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/test/helpers/bootstrap.js:
--------------------------------------------------------------------------------
1 | var server = require("./server");
2 | var dispatcher = require("../helpers/dispatcher");
3 | var assign = require('object-assign');
4 |
5 | var Bootstrap = function() {
6 | this.actions = [];
7 | };
8 |
9 | Bootstrap.prototype.login = function() {
10 | this.addAction("LOGIN_USER", dispatcher.login());
11 | return this;
12 | };
13 |
14 | Bootstrap.prototype.time = function(time, timezoneOffsetHours) {
15 | this.addAction("STUB_TIME", {time: time, timezoneOffset: -(timezoneOffsetHours * 60)});
16 | return this;
17 | };
18 |
19 | Bootstrap.prototype.nav = function(path) {
20 | this.addAction("LAUNCH_ROUTE_PATH", {routePath: path});
21 | return this;
22 | };
23 |
24 | Bootstrap.prototype.show = function(componentName, props, state) {
25 | var defaultProps = {
26 | currentRoute: {}
27 | };
28 |
29 | this.addAction("TEST_COMPONENT_ROUTE", {
30 | routeUnderTest: {
31 | component: componentName,
32 | passProps: assign(defaultProps, props),
33 | setState: state
34 | }
35 | });
36 | return this;
37 | };
38 |
39 | Bootstrap.prototype.addAction = function(type, options) {
40 | this.actions.push({type: type, options: options});
41 | return this;
42 | };
43 |
44 | Bootstrap.prototype.launch = function* (driver, options) {
45 | if (options && options.debug === true) {
46 | yield this.debug(driver);
47 | }
48 |
49 | var url = 'test/bootstrap.json';
50 |
51 | for (var i=0; i 0) {
54 | yield driver.sleep(50);
55 | }
56 | server.get(url, { actions: [action] });
57 | yield driver.elementById('Bootstrap').click();
58 | }
59 | return this;
60 | };
61 |
62 | Bootstrap.prototype.debug = function* (driver) {
63 | console.log('Bootstrap debugging');
64 |
65 | for (var i=0; i<50; i++) {
66 | // Open the menu
67 | try {
68 | yield driver.elementById('DevMenu').click();
69 | break;
70 | }
71 | catch (e) {
72 | // wait for a bit
73 | yield driver.sleep(500);
74 | }
75 | }
76 |
77 | for (var i=0; i<50; i++) {
78 | // Click on the Menu
79 | // If Debug in Chrome is not displayed then Disable Chrome Debugging should be
80 | try {
81 | try {
82 | yield driver.elementById('Debug in Chrome').click();
83 | }
84 | catch (e) {
85 | yield driver.elementById('Disable Chrome Debugging');
86 | }
87 | break;
88 | }
89 | catch (e) {
90 | // wait for a bit
91 | yield driver.sleep(500);
92 | }
93 | }
94 |
95 | for (var i=0; i<50; i++) {
96 | // wait til it reloads
97 | try {
98 | yield driver.elementById('ResetTest');
99 | break;
100 | }
101 | catch (e) {
102 | // wait for a bit
103 | yield driver.sleep(500);
104 | }
105 | }
106 | };
107 |
108 | module.exports = function() {
109 | return new Bootstrap();
110 | };
111 |
--------------------------------------------------------------------------------
/App/Stores/CurrentUserStore.js:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 | import assign from 'object-assign';
3 | import Keychain from '../Platform/Keychain';
4 |
5 | import CurrentUser from '../Models/CurrentUser';
6 | import LocalKeyStore from '../Stores/LocalKeyStore';
7 | import Dispatcher from '../Dispatcher';
8 | import AppConstants from '../Constants/AppConstants';
9 |
10 | var CHANGE_EVENT = 'change';
11 | var LOCAL_STORE_KEY = 'CurrentUser';
12 | var KEYCHAIN_SERVICE = 'Sample';
13 |
14 | // null so we know to initialize on app launch
15 | var _singleton = null;
16 |
17 | function initSingleton(storedProps, token, username) {
18 | if (storedProps && token && username) {
19 | if (storedProps.id && storedProps.id.toString() === username.toString()) {
20 | // matches
21 | setUserProps(storedProps, token);
22 | return;
23 | }
24 | }
25 | setUserProps(null, null);
26 | }
27 |
28 | function setUserProps(storedProps, token) {
29 | _singleton = new CurrentUser(storedProps, token);
30 | }
31 |
32 | function saveSingleton() {
33 | if(_singleton) {
34 | LocalKeyStore.setKey(LOCAL_STORE_KEY, _singleton.data);
35 | if (_singleton.data.id && _singleton.getToken()) {
36 | Keychain.setGenericPassword(_singleton.data.id.toString(), _singleton.getToken(), KEYCHAIN_SERVICE);
37 | }
38 | }
39 | }
40 |
41 | function clearData() {
42 | _singleton = new CurrentUser(null, null);
43 | LocalKeyStore.setKey(LOCAL_STORE_KEY, null);
44 | Keychain.resetGenericPassword(KEYCHAIN_SERVICE);
45 | }
46 |
47 | var SingletonStore = assign({}, EventEmitter.prototype, {
48 | get: function() {
49 | return _singleton;
50 | },
51 |
52 | emitChange: function() {
53 | this.emit(CHANGE_EVENT);
54 | },
55 |
56 | addChangeListener: function(callback) {
57 | this.on(CHANGE_EVENT, callback);
58 | },
59 |
60 | removeChangeListener: function(callback) {
61 | this.removeListener(CHANGE_EVENT, callback);
62 | }
63 | });
64 |
65 | // Register callback to handle all updates
66 | Dispatcher.register(function(action) {
67 | switch(action.actionType) {
68 | case AppConstants.APP_LAUNCHED:
69 | Keychain.getGenericPassword(KEYCHAIN_SERVICE).then((values) => {
70 | const { username, password } = values || {};
71 | // password is the stored token.
72 | LocalKeyStore.getKey(LOCAL_STORE_KEY, (storeError, props) => {
73 | initSingleton(props, password, username);
74 | SingletonStore.emitChange();
75 | });
76 | }).catch((error) => {
77 | console.log(error);
78 | initSingleton({}, null, null);
79 | SingletonStore.emitChange();
80 | });
81 | break;
82 | case AppConstants.LOGIN_USER:
83 | case AppConstants.USER_UPDATED:
84 | setUserProps(action.userProps, action.token);
85 | SingletonStore.emitChange();
86 | saveSingleton();
87 | break;
88 | case AppConstants.LOGOUT_REQUESTED:
89 | clearData();
90 | SingletonStore.emitChange();
91 | break;
92 | default:
93 | // no op
94 | }
95 | });
96 |
97 | export default SingletonStore;
98 |
--------------------------------------------------------------------------------
/test/helpers/driver.js:
--------------------------------------------------------------------------------
1 | require('./packager');
2 |
3 | // APPIUM -----------------
4 | var child_process = require('child_process');
5 | var appiumProc = child_process.spawn('appium', [
6 | '-p', '4724',
7 | '--default-capabilities', '{"fullReset":true}'
8 | ]);
9 |
10 | var Promise = require('Promise');
11 |
12 | var server = {
13 | host: 'localhost',
14 | port: 4724 // one off from normal
15 | };
16 |
17 | // {
18 | // host: 'ondemand.saucelabs.com',
19 | // port: 80,
20 | // username: process.env.SAUCE_USERNAME,
21 | // password: process.env.SAUCE_ACCESS_KEY
22 | // };
23 |
24 | var loadedAppium = null;
25 |
26 | var appiumPromise = new Promise(function (resolve, reject) {
27 | appiumProc.stdout.on('data', function (data) {
28 | if (loadedAppium) return;
29 | console.log('APPIUM: ' + data);
30 |
31 | if (data.indexOf('Appium REST http interface listener started') >= 0) {
32 | loadedAppium = true;
33 | resolve(data);
34 | }
35 | });
36 | });
37 |
38 | appiumProc.stderr.on('data', function (data) {
39 | console.log('APPIUM err: ' + data);
40 | appiumProc.kill();
41 | });
42 | process.on('exit', function () {
43 | appiumProc.kill();
44 | });
45 |
46 | // WD -----------------
47 |
48 | var realWd = require("wd");
49 | var wd = require("yiewd");
50 | var color = require('colors');
51 |
52 | // KOA -----------------
53 |
54 | var localServer = require("./server");
55 |
56 | // Config for Appium
57 |
58 | var UNLIMITED = 100000;
59 |
60 | var caps = {
61 | browserName: '',
62 | 'appium-version': '1.5.1',
63 | platformName: 'iOS',
64 | platformVersion: '9.3',
65 | deviceName: 'iPhone 6s',
66 | autoLaunch: 'true',
67 | newCommandTimeout: UNLIMITED,
68 | app: process.cwd() + "/testbuild/test_ios/sample_ios.zip"
69 | };
70 |
71 | module.exports = function(callback) {
72 | console.log("DRIVER: starting it up");
73 |
74 | appiumPromise.then(function () {
75 | console.log("DRIVER: will init");
76 | driver = wd.remote(server);
77 |
78 | driver.on('status', function(info) {
79 | console.log(info.cyan);
80 | });
81 | driver.on('command', function(meth, path, data) {
82 | console.log(' > ' + meth.yellow, path.grey, data || '');
83 | });
84 |
85 | current = {};
86 |
87 | handler = function(error, el){
88 | if (error) {
89 | console.log('error', error);
90 | }
91 | else if(typeof el === 'object'){
92 | console.log("Returned in current");
93 | current = el;
94 | }
95 | else {
96 | console.log("Returned following string", el);
97 | }
98 |
99 | };
100 |
101 | quit = function(){
102 | driver.quit(function(){
103 | process.exit(1);
104 | });
105 | };
106 |
107 | driver.init(caps, function(){
108 | console.log('driver started');
109 | callback({
110 | driver: driver,
111 | realWd: realWd,
112 | localServer: localServer,
113 | wd: wd,
114 | });
115 | });
116 | });
117 | };
118 |
--------------------------------------------------------------------------------
/tasks/translation-en-GB.json:
--------------------------------------------------------------------------------
1 | {
2 | "fail": {
3 | "lower": [
4 | "organiz",
5 | "neighbor",
6 | "zipcode",
7 | "zip code",
8 | "postal code",
9 | "furniture assembly",
10 | "apartment",
11 | "pantry",
12 | "dmv",
13 | "take-out",
14 | "crisper",
15 | "stand in line",
16 | "wait in line",
17 | "canceled",
18 | "revolutioniz",
19 | "categoriz",
20 | "organiz",
21 | "holiday help",
22 | "vacuum",
23 | "routing number",
24 | "www.taskrabbit.com",
25 | "@taskrabbit.com",
26 | "last name",
27 | "behavior",
28 | "favorite",
29 | "social security",
30 | "$"
31 | ],
32 | "sensitive": [
33 | "SSN"
34 | ]
35 | },
36 | "replace": [
37 | ["rganiz", "rganis"],
38 | ["eighbor", "eighbour"],
39 | ["evolutioniz", "evolutionis"],
40 | ["ategoriz", "ategoris"],
41 | ["vacuum", "hoover"],
42 | ["Vacuum", "Hoover"],
43 | ["Assemble Furniture", "Assemble Flat-pack"],
44 | ["assemble furniture", "assemble flat-pack"],
45 | ["Assemble furniture", "Assemble flat-pack"],
46 | ["Disassemble Furniture", "Disassemble Flat-pack"],
47 | ["disassemble furniture", "disassemble flat-pack"],
48 | ["Disassemble furniture", "Disassemble flat-pack"],
49 | ["Furniture assembly", "Flat-pack assembly"],
50 | ["Furniture Assembly", "Flat-pack Assembly"],
51 | ["furniture assembly", "flat-pack assembly"],
52 | ["Pantry", "Cupboard"],
53 | ["pantry", "cupboard"],
54 | ["sun-porch", "patio"],
55 | ["Sun-porch", "Patio"],
56 | ["DMV", "post office"],
57 | ["Wait in line", "Queue in line"],
58 | ["Wait In Line", "Queue In Line"],
59 | ["wait in line", "queue in line"],
60 | ["Wait in Line", "Queue in Line"],
61 | ["Stand in Line", "Queue in Line"],
62 | ["Stand in line", "Queue in line"],
63 | ["Stand In Line", "Queue In Line"],
64 | ["stand in line", "queue in line"],
65 | ["Apartment", "Flat"],
66 | ["an apartment", "a flat"],
67 | ["apartment", "flat"],
68 | ["crisper", "salad"],
69 | ["Crisper", "Salad"],
70 | ["Yard", "Garden"],
71 | ["yard", "garden"],
72 | ["Take-out", "Takeaway"],
73 | ["take-out", "takeaway"],
74 | ["avorite", "avourite"],
75 | ["Postal code", "Postcode"],
76 | ["Postal Code", "Postcode"],
77 | ["postal code", "postcode"],
78 | ["Zip code", "Postcode"],
79 | ["Zip Code", "Postcode"],
80 | ["zip code", "postcode"],
81 | ["anceled", "ancelled"],
82 | ["Holiday Help", "Christmas Help"],
83 | ["holiday help", "Christmas help"],
84 | ["Social Security Number", "identification"],
85 | ["burner", "hob"],
86 | ["routing number", "sort code"],
87 | ["Routing Number", "Sort Code"],
88 | ["Routing number", "Sort code"],
89 | ["www.taskrabbit.com", "www.taskrabbit.co.uk"],
90 | ["@taskrabbit.com", "@taskrabbit.co.uk"],
91 | ["ehavior", "ehaviour"],
92 | ["last name", "surname"],
93 | ["Last name", "Surname"],
94 | ["Last Name", "Surname"],
95 | ["$", "£"]
96 | ]
97 | }
98 |
--------------------------------------------------------------------------------
/App/Components/SegmentedControl.js:
--------------------------------------------------------------------------------
1 | // https://github.com/ide/react-native-button
2 |
3 | import React from 'react';
4 | import {
5 | StyleSheet,
6 | TouchableHighlight,
7 | View
8 | } from 'react-native';
9 |
10 | import cssVar from '../Lib/cssVar';
11 | import Text from '../Components/Text';
12 | import AppActions from '../Actions/AppActions';
13 |
14 | var SegmentedControl = React.createClass({
15 | segmentComponents: function() {
16 | var out = [];
17 | for(var i = 0; i < this.props.items.length; i++) {
18 | var item = this.props.items[i];
19 | var testID = null;
20 | if (!item.testID && this.props.appendTestId) {
21 | testID = 'seg' + item.title + '_' + this.props.appendTestId;
22 | }
23 | out.push(
24 |
25 | );
26 | };
27 | return out;
28 | },
29 |
30 | render: function() {
31 | return (
32 |
33 |
34 | {this.segmentComponents()}
35 |
36 |
37 | );
38 | }
39 | });
40 |
41 | var Segment = React.createClass({
42 | onSelection: function() {
43 | AppActions.launchRelativeItem(this.props.currentRoute, this.props);
44 | },
45 |
46 | render: function() {
47 | if (this.props.selected) {
48 | return (
49 |
52 |
53 |
54 | {this.props.title}
55 |
56 |
57 |
58 | );
59 | }
60 | else {
61 | return (
62 |
68 |
69 |
70 | {this.props.title}
71 |
72 |
73 |
74 | );
75 | }
76 | }
77 | });
78 |
79 |
80 | var styles = StyleSheet.create({
81 | container: {
82 | backgroundColor: cssVar('blue50'),
83 | padding: 10
84 | },
85 | control: {
86 | flexDirection: 'row',
87 | borderColor: 'white',
88 | borderWidth: 1,
89 | borderRadius: 4
90 | },
91 | flex: {
92 | flex: 1
93 | },
94 | button: {
95 | padding: 5,
96 | margin: 1,
97 | justifyContent: 'center',
98 | alignItems: 'center',
99 | },
100 | selectedSegment: {
101 | backgroundColor: 'white',
102 | },
103 | linkButton: {
104 | backgroundColor: cssVar('blue50'),
105 | },
106 | text: {
107 | fontSize: 16
108 | },
109 | selectedText: {
110 | color: cssVar('blue50'),
111 | },
112 | linkText: {
113 | color: 'white'
114 | }
115 | });
116 |
117 | export default SegmentedControl;
118 |
--------------------------------------------------------------------------------
/App/Navigation/NavigationBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | StyleSheet,
4 | View,
5 | NavigationExperimental,
6 | } from 'react-native';
7 |
8 | import cssVar from '../Lib/cssVar';
9 |
10 | import Back from '../Platform/Back';
11 | import NavigationHeader from '../Navigation/NavigationHeader';
12 | import Navigator from '../Navigation/Navigator';
13 |
14 | const {
15 | CardStack: NavigationCardStack,
16 | } = NavigationExperimental;
17 |
18 | var stacksEqual = function(one, two, length) {
19 | if (one.length < length) return false;
20 | if (two.length < length) return false;
21 |
22 | for (var i=0; i < length; i++) {
23 | if (one[i].routePath !== two[i].routePath) {
24 | return false;
25 | }
26 | }
27 | return true;
28 | };
29 |
30 | var Container = React.createClass({
31 | render: function() {
32 | var Component = this.props.route.component;
33 | return (
34 |
38 |
43 |
44 | );
45 | }
46 | });
47 |
48 | var NavigationBar = {
49 | getInitialState: function() {
50 | return {};
51 | },
52 |
53 | renderHeader: function(sceneProps) {
54 | if (this.props.navBarHidden) {
55 | //return
56 | return null;
57 | }
58 |
59 | return (
60 |
61 | );
62 |
63 | },
64 |
65 | renderScene: function(sceneProps) {
66 | var route = sceneProps.scene.route;
67 | console.log('renderScene: ' + route.routePath);
68 |
69 | return(
70 |
76 | );
77 | },
78 |
79 | onLoadedScene: function(component) {
80 | console.log("onLoadedScene");
81 | if (component) {
82 | this._currentComponent = component.refs.mainComponent;
83 | }
84 | else {
85 | this._currentComponent = null;
86 | }
87 | },
88 |
89 | componentWillMount: function() {
90 | this.navigation = new Navigator();
91 | },
92 |
93 | componentDidMount: function() {
94 | this.navigation.setStack(this.refs.stack);
95 | Back.setNavigator(this.navigation);
96 | },
97 |
98 | render: function() {
99 | return (
100 |
101 |
102 |
108 |
109 | );
110 | }
111 | }
112 |
113 | var styles = StyleSheet.create({
114 | appContainer: {
115 | flex: 1
116 | },
117 | navBar: {
118 | backgroundColor: cssVar('blue50'),
119 | },
120 | scene: {
121 | flex: 1,
122 | backgroundColor: cssVar('gray5'),
123 | },
124 | sceneHidden: {
125 | marginTop: 0
126 | },
127 | });
128 |
129 |
130 | export default NavigationBar;
131 |
--------------------------------------------------------------------------------
/test/helpers/server.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 |
3 | var _ = require('underscore');
4 | var colors = require('colors');
5 | var koa = require('koa');
6 | var bodyParser = require('koa-bodyparser');
7 |
8 | var app = koa();
9 |
10 | app.use(bodyParser());
11 |
12 | var mockRoutes = {};
13 | var MockApp = {
14 | before: function() {
15 | // before each test
16 | mockRoutes = {};
17 | },
18 | after: function() {
19 | // after each test
20 | mockRoutes = {};
21 | },
22 | get: function(path, response, params = null) {
23 | this.stub('GET', path, response, params);
24 | },
25 | post: function(path, response, params = null) {
26 | this.stub('POST', path, response, params);
27 | },
28 | put: function(path, response, params = null) {
29 | this.stub('PUT', path, response, params);
30 | },
31 | delete: function(path, response, params = null) {
32 | this.stub('DELETE', path, response, params);
33 | },
34 | stub: function(method, path, response, params = null) {
35 | if (path[0]!=='/') path = '/' + path;
36 | console.log('KOA stubbing: %s %s'.magenta, method.blue, path.green);
37 | if (typeof response === 'string') {
38 | response = {body: response, params: params};
39 | }
40 | if (!response.body) {
41 | // assume it's a json object
42 | response = {body: response, params: params};
43 | }
44 | response.params = params;
45 | mockRoutes[method + ':' + path] = response;
46 | },
47 | pageNotFound: function(req) {
48 | console.log('KOA ERROR: URL not registered: %s %s'.red, req.method.blue, req.url.green);
49 |
50 | req.status = 404;
51 |
52 | switch (req.accepts('html', 'json')) {
53 | case 'html':
54 | req.type = 'html';
55 | req.body = 'Test Page Not Found
';
56 | break;
57 | case 'json':
58 | // TODO: error message json format from v3?
59 | req.body = {
60 | message: 'Test Page Not Found'
61 | };
62 | break;
63 | default:
64 | req.type = 'text';
65 | req.body = 'Test Page Not Found';
66 | }
67 | },
68 | process: function(req) {
69 | var key = req.method + ':' + req.url;
70 | var found = mockRoutes[key];
71 |
72 | if (found) {
73 | if (found.params) {
74 | if (!_.isEqual(req.request.body, found.params)) {
75 | console.log('KOA: %s %s received with wrong params'.red, req.method.blue, req.url.green);
76 | console.log('expected: %s'.red, JSON.stringify(found.params));
77 | console.log('received: %s'.red, JSON.stringify(req.request.body));
78 |
79 | return this.pageNotFound(req);
80 | }
81 | }
82 | if (found.body) req.body = found.body;
83 | if (found.status) req.status = found.status;
84 |
85 | console.log('KOA: %s %s'.red, req.method.blue, req.url.green);
86 | // console.log('==== %s', JSON.stringify(found.body).blue);
87 | } else {
88 | this.pageNotFound(req);
89 | }
90 | },
91 | };
92 |
93 |
94 | app.use(function *(){
95 | // look up in current app
96 | if (this.path === '/test/console.json') {
97 | console[this.request.body.level](...["App Log:", ...this.request.body.arguments]);
98 | }
99 | else {
100 | MockApp.process(this);
101 | }
102 | });
103 |
104 | app.listen(3001);
105 |
106 | module.exports = MockApp;
107 |
--------------------------------------------------------------------------------
/test/integration/follows.test.js:
--------------------------------------------------------------------------------
1 | import it from '../helpers/appium';
2 | import server from '../helpers/server';
3 | import fixtures from '../helpers/fixtures';
4 | import bootstrap from '../helpers/bootstrap';
5 |
6 | describe("Follows", () => {
7 | it("should show my follows", function* (driver, done) {
8 | server.get("/api/follows/tester", fixtures.myFollows());
9 |
10 | yield bootstrap().login().nav("dashboard/follows").launch(driver);
11 |
12 | yield driver.elementById('Dashboard');
13 | yield driver.elementById('friend');
14 | yield driver.elementById('follow2');
15 |
16 | done();
17 | });
18 |
19 | it("should show an empty list", function* (driver, done) {
20 | server.get("/api/follows/tester", { username: "tester", follows: [] });
21 |
22 | yield bootstrap().login().nav("dashboard/follows").launch(driver);
23 |
24 | yield driver.elementById('Follows');
25 | yield driver.elementById('No items');
26 |
27 | done();
28 | });
29 |
30 | it("should my friends's follows", function* (driver, done) {
31 | server.get("/api/follows/friend", fixtures.friendFollows());
32 |
33 | yield bootstrap().login().nav("dashboard/follows/friend/follows").launch(driver);
34 |
35 | yield driver.elementById('friend');
36 | yield driver.elementById('tester');
37 | yield driver.elementById('follow4');
38 |
39 | done();
40 | });
41 |
42 | it("should navigate follows recursively", function* (driver, done) {
43 | server.get("/api/posts/tester", fixtures.home());
44 | server.get("/api/posts/friend", fixtures.friend());
45 | server.get("/api/posts/follow4", {
46 | username: "follow4",
47 | posts: [ { id: 1000, username: 'follow4', content: 'post1000' }]
48 | });
49 |
50 | server.get("/api/follows/friend", fixtures.friendFollows());
51 | server.get("/api/follows/tester", fixtures.myFollows());
52 | server.get("/api/follows/follow4", { username: "follow4", follows: [ { id: 123, username: 'tester' }] });
53 |
54 | yield bootstrap().login().launch(driver);
55 |
56 | yield driver.elementById("Dashboard");
57 | yield driver.elementById("post1"); // my post
58 | // yield driver.sleep(90000000);
59 | yield driver.elementById("segFollows_tester").click(); // open my follows
60 |
61 | yield driver.elementById('friend').click(); // click on friend
62 |
63 | yield driver.elementById("friend"); // friend's list
64 | yield driver.elementById("post4"); // friend's post
65 | yield driver.elementById("segFollows_friend").click(); // open friend's follows
66 |
67 | yield driver.elementById('follow4').click(); // click on other
68 |
69 | yield driver.elementById("follow4"); // other's list
70 | yield driver.elementById("post100"); // other's post
71 | yield driver.elementById("segFollows_follow4").click(); // open other's follows
72 |
73 | yield driver.elementById('tester').click(); // click on me
74 |
75 | // TODO: it should NOT say "Dashboard"
76 | yield driver.elementById("tester"); // my list
77 | yield driver.elementById("post1"); // my post
78 |
79 | // Now pop everything
80 | yield driver.elementById("back").click(); // back
81 | yield driver.elementById("follow4");
82 |
83 | yield driver.elementById("back").click(); // back
84 | yield driver.elementById("friend");
85 |
86 | yield driver.elementById("back").click(); // back
87 |
88 | yield driver.elementById("Dashboard");
89 |
90 | done();
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample Staging.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/App/Navigation/NavigationButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | TouchableOpacity
6 | } from 'react-native';
7 |
8 | import cssVar from '../Lib/cssVar';
9 |
10 | import DispatcherListener from '../Mixins/DispatcherListener';
11 | import AppConstants from '../Constants/AppConstants';
12 | import AppActions from '../Actions/AppActions';
13 |
14 | import Text from '../Components/Text';
15 |
16 | var NavigationButton = React.createClass({
17 | mixins: [DispatcherListener],
18 |
19 | getInitialState: function() {
20 | return {updatedRoute: null};
21 | },
22 |
23 | dispatchAction: function(action) {
24 | switch(action.actionType) {
25 | case AppConstants.NAVBAR_UPDATE:
26 | var route = this.state.updatedRoute || this.props.route;
27 | if (action.route.routePath === route.routePath) {
28 | this.setState({updatedRoute: action.route});
29 | }
30 | break;
31 | }
32 | },
33 |
34 | makeButton: function(item, style, callback) {
35 | var styleType;
36 | var text;
37 |
38 | if (item.icon) {
39 | styleType = styles.navBarIcon;
40 | text = item.icon;
41 | }
42 | else {
43 | styleType = styles.navBarText;
44 | text = item.label;
45 | }
46 |
47 | var button = (
48 |
49 |
50 | {text}
51 |
52 |
53 | );
54 |
55 | if (item.disabled) {
56 | return button;
57 | }
58 | else {
59 | return (
60 |
61 | {button}
62 |
63 | );
64 | }
65 | },
66 |
67 | renderRight: function() {
68 | var route = this.state.updatedRoute || this.props.route;
69 | if (!route.navRight) return null;
70 |
71 | return this.makeButton(route.navRight, styles.navBarRightButton, function() {
72 | AppActions.launchNavItem(route, route.navRight);
73 | });
74 | },
75 |
76 | renderLeft: function() {
77 | var route = this.state.updatedRoute || this.props.route;
78 | if (route.navLeft && !route.navBack) {
79 | return this.makeButton(route.navLeft, styles.navBarLeftButton, function() {
80 | AppActions.launchNavItem(route, route.navLeft);
81 | });
82 | }
83 |
84 | if (this.props.index === 0) {
85 | return null;
86 | }
87 |
88 | var backLabel = route.navBack || {label: 'back'}; //{icon: 'caret-left-semi'};
89 | return this.makeButton(backLabel, styles.navBarLeftButton, this.sendBack);
90 | },
91 |
92 | sendBack: function() {
93 | this.props.navigation.back();
94 | },
95 |
96 | render: function() {
97 | switch (this.props.direction) {
98 | case 'left':
99 | return this.renderLeft();
100 | case 'right':
101 | return this.renderRight();
102 | default:
103 | throw("Unknown direction: " + this.props.direction);
104 | }
105 | }
106 | });
107 |
108 | var styles = StyleSheet.create({
109 | navBarText: {
110 | fontSize: 16,
111 | marginVertical: 10,
112 | },
113 | navBarIcon: {
114 | fontSize: 20,
115 | marginVertical: 10,
116 | fontFamily: cssVar('fontIcon'),
117 | },
118 | navBarLeftButton: {
119 | paddingLeft: 10,
120 | },
121 | navBarRightButton: {
122 | paddingRight: 10,
123 | },
124 | navBarButtonText: {
125 | color: 'white',
126 | },
127 | disabledText: {
128 | color: cssVar('gray30')
129 | }
130 | });
131 |
132 | export default NavigationButton;
133 |
--------------------------------------------------------------------------------
/ios/Sample/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/App/Mixins/ListHelper.js:
--------------------------------------------------------------------------------
1 | // helper to handle showing lists of things
2 | // parent must implement methods
3 | // required: getListItem, getItemProps
4 | // optional: isListChange, reloadList
5 | //
6 | // and give props
7 | // required: store, currentRoute
8 | // optional: listProps, segment
9 |
10 | import React from 'react';
11 | import {
12 | View,
13 | StyleSheet,
14 | ListView
15 | } from 'react-native';
16 |
17 | import Locale from '../Locale';
18 |
19 | import CurrentUserStore from '../Stores/CurrentUserStore';
20 | import NavigationListener from '../Mixins/NavigationListener';
21 | import NavBarHelper from '../Mixins/NavBarHelper';
22 |
23 | import Loading from '../Screens/Loading';
24 | import Text from '../Components/Text';
25 | import SegmentedControl from '../Components/SegmentedControl';
26 | import SimpleList from '../Components/SimpleList';
27 |
28 | var ListHelper = {
29 | mixins: [NavigationListener, NavBarHelper],
30 |
31 | getInitialState: function() {
32 | return this.getListState();
33 | },
34 |
35 | getListState: function() {
36 | return {
37 | items: this.getListItems()
38 | };
39 | },
40 |
41 | onListChange: function(arg) {
42 | if (!this.isListChange || this.isListChange(arg)) {
43 | this.setState(this.getListState());
44 | }
45 | },
46 |
47 | onDidFocusNavigation: function() {
48 | // items may have changed
49 | this.setState(this.getListState());
50 | },
51 |
52 | componentDidMount: function() {
53 | this.props.store.addChangeListener(this.onListChange);
54 | if (this.reloadList) {
55 | this.reloadList();
56 | }
57 | },
58 |
59 | componentWillUnmount: function() {
60 | this.props.store.removeChangeListener(this.onListChange);
61 | },
62 |
63 | getNavBarState: function() {
64 | var title = this.props.username ? this.props.username : i18n.t('dashboard');
65 | return { title: title };
66 | },
67 |
68 | getUsername: function() {
69 | if (!this.username) {
70 | this.username = this.props.username || CurrentUserStore.get().data.username;
71 | }
72 | return this.username;
73 | },
74 |
75 | renderItems: function() {
76 | return (
77 |
85 | );
86 | },
87 |
88 | renderEmpty: function() {
89 | return(
90 |
91 | No Items
92 |
93 | );
94 | },
95 |
96 | renderHeader: function() {
97 | if (!this.props.segment) return null;
98 | return (
99 |
100 | );
101 | },
102 |
103 | renderContent: function() {
104 | var header = this.renderHeader();
105 | var content;
106 | if (this.state.items.length === 0) {
107 | content = this.renderEmpty();
108 | }
109 | else {
110 | content = this.renderItems();
111 | }
112 |
113 | return (
114 |
115 | {header}
116 | {content}
117 |
118 | );
119 | },
120 |
121 | render: function() {
122 | if (!this.state.items) {
123 | // TODO: load error?
124 | return ;
125 | }
126 | else {
127 | return this.renderContent();
128 | }
129 | }
130 | };
131 |
132 | var i18n = Locale.key('ListHelper', {
133 | dashboard: 'Dashboard'
134 | });
135 |
136 | var styles = StyleSheet.create({
137 | flex: {
138 | flex: 1
139 | }
140 | });
141 |
142 | export default ListHelper;
143 |
--------------------------------------------------------------------------------
/App/Vendor/react-native-refreshable-listview/lib/ControlledRefreshableListView.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | var ListView = require('./ListView')
4 | var createElementFrom = require('./createElementFrom')
5 | var RefreshingIndicator = require('./RefreshingIndicator')
6 |
7 | const SCROLL_EVENT_THROTTLE = 32
8 | const MIN_PULLDOWN_DISTANCE = 40
9 |
10 | const LISTVIEW_REF = 'listview'
11 |
12 | var ControlledRefreshableListView = React.createClass({
13 | propTypes: {
14 | onRefresh: PropTypes.func.isRequired,
15 | isRefreshing: PropTypes.bool.isRequired,
16 | refreshDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
17 | refreshingIndictatorComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
18 | minPulldownDistance: PropTypes.number,
19 | ignoreInertialScroll: PropTypes.bool,
20 | scrollEventThrottle: PropTypes.number,
21 | onScroll: PropTypes.func,
22 | renderHeader: PropTypes.func,
23 | renderHeaderWrapper: PropTypes.func,
24 | onResponderGrant: PropTypes.func,
25 | onResponderRelease: PropTypes.func,
26 | },
27 | getDefaultProps() {
28 | return {
29 | minPulldownDistance: MIN_PULLDOWN_DISTANCE,
30 | scrollEventThrottle: SCROLL_EVENT_THROTTLE,
31 | ignoreInertialScroll: true,
32 | refreshingIndictatorComponent: RefreshingIndicator,
33 | }
34 | },
35 | handleScroll(e) {
36 | var scrollY = e.nativeEvent.contentInset.top + e.nativeEvent.contentOffset.y
37 |
38 | if (this.isTouching || (!this.isTouching && !this.props.ignoreInertialScroll)) {
39 | if (scrollY < -this.props.minPulldownDistance) {
40 | if (!this.props.isRefreshing) {
41 | if (this.props.onRefresh) {
42 | this.props.onRefresh()
43 | }
44 | }
45 | }
46 | }
47 |
48 | this.props.onScroll && this.props.onScroll(e)
49 | },
50 | handleResponderGrant() {
51 | this.isTouching = true
52 | if (this.props.onResponderGrant) {
53 | this.props.onResponderGrant.apply(null, arguments)
54 | }
55 | },
56 | handleResponderRelease() {
57 | this.isTouching = false
58 | if (this.props.onResponderRelease) {
59 | this.props.onResponderRelease.apply(null, arguments)
60 | }
61 | },
62 | getScrollResponder() {
63 | return this.refs[LISTVIEW_REF].getScrollResponder()
64 | },
65 | setNativeProps(props) {
66 | this.refs[LISTVIEW_REF].setNativeProps(props)
67 | },
68 | renderHeader() {
69 | var description = this.props.refreshDescription
70 |
71 | var refreshingIndictator
72 | if (this.props.isRefreshing) {
73 | refreshingIndictator = createElementFrom(this.props.refreshingIndictatorComponent, {description})
74 | } else {
75 | refreshingIndictator = null
76 | }
77 |
78 | if (this.props.renderHeaderWrapper) {
79 | return this.props.renderHeaderWrapper(refreshingIndictator)
80 | } else if (this.props.renderHeader) {
81 | console.warn('renderHeader is deprecated. Use renderHeaderWrapper instead.')
82 | return this.props.renderHeader(refreshingIndictator)
83 | } else {
84 | return refreshingIndictator
85 | }
86 | },
87 | render() {
88 | return (
89 |
98 | )
99 | },
100 | })
101 |
102 | ControlledRefreshableListView.DataSource = ListView.DataSource
103 | ControlledRefreshableListView.RefreshingIndicator = RefreshingIndicator
104 |
105 | module.exports = ControlledRefreshableListView
106 |
--------------------------------------------------------------------------------
/ios/Sample.xcodeproj/xcshareddata/xcschemes/Sample Test.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/App/Root/TestRunner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | Text,
5 | View,
6 | StyleSheet,
7 | TouchableOpacity,
8 | NativeModules,
9 | } from 'react-native';
10 |
11 | const {
12 | TestRunnerManager,
13 | DevMenu,
14 | } = NativeModules;
15 |
16 | import client from '../Api/HTTPClient';
17 | import Dispatcher from '../Dispatcher';
18 |
19 | import StatusBar from '../Platform/StatusBar';
20 |
21 | var TestRunner = React.createClass({
22 | componentDidMount: function() {
23 | StatusBar.setHidden(true);
24 | },
25 |
26 | resetApp: function() {
27 | TestRunnerManager.reset(function() {
28 | console.log("reset")
29 | });
30 | },
31 |
32 | showDevMenu: function() {
33 | DevMenu.show();
34 | },
35 |
36 | hookConsole: function() {
37 | global.console = {
38 | log: function(...args) {
39 | client.post("test/console.json", {level: 'log', arguments: args});
40 | },
41 | error: function(...args) {
42 | client.post("test/console.json", {level: 'error', arguments: args});
43 | },
44 | warn: function(...args) {
45 | client.post("test/console.json", {level: 'warn', arguments: args});
46 | }
47 | }
48 | },
49 |
50 | bootstrapApp: function() {
51 | client.get("test/bootstrap.json", null,
52 | (error, response) => {
53 | if (error) {
54 | console.log("BOOTSTRAP ERROR");
55 | alert('"BOOTSTRAP ERROR"');
56 | }
57 | else {
58 | this.hookConsole();
59 | if (response.actions) {
60 | for (var i=0; i
87 | );
88 | },
89 |
90 | render: function() {
91 | var containerStyle = (this.props.routeUnderTest ? styles.withComponent : styles.noComponent);
92 | return (
93 |
94 | {this.renderRouteUnderTest()}
95 |
96 |
97 |
98 | ResetTest
99 |
100 |
101 |
102 |
103 | Bootstrap
104 |
105 |
106 |
107 |
108 | DevMenu
109 |
110 |
111 |
112 |
113 | )
114 | }
115 | });
116 |
117 | var styles = StyleSheet.create({
118 | withComponent: {
119 | flex: 1
120 | },
121 | noComponent: {
122 | height: 10
123 | },
124 | bar: {
125 | height: 10,
126 | backgroundColor: 'red',
127 | justifyContent: 'center',
128 | flexDirection: 'row'
129 | },
130 | action: {
131 | marginHorizontal: 4,
132 | fontSize: 10
133 | }
134 | });
135 |
136 | export default TestRunner;
137 |
--------------------------------------------------------------------------------
/App/Api/HTTPClient.js:
--------------------------------------------------------------------------------
1 | // http://visionmedia.github.io/superagent
2 | import superagent from 'superagent';
3 |
4 | import Network from '../Api/Network';
5 |
6 | import CurrentUserStore from '../Stores/CurrentUserStore';
7 | import EnvironmentStore from '../Stores/EnvironmentStore';
8 |
9 | var HTTPClient = {
10 | wrapper: function(inner) {
11 | return function(error, response) {
12 | Network.completed();
13 |
14 | if(!inner) return;
15 | // chance to wrap and call original
16 |
17 | var parsed = null;
18 | if(response && response.text && response.text.length > 0) {
19 | try {
20 | parsed = JSON.parse(response.text);
21 | }
22 | catch (e) {
23 | parsed = null;
24 | // TODO: some other error?
25 | console.log("HTTPClient could not parse:\n\n" + response.text);
26 | }
27 | }
28 |
29 | var errorObj = null;
30 | var valueObj = null;
31 |
32 | if (error) {
33 | // error.status => 422
34 | errorObj = {};
35 | if (error.status) {
36 | errorObj.status = error.status; // 422
37 | }
38 | else {
39 | errorObj.status = 520; // Unknown error
40 | }
41 |
42 | errorObj.errors = [];
43 | if (parsed && parsed.error) {
44 | errorObj.message = parsed.error
45 | }
46 | if (!errorObj.message) {
47 | errorObj.message = 'Server Error: ' + errorObj.status;
48 | }
49 | console.log("http error (" + errorObj.status + "): " + errorObj.message);
50 | }
51 | else {
52 | valueObj = parsed;
53 | }
54 | inner(errorObj, valueObj);
55 | };
56 | },
57 |
58 | addHeaders: function(req) {
59 | // TODO: load version from somewhere
60 | var appVersion = "1.0";
61 | var userAgent = "Sample iPhone v" + appVersion;
62 | var locale = 'en-US';
63 |
64 | req = req.accept('application/json');
65 | req = req.type('application/json');
66 | req = req.set('User-Agent', userAgent);
67 | req = req.set('X-CLIENT-VERSION', appVersion);
68 | req = req.set('X-Sample-User-Agent', userAgent);
69 | req = req.set('X-LOCALE', locale);
70 |
71 | var currentUser = CurrentUserStore.get();
72 | if (currentUser && currentUser.getToken()) {
73 | req = req.set('Authorization', 'Bearer ' + currentUser.getToken());
74 | }
75 |
76 | // if (currentUser && currentUser.data.guid) {
77 | // req = req.set('X-GUID', currentUser.data.guid);
78 | // }
79 | // if (currentUser && currentUser.data.ab_decision_group_id) {
80 | // req = req.set('X-AB-DECISION-GROUP-ID', currentUser.data.ab_decision_group_id.toString());
81 | // }
82 | // if (currentUser && currentUser.data.ab_decision) {
83 | // req = req.set('X-AB-DECISION', currentUser.data.ab_decision);
84 | // }
85 |
86 | return req;
87 | },
88 |
89 | fetch: function(req, callback) {
90 | req = this.addHeaders(req);
91 | Network.started();
92 | req.end(this.wrapper(callback));
93 | },
94 |
95 | url: function(path) {
96 | var host = EnvironmentStore.get().getApiHost();
97 | return host + "/" + path;
98 | },
99 |
100 | post: function(path, values, callback) {
101 | var req = superagent.post(this.url(path));
102 | if (values) {
103 | req = req.send(values);
104 | }
105 | this.fetch(req, callback);
106 | },
107 |
108 | put: function(path, values, callback) {
109 | var req = superagent.put(this.url(path));
110 | if (values) {
111 | req = req.send(values);
112 | }
113 | this.fetch(req, callback);
114 | },
115 |
116 |
117 | get: function(path, params, callback) {
118 | var req = superagent.get(this.url(path));
119 | if (params) {
120 | req = req.query(params);
121 | }
122 | this.fetch(req, callback);
123 | }
124 | };
125 |
126 | export default HTTPClient;
127 |
--------------------------------------------------------------------------------
/App/Navigation/Router.js:
--------------------------------------------------------------------------------
1 | // parseUri 1.2.2
2 | // (c) Steven Levithan
3 | // MIT License
4 |
5 | import Routes from '../Navigation/Routes';
6 | import assign from 'object-assign';
7 |
8 | function parseUri (str) {
9 | var o = parseUri.options,
10 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
11 | uri = {},
12 | i = 14;
13 |
14 | while (i--) uri[o.key[i]] = m[i] || "";
15 |
16 | uri[o.q.name] = {};
17 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
18 | if ($1) uri[o.q.name][$1] = $2;
19 | });
20 |
21 | return uri;
22 | };
23 |
24 | parseUri.options = {
25 | strictMode: false,
26 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
27 | q: {
28 | name: "queryKey",
29 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g
30 | },
31 | parser: {
32 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
33 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
34 | }
35 | };
36 |
37 | // TODO: can we use https://github.com/visionmedia/page.js
38 | var Router = {
39 |
40 | parse: function(str, parent, defaulted) {
41 | if (!str) {
42 | str = "";
43 | }
44 |
45 | var toParse = str.toLowerCase();
46 | if (toParse.indexOf("://") < 0) {
47 | toParse = "sample://" + str;
48 | }
49 | var parsed = parseUri(toParse);
50 | var pieces = parsed.path.split("/");
51 | pieces.unshift(parsed.host);
52 | pieces = pieces.filter(function(v) { return v && v.length > 0 });
53 | var current = parent;
54 | var stack = [];
55 |
56 | for(var i=0; i < pieces.length; i++) {
57 | var piece = pieces[i];
58 | if (piece.length > 0) {
59 | if (!current.parse) {
60 | if(defaulted) {
61 | break; // as deep as we can
62 | }
63 | else {
64 | return null;
65 | }
66 | }
67 | var route = current.parse(piece);
68 | if (route) {
69 | if (current.routePath) {
70 | route.routePath = current.routePath + "/" + piece;
71 | }
72 | else {
73 | route.routePath = piece;
74 | }
75 |
76 | if (route._routerReplace) {
77 | stack[stack.length-1] = route;
78 | }
79 | else if (route._notAddressable) {
80 | if (route._routerAppend && i === (pieces.length-1)) {
81 | // add soemthing to route on the last one (kind of like a forward)
82 | var child = route.parse(route._routerAppend);
83 | child.routePath = route.routePath + "/" + route._routerAppend;
84 | route = child;
85 | stack.push(route);
86 | }
87 | }
88 | else {
89 | if (route._routerAppend && i === (pieces.length-1)) {
90 | // add soemthing to route on the last one (kind of like a forward)
91 | route.routePath = route.routePath + "/" + route._routerAppend;
92 | }
93 | stack.push(route);
94 | }
95 | current = route; // recursive
96 | }
97 | else if(defaulted) {
98 | break; // as deep as we can
99 | }
100 | else {
101 | return null;
102 | }
103 | }
104 | }
105 |
106 | if (stack.length === 0) {
107 | return null;
108 | }
109 |
110 | var found = {};
111 | // TODO: add query parameters to last item on stack
112 | found.index = stack.length - 1;
113 | for(var j=0; j < stack.length; j++) {
114 | stack[j].key = stack[j].routePath;
115 | }
116 | found.routes = stack;
117 | return found;
118 | }
119 | };
120 |
121 | export default Router;
122 |
--------------------------------------------------------------------------------
/App/Navigation/Routes.js:
--------------------------------------------------------------------------------
1 | import Router from '../Navigation/Router';
2 |
3 | import Locale from '../Locale';
4 |
5 | var i18n = Locale.key('Routes', {
6 | login: 'Log in',
7 | signup: 'Sign Up',
8 | settings: 'Settings',
9 | new_post: 'New Post',
10 | cancel: 'Cancel',
11 | account: 'Me'
12 | });
13 |
14 | var Routes = {
15 |
16 | LogIn: function() {
17 | return {
18 | component: require('../Screens/LogIn').default,
19 | title: i18n.t('login')
20 | };
21 | },
22 |
23 | SignUp: function() {
24 | return {
25 | component: require('../Screens/SignUp').default,
26 | title: i18n.t('signup')
27 | };
28 | },
29 |
30 | PostList: function(username) {
31 | return {
32 | component: require('../Screens/PostList').default,
33 | title: '', // set to name
34 | passProps: {
35 | username: username
36 | }
37 | };
38 | },
39 |
40 | FollowList: function(username) {
41 | return {
42 | component: require('../Screens/FollowList').default,
43 | title: '', // set to name
44 | passProps: {
45 | username: username
46 | }
47 | };
48 | },
49 |
50 | Settings: function() {
51 | return {
52 | component: require('../Screens/Settings').default,
53 | title: i18n.t('settings')
54 | };
55 | },
56 |
57 | CreatePost: function() {
58 | return {
59 | component: require('../Screens/CreatePost').default,
60 | title: i18n.t('new_post'),
61 | navBack: {
62 | label: i18n.t('cancel')
63 | }
64 | };
65 | },
66 | };
67 |
68 |
69 | var listRoute = function(route, defaultRoute) {
70 | var username = route.passProps ? route.passProps.username : null;
71 | route.parse = function(path) {
72 | switch(path) {
73 | case '_post':
74 | return Routes.CreatePost();
75 | case '_settings':
76 | // only on 'Dashboard'
77 | if(username) return null;
78 | return Routes.Settings();
79 | default:
80 | if (!defaultRoute) return null;
81 | return defaultRoute(path);
82 | }
83 | }
84 |
85 | if(!route.navRight) {
86 | route.navRight = {
87 | subPath: '_post',
88 | label: '+' // TODO: icon font
89 | };
90 | }
91 |
92 | if(!route.navLeft && !username) {
93 | route.navLeft = {
94 | subPath: '_settings',
95 | label: i18n.t('account') // TODO: icon font
96 | };
97 | }
98 | return route;
99 | };
100 |
101 | var userRoute = function(username) {
102 | var route = {}
103 | route._notAddressable = true;
104 | route._routerAppend = 'posts';
105 |
106 | route.parse = function(path) {
107 | switch(path) {
108 | case 'posts':
109 | return listRoute(Routes.PostList(username), function(postId) {
110 | // TOOD: show post
111 | return null;
112 | });
113 | case 'follows':
114 | return listRoute(Routes.FollowList(username), function(follow) {
115 | // it's a user
116 | return userRoute(follow);
117 | });
118 | default:
119 | return null;
120 | };
121 | };
122 | return route;
123 | };
124 |
125 | var LoggedIn = {
126 | parse: function(host) {
127 | switch (host) {
128 | case 'dashboard':
129 | return userRoute(null);
130 | default:
131 | return null;
132 | }
133 | }
134 | };
135 |
136 | var LoggedOut = {
137 | parse: function(host) {
138 | switch (host) {
139 | case 'signup':
140 | return Routes.SignUp();
141 | case 'login':
142 | return Routes.LogIn();
143 | default:
144 | return null;
145 | }
146 | }
147 | };
148 |
149 | export default {
150 | parse: function(str, loggedIn, defaulted) {
151 | var parent = loggedIn ? LoggedIn : LoggedOut;
152 | var found = Router.parse(str, parent, defaulted);
153 | if (!found && defaulted) {
154 | if (loggedIn) {
155 | found = this.parse('dashboard', true, false);
156 | }
157 | else {
158 | found = this.parse('signup', false, false);
159 | }
160 | }
161 | return found;
162 | }
163 | };
164 |
--------------------------------------------------------------------------------
/server/models.js:
--------------------------------------------------------------------------------
1 | // in memory database for testing
2 |
3 | var _postsCount = 0;
4 | var Post = function(user, content) {
5 | this.id = ++_postsCount;
6 | this.user_id = user.id;
7 | this.username = user.username;
8 | this.user = user;
9 | this.content = content;
10 | };
11 |
12 | Post.create = function(user, content) {
13 | var post = new Post(user, content);
14 | return post;
15 | };
16 |
17 |
18 | var _followCount = 0;
19 | var Follow = function(user, other) {
20 | this.id = ++_followCount;
21 | this.user_id = user.id;
22 | this.user = user;
23 | this.follow_id = other.id;
24 | this.username = other.username;
25 | this.folows = other;
26 | };
27 |
28 | Follow.create = function(user, other) {
29 | var follow = new Follow(user, other);
30 | return follow;
31 | };
32 |
33 |
34 | var _users = {} // id => user object
35 | var _usersCount = 0;
36 | var User = function(username) {
37 | this.id = ++_usersCount;
38 | this.username = username;
39 | this.password = null;
40 | this.posts = [];
41 | this.follows = [];
42 | };
43 |
44 | User.findByUsername = function(username) {
45 | for(var id in _users) {
46 | if (_users[id].username === username) {
47 | return _users[id];
48 | }
49 | }
50 | return null;
51 | };
52 |
53 | User.findByToken = function(token) {
54 | for(var id in _users) {
55 | if (_users[id].token === token) {
56 | return _users[id];
57 | }
58 | }
59 | return null;
60 | };
61 |
62 | User.create = function(username, password, token) {
63 | var user = new User(username);
64 | user.password = password;
65 | user.token = token || Math.random().toString(36).substr(2);
66 | _users[user.id] = user;
67 | return user;
68 | };
69 |
70 | User.prototype.authenticate = function(password) {
71 | return this.password === password;
72 | };
73 |
74 | User.prototype.addPost = function(content) {
75 | var post = Post.create(this, content);
76 | this.posts.unshift(post);
77 | return post;
78 | };
79 |
80 | User.prototype.getPosts = function() {
81 | return this.posts;
82 | };
83 |
84 | User.prototype.addFollow = function(other) {
85 | var follow = Follow.create(this, other);
86 | this.follows.unshift(follow);
87 | return follow;
88 | };
89 |
90 | User.prototype.getFollows = function() {
91 | return this.follows;
92 | };
93 |
94 | // Seed some users
95 | var bleonard = User.create('bleonard', 'sample', 'qwertyuiopasdfghjkl');
96 | var jrichardlai = User.create('jrichardlai', 'sample', 'poiuytrewqlkjhgfdsa');
97 | var taskrabbit = User.create('taskrabbit', 'sample', 'zxcvbnmqwertlkjhg');
98 |
99 | bleonard.addPost("When you have a use case and wish an app existed. Then you remember you made it 7 years ago. #boston");
100 | bleonard.addPost("I keep getting a facebook notification that asks if I know Nathan Cron.\nIt seems like every few days around this time #destiny");
101 | bleonard.addPost("Learning CSS...\nDaughter: Put it in the middle between the top and bottom.\nDad: (googles again) Are you sure?");
102 | bleonard.addPost("#reactjs #flux child components are like teenagers. they don't tell parents what they are up to. parent has to hear later from dispatcher.");
103 |
104 | jrichardlai.addPost("The problem with Rails today is that 1/2 the people are afraid Rails is turning into Java and the other 1/2 are trying to turn it into Java");
105 | jrichardlai.addPost("Thanks @TaskRabbit to allow my party of 10 people to eat at House of Prime Ribs with 0 minutes wait :)! Yummy! #TaskTuesday");
106 |
107 | taskrabbit.addPost("When you're a #NewParent tasks can really pile up. We can help! Check out our Task of the Week http://tinyurl.com/pxh88f8");
108 | taskrabbit.addPost("@TaskRabbit CEO @leahbusque talks about confidence necessary to propel your idea forward #DF15WomenLead #df15");
109 | taskrabbit.addPost("Happiness is the sound of someone else building your IKEA furniture. #taskrabbit");
110 |
111 | bleonard.addFollow(jrichardlai);
112 | bleonard.addFollow(taskrabbit);
113 |
114 | jrichardlai.addFollow(bleonard);
115 | jrichardlai.addFollow(taskrabbit);
116 |
117 | taskrabbit.addFollow(bleonard);
118 |
119 | exports.User = User;
120 |
--------------------------------------------------------------------------------