diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4143e75 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["env"], + "plugins": ["transform-async-to-generator", "transform-object-rest-spread"] +} diff --git a/.gitignore b/.gitignore index 321c7bf..0a45abc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ xcuserdata/ .idea .vscode +javac-services.0.log* +dist/ diff --git a/README.md b/README.md index 46f9b2b..30427a1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ ## react-native-oauth -The `react-native-oauth` library provides an interface to OAuth 1.0 and OAuth 2.0 providers, such as [Twitter](http://twitter.com) and [Facebook](http://facebook.com) to React native. +The `react-native-oauth` library provides an interface to OAuth 1.0 and OAuth 2.0 providers with support for the following providers for React Native apps: + +* Twitter +* Facebook +* Google +* Github +* Slack ## TL;DR; This library cuts out the muck of dealing with the [OAuth 1.0](https://tools.ietf.org/html/rfc5849) and [OAuth 2.0](http://oauth.net/2/) protocols in react-native apps. The API is incredibly simple and straight-forward and is intended on getting you up and running quickly with OAuth providers (such as Facebook, Github, Twitter, etc). ```javascript +import OAuthManager from 'react-native-oauth'; + const manager = new OAuthManager('firestackexample') manager.configure({ twitter: { @@ -26,11 +34,18 @@ manager.authorize('google', {scopes: 'profile email'}) .catch(err => console.log('There was an error')); ``` +### Help + +Due to other time contraints, I cannot continue to work on react-native-oauth for the time it deserves. If you're interested in supporting this library, please help! It's a widely used library and I'd love to continue supporting it. Looking for maintainers! + ## Features * Isolates the OAuth experience to a few simple methods. * Atomatically stores the tokens for later retrieval -* Works with many providers and relatively simple to add a new provider +* Works with many providers and simple to add new providers +* Works on both Android and iOS +* Makes calling API methods a snap +* Integrates seamlessly with Firestack (but can be used without it) ## Installation @@ -44,18 +59,31 @@ As we are integrating with react-native, we have a little more setup to integrat ### iOS setup +**Important**: This will _not_ work if you do not complete all the steps: + +- [ ] Link the `RCTLinkingManager` project +- [ ] Update your `AppDelegate.h` file +- [ ] Add KeychainSharing in your app +- [ ] Link the `react-native-oauth` project with your application (`react-native link`) +- [ ] Register a URL type of your application (Info tab -- see below) + #### RCTLinkingManager -Since `react-native-oauth` depends upon the `RCTLinkingManager` (from react-native core), we'll need to make sure we link this in our app. +Since `react-native-oauth` depends upon the `RCTLinkingManager` (from react-native core), we'll need to make sure we link this in our app. In your app, add the following line to your `HEADER SEARCH PATHS`: ``` -$(SRCROOT)/../node_modules/react-native/Libraries/LinkingiOS +$(SRCROOT)/../node_modules/react-native-oauth/ios/OAuthManager +$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS ``` ![](./resources/header-search-paths.png) +Next, navigate to the neighboring "Build Phases" section of project settings, find the "Link Binary with Library" drop down, expand it, and click the "+" to add _libOAuthManager.a_ to the list. + +Make sure to Update your `AppDelegate.m` as below, otherwise it will _not_ work. + #### Automatically with [rnpm](https://github.com/rnpm/rnpm) To automatically link our `react-native-oauth` client to our application, use the `rnpm` tool. [rnpm](https://github.com/rnpm/rnpm) is a React Native package manager which can help to automate the process of linking package environments. @@ -64,19 +92,19 @@ To automatically link our `react-native-oauth` client to our application, use th react-native link react-native-oauth ``` -Note: due to some restrictions on iOS, this module requires you to install cocoapods. The process has been semi-automated through using the above `react-native link` command. +Note: due to some restrictions on iOS, this module requires you to install cocoapods. The process has been semi-automated through using the above `react-native link` command. -Once you have linked this library, run the following command in the root directory: +Once you have linked this library, run the following command in the root directory: ``` (cd ios && pod install) ``` -Finally, open the created `.xcworkspace` in the `ios/` directory (**NOT THE `.xproj` file**) when it's complete. +Open in xcode the created `.xcworkspace` in the `ios/` directory (**NOT THE `.xproj` file**) when it's complete. -### Android setup +When working on iOS 10, we'll need to enable _Keychain Sharing Entitlement_ in _Capabilities_ of the build target of `io.fullstack.oauth.AUTH_MANAGER`. -Coming soon (looking for contributors). +![](./resources/capabilities.png) ## Handle deep linking loading @@ -110,13 +138,13 @@ In addition, we'll need to set up the handlers within the iOS app. Add the follo NSURL *jsCodeLocation; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; - + // other existing setup here - + // ADD THIS LINE SOMEWHERE IN THIS FUNCTION [OAuthManager setupOAuthHandler:application]; // ... - + [self.window makeKeyAndVisible]; return YES; } @@ -126,16 +154,49 @@ When our app loads up with a request that is coming back from OAuthManager _and_ ### Adding URL schemes -In order for our app to load through these callbacks, we need to tell our iOS app that we want to load them. In order to do that, we'll have to create some URL schemes to register our app. Some providers require specific schemes (mentioned later). +In order for our app to load through these callbacks, we need to tell our iOS app that we want to load them. In order to do that, we'll have to create some URL schemes to register our app. Some providers require specific schemes (mentioned later). These URL schemes can be added by navigating to to the `info` panel of our app in Xcode (see screenshot). ![](./resources/info-panel.png) -Let's add the appropriate one for our provider. For instance, to set up twitter, add the app name as a URL scheme in the URL scheme box. +Let's add the appropriate one for our provider. For instance, to set up twitter, add the app name as a URL scheme in the URL scheme box. ![](./resources/url-schemes.png) +### Android setup + +After we link `react-native-oauth` to our application, we're ready to go. Android integration is much simpler, thanks to the in-app browser ability for our apps. `react-native-oauth` handles this for you. + +One note, *all* of the callback urls follow the scheme: `http://localhost/[provider_name]`. Make sure this is set as a configuration for each provider below (documented in the provider setup sections). + +Make sure you add the following to your `android/build.gradle` file: + +``` +maven { url "https://jitpack.io" } +``` + +For instance, an example `android/build.gradle` file would look like this: + +``` +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + // ... +} + +allprojects { + repositories { + mavenLocal() + jcenter() + maven { url "https://jitpack.io" } // <~ ADD THIS LINE + maven { + url "$rootDir/../node_modules/react-native/android" + } + } +} +``` + ## Creating the manager In our JS, we can create the manager by instantiating a new instance of it using the `new` method and passing it the name of our app: @@ -165,7 +226,7 @@ const config = { }, facebook: { client_id: 'YOUR_CLIENT_ID', - client_Secret: 'YOUR_CLIENT_SECRET' + client_secret: 'YOUR_CLIENT_SECRET' } } // Create the manager @@ -180,9 +241,9 @@ The `consumer_key` and `consumer_secret` values are _generally_ provided by the The following list are the providers we've implemented thus far in `react-native-oauth` and the _required_ keys to pass when configuring the provider: -#### Twitter +#### Twitter (iOS/Android) -To authenticate against twitter, we need to register a Twitter application. Register your twitter application (or create a new one at [apps.twitter.com](https://apps.twitter.com)). +To authenticate against twitter, we need to register a Twitter application. Register your twitter application (or create a new one at [apps.twitter.com](https://apps.twitter.com)). ![](./resources/twitter/app.png) @@ -190,6 +251,10 @@ Once you have created one, navigate to the application and find the `Keys and Ac ![](./resources/twitter/api-key.png) +For the authentication to work properly, you need to set the Callback URL. It doesn't matter what you choose as long as its a valid url. + +![](./resources/twitter/callback-url.png) + Twitter's URL scheme needs to be the app name (that we pass into the constructor method). Make sure we have one registered in Xcode as the same name: ![](./resources/twitter/url-scheme.png) @@ -205,9 +270,9 @@ const config = { } ``` -#### Facebook +#### Facebook (iOS/Android) -To add facebook authentication, we'll need to have a Facebook app. To create one (or use an existing one), navigate to [developers.facebook.com/](https://developers.facebook.com/). +To add facebook authentication, we'll need to have a Facebook app. To create one (or use an existing one), navigate to [developers.facebook.com/](https://developers.facebook.com/). ![](./resources/facebook/dev.facebook.png) @@ -219,13 +284,17 @@ Before we leave the Facebook settings, we need to tell Facebook we have a new re `fb{YOUR_APP_ID}` -For instance, my app ID in this example is: `1745641015707619`. In the `Bundle ID` field, I have added `fb1745641015707619`. +For instance, my app ID in this example is: `1745641015707619`. In the `Bundle ID` field, I have added `fb1745641015707619`. + +![](./resources/facebook/redirect-url.png) + +For Android, you will also need to set the redirect url to `http://localhost/facebook` in the Facebook Login settings. ![](./resources/facebook/redirect-url.png) We'll need to create a new URL scheme for Facebook and (this is a weird bug on the Facebook side) the facebook redirect URL scheme _must be the first one_ in the list. The URL scheme needs to be the same id as the `Bundle ID` copied from above: -![](./resources/facebook/url-scheme.png) +![](./resources/facebook/facebook-redirect.png) Back in our application, add the App ID and the secret as: @@ -238,15 +307,15 @@ const config = { } ``` -#### Google +#### Google (iOS) -To add Google auth to our application, first we'll need to create a google application. Create or use an existing one by heading to the [developers.google.com/](https://developers.google.com/) page (or the console directly at [https://console.developers.google.com](https://console.developers.google.com)). +To add Google auth to our application, first we'll need to create a google application. Create or use an existing one by heading to the [developers.google.com/](https://developers.google.com/) page (or the console directly at [https://console.developers.google.com](https://console.developers.google.com)). ![](./resources/google/auth-page.png) We need to enable the `Identity Toolkit API` API. Click on `Enable API` and add this api to your app. Once it's enabled, we'll need to collect our credentials. -Navigate to the `Credentials` tab and create a new credential. Create a web API credential. Take note of the client id and the URL scheme. In addition, make sure to set the bundle ID as the bundle id in our application in Xcode: +Navigate to the `Credentials` tab and create a new credential. Create an **iOS API credential**. Take note of the `client_id` and the `iOS URL scheme`. In addition, make sure to set the bundle ID as the bundle id in our application in Xcode: ![](./resources/google/creds.png) @@ -260,14 +329,77 @@ Finally, add the `client_id` credential as the id from the url page as well as t const config = { google: { callback_url: `[IOS SCHEME]:/google`, + client_id: 'YOUR_CLIENT_ID' + } +} +``` + +#### Google (Android) + +To set up Google on Android, follow the same steps as before, except this time instead of creating an iOS API, create a **web api credential**. Make sure to add the **redirect url** at the bottom (it defaults to `http://localhost/google`): + +![](./resources/google/android-creds.png) + +When creating an Android-specific configuration, create a file called `config/development.android.js`. React Native will load it instead of the `config/development.js` file automatically on Android. + +#### Github (iOS/Android) + +Adding Github auth to our application is pretty simple as well. We'll need to create a web application on the github apps page, which can be found at [https://github.com/settings/developers](https://github.com/settings/developers). Create one, making sure to add _two_ apps (one for iOS and one for Android) with the callback urls as: + +* ios: [app_name]:// oauth (for example: `firestackexample://oauth`) +* android: http://localhost/github + +Take note of the `client_id` and `client_secret` + +![](./resources/github/apps.png) + +The `iOS URL Scheme` is the same as the twitter version, which means we'll just add the app name as a URL scheme (i.e. `firestackexample`). + +Add the `client_id` and `client_secret` credentials to your configuration object: + +```javascript +const config = { + github: { client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET' } } ``` -## Authenticating against our providers +## Slack + +We'll need to create an app first. Head to the slack developer docs at [https://slack.com/developers](https://slack.com/developers). + +![](./resources/slack/dev.png) + +Click on the Getting Started button: + +![](./resources/slack/getting_started.png) + From here, find the `create an app` link: + +![](./resources/slack/create.png) + + Take note of the `client_id` and the `client_secret`. We'll place these in our configuration object just like so: + +```javascript +const config = { + slack: { + client_id: 'YOUR_CLIENT_ID', + client_secret: 'YOUR_CLIENT_SECRET' + } +} +``` + +Lastly, Slack requires us to add a redirect_url. + +For **iOS**: the callback_url pattern is `${app_name}://oauth`, so make sure to add your redirect_url where it asks for them before starting to work with the API. + +for **Android**: the `callback_url` pattern is `http://localhost/slack`. Be sure to add this to your list of redirect urls. + +![](./resources/slack/redirect.png) + +## Authenticating against our providers We can use the manager in our app using the `authorize()` method on the manager. @@ -295,7 +427,7 @@ The `resp` object is set as follows: authorized: true, (boolean) uuid: "UUID", (user UUID) credentials: { - access_token: "access token", + access_token: "access token", refresh_token: "refresh token", type: 1 } @@ -303,11 +435,21 @@ The `resp` object is set as follows: } ``` +The second argument accepts an object where we can ask for additional scopes, override default values, etc. + +```javascript +manager.authorize('google', {scopes: 'email,profile'}) + .then(resp => console.log(resp)) + .catch(err => console.log(err)); +``` + +* Scopes are a list of scopes _comma separated_ as a string. + ## Calling a provider's API We can use OAuthManager to make requests to endpoints from our providers as well. For instance, let's say we want to get a user's time line from twitter. We would make the request to the url [https://api.twitter.com/1.1/statuses/user_timeline.json](https://api.twitter.com/1.1/statuses/user_timeline.json) -If our user has been authorized for thi request, we can execute the request using the credentials stored by the OAuthManager. +If our user has been authorized for thi request, we can execute the request using the credentials stored by the OAuthManager. The `makeRequest()` method accepts 3 parameters: @@ -318,7 +460,7 @@ The `makeRequest()` method accepts 3 parameters: We can pass a list of options for our request with the last argument. The keys OAuthManager recognizes are: 1. `params` - The query parameters -2. `method` - The http method to make the request with. +2. `method` - The http method to make the request with. Available HTTP methods: * get @@ -332,23 +474,48 @@ Available HTTP methods: ```javascript const userTimelineUrl = 'https://api.twitter.com/1.1/statuses/user_timeline.json'; -authManager +manager .makeRequest('twitter', userTimelineUrl) .then(resp => { console.log('Data ->', resp.data); }); ``` +"me" represents the authenticated user, in any call to the Google+ API + +```javascript +const googleUrl = 'https://www.googleapis.com/plus/v1/people/me'; +manager + .makeRequest('google', googleUrl) + .then(resp => { + console.log('Data -> ', resp.data); + }); + +``` + It's possible to use just the path as well. For instance, making a request with Facebook at the `/me` endpoint can be: ```javascript -authManager +manager .makeRequest('facebook', '/me') .then(resp => { console.log('Data ->', resp.data); }); ``` +To add more data to our requests, we can pass a third argument: + +```javascript +manager + .makeRequest('facebook', '/me', { + headers: { 'Content-Type': 'application/java' }, + params: { email: 'me+rocks@ari.io' } + }) + .then(resp => { + console.log('Data ->', resp.data); + }); +``` + ## Getting authorized accounts Since OAuthManager handles storing user accounts, we can query it to see which accounts have already been authorized or not using `savedAccounts()`: @@ -367,7 +534,22 @@ We can `deauthorize()` our user's from using the provider by calling the `deauth 1. The `provider` we want to remove from our user credentials. ```javascript -authManager.deauthorize('twitter'); +manager.deauthorize('twitter'); +``` + +## Adding your own providers + +To add your own providers you can use the `addProvider()` method and fill in your provider details: + +```javascript +manager.addProvider({ + 'name_of_provider': { + auth_version: '2.0', + authorize_url: 'https://provider.dev/oauth', + access_token_url: 'https://provider.dev/oauth/token', + callback_url: ({app_name}) => `${app_name}://oauth`, + } +}); ``` ## Contributing @@ -383,9 +565,8 @@ ___ ## TODOS: -* [ ] Handle reauthenticating tokens (automatically?) * [x] Simplify method of adding providers -* [x] Complete [facebook](https://developers.facebook.com/docs/facebook-login) support -* [ ] Add [github](https://developer.github.com/v3/oauth/) support -* [x] Add [Google]() support -* [ ] Add Android support +* [x] Add github(https://developer.github.com/v3/oauth/) support +* [x] Add Google support +* [x] Add Facebook support +* [x] Add Android support diff --git a/android/android.iml b/android/android.iml index a162bd1..0ee1481 100644 --- a/android/android.iml +++ b/android/android.iml @@ -1,5 +1,5 @@ - + @@ -8,14 +8,138 @@ + - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index bef5612..b9ff010 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,22 +44,7 @@ allprojects { dependencies { compile 'com.facebook.react:react-native:+' - // compile 'com.wu-man:android-oauth-client:0.4.5@aar' - // compile 'com.google.android.gms:play-services-base:+' - // compile 'com.android.volley:volley:+' - compile 'com.github.scribejava:scribejava-apis:+' + compile 'com.github.scribejava:scribejava-apis:3.4.1' compile 'com.github.delight-im:Android-AdvancedWebView:v3.0.0' compile 'com.google.code.gson:gson:+' - - // compile 'com.google.api-client:google-api-client-android:1.19.0' exclude module: 'httpclient' - // compile 'com.google.http-client:google-http-client-gson:1.19.0' exclude module: 'httpclient' - - // compile('com.google.api-client:google-api-client-android:1.15.0-rc') { - // exclude group: 'com.google.android.google-play-services', module: 'google-play-services' - // exclude group: 'junit', module: 'junit' - // exclude group: 'com.google.android', module: 'android' - // } - // // compile 'com.google.oauth-client:google-oauth-client-java6:1.15.0-rc' - // compile 'com.google.apis:google-api-services-oauth2:v1-rev129-1.22.0' - // compile 'com.google.http-client:google-http-client-jackson2:1.22.0' } diff --git a/android/local.properties b/android/local.properties index d9dc6da..ffb1ddd 100644 --- a/android/local.properties +++ b/android/local.properties @@ -7,6 +7,5 @@ # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. -#Fri Sep 02 11:41:31 PDT 2016 -sdk.dir=/usr/local/opt/android-sdk -ndk.dir=/usr/local/opt/android-ndk \ No newline at end of file +#Tue Apr 11 11:36:49 IST 2017 +sdk.dir=/Users/divyanshunegi/Downloads/adt-bundle-mac-x86_64-20140321/sdk diff --git a/android/src/main/java/io/fullstack/oauth/OAuthManagerDialogFragment.java b/android/src/main/java/io/fullstack/oauth/OAuthManagerDialogFragment.java index f0feace..12202bb 100644 --- a/android/src/main/java/io/fullstack/oauth/OAuthManagerDialogFragment.java +++ b/android/src/main/java/io/fullstack/oauth/OAuthManagerDialogFragment.java @@ -1,93 +1,134 @@ package io.fullstack.oauth; -import im.delight.android.webview.AdvancedWebView; -import android.app.Dialog; - -import android.net.Uri; -import java.util.Set; -import java.net.URL; -import java.net.MalformedURLException; -import android.text.TextUtils; import android.annotation.SuppressLint; -import android.widget.LinearLayout; -import android.view.Gravity; - +import android.app.Dialog; import android.app.DialogFragment; -import android.content.DialogInterface; -import android.widget.FrameLayout; - -import android.webkit.WebView; -import android.view.View; -import android.webkit.WebViewClient; +import android.content.Context; import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.content.Context; -import android.util.DisplayMetrics; +import android.view.Window; +import android.view.WindowManager; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import com.facebook.react.bridge.ReactContext; import com.github.scribejava.core.model.OAuth1AccessToken; -import com.github.scribejava.core.model.OAuth1RequestToken; -import android.util.Log; -import android.graphics.Bitmap; -import android.os.Bundle; -import android.app.Fragment; -import java.io.IOException; + +import java.lang.reflect.Method; +import java.util.Set; + +import im.delight.android.webview.AdvancedWebView; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class OAuthManagerDialogFragment extends DialogFragment implements AdvancedWebView.Listener { private static final int WEBVIEW_TAG = 100001; private static final int WIDGET_TAG = 100002; - private static final String TAG = "OAuthManagerDialogFragment"; + private static final String TAG = "OauthFragment"; private OAuthManagerFragmentController mController; + private ReactContext mReactContext; private AdvancedWebView mWebView; + private ProgressBar mProgressBar; public static final OAuthManagerDialogFragment newInstance( + final ReactContext reactContext, OAuthManagerFragmentController controller ) { Bundle args = new Bundle(); OAuthManagerDialogFragment frag = - new OAuthManagerDialogFragment(controller); - + new OAuthManagerDialogFragment(reactContext, controller); return frag; } public OAuthManagerDialogFragment( + final ReactContext reactContext, OAuthManagerFragmentController controller ) { this.mController = controller; + this.mReactContext = reactContext; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + Dialog dialog = getDialog(); + if (dialog != null) { + dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // View rootView = inflater.inflate(R.id.primary, container, false); - final Context context = inflater.getContext(); - // DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - // final int DIALOG_HEIGHT = (int) Math.min(0.8f * metrics.heightPixels, 1024); + final Context context = mReactContext; + LayoutParams rootViewLayoutParams = this.getFullscreenLayoutParams(context); + + RelativeLayout rootView = new RelativeLayout(context); + + mProgressBar = new ProgressBar(context); + RelativeLayout.LayoutParams progressParams = new RelativeLayout.LayoutParams(convertDpToPixel(50f,context),convertDpToPixel(50f,context)); + progressParams.addRule(RelativeLayout.CENTER_IN_PARENT); + mProgressBar.setLayoutParams(progressParams); + mProgressBar.setIndeterminate(true); - FrameLayout rootView = new FrameLayout(context); getDialog().setCanceledOnTouchOutside(true); - rootView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + rootView.setLayoutParams(rootViewLayoutParams); // mWebView = (AdvancedWebView) rootView.findViewById(R.id.webview); Log.d(TAG, "Creating webview"); mWebView = new AdvancedWebView(context); - mWebView.setId(WEBVIEW_TAG); +// mWebView.setId(WEBVIEW_TAG); mWebView.setListener(this, this); mWebView.setVisibility(View.VISIBLE); + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.getSettings().setDomStorageEnabled(true); - rootView.addView(mWebView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); - - LinearLayout pframe = new LinearLayout(context); - pframe.setId(WIDGET_TAG); - pframe.setOrientation(LinearLayout.VERTICAL); - pframe.setVisibility(View.GONE); - pframe.setGravity(Gravity.CENTER); - rootView.addView(pframe, - new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + LayoutParams layoutParams = this.getFullscreenLayoutParams(context); + //new LayoutParams( + // LayoutParams.FILL_PARENT, + // DIALOG_HEIGHT + // ); + // mWebView.setLayoutParams(layoutParams); + + rootView.addView(mWebView, layoutParams); + rootView.addView(mProgressBar,progressParams); + + // LinearLayout pframe = new LinearLayout(context); + // pframe.setId(WIDGET_TAG); + // pframe.setOrientation(LinearLayout.VERTICAL); + // pframe.setVisibility(View.GONE); + // pframe.setGravity(Gravity.CENTER); + // pframe.setLayoutParams(layoutParams); + + // rootView.addView(pframe, layoutParams); this.setupWebView(mWebView); mController.getRequestTokenUrlAndLoad(mWebView); @@ -96,15 +137,58 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa return rootView; } + private LayoutParams getFullscreenLayoutParams(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + // DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + Display display = wm.getDefaultDisplay(); + int realWidth; + int realHeight; + + if (Build.VERSION.SDK_INT >= 17){ + //new pleasant way to get real metrics + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + realWidth = realMetrics.widthPixels; + realHeight = realMetrics.heightPixels; + + } else if (Build.VERSION.SDK_INT >= 14) { + //reflection for this weird in-between time + try { + Method mGetRawH = Display.class.getMethod("getRawHeight"); + Method mGetRawW = Display.class.getMethod("getRawWidth"); + realWidth = (Integer) mGetRawW.invoke(display); + realHeight = (Integer) mGetRawH.invoke(display); + } catch (Exception e) { + //this may not be 100% accurate, but it's all we've got + realWidth = display.getWidth(); + realHeight = display.getHeight(); + Log.e("Display Info", "Couldn't use reflection to get the real display metrics."); + } + + } else { + //This should be close, as lower API devices should not have window navigation bars + realWidth = display.getWidth(); + realHeight = display.getHeight(); + } + + return new LayoutParams(realWidth, realHeight); + } + + private void setupWebView(AdvancedWebView webView) { webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - interceptUrl(view, url, true); - return true; + return interceptUrl(view, url, true); } - @Override + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + mProgressBar.setVisibility(View.GONE); + } + + @Override public void onReceivedError(WebView view, int code, String desc, String failingUrl) { Log.i(TAG, "onReceivedError: " + failingUrl); super.onReceivedError(view, code, desc, failingUrl); @@ -112,9 +196,18 @@ public void onReceivedError(WebView view, int code, String desc, String failingU } private boolean interceptUrl(WebView view, String url, boolean loadUrl) { + Log.i(TAG, "interceptUrl called with url: " + url); + + // url would be http://localhost/twitter?denied=xxx when it's canceled + Pattern p = Pattern.compile("\\S*denied\\S*"); + Matcher m = p.matcher(url); + if(m.matches()){ + Log.i(TAG, "authentication is canceled"); + return false; + } + if (isCallbackUri(url, mController.getCallbackUrl())) { mController.getAccessToken(mWebView, url); - return true; } @@ -131,18 +224,12 @@ public void setComplete(final OAuth1AccessToken accessToken) { Log.d(TAG, "Completed: " + accessToken); } - @Override - public void onStart() { - super.onStart(); - - Log.d(TAG, "onStart for DialogFragment"); - } - @Override - public void onDismiss(final DialogInterface dialog) { - super.onDismiss(dialog); - Log.d(TAG, "Dismissing dialog"); - } +// @Override +// public void onDismiss(final DialogInterface dialog) { +// super.onDismiss(dialog); +// Log.d(TAG, "Dismissing dialog"); +// } // @Override @@ -192,11 +279,13 @@ public void onPageStarted(String url, Bitmap favicon) { @Override public void onPageFinished(String url) { Log.d(TAG, "onPageFinished: " + url); + // mController.onComplete(url); } @Override public void onPageError(int errorCode, String description, String failingUrl) { Log.e(TAG, "onPageError: " + failingUrl); + mController.onError(errorCode, description, failingUrl); } @Override @@ -241,4 +330,11 @@ static boolean isCallbackUri(String uri, String callbackUrl) { if (!TextUtils.isEmpty(frag) && !TextUtils.equals(frag, u.getFragment())) return false; return true; } -} \ No newline at end of file + + public static int convertDpToPixel(float dp, Context context){ + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + float px = dp * ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + return (int)px; + } +} diff --git a/android/src/main/java/io/fullstack/oauth/OAuthManagerFragmentController.java b/android/src/main/java/io/fullstack/oauth/OAuthManagerFragmentController.java index 615c839..5281699 100644 --- a/android/src/main/java/io/fullstack/oauth/OAuthManagerFragmentController.java +++ b/android/src/main/java/io/fullstack/oauth/OAuthManagerFragmentController.java @@ -1,34 +1,26 @@ package io.fullstack.oauth; -import android.app.Dialog; -import android.os.Bundle; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; -import android.os.AsyncTask; -import android.text.TextUtils; -import im.delight.android.webview.AdvancedWebView; - -import android.net.Uri; import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.app.Fragment; -import android.app.FragmentTransaction; -import java.util.HashMap; -import java.util.Map; -import java.io.IOException; - -import com.github.scribejava.core.model.OAuth1RequestToken; +import com.facebook.react.bridge.ReactContext; +import com.github.scribejava.core.exceptions.OAuthConnectionException; import com.github.scribejava.core.model.OAuth1AccessToken; +import com.github.scribejava.core.model.OAuth1RequestToken; import com.github.scribejava.core.model.OAuth2AccessToken; -import com.github.scribejava.core.model.Token; - -import com.github.scribejava.core.model.OAuthRequest; -import com.github.scribejava.core.oauth.OAuthService; import com.github.scribejava.core.oauth.OAuth10aService; import com.github.scribejava.core.oauth.OAuth20Service; -import com.github.scribejava.core.exceptions.OAuthConnectionException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import im.delight.android.webview.AdvancedWebView; // Credit where credit is due: // Mostly taken from @@ -40,12 +32,15 @@ public class OAuthManagerFragmentController { private final android.app.FragmentManager fragmentManager; private final Handler uiHandler; + private ReactContext context; + private String providerName; private String authVersion; private OAuth10aService oauth10aService; private OAuth20Service oauth20Service; private String callbackUrl; private OAuth1RequestToken oauth1RequestToken; private HashMap mCfg; + private AdvancedWebView mWebView; private Runnable onAccessToken; private OAuthManagerOnAccessTokenListener mListener; @@ -55,6 +50,7 @@ private void runOnMainThread(Runnable runnable) { } public OAuthManagerFragmentController( + final ReactContext mReactContext, android.app.FragmentManager fragmentManager, final String providerName, OAuth10aService oauthService, @@ -63,12 +59,15 @@ public OAuthManagerFragmentController( this.uiHandler = new Handler(Looper.getMainLooper()); this.fragmentManager = fragmentManager; + this.context = mReactContext; + this.providerName = providerName; this.authVersion = "1.0"; this.oauth10aService = oauthService; this.callbackUrl = callbackUrl; } public OAuthManagerFragmentController( + final ReactContext mReactContext, android.app.FragmentManager fragmentManager, final String providerName, OAuth20Service oauthService, @@ -77,6 +76,8 @@ public OAuthManagerFragmentController( this.uiHandler = new Handler(Looper.getMainLooper()); this.fragmentManager = fragmentManager; + this.context = mReactContext; + this.providerName = providerName; this.authVersion = "2.0"; this.oauth20Service = oauthService; this.callbackUrl = callbackUrl; @@ -107,7 +108,7 @@ public void run() { Log.d(TAG, "Creating new Fragment"); OAuthManagerDialogFragment frag = - OAuthManagerDialogFragment.newInstance(OAuthManagerFragmentController.this); + OAuthManagerDialogFragment.newInstance(context, OAuthManagerFragmentController.this); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.add(frag, TAG); @@ -141,20 +142,34 @@ public void loaded10aAccessToken(final OAuth1AccessToken accessToken) { Log.d(TAG, "Loaded access token in OAuthManagerFragmentController"); Log.d(TAG, "AccessToken: " + accessToken + " (raw: " + accessToken.getRawResponse() + ")"); + mWebView = null; this.dismissDialog(); mListener.onOAuth1AccessToken(accessToken); } public void loaded20AccessToken(final OAuth2AccessToken accessToken) { + mWebView = null; this.dismissDialog(); mListener.onOAuth2AccessToken(accessToken); } - public void onError() { + public void onComplete(String url) { + Log.d(TAG, "onComplete called in fragment controller " + url); + // if (mWebView != null) { + // this.getAccessToken(mWebView, url); + // } else { + // this.dismissDialog(); + // } + } + + public void onError(int errorCode, String description, String failingUrl) { + Log.e(TAG, "Error in OAuthManagerFragmentController: " + description); this.dismissDialog(); + mListener.onRequestTokenError(new Exception(description)); } public void getRequestTokenUrlAndLoad(AdvancedWebView webView) { + mWebView = webView; LoadRequestTokenTask task = new LoadRequestTokenTask(this, webView); task.execute(); } @@ -172,9 +187,15 @@ public void getAccessToken( task.execute(); } else if (authVersion.equals("2.0")) { String code = responseUri.getQueryParameter("code"); - Load2AccessTokenTask task = new Load2AccessTokenTask( - this, webView, code); - task.execute(); + Log.d(TAG, "Called getAccessToken with code: " + code + " at " + url); + if (code != null) { + Load2AccessTokenTask task = new Load2AccessTokenTask( + this, webView, code); + task.execute(); + } else { + this.dismissDialog(); + mListener.onRequestTokenError(new Exception("No token found")); + } } } @@ -226,7 +247,7 @@ protected String doInBackground(Void... params) { String authorizationUrl; if (mCfg.containsKey("authorization_url_params")) { - final HashMap additionalParams = new HashMap(); + final HashMap additionalParams = new HashMap(); additionalParams.put("access_type", "offline"); additionalParams.put("prompt", "consent"); @@ -265,7 +286,7 @@ protected void onPostExecute(final String url) { @Override public void run() { if (url == null) { - mCtrl.onError(); + mCtrl.onError(-1, "No url", ""); return; } if (authVersion.equals("1.0")) { @@ -315,7 +336,7 @@ protected void onPostExecute(final OAuth1AccessToken accessToken) { @Override public void run() { if (accessToken == null) { - mCtrl.onError(); + mCtrl.onError(-1, "No accessToken found", ""); return; } mCtrl.loaded10aAccessToken(accessToken); @@ -359,7 +380,7 @@ protected void onPostExecute(final OAuth2AccessToken accessToken) { @Override public void run() { if (accessToken == null) { - mCtrl.onError(); + mCtrl.onError(-1, "No accessToken found", ""); return; } mCtrl.loaded20AccessToken(accessToken); @@ -371,4 +392,4 @@ public void run() { public String getCallbackUrl() { return this.callbackUrl; } -} \ No newline at end of file +} diff --git a/android/src/main/java/io/fullstack/oauth/OAuthManagerModule.java b/android/src/main/java/io/fullstack/oauth/OAuthManagerModule.java index ec07739..4ac136f 100644 --- a/android/src/main/java/io/fullstack/oauth/OAuthManagerModule.java +++ b/android/src/main/java/io/fullstack/oauth/OAuthManagerModule.java @@ -1,53 +1,41 @@ package io.fullstack.oauth; -import android.util.Log; +import android.app.Activity; +import android.app.FragmentManager; import android.content.Context; -import android.net.Uri; -import android.os.Handler; -import android.content.SharedPreferences; - -import java.net.URL; -import java.net.MalformedURLException; - import android.support.annotation.Nullable; -import android.app.FragmentManager; -import android.support.v4.app.FragmentActivity; -import android.app.Activity; -import android.text.TextUtils; +import android.util.Log; -import java.io.IOException; -import java.util.Map; -import java.util.Set; -import java.util.HashMap; -import java.util.List; -import java.util.ArrayList; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.ReactContext; - -import com.github.scribejava.core.builder.api.BaseApi; -import com.github.scribejava.core.model.Verb; - -import com.github.scribejava.core.builder.ServiceBuilder; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableMap; import com.github.scribejava.core.model.OAuth1AccessToken; -import com.github.scribejava.core.model.OAuth1RequestToken; +import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Response; import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.OAuth10aService; - -import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.oauth.OAuth20Service; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + class ProviderNotConfiguredException extends Exception { public ProviderNotConfiguredException(String message) { super(message); @@ -68,7 +56,6 @@ class OAuthManagerModule extends ReactContextBaseJavaModule { public OAuthManagerModule(ReactApplicationContext reactContext) { super(reactContext); mReactContext = reactContext; - _credentialsStore = OAuthManagerStore.getOAuthManagerStore(mReactContext, TAG, Context.MODE_PRIVATE); Log.d(TAG, "New instance"); } @@ -90,6 +77,7 @@ public void configureProvider( String callbackUrlStr = params.getString("callback_url"); _callbackUrls.add(callbackUrlStr); + Log.d(TAG, "Added callback url " + callbackUrlStr + " for providler " + providerName); // Keep configuration map HashMap cfg = new HashMap(); @@ -122,44 +110,48 @@ public void authorize( { try { final OAuthManagerModule self = this; - HashMap cfg = this.getConfiguration(providerName); + final HashMap cfg = this.getConfiguration(providerName); final String authVersion = (String) cfg.get("auth_version"); - Activity activity = mReactContext.getCurrentActivity(); + Activity activity = this.getCurrentActivity(); FragmentManager fragmentManager = activity.getFragmentManager(); String callbackUrl = "http://localhost/" + providerName; OAuthManagerOnAccessTokenListener listener = new OAuthManagerOnAccessTokenListener() { - public void onRequestTokenError(final Exception ex) {} + public void onRequestTokenError(final Exception ex) { + Log.e(TAG, "Exception with request token: " + ex.getMessage()); + _credentialsStore.delete(providerName); + _credentialsStore.commit(); + } public void onOAuth1AccessToken(final OAuth1AccessToken accessToken) { _credentialsStore.store(providerName, accessToken); _credentialsStore.commit(); - WritableMap resp = self.accessTokenResponse(providerName, accessToken, authVersion); + WritableMap resp = self.accessTokenResponse(providerName, cfg, accessToken, authVersion); callback.invoke(null, resp); } public void onOAuth2AccessToken(final OAuth2AccessToken accessToken) { _credentialsStore.store(providerName, accessToken); _credentialsStore.commit(); - WritableMap resp = self.accessTokenResponse(providerName, accessToken, authVersion); + WritableMap resp = self.accessTokenResponse(providerName, cfg, accessToken, authVersion); callback.invoke(null, resp); } }; if (authVersion.equals("1.0")) { final OAuth10aService service = - OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, callbackUrl); + OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, params, callbackUrl); OAuthManagerFragmentController ctrl = - new OAuthManagerFragmentController(fragmentManager, providerName, service, callbackUrl); + new OAuthManagerFragmentController(mReactContext, fragmentManager, providerName, service, callbackUrl); ctrl.requestAuth(cfg, listener); } else if (authVersion.equals("2.0")) { final OAuth20Service service = - OAuthManagerProviders.getApiFor20Provider(providerName, cfg, callbackUrl); + OAuthManagerProviders.getApiFor20Provider(providerName, cfg, params, callbackUrl); OAuthManagerFragmentController ctrl = - new OAuthManagerFragmentController(fragmentManager, providerName, service, callbackUrl); + new OAuthManagerFragmentController(mReactContext, fragmentManager, providerName, service, callbackUrl); ctrl.requestAuth(cfg, listener); } else { @@ -223,22 +215,25 @@ public void makeRequest( httpVerb = Verb.TRACE; } else { httpVerb = Verb.GET; - } + } - OAuthRequest request; + ReadableMap requestParams = null; + if (params != null && params.hasKey("params")) { + requestParams = params.getMap("params"); + } + OAuthRequest request = oauthRequestWithParams(providerName, cfg, authVersion, httpVerb, url, requestParams); + if (authVersion.equals("1.0")) { final OAuth10aService service = - OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, null); + OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, requestParams, null); OAuth1AccessToken token = _credentialsStore.get(providerName, OAuth1AccessToken.class); - request = new OAuthRequest(httpVerb, url.toString(), service); service.signRequest(token, request); } else if (authVersion.equals("2.0")) { final OAuth20Service service = - OAuthManagerProviders.getApiFor20Provider(providerName, cfg, null); + OAuthManagerProviders.getApiFor20Provider(providerName, cfg, requestParams, null); OAuth2AccessToken token = _credentialsStore.get(providerName, OAuth2AccessToken.class); - request = new OAuthRequest(httpVerb, url.toString(), service); service.signRequest(token, request); } else { // Some kind of error here @@ -271,6 +266,55 @@ public void makeRequest( } } + private OAuthRequest oauthRequestWithParams( + final String providerName, + final HashMap cfg, + final String authVersion, + final Verb httpVerb, + final URL url, + @Nullable final ReadableMap params + ) throws Exception { + OAuthRequest request; + // OAuthConfig config; + + if (authVersion.equals("1.0")) { + // final OAuth10aService service = + // OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, null, null); + OAuth1AccessToken oa1token = _credentialsStore.get(providerName, OAuth1AccessToken.class); + request = OAuthManagerProviders.getRequestForProvider( + providerName, + httpVerb, + oa1token, + url, + cfg, + params); + + // config = service.getConfig(); + // request = new OAuthRequest(httpVerb, url.toString(), config); + } else if (authVersion.equals("2.0")) { + // final OAuth20Service service = + // OAuthManagerProviders.getApiFor20Provider(providerName, cfg, null, null); + // oa2token = _credentialsStore.get(providerName, OAuth2AccessToken.class); + + OAuth2AccessToken oa2token = _credentialsStore.get(providerName, OAuth2AccessToken.class); + request = OAuthManagerProviders.getRequestForProvider( + providerName, + httpVerb, + oa2token, + url, + cfg, + params); + + // config = service.getConfig(); + // request = new OAuthRequest(httpVerb, url.toString(), config); + } else { + Log.e(TAG, "Error in making request method"); + throw new Exception("Provider not handled yet"); + } + + return request; + } + @ReactMethod public void getSavedAccounts(final ReadableMap options, final Callback onComplete) { // Log.d(TAG, "getSavedAccounts"); @@ -295,7 +339,7 @@ public void getSavedAccount( throw new Exception("No token found"); } - WritableMap resp = this.accessTokenResponse(providerName, token, authVersion); + WritableMap resp = this.accessTokenResponse(providerName, cfg, token, authVersion); onComplete.invoke(null, resp); } else if (authVersion.equals("2.0")) { OAuth2AccessToken token = _credentialsStore.get(providerName, OAuth2AccessToken.class); @@ -303,7 +347,7 @@ public void getSavedAccount( if (token == null || token.equals("")) { throw new Exception("No token found"); } - WritableMap resp = this.accessTokenResponse(providerName, token, authVersion); + WritableMap resp = this.accessTokenResponse(providerName, cfg, token, authVersion); onComplete.invoke(null, resp); } else { @@ -351,19 +395,54 @@ private HashMap getConfiguration( private WritableMap accessTokenResponse( final String providerName, + final HashMap cfg, final OAuth1AccessToken accessToken, final String oauthVersion ) { WritableMap resp = Arguments.createMap(); WritableMap response = Arguments.createMap(); + Log.d(TAG, "Credential raw response: " + accessToken.getRawResponse()); + + /* Some things return as JSON, some as x-www-form-urlencoded (querystring) */ + + Map accessTokenMap = null; + try { + accessTokenMap = new Gson().fromJson(accessToken.getRawResponse(), Map.class); + } catch (JsonSyntaxException e) { + /* + failed to parse as JSON, so turn it into a HashMap which looks like the one we'd + get back from the JSON parser, so the rest of the code continues unchanged. + */ + Log.d(TAG, "Credential looks like a querystring; parsing as such"); + accessTokenMap = new HashMap(); + accessTokenMap.put("user_id", accessToken.getParameter("user_id")); + accessTokenMap.put("oauth_token_secret", accessToken.getParameter("oauth_token_secret")); + accessTokenMap.put("token_type", accessToken.getParameter("token_type")); + } + + resp.putString("status", "ok"); + resp.putBoolean("authorized", true); resp.putString("provider", providerName); - response.putString("uuid", accessToken.getParameter("user_id")); + + String uuid = accessToken.getParameter("user_id"); + response.putString("uuid", uuid); + String oauthTokenSecret = (String) accessToken.getParameter("oauth_token_secret"); + String tokenType = (String) accessToken.getParameter("token_type"); + if (tokenType == null) { + tokenType = "Bearer"; + } + + String consumerKey = (String) cfg.get("consumer_key"); + WritableMap credentials = Arguments.createMap(); - credentials.putString("oauth_token", accessToken.getToken()); - credentials.putString("oauth_secret", accessToken.getTokenSecret()); + credentials.putString("access_token", accessToken.getToken()); + credentials.putString("access_token_secret", oauthTokenSecret); + credentials.putString("type", tokenType); + credentials.putString("consumerKey", consumerKey); + response.putMap("credentials", credentials); resp.putMap("response", response); @@ -373,6 +452,7 @@ private WritableMap accessTokenResponse( private WritableMap accessTokenResponse( final String providerName, + final HashMap cfg, final OAuth2AccessToken accessToken, final String oauthVersion ) { @@ -380,25 +460,44 @@ private WritableMap accessTokenResponse( WritableMap response = Arguments.createMap(); resp.putString("status", "ok"); + resp.putBoolean("authorized", true); resp.putString("provider", providerName); - try { - response.putString("uuid", accessToken.getParameter("user_id")); - } catch (Exception ex) { - Log.e(TAG, "Exception while getting the access token"); - ex.printStackTrace(); - } + + String uuid = accessToken.getParameter("user_id"); + response.putString("uuid", uuid); WritableMap credentials = Arguments.createMap(); - credentials.putString("oauth_token", accessToken.getAccessToken()); - credentials.putString("oauth_secret", ""); - credentials.putString("scope", accessToken.getScope()); + Log.d(TAG, "Credential raw response: " + accessToken.getRawResponse()); + + credentials.putString("accessToken", accessToken.getAccessToken()); + String authHeader; + + String tokenType = accessToken.getTokenType(); + if (tokenType == null) { + tokenType = "Bearer"; + } + + String scope = accessToken.getScope(); + if (scope == null) { + scope = (String) cfg.get("scopes"); + } + + String clientID = (String) cfg.get("client_id"); + String idToken = accessToken.getParameter("id_token"); + + authHeader = tokenType + " " + accessToken.getAccessToken(); + credentials.putString("authorizationHeader", authHeader); + credentials.putString("type", tokenType); + credentials.putString("scopes", scope); + credentials.putString("clientID", clientID); + credentials.putString("idToken", idToken); response.putMap("credentials", credentials); resp.putMap("response", response); return resp; } - + private void exceptionCallback(Exception ex, final Callback onFail) { WritableMap error = Arguments.createMap(); @@ -408,4 +507,71 @@ private void exceptionCallback(Exception ex, final Callback onFail) { onFail.invoke(error); } + + public static Map recursivelyDeconstructReadableMap(ReadableMap readableMap) { + Map deconstructedMap = new HashMap<>(); + if (readableMap == null) { + return deconstructedMap; + } + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType type = readableMap.getType(key); + switch (type) { + case Null: + deconstructedMap.put(key, null); + break; + case Boolean: + deconstructedMap.put(key, readableMap.getBoolean(key)); + break; + case Number: + deconstructedMap.put(key, readableMap.getDouble(key)); + break; + case String: + deconstructedMap.put(key, readableMap.getString(key)); + break; + case Map: + deconstructedMap.put(key, OAuthManagerModule.recursivelyDeconstructReadableMap(readableMap.getMap(key))); + break; + case Array: + deconstructedMap.put(key, OAuthManagerModule.recursivelyDeconstructReadableArray(readableMap.getArray(key))); + break; + default: + throw new IllegalArgumentException("Could not convert object with key: " + key + "."); + } + + } + return deconstructedMap; + } + + public static List recursivelyDeconstructReadableArray(ReadableArray readableArray) { + List deconstructedList = new ArrayList<>(readableArray.size()); + for (int i = 0; i < readableArray.size(); i++) { + ReadableType indexType = readableArray.getType(i); + switch (indexType) { + case Null: + deconstructedList.add(i, null); + break; + case Boolean: + deconstructedList.add(i, readableArray.getBoolean(i)); + break; + case Number: + deconstructedList.add(i, readableArray.getDouble(i)); + break; + case String: + deconstructedList.add(i, readableArray.getString(i)); + break; + case Map: + deconstructedList.add(i, OAuthManagerModule.recursivelyDeconstructReadableMap(readableArray.getMap(i))); + break; + case Array: + deconstructedList.add(i, OAuthManagerModule.recursivelyDeconstructReadableArray(readableArray.getArray(i))); + break; + default: + throw new IllegalArgumentException("Could not convert object at index " + i + "."); + } + } + return deconstructedList; + } } diff --git a/android/src/main/java/io/fullstack/oauth/OAuthManagerPackage.java b/android/src/main/java/io/fullstack/oauth/OAuthManagerPackage.java index 4dddd8e..14c79e6 100644 --- a/android/src/main/java/io/fullstack/oauth/OAuthManagerPackage.java +++ b/android/src/main/java/io/fullstack/oauth/OAuthManagerPackage.java @@ -22,7 +22,6 @@ public OAuthManagerPackage() { * @param reactContext react application context that can be used to create modules * @return list of native modules to register with the newly created catalyst instance */ - @Override public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); modules.add(new OAuthManagerModule(reactContext)); @@ -36,7 +35,7 @@ public List createNativeModules(ReactApplicationContext reactConte * listed here. Also listing a native module here doesn't imply that the JS implementation of it * will be automatically included in the JS bundle. */ - @Override + public List> createJSModules() { return Collections.emptyList(); } @@ -45,7 +44,6 @@ public List> createJSModules() { * @param reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} */ - @Override public List createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } diff --git a/android/src/main/java/io/fullstack/oauth/OAuthManagerProviders.java b/android/src/main/java/io/fullstack/oauth/OAuthManagerProviders.java index 812b601..86c64c3 100644 --- a/android/src/main/java/io/fullstack/oauth/OAuthManagerProviders.java +++ b/android/src/main/java/io/fullstack/oauth/OAuthManagerProviders.java @@ -3,7 +3,14 @@ import android.util.Log; import java.util.HashMap; import java.util.Random; +import java.util.List; +import android.support.annotation.Nullable; +import java.net.URL; +import java.net.MalformedURLException; +import android.text.TextUtils; +import java.util.Arrays; +import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.builder.api.BaseApi; import com.github.scribejava.core.oauth.OAuthService; import com.github.scribejava.core.oauth.OAuth10aService; @@ -11,6 +18,7 @@ import com.github.scribejava.core.model.OAuth1AccessToken; import com.github.scribejava.core.model.OAuth1RequestToken; import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.OAuthConfig; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.oauth.OAuth20Service; @@ -18,6 +26,15 @@ import com.github.scribejava.apis.TwitterApi; import com.github.scribejava.apis.FacebookApi; import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.apis.GitHubApi; + +import com.github.scribejava.apis.ConfigurableApi; +import com.github.scribejava.apis.SlackApi; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.ReadableMapKeySetIterator; public class OAuthManagerProviders { private static final String TAG = "OAuthManagerProviders"; @@ -25,10 +42,11 @@ public class OAuthManagerProviders { static public OAuth10aService getApiFor10aProvider( final String providerName, final HashMap params, + @Nullable final ReadableMap opts, final String callbackUrl ) { if (providerName.equalsIgnoreCase("twitter")) { - return OAuthManagerProviders.twitterService(params, callbackUrl); + return OAuthManagerProviders.twitterService(params, opts, callbackUrl); } else { return null; } @@ -37,56 +55,196 @@ static public OAuth10aService getApiFor10aProvider( static public OAuth20Service getApiFor20Provider( final String providerName, final HashMap params, + @Nullable final ReadableMap opts, final String callbackUrl ) { if (providerName.equalsIgnoreCase("facebook")) { - return OAuthManagerProviders.facebookService(params, callbackUrl); - } else if (providerName.equalsIgnoreCase("google")) { - return OAuthManagerProviders.googleService(params, callbackUrl); - } else { - return null; + return OAuthManagerProviders.facebookService(params, opts, callbackUrl); + } + + if (providerName.equalsIgnoreCase("google")) { + return OAuthManagerProviders.googleService(params, opts, callbackUrl); + } + + if (providerName.equalsIgnoreCase("github")) { + return OAuthManagerProviders.githubService(params, opts, callbackUrl); + } + + if (providerName.equalsIgnoreCase("slack")) { + return OAuthManagerProviders.slackService(params, opts, callbackUrl); } + + if (params.containsKey("access_token_url") && params.containsKey("authorize_url")) { + return OAuthManagerProviders.configurableService(params, opts, callbackUrl); + } + + return null; + } + + static public OAuthRequest getRequestForProvider( + final String providerName, + final Verb httpVerb, + final OAuth1AccessToken oa1token, + final URL url, + final HashMap cfg, + @Nullable final ReadableMap params + ) { + final OAuth10aService service = + OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, null, null); + + String token = oa1token.getToken(); + OAuthConfig config = service.getConfig(); + OAuthRequest request = new OAuthRequest(httpVerb, url.toString(), config); + + request = OAuthManagerProviders.addParametersToRequest(request, token, params); + // Nothing special for Twitter + return request; } - private static OAuth10aService twitterService(final HashMap cfg, final String callbackUrl) { + static public OAuthRequest getRequestForProvider( + final String providerName, + final Verb httpVerb, + final OAuth2AccessToken oa2token, + final URL url, + final HashMap cfg, + @Nullable final ReadableMap params + ) { + final OAuth20Service service = + OAuthManagerProviders.getApiFor20Provider(providerName, cfg, null, null); + + OAuthConfig config = service.getConfig(); + OAuthRequest request = new OAuthRequest(httpVerb, url.toString(), config); + String token = oa2token.getAccessToken(); + + request = OAuthManagerProviders.addParametersToRequest(request, token, params); + + // + Log.d(TAG, "Making request for " + providerName + " to add token " + token); + // Need a way to standardize this, but for now + if (providerName.equalsIgnoreCase("slack")) { + request.addParameter("token", token); + } + + return request; + } + + // Helper to add parameters to the request + static private OAuthRequest addParametersToRequest( + OAuthRequest request, + final String access_token, + @Nullable final ReadableMap params + ) { + if (params != null && params.hasKey("params")) { + ReadableMapKeySetIterator iterator = params.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableType readableType = params.getType(key); + switch(readableType) { + case String: + String val = params.getString(key); + // String escapedVal = Uri.encode(val); + if (val.equals("access_token")) { + val = access_token; + } + request.addParameter(key, val); + break; + default: + throw new IllegalArgumentException("Could not read object with key: " + key); + } + } + } + return request; + } + + private static OAuth10aService twitterService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl) { String consumerKey = (String) cfg.get("consumer_key"); String consumerSecret = (String) cfg.get("consumer_secret"); - + ServiceBuilder builder = new ServiceBuilder() .apiKey(consumerKey) .apiSecret(consumerSecret) .debug(); + String scopes = (String) cfg.get("scopes"); + if (scopes != null) { + // String scopeStr = OAuthManagerProviders.getScopeString(scopes, "+"); + // Log.d(TAG, "scopeStr: " + scopeStr); + // builder.scope(scopeStr); + } + if (callbackUrl != null) { builder.callback(callbackUrl); } + return builder.build(TwitterApi.instance()); } - private static OAuth20Service facebookService(final HashMap cfg, final String callbackUrl) { - String clientKey = (String) cfg.get("client_id"); - String clientSecret = (String) cfg.get("client_secret"); - String state; - if (cfg.containsKey("state")) { - state = (String) cfg.get("state"); - } else { - state = TAG + new Random().nextInt(999_999); - } + private static OAuth20Service facebookService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl) { + ServiceBuilder builder = OAuthManagerProviders._oauth2ServiceBuilder(cfg, opts, callbackUrl); + return builder.build(FacebookApi.instance()); + } - ServiceBuilder builder = new ServiceBuilder() - .apiKey(clientKey) - .apiSecret(clientSecret) - .state(state) - .debug(); - - if (callbackUrl != null) { - builder.callback(callbackUrl); + private static OAuth20Service googleService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl) + { + ServiceBuilder builder = OAuthManagerProviders._oauth2ServiceBuilder(cfg, opts, callbackUrl); + return builder.build(GoogleApi20.instance()); + } + + private static OAuth20Service githubService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl) + { + + ServiceBuilder builder = OAuthManagerProviders._oauth2ServiceBuilder(cfg, opts, callbackUrl); + return builder.build(GitHubApi.instance()); + } + + private static OAuth20Service configurableService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl + ) { + ServiceBuilder builder = OAuthManagerProviders._oauth2ServiceBuilder(cfg, opts, callbackUrl); + Log.d(TAG, "Creating ConfigurableApi"); + //Log.d(TAG, " authorize_url: " + cfg.get("authorize_url")); + //Log.d(TAG, " access_token_url: " + cfg.get("access_token_url")); + ConfigurableApi api = ConfigurableApi.instance() + .setAccessTokenEndpoint((String) cfg.get("access_token_url")) + .setAuthorizationBaseUrl((String) cfg.get("authorize_url")); + if (cfg.containsKey("access_token_verb")) { + //Log.d(TAG, " access_token_verb: " + cfg.get("access_token_verb")); + api.setAccessTokenVerb((String) cfg.get("access_token_verb")); } - return builder.build(FacebookApi.instance()); + return builder.build(api); + } + + private static OAuth20Service slackService( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl + ) { + + Log.d(TAG, "Make the builder: " + SlackApi.class); + ServiceBuilder builder = OAuthManagerProviders._oauth2ServiceBuilder(cfg, opts, callbackUrl); + return builder.build(SlackApi.instance()); } - private static OAuth20Service googleService(final HashMap cfg, final String callbackUrl) { + private static ServiceBuilder _oauth2ServiceBuilder( + final HashMap cfg, + @Nullable final ReadableMap opts, + final String callbackUrl + ) { String clientKey = (String) cfg.get("client_id"); String clientSecret = (String) cfg.get("client_secret"); String state; @@ -95,22 +253,43 @@ private static OAuth20Service googleService(final HashMap cfg, final String call } else { state = TAG + new Random().nextInt(999_999); } - String scope = "profile"; - if (cfg.containsKey("scope")) { - scope = (String) cfg.get("scope"); - } + // Builder ServiceBuilder builder = new ServiceBuilder() .apiKey(clientKey) .apiSecret(clientSecret) .state(state) - .scope(scope) .debug(); - + + String scopes = ""; + if (cfg.containsKey("scopes")) { + scopes = (String) cfg.get("scopes"); + String scopeStr = OAuthManagerProviders.getScopeString(scopes, ","); + builder.scope(scopeStr); + } + + if (opts != null && opts.hasKey("scopes")) { + scopes = (String) opts.getString("scopes"); + String scopeStr = OAuthManagerProviders.getScopeString(scopes, ","); + builder.scope(scopeStr); + } + if (callbackUrl != null) { builder.callback(callbackUrl); } - return builder.build(GoogleApi20.instance()); + return builder; + } + + /** + * Convert a list of scopes by space or string into an array + */ + private static String getScopeString( + final String scopes, + final String joinBy + ) { + List array = Arrays.asList(scopes.replaceAll("\\s", "").split("[ ,]+")); + Log.d(TAG, "array: " + array + " (" + array.size() + ") from " + scopes); + return TextUtils.join(joinBy, array); } -} \ No newline at end of file +} diff --git a/android/src/main/java/io/fullstack/oauth/services/ConfigurableApi.java b/android/src/main/java/io/fullstack/oauth/services/ConfigurableApi.java new file mode 100644 index 0000000..2019f53 --- /dev/null +++ b/android/src/main/java/io/fullstack/oauth/services/ConfigurableApi.java @@ -0,0 +1,66 @@ +package com.github.scribejava.apis; + +import android.util.Log; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; +import com.github.scribejava.core.extractors.TokenExtractor; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.Verb; + +public class ConfigurableApi extends DefaultApi20 { + + private String accessTokenEndpoint; + + private String authorizationBaseUrl; + + private Verb accessTokenVerb = Verb.GET; + + protected ConfigurableApi() { + } + + private static class InstanceHolder { + private static final ConfigurableApi INSTANCE = new ConfigurableApi(); + } + + public static ConfigurableApi instance() { + return InstanceHolder.INSTANCE; + } + + public ConfigurableApi setAccessTokenEndpoint(String endpoint) { + accessTokenEndpoint = endpoint; + return this; + } + + public ConfigurableApi setAuthorizationBaseUrl(String baseUrl) { + authorizationBaseUrl = baseUrl; + return this; + } + + public ConfigurableApi setAccessTokenVerb(String verb) { + if (verb.equalsIgnoreCase("GET")) { + accessTokenVerb = Verb.GET; + } else if (verb.equalsIgnoreCase("POST")) { + accessTokenVerb = Verb.POST; + } else { + Log.e("ConfigurableApi", "Expected GET or POST string values for accessTokenVerb."); + } + + return this; + } + + @Override + public Verb getAccessTokenVerb() { + return accessTokenVerb; + } + + @Override + public String getAccessTokenEndpoint() { + return accessTokenEndpoint; + } + + @Override + protected String getAuthorizationBaseUrl() { + return authorizationBaseUrl; + } +} diff --git a/android/src/main/java/io/fullstack/oauth/services/SlackApi.java b/android/src/main/java/io/fullstack/oauth/services/SlackApi.java new file mode 100644 index 0000000..b5fc61c --- /dev/null +++ b/android/src/main/java/io/fullstack/oauth/services/SlackApi.java @@ -0,0 +1,38 @@ +package com.github.scribejava.apis; + +import android.util.Log; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor; +import com.github.scribejava.core.extractors.TokenExtractor; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.Verb; + +public class SlackApi extends DefaultApi20 { + + protected SlackApi() { + } + + private static class InstanceHolder { + private static final SlackApi INSTANCE = new SlackApi(); + } + + public static SlackApi instance() { + return InstanceHolder.INSTANCE; + } + + @Override + public Verb getAccessTokenVerb() { + return Verb.GET; + } + + @Override + public String getAccessTokenEndpoint() { + return "https://slack.com/api/oauth.access"; + } + + @Override + protected String getAuthorizationBaseUrl() { + return "https://slack.com/oauth/authorize"; + } +} \ No newline at end of file diff --git a/android/src/main/res/layout/webview_layout.xml b/android/src/main/res/layout/webview_layout.xml new file mode 100644 index 0000000..29f061f --- /dev/null +++ b/android/src/main/res/layout/webview_layout.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/bin/cocoapods.sh b/bin/cocoapods.sh index 4605a67..7a474ae 100755 --- a/bin/cocoapods.sh +++ b/bin/cocoapods.sh @@ -3,7 +3,7 @@ ## https://github.com/auth0/react-native-lock/blob/master/bin/cocoapods.sh ios_dir=`pwd`/ios -if [ -d ios_dir ] +if [ ! -d $ios_dir ] then exit 0 fi @@ -45,4 +45,4 @@ cd .. echo "Installing Pods" -pod install --project-directory=ios \ No newline at end of file +pod install --project-directory=ios diff --git a/ios/OAuthManager.xcodeproj/project.pbxproj b/ios/OAuthManager.xcodeproj/project.pbxproj index 6580450..9929888 100644 --- a/ios/OAuthManager.xcodeproj/project.pbxproj +++ b/ios/OAuthManager.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + B76E094E1E768CE100A9AF9A /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = B76E094B1E768CE100A9AF9A /* README.md */; }; + B76E094F1E768CE100A9AF9A /* XMLReader.h in Headers */ = {isa = PBXBuildFile; fileRef = B76E094C1E768CE100A9AF9A /* XMLReader.h */; }; + B76E09501E768CE100A9AF9A /* XMLReader.m in Sources */ = {isa = PBXBuildFile; fileRef = B76E094D1E768CE100A9AF9A /* XMLReader.m */; }; D935004D1D513CF700C7BA47 /* OAuthManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D935004C1D513CF700C7BA47 /* OAuthManager.m */; }; D9F2EAD31DA9A9650000BD52 /* OAuthClient.h in Headers */ = {isa = PBXBuildFile; fileRef = D9F2EAD11DA9A9650000BD52 /* OAuthClient.h */; }; D9F2EAD41DA9A9650000BD52 /* OAuthClient.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F2EAD21DA9A9650000BD52 /* OAuthClient.m */; }; @@ -65,6 +68,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + B76E094B1E768CE100A9AF9A /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + B76E094C1E768CE100A9AF9A /* XMLReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMLReader.h; sourceTree = ""; }; + B76E094D1E768CE100A9AF9A /* XMLReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMLReader.m; sourceTree = ""; }; D91353961DA7849100AABC96 /* libOAuthManager.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libOAuthManager.a; sourceTree = BUILT_PRODUCTS_DIR; }; D935004C1D513CF700C7BA47 /* OAuthManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OAuthManager.m; path = OAuthManager/OAuthManager.m; sourceTree = ""; }; D991AB771D1B237400DE9E58 /* Pods.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Pods.xcodeproj; path = Pods/Pods.xcodeproj; sourceTree = ""; }; @@ -91,9 +97,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B76E094A1E768CE100A9AF9A /* XMLReader */ = { + isa = PBXGroup; + children = ( + B76E094B1E768CE100A9AF9A /* README.md */, + B76E094C1E768CE100A9AF9A /* XMLReader.h */, + B76E094D1E768CE100A9AF9A /* XMLReader.m */, + ); + name = XMLReader; + path = OAuthManager/XMLReader; + sourceTree = SOURCE_ROOT; + }; D93EF9941DA77CBB00EC55A0 /* /Users/auser/Development/react-native/mine/OAuthManager/ios/OAuthManager.xcodeproj */ = { isa = PBXGroup; children = ( + B76E094A1E768CE100A9AF9A /* XMLReader */, D9F2EADF1DA9A9930000BD52 /* OAuthManager.h */, D9F2EAE01DA9A9930000BD52 /* OAuthManager.m */, D9F2EAE11DA9A9930000BD52 /* OAuthManagerConstants.h */, @@ -143,6 +161,7 @@ files = ( D9F2EAD91DA9A9730000BD52 /* OAuth1Client.h in Headers */, D9F2EADE1DA9A9840000BD52 /* OAuthClientProtocol.h in Headers */, + B76E094F1E768CE100A9AF9A /* XMLReader.h in Headers */, D9F2EADB1DA9A9730000BD52 /* OAuth2Client.h in Headers */, D9F2EAD31DA9A9650000BD52 /* OAuthClient.h in Headers */, D9F2EAE41DA9A9930000BD52 /* OAuthManagerConstants.h in Headers */, @@ -232,11 +251,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B76E094E1E768CE100A9AF9A /* README.md in Sources */, D9F2EADC1DA9A9730000BD52 /* OAuth2Client.m in Sources */, D935004D1D513CF700C7BA47 /* OAuthManager.m in Sources */, D9F2EAD41DA9A9650000BD52 /* OAuthClient.m in Sources */, D9F2EADA1DA9A9730000BD52 /* OAuth1Client.m in Sources */, D9F2EAE31DA9A9930000BD52 /* OAuthManager.m in Sources */, + B76E09501E768CE100A9AF9A /* XMLReader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -275,7 +296,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -328,7 +349,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -361,7 +382,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; DYLIB_INSTALL_NAME_BASE = ""; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../React/**", @@ -388,7 +409,7 @@ CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; DYLIB_INSTALL_NAME_BASE = ""; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; - ENABLE_BITCODE = NO; + ENABLE_BITCODE = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)/../../React/**", diff --git a/ios/OAuthManager/OAuth1Client.m b/ios/OAuthManager/OAuth1Client.m index 97ff342..b570104 100644 --- a/ios/OAuthManager/OAuth1Client.m +++ b/ios/OAuthManager/OAuth1Client.m @@ -31,13 +31,17 @@ - (void) authorizeWithUrl:(NSString *)providerName __weak id client = self; [account authenticateWithHandler:^(NSArray *responses, NSError *error) { - [client clearPendingAccount]; - if (error != nil) { - onError(error); + NSString *response = ((DCTAuthResponse *)responses[0]).responseDescription; + NSError *err = [NSError errorWithDomain:error.domain + code:error.code + userInfo:@{@"response": response}]; + onError(err); return; } + [client clearPendingAccount]; + if (!account.authorized) { NSError *err = QUICK_ERROR(E_ACCOUNT_NOT_AUTHORIZED, @"account not authorized"); onError(err); diff --git a/ios/OAuthManager/OAuth2Client.m b/ios/OAuthManager/OAuth2Client.m index d260967..092a255 100644 --- a/ios/OAuthManager/OAuth2Client.m +++ b/ios/OAuthManager/OAuth2Client.m @@ -34,13 +34,17 @@ - (void) authorizeWithUrl:(NSString *)providerName // authorizeWithClientID [account authenticateWithHandler:^(NSArray *responses, NSError *error) { NSLog(@"authenticateWithHandler: %@", responses); - [client clearPendingAccount]; if (error != nil) { - NSLog(@"Some error: %@", error); - onError(error); + NSString *response = ((DCTAuthResponse *)responses[0]).responseDescription; + NSError *err = [NSError errorWithDomain:error.domain + code:error.code + userInfo:@{@"response": response}]; + onError(err); return; } + + [client clearPendingAccount]; if (!account.authorized) { NSError *err = QUICK_ERROR(E_ACCOUNT_NOT_AUTHORIZED, @"account not authorized"); @@ -63,7 +67,10 @@ - (void) reauthenticateWithHandler:(NSString *) providerName [account reauthenticateWithHandler:^(DCTAuthResponse *response, NSError *error) { NSLog(@"Reauthenticating..."); if (error != nil) { - onError(error); + NSError *err = [NSError errorWithDomain:error.domain + code:error.code + userInfo:@{@"response": response.responseDescription}]; + onError(err); return; } @@ -86,7 +93,12 @@ - (DCTOAuth2Account *) getAccount:(NSString *)providerName // Required NSURL *authorize_url = [cfg objectForKey:@"authorize_url"]; NSString *scopeStr = [cfg valueForKey:@"scopes"]; - NSArray *scopes = [scopeStr componentsSeparatedByString:@","]; + // NSArray *scopes = [scopeStr componentsSeparatedByString:@","]; + + NSString *sep = @", "; + NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:sep]; + NSArray *scopes = [scopeStr componentsSeparatedByCharactersInSet:set]; + // Optional NSURL *access_token_url = [cfg objectForKey:@"access_token_url"]; diff --git a/ios/OAuthManager/OAuthClient.m b/ios/OAuthManager/OAuthClient.m index 9120dd9..b1e18f5 100644 --- a/ios/OAuthManager/OAuthClient.m +++ b/ios/OAuthManager/OAuthClient.m @@ -30,7 +30,12 @@ - (void) reauthenticateWithHandler:(NSString *) providerName - (void) cancelAuthentication { if (_account != nil) { - [_account cancelAuthentication]; + @try { + [_account cancelAuthentication]; + } + @catch (NSException *exception) { + NSLog(@"An exception occurred while cancelling authentication: %@", [exception reason]); + } } } @@ -43,7 +48,9 @@ - (void) savePendingAccount:(DCTAuthAccount *) account - (void) clearPendingAccount { - _account = nil; + NSLog(@"called clearPendingAccount: %@", _account); + [_account cancelAuthentication]; + _account = nil; } - (void (^)(DCTAuthResponse *response, NSError *error)) getHandler:(DCTAuthAccount *) account diff --git a/ios/OAuthManager/OAuthManager.h b/ios/OAuthManager/OAuthManager.h index e0df954..cdefdda 100644 --- a/ios/OAuthManager/OAuthManager.h +++ b/ios/OAuthManager/OAuthManager.h @@ -7,8 +7,19 @@ #import -#import "RCTBridgeModule.h" -#import "RCTLinkingManager.h" +#if __has_include() + #import +#else + #import "RCTBridgeModule.h" +#endif + +#if __has_include() + #import +#else + #import "RCTLinkingManager.h" +#endif + + @class OAuthClient; diff --git a/ios/OAuthManager/OAuthManager.m b/ios/OAuthManager/OAuthManager.m index ec06a42..2e51cea 100644 --- a/ios/OAuthManager/OAuthManager.m +++ b/ios/OAuthManager/OAuthManager.m @@ -6,6 +6,8 @@ #import #import +#import +#import #import "OAuthManager.h" #import "DCTAuth.h" @@ -14,10 +16,11 @@ #import "OAuthClient.h" #import "OAuth1Client.h" #import "OAuth2Client.h" +#import "XMLReader.h" @interface OAuthManager() - @property (nonatomic) NSArray *pendingClients; - @property BOOL pendingAuthentication; +@property (nonatomic) NSArray *pendingClients; +@property BOOL pendingAuthentication; @end @implementation OAuthManager @@ -25,18 +28,30 @@ @implementation OAuthManager @synthesize callbackUrls = _callbackUrls; static NSString *const AUTH_MANAGER_TAG = @"AUTH_MANAGER"; +static OAuthManager *manager; +static dispatch_once_t onceToken; +static SFSafariViewController *safariViewController = nil; RCT_EXPORT_MODULE(OAuthManager); +// Run on a different thread +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("io.fullstack.oauth", DISPATCH_QUEUE_SERIAL); +} + + (instancetype)sharedManager { - static OAuthManager *manager; - static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager = [self new]; }); return manager; } ++ (void) reset { + onceToken = nil; + manager = nil; +} + - (instancetype) init { self = [super init]; if (self != nil) { @@ -47,7 +62,7 @@ - (instancetype) init { selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; - + } return self; } @@ -59,10 +74,7 @@ - (void) dealloc - (void) didBecomeActive:(NSNotification *)notification { - NSLog(@"Application reopened: %@", @(self.pendingAuthentication)); - for (OAuthClient *client in _pendingClients) { - [self removePending:client]; - } + // TODO? } /* @@ -74,8 +86,18 @@ + (BOOL)setupOAuthHandler:(UIApplication *)application DCTAuthPlatform *authPlatform = [DCTAuthPlatform sharedPlatform]; [authPlatform setURLOpener: ^void(NSURL *URL, DCTAuthPlatformCompletion completion) { - [sharedManager setPendingAuthentication:YES]; - [application openURL:URL]; + // [sharedManager setPendingAuthentication:YES]; + if ([SFSafariViewController class] != nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + safariViewController = [[SFSafariViewController alloc] initWithURL:URL]; + UIViewController *viewController = application.keyWindow.rootViewController; + dispatch_async(dispatch_get_main_queue(), ^{ + [viewController presentViewController:safariViewController animated:YES completion: nil]; + }); + }); + } else { + [application openURL:URL]; + } completion(YES); }]; @@ -100,15 +122,15 @@ + (BOOL)handleOpenUrl:(UIApplication *)application openURL:(NSURL *)url { OAuthManager *manager = [OAuthManager sharedManager]; NSString *strUrl = [manager stringHost:url]; - - NSLog(@"Handling handleOpenUrl: %@", strUrl); if ([manager.callbackUrls indexOfObject:strUrl] != NSNotFound) { + if(safariViewController != nil) { + [safariViewController dismissViewControllerAnimated:YES completion:nil]; + } return [DCTAuth handleURL:url]; } - - [manager clearPending]; + // [manager clearPending]; return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; @@ -128,23 +150,25 @@ - (BOOL) _configureProvider:(NSString *)providerName andConfig:(NSDictionary *)c NSMutableArray *arr = [_callbackUrls mutableCopy]; NSString *callbackUrlStr = [config valueForKey:@"callback_url"]; NSURL *callbackUrl = [NSURL URLWithString:callbackUrlStr]; - NSString *saveCallbackUrl = [self stringHost:callbackUrl]; - [arr addObject:saveCallbackUrl]; - _callbackUrls = [arr copy]; + NSString *saveCallbackUrl = [[self stringHost:callbackUrl] lowercaseString]; + + if ([arr indexOfObject:saveCallbackUrl] == NSNotFound) { + [arr addObject:saveCallbackUrl]; + _callbackUrls = [arr copy]; + NSLog(@"Saved callback url: %@ in %@", saveCallbackUrl, _callbackUrls); + } + // Convert objects of url type for (NSString *name in [config allKeys]) { if ([name rangeOfString:@"_url"].location != NSNotFound) { // This is a URL representation NSString *urlStr = [config valueForKey:name]; - NSURL *url = [NSURL URLWithString:[urlStr - stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + NSURL *url = [NSURL URLWithString:urlStr]; [objectProps setObject:url forKey:name]; } else { NSString *str = [NSString stringWithString:[config valueForKey:name]]; - NSString *escapedStr = [str - stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; - [objectProps setValue:[escapedStr copy] forKey:name]; + [objectProps setValue:str forKey:name]; } } @@ -187,18 +211,19 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name #pragma mark OAuth +// TODO: Remove opts RCT_EXPORT_METHOD(getSavedAccounts:(NSDictionary *) opts - callback:(RCTResponseSenderBlock) callback) + callback:(RCTResponseSenderBlock) callback) { OAuthManager *manager = [OAuthManager sharedManager]; - DCTAuthAccountStore *store = [self accountStore]; + DCTAuthAccountStore *store = [manager accountStore]; NSSet *accounts = [store accounts]; NSMutableArray *respAccounts = [[NSMutableArray alloc] init]; for (DCTAuthAccount *account in [accounts allObjects]) { NSString *providerName = account.type; NSMutableDictionary *cfg = [[manager getConfigForProvider:providerName] mutableCopy]; - NSMutableDictionary *acc = [[self getAccountResponse:account cfg:cfg] mutableCopy]; + NSMutableDictionary *acc = [[manager getAccountResponse:account cfg:cfg] mutableCopy]; [acc setValue:providerName forKey:@"provider"]; [respAccounts addObject:acc]; } @@ -208,6 +233,7 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name }]); } +// TODO: Remove opts RCT_EXPORT_METHOD(getSavedAccount:(NSString *)providerName opts:(NSDictionary *) opts callback:(RCTResponseSenderBlock)callback) @@ -215,17 +241,18 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name OAuthManager *manager = [OAuthManager sharedManager]; NSMutableDictionary *cfg = [[manager getConfigForProvider:providerName] mutableCopy]; - DCTAuthAccount *existingAccount = [self accountForProvider:providerName]; + DCTAuthAccount *existingAccount = [manager accountForProvider:providerName]; if (existingAccount != nil) { if ([existingAccount isAuthorized]) { NSDictionary *accountResponse = [manager getAccountResponse:existingAccount cfg:cfg]; callback(@[[NSNull null], @{ @"status": @"ok", + @"provider": providerName, @"response": accountResponse }]); return; } else { - DCTAuthAccountStore *store = [self accountStore]; + DCTAuthAccountStore *store = [manager accountStore]; [store deleteAccount:existingAccount]; NSDictionary *errResp = @{ @"status": @"error", @@ -250,14 +277,14 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name callback:(RCTResponseSenderBlock) callback) { OAuthManager *manager = [OAuthManager sharedManager]; - DCTAuthAccountStore *store = [self accountStore]; + DCTAuthAccountStore *store = [manager accountStore]; - DCTAuthAccount *existingAccount = [self accountForProvider:providerName]; + DCTAuthAccount *existingAccount = [manager accountForProvider:providerName]; if (existingAccount != nil) { [store deleteAccount:existingAccount]; callback(@[[NSNull null], @{ @"status": @"ok" - }]); + }]); } else { NSDictionary *resp = @{ @"status": @"error", @@ -276,23 +303,28 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name callback:(RCTResponseSenderBlock)callback) { OAuthManager *manager = [OAuthManager sharedManager]; + [manager clearPending]; NSMutableDictionary *cfg = [[manager getConfigForProvider:providerName] mutableCopy]; - DCTAuthAccount *existingAccount = [self accountForProvider:providerName]; - if (existingAccount != nil) { + DCTAuthAccount *existingAccount = [manager accountForProvider:providerName]; + NSString *clientID = ([providerName isEqualToString:@"google"]) ? ((DCTOAuth2Credential *) existingAccount).clientID : (NSString *)nil; + + if (([providerName isEqualToString:@"google"] && existingAccount && clientID != nil) + || (![providerName isEqualToString:@"google"] && existingAccount != nil)) { if ([existingAccount isAuthorized]) { NSDictionary *accountResponse = [manager getAccountResponse:existingAccount cfg:cfg]; callback(@[[NSNull null], @{ @"status": @"ok", + @"provider": providerName, @"response": accountResponse }]); return; } else { - DCTAuthAccountStore *store = [self accountStore]; + DCTAuthAccountStore *store = [manager accountStore]; [store deleteAccount:existingAccount]; } } - + NSString *callbackUrl; NSURL *storedCallbackUrl = [cfg objectForKey:@"callback_url"]; @@ -317,30 +349,28 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name } else if ([version isEqualToString:@"2.0"]) { client = (OAuthClient *)[[OAuth2Client alloc] init]; } else { - NSLog(@"Provider number: %@", version); return callback(@[@{ @"status": @"error", @"msg": @"Unknown provider" }]); } - + // Store pending client - [self addPending:client]; - _pendingAuthentication = YES; + [manager addPending:client]; + _pendingAuthentication = YES; + + NSLog(@"Calling authorizeWithUrl: %@ with callbackURL: %@\n %@", providerName, callbackUrl, cfg); [client authorizeWithUrl:providerName url:callbackUrl cfg:cfg - onSuccess:^(DCTAuthAccount *account) { - NSLog(@"authorizeWithUrl: %@", account); + NSLog(@"on success called with account: %@", account); NSDictionary *accountResponse = [manager getAccountResponse:account cfg:cfg]; _pendingAuthentication = NO; [manager removePending:client]; - - DCTAuthAccountStore *store = [self accountStore]; - [store saveAccount:account]; + [[manager accountStore] saveAccount:account]; // <~ callback(@[[NSNull null], @{ @"status": @"ok", @@ -349,11 +379,12 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name } onError:^(NSError *error) { NSLog(@"Error in authorizeWithUrl: %@", error); _pendingAuthentication = NO; - [manager removePending:client]; callback(@[@{ @"status": @"error", - @"msg": [error localizedDescription] + @"msg": [error localizedDescription], + @"userInfo": error.userInfo }]); + [manager removePending:client]; }]; } @@ -362,50 +393,81 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name opts:(NSDictionary *) opts callback:(RCTResponseSenderBlock)callback) { - OAuthManager *manager = [OAuthManager sharedManager]; - NSMutableDictionary *cfg = [[manager getConfigForProvider:providerName] mutableCopy]; + OAuthManager *manager = [OAuthManager sharedManager]; + NSMutableDictionary *cfg = [[manager getConfigForProvider:providerName] mutableCopy]; - DCTAuthAccount *existingAccount = [self accountForProvider:providerName]; + DCTAuthAccount *existingAccount = [manager accountForProvider:providerName]; if (existingAccount == nil) { NSDictionary *errResp = @{ - @"status": @"error", - @"msg": [NSString stringWithFormat:@"No account found for %@", providerName] - }; + @"status": @"error", + @"msg": [NSString stringWithFormat:@"No account found for %@", providerName] + }; callback(@[errResp]); return; } - - // If we have the http in the string, use it as the URL, otherwise create one - // with the configuration - NSURL *apiUrl; - if ([urlOrPath hasPrefix:@"http"]) { - apiUrl = [NSURL URLWithString:urlOrPath]; - } else { - NSURL *apiHost = [cfg objectForKey:@"api_url"]; - apiUrl = [NSURL URLWithString:[[apiHost absoluteString] stringByAppendingString:urlOrPath]]; - } - - // If there are params + + NSDictionary *creds = [self credentialForAccount:providerName cfg:cfg]; + + // If we have the http in the string, use it as the URL, otherwise create one + // with the configuration + NSURL *apiUrl; + if ([urlOrPath hasPrefix:@"http"]) { + apiUrl = [NSURL URLWithString:urlOrPath]; + } else { + NSURL *apiHost = [cfg objectForKey:@"api_url"]; + apiUrl = [NSURL URLWithString:[[apiHost absoluteString] stringByAppendingString:urlOrPath]]; + } + + // If there are params NSMutableArray *items = [NSMutableArray array]; NSDictionary *params = [opts objectForKey:@"params"]; if (params != nil) { for (NSString *key in params) { - NSURLQueryItem *item = [NSURLQueryItem queryItemWithName:key value:[params valueForKey:key]]; - [items addObject:item]; + + NSString *value = [params valueForKey:key]; + + if ([value isEqualToString:@"access_token"]) { + value = [creds valueForKey:@"access_token"]; + } + + NSURLQueryItem *item = [NSURLQueryItem queryItemWithName:key value:value]; + + if (item != nil) { + [items addObject:item]; + } } } NSString *methodStr = [opts valueForKey:@"method"]; - DCTAuthRequestMethod method = [self getRequestMethodByString:methodStr]; + DCTAuthRequestMethod method = [manager getRequestMethodByString:methodStr]; DCTAuthRequest *request = - [[DCTAuthRequest alloc] - initWithRequestMethod:method - URL:apiUrl - items:items]; - - request.account = existingAccount; + [[DCTAuthRequest alloc] + initWithRequestMethod:method + URL:apiUrl + items:items]; + + // Allow json body in POST / PUT requests + NSDictionary *body = [opts objectForKey:@"body"]; + if (body != nil) { + NSMutableArray *items = [NSMutableArray array]; + + for (NSString *key in body) { + NSString *value = [body valueForKey:key]; + + DCTAuthContentItem *item = [[DCTAuthContentItem alloc] initWithName:key value:value]; + + if(item != nil) { + [items addObject: item]; + } + } + + DCTAuthContent *content = [[DCTAuthContent alloc] initWithEncoding:NSUTF8StringEncoding + type:DCTAuthContentTypeJSON + items:items]; + [request setContent:content]; + } // If there are headers NSDictionary *headers = [opts objectForKey:@"headers"]; @@ -416,7 +478,9 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name } request.HTTPHeaders = existingHeaders; } - + + request.account = existingAccount; + [request performRequestWithHandler:^(DCTAuthResponse *response, NSError *error) { if (error != nil) { NSDictionary *errorDict = @{ @@ -427,11 +491,26 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name } else { NSInteger statusCode = response.statusCode; NSData *rawData = response.data; + NSDictionary *headers = response.HTTPHeaders; NSError *err; - NSArray *data = [NSJSONSerialization JSONObjectWithData:rawData - options:kNilOptions - error:&err]; + NSArray *data; + + + + // Check if returned data is a valid JSON + // != nil returned if the rawdata is not a valid JSON + if ((data = [NSJSONSerialization JSONObjectWithData:rawData + options:kNilOptions + error:&err]) == nil) { + // Resetting err variable. + err = nil; + + // Parse XML + data = [XMLReader dictionaryForXMLData:rawData + options:XMLReaderOptionsProcessNamespaces + error:&err]; + } if (err != nil) { NSDictionary *errResp = @{ @"status": @"error", @@ -439,9 +518,11 @@ - (NSDictionary *) getConfigForProvider:(NSString *)name }; callback(@[errResp]); } else { + NSDictionary *resp = @{ @"status": @(statusCode), - @"data": data + @"data": data != nil ? data : @[], + @"headers": headers, }; callback(@[[NSNull null], resp]); } @@ -467,6 +548,46 @@ - (DCTAuthAccount *) accountForProvider:(NSString *) providerName } } +- (NSDictionary *) credentialForAccount:(NSString *)providerName + cfg:(NSDictionary *)cfg +{ + DCTAuthAccount *account = [self accountForProvider:providerName]; + if (!account) { + return nil; + } + + NSString *version = [cfg valueForKey:@"auth_version"]; + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + + if ([version isEqualToString:@"1.0"]) { + DCTOAuth1Credential *credentials = [account credential]; + + if (credentials) { + if (credentials.oauthToken) { + NSString *token = credentials.oauthToken; + [dict setObject:token forKey:@"access_token"]; + } + + if (credentials.oauthTokenSecret) { + NSString *secret = credentials.oauthTokenSecret; + [dict setObject:secret forKey:@"access_token_secret"]; + } + } + + } else if ([version isEqualToString:@"2.0"]) { + DCTOAuth2Credential *credentials = [account credential]; + + if (credentials) { + if (credentials.accessToken) { + NSString *token = credentials.accessToken; + [dict setObject:token forKey:@"access_token"]; + } + } + } + + return dict; +} + - (DCTAuthRequestMethod) getRequestMethodByString:(NSString *) method { if ([method compare:@"get" options:NSCaseInsensitiveSearch] == NSOrderedSame) { @@ -493,6 +614,7 @@ - (DCTAuthRequestMethod) getRequestMethodByString:(NSString *) method - (NSDictionary *) getAccountResponse:(DCTAuthAccount *) account cfg:(NSDictionary *)cfg { + NSArray *ignoredCredentialProperties = @[@"superclass", @"hash", @"description", @"debugDescription"]; NSString *version = [cfg valueForKey:@"auth_version"]; NSMutableDictionary *accountResponse = [@{ @"authorized": @(account.authorized), @@ -501,29 +623,67 @@ - (NSDictionary *) getAccountResponse:(DCTAuthAccount *) account if ([version isEqualToString:@"1.0"]) { DCTOAuth1Credential *credential = account.credential; - NSDictionary *cred = @{ - @"oauth_token": credential.oauthToken, - @"oauth_secret": credential.oauthTokenSecret - }; - [accountResponse setObject:cred forKey:@"credentials"]; + if (credential != nil) { + NSDictionary *cred = @{ + @"access_token": credential.oauthToken, + @"access_token_secret": credential.oauthTokenSecret + }; + [accountResponse setObject:cred forKey:@"credentials"]; + } } else if ([version isEqualToString:@"2.0"]) { DCTOAuth2Credential *credential = account.credential; - NSMutableDictionary *cred = [@{ - @"access_token": credential.accessToken, - @"type": @(credential.type) - } mutableCopy]; - if (credential.refreshToken != nil) { - [cred setValue:credential.refreshToken forKey:@"refresh_token"]; + if (credential != nil) { + + NSMutableDictionary *cred = [self dictionaryForCredentialKeys: credential]; + + DCTOAuth2Account *oauth2Account = (DCTOAuth2Account *) account; + if (oauth2Account.scopes) { + [cred setObject:oauth2Account.scopes forKey:@"scopes"]; + } + + [accountResponse setObject:cred forKey:@"credentials"]; } - [accountResponse setObject:cred forKey:@"credentials"]; } [accountResponse setValue:[account identifier] forKey:@"identifier"]; if (account.userInfo != nil) { - [accountResponse setObject:[account userInfo] forKey:@"user_info"]; - } + [accountResponse setObject:[account userInfo] forKey:@"user_info"]; + } + return accountResponse; } +- (NSDictionary *) dictionaryForCredentialKeys:(NSObject *) credential +{ + NSArray *ignoredCredentialProperties = @[@"superclass", @"hash", @"description", @"debugDescription"]; + unsigned int count = 0; + NSMutableDictionary *cred = [NSMutableDictionary new]; + objc_property_t *properties = class_copyPropertyList([credential class], &count); + + for (int i = 0; i < count; i++) { + + NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])]; + if ([ignoredCredentialProperties containsObject:key]) { + NSLog(@"Ignoring credentials key: %@", key); + } else { + id value = [credential valueForKey:key]; + + if (value == nil) { + + } else if ([value isKindOfClass:[NSNumber class]] + || [value isKindOfClass:[NSString class]] + || [value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSMutableArray class]]) { + // TODO: extend to other types + [cred setObject:value forKey:key]; + } else if ([value isKindOfClass:[NSObject class]]) { + [cred setObject:[value dictionary] forKey:key]; + } else { + NSLog(@"Invalid type for %@ (%@)", NSStringFromClass([self class]), key); + } + } + } + return cred; +} + - (void) clearPending { OAuthManager *manager = [OAuthManager sharedManager]; @@ -531,6 +691,7 @@ - (void) clearPending [manager removePending:client]; } manager.pendingClients = [NSArray array]; + _pendingAuthentication = NO; } - (void) addPending:(OAuthClient *) client @@ -543,12 +704,12 @@ - (void) addPending:(OAuthClient *) client - (void) removePending:(OAuthClient *) client { + [client clearPendingAccount]; OAuthManager *manager = [OAuthManager sharedManager]; NSUInteger idx = [manager.pendingClients indexOfObject:client]; if ([manager.pendingClients count] <= idx) { NSMutableArray *newPendingClients = [manager.pendingClients mutableCopy]; [newPendingClients removeObjectAtIndex:idx]; - [client cancelAuthentication]; manager.pendingClients = newPendingClients; } } @@ -576,3 +737,4 @@ - (NSString *) stringHost:(NSURL *)url } @end + diff --git a/ios/OAuthManager/XMLReader/README.md b/ios/OAuthManager/XMLReader/README.md new file mode 100755 index 0000000..634d324 --- /dev/null +++ b/ios/OAuthManager/XMLReader/README.md @@ -0,0 +1,77 @@ +# XMLReader + +This project comes from a component developed by Troy Brant and published on his website : http://troybrant.net/blog/2010/09/simple-xml-to-nsdictionary-converter/ + +I'm open sourcing some of the updates I've made on it. + + +## Usage + + NSData *data = ...; // some data that can be received from remote service + NSError *error = nil; + NSDictionary *dict = [XMLReader dictionaryForXMLData:data + options:XMLReaderOptionsProcessNamespaces + error:&error]; + + +## Requirements + +Xcode 4.4 and above because project use the "auto-synthesized property" feature. + + +## FAQ + +#### Sometimes I get an `NSDictionary` while I must get an `NSArray`, why ? + +In the algorithm of the `XMLReader`, when the parser found a new tag it automatically creates an `NSDictionary`, if it found another occurrence of the same tag at the same level in the XML tree it creates another dictionary and put both dictionaries inside an `NSArray`. + +The consequence is: if you have a list that contains only one item, you will get an `NSDictionary` as result and not an `NSArray`. +The only workaround is to check the class of the object contained for in the dictionary using `isKindOfClass:`. See sample code below : + + NSData *data = ...; + NSError *error = nil; + NSDictionary *dict = [XMLReader dictionaryForXMLData:data error:&error]; + + NSArray *list = [dict objectForKey:@"list"]; + if (![list isKindOfClass:[NSArray class]]) + { + // if 'list' isn't an array, we create a new array containing our object + list = [NSArray arrayWithObject:list]; + } + + // we can loop through items safely now + for (NSDictionary *item in list) + { + // ... + } + + +#### I don't have enable ARC on my project, how can I use your library ? + +You have 2 options: + +* Use the branch "[no-objc-arc](https://github.com/amarcadet/XMLReader/tree/no-objc-arc)" that use manual reference counting. +* **Better choice:** add the "-fobjc-arc" compiler flag on `XMLReader.m` file in your build phases. + +#### I have trust issues, I don't want ARC, I prefer MRC, what can I do ? + +Well, nobody is perfect but, still, you can use the branch "[no-objc-arc](https://github.com/amarcadet/XMLReader/tree/no-objc-arc)". + + +## Contributions + +Thanks to the original author of this component Troy Brant and to [Divan "snip3r8" Visagie](https://github.com/snip3r8) for providing ARC support. + + +## License + +Copyright (C) 2012 Antoine Marcadet + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/amarcadet/XMLReader/trend.png)](https://bitdeli.com/free "Bitdeli Badge") + diff --git a/ios/OAuthManager/XMLReader/XMLReader.h b/ios/OAuthManager/XMLReader/XMLReader.h new file mode 100755 index 0000000..aeca0b8 --- /dev/null +++ b/ios/OAuthManager/XMLReader/XMLReader.h @@ -0,0 +1,25 @@ +// +// XMLReader.h +// +// Created by Troy Brant on 9/18/10. +// Updated by Antoine Marcadet on 9/23/11. +// Updated by Divan Visagie on 2012-08-26 +// + +#import + +enum { + XMLReaderOptionsProcessNamespaces = 1 << 0, // Specifies whether the receiver reports the namespace and the qualified name of an element. + XMLReaderOptionsReportNamespacePrefixes = 1 << 1, // Specifies whether the receiver reports the scope of namespace declarations. + XMLReaderOptionsResolveExternalEntities = 1 << 2, // Specifies whether the receiver reports declarations of external entities. +}; +typedef NSUInteger XMLReaderOptions; + +@interface XMLReader : NSObject + ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data error:(NSError **)errorPointer; ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string error:(NSError **)errorPointer; ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data options:(XMLReaderOptions)options error:(NSError **)errorPointer; ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string options:(XMLReaderOptions)options error:(NSError **)errorPointer; + +@end diff --git a/ios/OAuthManager/XMLReader/XMLReader.m b/ios/OAuthManager/XMLReader/XMLReader.m new file mode 100755 index 0000000..754c95a --- /dev/null +++ b/ios/OAuthManager/XMLReader/XMLReader.m @@ -0,0 +1,176 @@ +// +// XMLReader.m +// +// Created by Troy Brant on 9/18/10. +// Updated by Antoine Marcadet on 9/23/11. +// Updated by Divan Visagie on 2012-08-26 +// + +#import "XMLReader.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "XMLReader requires ARC support." +#endif + +NSString *const kXMLReaderTextNodeKey = @"text"; +NSString *const kXMLReaderAttributePrefix = @"@"; + +@interface XMLReader () + +@property (nonatomic, strong) NSMutableArray *dictionaryStack; +@property (nonatomic, strong) NSMutableString *textInProgress; +@property (nonatomic, strong) NSError *errorPointer; + +@end + + +@implementation XMLReader + +#pragma mark - Public methods + ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data error:(NSError **)error +{ + XMLReader *reader = [[XMLReader alloc] initWithError:error]; + NSDictionary *rootDictionary = [reader objectWithData:data options:0]; + return rootDictionary; +} + ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string error:(NSError **)error +{ + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [XMLReader dictionaryForXMLData:data error:error]; +} + ++ (NSDictionary *)dictionaryForXMLData:(NSData *)data options:(XMLReaderOptions)options error:(NSError **)error +{ + XMLReader *reader = [[XMLReader alloc] initWithError:error]; + NSDictionary *rootDictionary = [reader objectWithData:data options:options]; + return rootDictionary; +} + ++ (NSDictionary *)dictionaryForXMLString:(NSString *)string options:(XMLReaderOptions)options error:(NSError **)error +{ + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [XMLReader dictionaryForXMLData:data options:options error:error]; +} + + +#pragma mark - Parsing + +- (id)initWithError:(NSError **)error +{ + self = [super init]; + if (self) + { + self.errorPointer = *error; + } + return self; +} + +- (NSDictionary *)objectWithData:(NSData *)data options:(XMLReaderOptions)options +{ + // Clear out any old data + self.dictionaryStack = [[NSMutableArray alloc] init]; + self.textInProgress = [[NSMutableString alloc] init]; + + // Initialize the stack with a fresh dictionary + [self.dictionaryStack addObject:[NSMutableDictionary dictionary]]; + + // Parse the XML + NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; + + [parser setShouldProcessNamespaces:(options & XMLReaderOptionsProcessNamespaces)]; + [parser setShouldReportNamespacePrefixes:(options & XMLReaderOptionsReportNamespacePrefixes)]; + [parser setShouldResolveExternalEntities:(options & XMLReaderOptionsResolveExternalEntities)]; + + parser.delegate = self; + BOOL success = [parser parse]; + + // Return the stack's root dictionary on success + if (success) + { + NSDictionary *resultDict = [self.dictionaryStack objectAtIndex:0]; + return resultDict; + } + + return nil; +} + + +#pragma mark - NSXMLParserDelegate methods + +- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict +{ + // Get the dictionary for the current level in the stack + NSMutableDictionary *parentDict = [self.dictionaryStack lastObject]; + + // Create the child dictionary for the new element, and initilaize it with the attributes + NSMutableDictionary *childDict = [NSMutableDictionary dictionary]; + [childDict addEntriesFromDictionary:attributeDict]; + + // If there's already an item for this key, it means we need to create an array + id existingValue = [parentDict objectForKey:elementName]; + if (existingValue) + { + NSMutableArray *array = nil; + if ([existingValue isKindOfClass:[NSMutableArray class]]) + { + // The array exists, so use it + array = (NSMutableArray *) existingValue; + } + else + { + // Create an array if it doesn't exist + array = [NSMutableArray array]; + [array addObject:existingValue]; + + // Replace the child dictionary with an array of children dictionaries + [parentDict setObject:array forKey:elementName]; + } + + // Add the new child dictionary to the array + [array addObject:childDict]; + } + else + { + // No existing value, so update the dictionary + [parentDict setObject:childDict forKey:elementName]; + } + + // Update the stack + [self.dictionaryStack addObject:childDict]; +} + +- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName +{ + // Update the parent dict with text info + NSMutableDictionary *dictInProgress = [self.dictionaryStack lastObject]; + + // Set the text property + if ([self.textInProgress length] > 0) + { + // trim after concatenating + NSString *trimmedString = [self.textInProgress stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [dictInProgress setObject:[trimmedString mutableCopy] forKey:kXMLReaderTextNodeKey]; + + // Reset the text + self.textInProgress = [[NSMutableString alloc] init]; + } + + // Pop the current dict + [self.dictionaryStack removeLastObject]; +} + +- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string +{ + // Build the text value + [self.textInProgress appendString:string]; +} + +- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError +{ + // Set the error pointer to the parser's error object + self.errorPointer = parseError; +} + +@end diff --git a/lib/authProviders.js b/lib/authProviders.js index bba8788..21601b1 100644 --- a/lib/authProviders.js +++ b/lib/authProviders.js @@ -49,9 +49,42 @@ export const authProviders = { auth_version: "2.0", authorize_url: 'https://accounts.google.com/o/oauth2/auth', access_token_url: 'https://accounts.google.com/o/oauth2/token', - callback_url: ({app_name}) => `${app_name}:/oauth-response`, + callback_url: ({app_name}) => `${app_name}://oauth-response`, validate: validate() - } + }, + 'github': { + auth_version: '2.0', + authorize_url: 'https://github.com/login/oauth/authorize', + access_token_url: 'https://github.com/login/oauth/access_token', + api_url: 'https://api.github.com', + callback_url: ({app_name}) => `${app_name}://oauth`, + validate: validate() + }, + 'slack': { + auth_version: '2.0', + authorize_url: 'https://slack.com/oauth/authorize', + access_token_url: 'https://slack.com/api/oauth.access', + api_url: 'https://slack.com/api', + callback_url: ({app_name}) => `${app_name}://oauth`, + defaultParams: { + token: 'access_token' + }, + validate: validate({ + client_id: [notEmpty], + client_secret: [notEmpty] + }) + }, + 'spotify': { + auth_version: "2.0", + authorize_url: 'https://accounts.spotify.com/authorize', + api_url: 'https://api.spotify.com/', + callback_url: ({app_name}) => `${app_name}://authorize`, + + validate: validate({ + client_id: [notEmpty], + client_secret: [notEmpty] + }) + }, } -export default authProviders; \ No newline at end of file +export default authProviders; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b3af007 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,801 @@ +{ + "name": "react-native-oauth", + "version": "2.1.18", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + } + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-preset-env": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", + "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-classes": "^6.23.0", + "babel-plugin-transform-es2015-computed-properties": "^6.22.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-es2015-function-name": "^6.22.0", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-umd": "^6.23.0", + "babel-plugin-transform-es2015-object-super": "^6.22.0", + "babel-plugin-transform-es2015-parameters": "^6.23.0", + "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", + "babel-plugin-transform-regenerator": "^6.22.0", + "browserslist": "^3.2.6", + "invariant": "^2.2.2", + "semver": "^5.3.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000844", + "electron-to-chromium": "^1.3.47" + } + }, + "caniuse-lite": { + "version": "1.0.30000865", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz", + "integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "electron-to-chromium": { + "version": "1.3.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz", + "integrity": "sha1-0tnxJwuko7lnuDHEDvcftNmrXOA=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "valib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/valib/-/valib-2.0.0.tgz", + "integrity": "sha1-4NRVsQ4XrmBcDzOXGtrHNWTuGKc=" + } + } +} diff --git a/package.json b/package.json index 79aedfc..453f8c4 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "react-native-oauth", - "version": "2.0.1", + "version": "2.1.18", "author": "Ari Lerner (https://fullstackreact.com)", "description": "An oauth manager for dealing with the complexities of oauth", "main": "./react-native-oauth.js", "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", - "build": "./node_modules/.bin/babel --source-maps=true --out-dir=dist .", + "build": "./node_modules/.bin/babel --ignore 'node_modules,dist' --source-maps=true --out-dir=dist .", "dev": "npm run compile -- --watch", "lint": "eslint ./src", "publish_pages": "gh-pages -d public/", @@ -45,5 +45,12 @@ "dependencies": { "invariant": "^2.2.1", "valib": "^2.0.0" + }, + "devDependencies": { + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-env": "^1.7.0", + "babel-runtime": "^6.26.0" } } diff --git a/react-native-oauth.js b/react-native-oauth.js index db4f6e2..3008cf0 100644 --- a/react-native-oauth.js +++ b/react-native-oauth.js @@ -13,7 +13,9 @@ const OAuthManagerBridge = NativeModules.OAuthManager; let configured = false; const STORAGE_KEY = 'ReactNativeOAuth'; import promisify from './lib/promisify' -import authProviders from './lib/authProviders'; +import defaultProviders from './lib/authProviders'; + +let authProviders = defaultProviders; const identity = (props) => props; /** @@ -27,6 +29,10 @@ export default class OAuthManager { this._options = opts; } + addProvider(provider) { + authProviders = Object.assign({}, authProviders, provider); + } + configure(providerConfigs) { return this.configureProviders(providerConfigs) } @@ -45,7 +51,7 @@ export default class OAuthManager { // return promisify('getSavedAccounts')(options); const promises = this.providers() .map(name => { - return this.savedAccount(name, opts) + return this.savedAccount(name) .catch(err => ({provider: name, status: "error"})); }); return Promise.all(promises) @@ -55,8 +61,8 @@ export default class OAuthManager { }); } - savedAccount(provider, opts={}) { - const options = Object.assign({}, this._options, opts, { + savedAccount(provider) { + const options = Object.assign({}, this._options, { app_name: this.appName }) return promisify('getSavedAccount')(provider, options); @@ -67,6 +73,8 @@ export default class OAuthManager { app_name: this.appName }); + console.log('making request', provider, url, opts); + return promisify('makeRequest')(provider, url, options) .then(response => { // Little bit of a hack to support Android until we have a better @@ -97,11 +105,11 @@ export default class OAuthManager { // Private /** * Configure a single provider - * - * + * + * * @param {string} name of the provider * @param {object} additional configuration - * + * **/ configureProvider(name, props) { invariant(OAuthManager.isSupported(name), `The provider ${name} is not supported yet`); @@ -116,13 +124,18 @@ export default class OAuthManager { callback_url }, providerCfg, props); + if (config.defaultParams) { + delete config.defaultParams; + } + config = Object.keys(config) .reduce((sum, key) => ({ ...sum, [key]: typeof config[key] === 'function' ? config[key](config) : config[key] - }), {}) + }), {}); validate(config); + return promisify('configureProvider')(name, config); } diff --git a/resources/capabilities.png b/resources/capabilities.png new file mode 100644 index 0000000..9dfca7c Binary files /dev/null and b/resources/capabilities.png differ diff --git a/resources/facebook/facebook-redirect.png b/resources/facebook/facebook-redirect.png new file mode 100644 index 0000000..57a6cd0 Binary files /dev/null and b/resources/facebook/facebook-redirect.png differ diff --git a/resources/github/apps.png b/resources/github/apps.png new file mode 100644 index 0000000..7920910 Binary files /dev/null and b/resources/github/apps.png differ diff --git a/resources/google/android-creds.png b/resources/google/android-creds.png new file mode 100644 index 0000000..4da2ef2 Binary files /dev/null and b/resources/google/android-creds.png differ diff --git a/resources/google/creds.png b/resources/google/creds.png index 966478e..e089e5a 100644 Binary files a/resources/google/creds.png and b/resources/google/creds.png differ diff --git a/resources/slack/create.png b/resources/slack/create.png new file mode 100644 index 0000000..46c4fc5 Binary files /dev/null and b/resources/slack/create.png differ diff --git a/resources/slack/creds.png b/resources/slack/creds.png new file mode 100644 index 0000000..4126720 Binary files /dev/null and b/resources/slack/creds.png differ diff --git a/resources/slack/dev.png b/resources/slack/dev.png new file mode 100644 index 0000000..63f1120 Binary files /dev/null and b/resources/slack/dev.png differ diff --git a/resources/slack/getting_started.png b/resources/slack/getting_started.png new file mode 100644 index 0000000..fc2b880 Binary files /dev/null and b/resources/slack/getting_started.png differ diff --git a/resources/slack/redirect.png b/resources/slack/redirect.png new file mode 100644 index 0000000..86e40ae Binary files /dev/null and b/resources/slack/redirect.png differ diff --git a/resources/twitter/callback-url.png b/resources/twitter/callback-url.png new file mode 100644 index 0000000..8ac2d7b Binary files /dev/null and b/resources/twitter/callback-url.png differ