diff --git a/README.md b/README.md index 1c458d0a..1cd56a6e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ __Features__ - __Effective__. Faster checkouts that increase conversion. - __Future-proof__. Use a W3C Standards API, supported by companies like Google, Firefox and others. - __Cross-platform.__ Share payments code between your iOS and web apps. -- __Payment Processor Support__. Process payments with payment processors like Stripe. +- __Payment Processor Support__. Process payments with payment processors like Braintree and Stripe. @@ -330,6 +330,7 @@ paymentRequest.show() ## Payment Processors - [Stripe](#stripe) +- [Braintree](#braintree) ### Stripe #### Creating an Apple Pay certificate @@ -343,9 +344,9 @@ Finally, in Xcode: 2. Select `Build Settings` and search for `Framework Search Paths` 3. Then add the path to where you added the Framework (remember, it's relative to `/node_modules/react-native-payments/lib/ios`) -screen shot 2017-07-16 at 11 11 13 am +xcode-stripe -#### Adding your Stripe Tokens +#### Adding your Stripe Config Now that you've added Stripe's SDK to your app, you're setup to use Stripe as a payment processor. In order to do so, you'll need to define a `paymentMethodTokenizationParameters` on your `PaymentMethodData` with Stripe specific parameters. Here's an example of what Stripe `paramaters` look like: @@ -372,6 +373,48 @@ In order to do so, you'll need to define a `paymentMethodTokenizationParameters` Now you're all set to receive Stripe payment tokens in your `PaymentResponse`. +### Braintree +#### Creating an Apple Pay certificate +Follow Braintree's [documentation](https://developers.braintreepayments.com/guides/apple-pay/configuration/ios/v4#apple-pay-certificate-request-and-provisioning) on how to create and upload the Apple Pay certificate back to Braintree. + +#### Adding and Linking the Braintree SDK +Next, you'll need to add Braintree's SDK to your project. You can install it by following one of the methods [listed in Braintree's documentation](https://github.com/braintree/braintree_ios#installation). + +Finally, in Xcode: +1. Select the `ReactNativePayments` project from the left sidebar (under Libraries) +2. Select `Build Settings` and search for `Header Search Paths` +3. Then add the path to where you added the Library (remember, it's relative to `/node_modules/react-native-payments/lib/ios`) + +xcode-braintree + + +#### Adding your Braintree Config +Now that you've added Braintree's SDK to your app, you're setup to use Braintree as a payment processor. + +In order to do so, you'll need to define a `paymentMethodTokenizationParameters` on your `PaymentMethodData` with Braintree specific parameters. Here's an example of what Braintree `paramaters` look like: + +```diff + const supportedMethods = [ + { + supportedMethods: ['apple-pay'], + data: { + merchantIdentifier: 'merchant.com.your-app.namespace', + supportedNetworks: ['visa', 'mastercard'], + countryCode: 'US', + currencyCode: 'USD', ++ paymentMethodTokenizationParameters: { ++ parameters: { ++ 'gateway': 'braintree', ++ 'braintree:tokenizationKey': 'your-tokenization-key' ++ } ++ } + } + } + ]; +``` + +Now you're all set to receive Braintree payment tokens in your `PaymentResponse`. + ## API ### [PaymentRequest](https://github.com/naoufal/react-native-payments/tree/master/docs/PaymentRequest.md) ### [PaymentRequestUpdateEvent](https://github.com/naoufal/react-native-payments/tree/master/docs/PaymentRequestUpdateEvent.md) @@ -396,6 +439,11 @@ Now you're all set to receive Stripe payment tokens in your `PaymentResponse`. - [Creating a new Apple Pay certificate](https://stripe.com/docs/apple-pay/apps#csr) - [Installing the Stripe SDK](https://stripe.com/docs/mobile/ios#getting-started) +#### Braintree +- [Creating a new Apple Pay certificate](https://developers.braintreepayments.com/guides/apple-pay/configuration/ios/v4#apple-pay-certificate-request-and-provisioning) +- [Installing the Braintree SDK](https://github.com/braintree/braintree_ios#installation) + + # License Licensed under the MIT License, Copyright © 2017, [Naoufal Kadhom](https://twitter.com/naoufal). diff --git a/examples/braintree/.babelrc b/examples/braintree/.babelrc new file mode 100644 index 00000000..8df53fe4 --- /dev/null +++ b/examples/braintree/.babelrc @@ -0,0 +1,3 @@ +{ +"presets": ["react-native"] +} \ No newline at end of file diff --git a/examples/braintree/.buckconfig b/examples/braintree/.buckconfig new file mode 100644 index 00000000..934256cb --- /dev/null +++ b/examples/braintree/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/examples/braintree/.flowconfig b/examples/braintree/.flowconfig new file mode 100644 index 00000000..b38ea97e --- /dev/null +++ b/examples/braintree/.flowconfig @@ -0,0 +1,44 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +.*/Libraries/react-native/React.js +.*/Libraries/react-native/ReactNative.js + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow +flow/ + +[options] +module.system=haste + +experimental.strict_type_args=true + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-7]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-7]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy + +unsafe.enable_getters_and_setters=true + +[version] +^0.37.0 diff --git a/examples/braintree/.gitattributes b/examples/braintree/.gitattributes new file mode 100644 index 00000000..d42ff183 --- /dev/null +++ b/examples/braintree/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/examples/braintree/.gitignore b/examples/braintree/.gitignore new file mode 100644 index 00000000..989e1e64 --- /dev/null +++ b/examples/braintree/.gitignore @@ -0,0 +1,59 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +*.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots + +# Haul +# +haul-debug.log +.happypack diff --git a/examples/braintree/.watchmanconfig b/examples/braintree/.watchmanconfig new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/examples/braintree/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/braintree/__tests__/index.android.js b/examples/braintree/__tests__/index.android.js new file mode 100644 index 00000000..a49559bf --- /dev/null +++ b/examples/braintree/__tests__/index.android.js @@ -0,0 +1,10 @@ +import 'react-native'; +import React from 'react'; +import Index from '../index.android.js'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; + +it('renders correctly', () => { + const tree = renderer.create(); +}); diff --git a/examples/braintree/__tests__/index.ios.js b/examples/braintree/__tests__/index.ios.js new file mode 100644 index 00000000..a21e84c1 --- /dev/null +++ b/examples/braintree/__tests__/index.ios.js @@ -0,0 +1,10 @@ +import 'react-native'; +import React from 'react'; +import Index from '../index.ios.js'; + +// Note: test renderer must be required after react-native. +import renderer from 'react-test-renderer'; + +it('renders correctly', () => { + const tree = renderer.create(); +}); diff --git a/examples/braintree/android/app/BUCK b/examples/braintree/android/app/BUCK new file mode 100644 index 00000000..a9fd5dd4 --- /dev/null +++ b/examples/braintree/android/app/BUCK @@ -0,0 +1,66 @@ +import re + +# To learn about Buck see [Docs](https://buckbuild.com/). +# To run your application with Buck: +# - install Buck +# - `npm start` - to start the packager +# - `cd android` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` +# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck +# - `buck install -r android/app` - compile, install and run application +# + +lib_deps = [] +for jarfile in glob(['libs/*.jar']): + name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) + lib_deps.append(':' + name) + prebuilt_jar( + name = name, + binary_jar = jarfile, + ) + +for aarfile in glob(['libs/*.aar']): + name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) + lib_deps.append(':' + name) + android_prebuilt_aar( + name = name, + aar = aarfile, + ) + +android_library( + name = 'all-libs', + exported_deps = lib_deps +) + +android_library( + name = 'app-code', + srcs = glob([ + 'src/main/java/**/*.java', + ]), + deps = [ + ':all-libs', + ':build_config', + ':res', + ], +) + +android_build_config( + name = 'build_config', + package = 'com.braintreeexample', +) + +android_resource( + name = 'res', + res = 'src/main/res', + package = 'com.braintreeexample', +) + +android_binary( + name = 'app', + package_type = 'debug', + manifest = 'src/main/AndroidManifest.xml', + keystore = '//android/keystores:debug', + deps = [ + ':app-code', + ], +) diff --git a/examples/braintree/android/app/build.gradle b/examples/braintree/android/app/build.gradle new file mode 100644 index 00000000..c9e8581f --- /dev/null +++ b/examples/braintree/android/app/build.gradle @@ -0,0 +1,139 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"] + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +apply from: "../../node_modules/react-native/react.gradle" + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = false + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.braintreeexample" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits + def versionCodes = ["armeabi-v7a":1, "x86":2] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + } + } +} + +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:+" // From node_modules +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} diff --git a/examples/braintree/android/app/proguard-rules.pro b/examples/braintree/android/app/proguard-rules.pro new file mode 100644 index 00000000..48361a90 --- /dev/null +++ b/examples/braintree/android/app/proguard-rules.pro @@ -0,0 +1,66 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Disabling obfuscation is useful if you collect stack traces from production crashes +# (unless you are using a system that supports de-obfuscate the stack traces). +-dontobfuscate + +# React Native + +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip +-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters +-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip + +# Do not strip any method/class that is annotated with @DoNotStrip +-keep @com.facebook.proguard.annotations.DoNotStrip class * +-keep @com.facebook.common.internal.DoNotStrip class * +-keepclassmembers class * { + @com.facebook.proguard.annotations.DoNotStrip *; + @com.facebook.common.internal.DoNotStrip *; +} + +-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { + void set*(***); + *** get*(); +} + +-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } +-keep class * extends com.facebook.react.bridge.NativeModule { *; } +-keepclassmembers,includedescriptorclasses class * { native ; } +-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } + +-dontwarn com.facebook.react.** + +# okhttp + +-keepattributes Signature +-keepattributes *Annotation* +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** + +# okio + +-keep class sun.misc.Unsafe { *; } +-dontwarn java.nio.file.* +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn okio.** diff --git a/examples/braintree/android/app/src/main/AndroidManifest.xml b/examples/braintree/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0426380a --- /dev/null +++ b/examples/braintree/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/examples/braintree/android/app/src/main/java/com/braintreeexample/MainActivity.java b/examples/braintree/android/app/src/main/java/com/braintreeexample/MainActivity.java new file mode 100644 index 00000000..84efe53b --- /dev/null +++ b/examples/braintree/android/app/src/main/java/com/braintreeexample/MainActivity.java @@ -0,0 +1,15 @@ +package com.braintreeexample; + +import com.facebook.react.ReactActivity; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "BraintreeExample"; + } +} diff --git a/examples/braintree/android/app/src/main/java/com/braintreeexample/MainApplication.java b/examples/braintree/android/app/src/main/java/com/braintreeexample/MainApplication.java new file mode 100644 index 00000000..b0934bc9 --- /dev/null +++ b/examples/braintree/android/app/src/main/java/com/braintreeexample/MainApplication.java @@ -0,0 +1,42 @@ +package com.braintreeexample; + +import android.app.Application; +import android.util.Log; + +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; +import com.facebook.soloader.SoLoader; + +import java.util.Arrays; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } + + @Override + public void onCreate() { + super.onCreate(); + SoLoader.init(this, /* native exopackage */ false); + } +} diff --git a/examples/braintree/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/braintree/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cde69bcc Binary files /dev/null and b/examples/braintree/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/braintree/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/braintree/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c133a0cb Binary files /dev/null and b/examples/braintree/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/braintree/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/braintree/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bfa42f0e Binary files /dev/null and b/examples/braintree/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/braintree/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/braintree/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..324e72cd Binary files /dev/null and b/examples/braintree/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/braintree/android/app/src/main/res/values/strings.xml b/examples/braintree/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..3f66f6e9 --- /dev/null +++ b/examples/braintree/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + BraintreeExample + diff --git a/examples/braintree/android/app/src/main/res/values/styles.xml b/examples/braintree/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..319eb0ca --- /dev/null +++ b/examples/braintree/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/examples/braintree/android/build.gradle b/examples/braintree/android/build.gradle new file mode 100644 index 00000000..fcba4c58 --- /dev/null +++ b/examples/braintree/android/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + } +} diff --git a/examples/braintree/android/gradle.properties b/examples/braintree/android/gradle.properties new file mode 100644 index 00000000..1fd964e9 --- /dev/null +++ b/examples/braintree/android/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +android.useDeprecatedNdk=true diff --git a/examples/braintree/android/gradle/wrapper/gradle-wrapper.jar b/examples/braintree/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..b5166dad Binary files /dev/null and b/examples/braintree/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/braintree/android/gradle/wrapper/gradle-wrapper.properties b/examples/braintree/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b9fbfaba --- /dev/null +++ b/examples/braintree/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/examples/braintree/android/gradlew b/examples/braintree/android/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/examples/braintree/android/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/examples/braintree/android/gradlew.bat b/examples/braintree/android/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/examples/braintree/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/braintree/android/keystores/BUCK b/examples/braintree/android/keystores/BUCK new file mode 100644 index 00000000..15da20e6 --- /dev/null +++ b/examples/braintree/android/keystores/BUCK @@ -0,0 +1,8 @@ +keystore( + name = 'debug', + store = 'debug.keystore', + properties = 'debug.keystore.properties', + visibility = [ + 'PUBLIC', + ], +) diff --git a/examples/braintree/android/keystores/debug.keystore.properties b/examples/braintree/android/keystores/debug.keystore.properties new file mode 100644 index 00000000..121bfb49 --- /dev/null +++ b/examples/braintree/android/keystores/debug.keystore.properties @@ -0,0 +1,4 @@ +key.store=debug.keystore +key.alias=androiddebugkey +key.store.password=android +key.alias.password=android diff --git a/examples/braintree/android/settings.gradle b/examples/braintree/android/settings.gradle new file mode 100644 index 00000000..ed24c202 --- /dev/null +++ b/examples/braintree/android/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'BraintreeExample' + +include ':app' diff --git a/examples/braintree/index.android.js b/examples/braintree/index.android.js new file mode 100644 index 00000000..fb7524af --- /dev/null +++ b/examples/braintree/index.android.js @@ -0,0 +1,48 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * @flow + */ + +import React, { Component } from 'react'; +import { AppRegistry, StyleSheet, Text, View } from 'react-native'; + +export default class BraintreeExample extends Component { + render() { + return ( + + + Welcome to React Native! + + + To get started, edit index.android.js + + + Double tap R on your keyboard to reload,{'\n'} + Shake or press menu button for dev menu + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF' + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10 + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 5 + } +}); + +AppRegistry.registerComponent('BraintreeExample', () => BraintreeExample); diff --git a/examples/braintree/index.ios.js b/examples/braintree/index.ios.js new file mode 100644 index 00000000..f222c913 --- /dev/null +++ b/examples/braintree/index.ios.js @@ -0,0 +1,103 @@ +import React, { Component } from 'react'; +import { AppRegistry, StyleSheet, Text, View, Button } from 'react-native'; + +global.PaymentRequest = require('react-native-payments').PaymentRequest; +const ReactNativePaymentsVersion = require('react-native-payments/package.json') + .version; + +import Header from '../common/components/Header'; + +export default class BraintreeExample extends Component { + constructor() { + super(); + + this.state = { + text: null + }; + } + + handlePress() { + const supportedMethods = [ + { + supportedMethods: ['apple-pay'], + data: { + merchantIdentifier: 'merchant.com.react-native-payments.naoufal', + supportedNetworks: ['visa', 'mastercard'], + countryCode: 'US', + currencyCode: 'USD', + paymentMethodTokenizationParameters: { + parameters: { + gateway: 'braintree', + 'braintree:tokenizationKey': 'sandbox_np7393pq_sh6czsvsq9nvjc3j' + } + } + } + } + ]; + + const details = { + id: 'basic-example', + displayItems: [ + { + label: 'Movie Ticket', + amount: { currency: 'USD', value: '15.00' } + } + ], + total: { + label: 'Merchant Name', + amount: { currency: 'USD', value: '15.00' } + } + }; + + const pr = new PaymentRequest(supportedMethods, details); + + pr + .show() + .then(paymentResponse => { + this.setState({ + text: paymentResponse.details.paymentToken + }); + + paymentResponse.complete('success'); + }) + .catch(e => { + pr.abort(); + + this.setState({ + text: e.message + }); + }); + } + + render() { + return ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.h new file mode 100755 index 00000000..799b8e3c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.h @@ -0,0 +1,9 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@class BTThreeDSecureDriver; + +@interface BraintreeDemoThreeDSecureViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.m new file mode 100755 index 00000000..f20570c3 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Credit Cards - 3D Secure/BraintreeDemoThreeDSecureViewController.m @@ -0,0 +1,126 @@ +#import "BraintreeDemoThreeDSecureViewController.h" +#import "ALView+PureLayout.h" + +#import +#import + +@interface BraintreeDemoThreeDSecureViewController () +@property (nonatomic, strong) BTUICardFormView *cardFormView; +@property (nonatomic, strong) UILabel *callbackCountLabel; +@property (nonatomic) int callbackCount; +@end + +@implementation BraintreeDemoThreeDSecureViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"3D Secure"; + + self.cardFormView = [[BTUICardFormView alloc] initForAutoLayout]; + self.cardFormView.optionalFields = BTUICardFormOptionalFieldsNone; + [self.view addSubview:self.cardFormView]; + [self.cardFormView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [self.cardFormView autoPinEdgeToSuperviewEdge:ALEdgeLeft]; + [self.cardFormView autoPinEdgeToSuperviewEdge:ALEdgeRight]; +} + +- (UIView *)createPaymentButton { + UIButton *verifyNewCardButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [verifyNewCardButton setTitle:@"Tokenize and Verify New Card" forState:UIControlStateNormal]; + [verifyNewCardButton addTarget:self action:@selector(tappedToVerifyNewCard) forControlEvents:UIControlEventTouchUpInside]; + + UIView *threeDSecureButtonsContainer = [[UIView alloc] initForAutoLayout]; + [threeDSecureButtonsContainer addSubview:verifyNewCardButton]; + + [verifyNewCardButton autoPinEdgeToSuperviewEdge:ALEdgeTop]; + + [verifyNewCardButton autoAlignAxisToSuperviewMarginAxis:ALAxisVertical]; + + self.callbackCountLabel = [[UILabel alloc] initForAutoLayout]; + self.callbackCountLabel.textAlignment = NSTextAlignmentCenter; + self.callbackCountLabel.font = [UIFont systemFontOfSize:UIFont.smallSystemFontSize]; + [threeDSecureButtonsContainer addSubview:self.callbackCountLabel]; + [self.callbackCountLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:verifyNewCardButton withOffset:20]; + [self.callbackCountLabel autoPinEdgeToSuperviewEdge:ALEdgeLeft]; + [self.callbackCountLabel autoPinEdgeToSuperviewEdge:ALEdgeRight]; + self.callbackCount = 0; + [self updateCallbackCount]; + + return threeDSecureButtonsContainer; +} + +- (BTCard *)newCard { + BTCard *card = [[BTCard alloc] init]; + if (self.cardFormView.valid && + self.cardFormView.number && + self.cardFormView.expirationMonth && + self.cardFormView.expirationYear) { + card.number = self.cardFormView.number; + card.expirationMonth = self.cardFormView.expirationMonth; + card.expirationYear = self.cardFormView.expirationYear; + } else { + [self.cardFormView showTopLevelError:@"Not valid. Using default 3DS test card..."]; + card.number = @"4000000000000002"; + card.expirationMonth = @"12"; + card.expirationYear = @"2020"; + } + return card; +} + +- (void)updateCallbackCount { + self.callbackCountLabel.text = [NSString stringWithFormat:@"Callback Count: %i", self.callbackCount]; +} + +/// "Tokenize and Verify New Card" +- (void)tappedToVerifyNewCard { + self.callbackCount = 0; + [self updateCallbackCount]; + + BTCard *card = [self newCard]; + + self.progressBlock([NSString stringWithFormat:@"Tokenizing card ending in %@", [card.number substringFromIndex:(card.number.length - 4)]]); + + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:self.apiClient]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + + if (error) { + self.progressBlock(error.localizedDescription); + return; + } + + self.progressBlock(@"Tokenized card, now verifying with 3DS"); + + BTThreeDSecureDriver *threeDSecure = [[BTThreeDSecureDriver alloc] initWithAPIClient:self.apiClient delegate:self]; + + [threeDSecure verifyCardWithNonce:tokenizedCard.nonce + amount:[NSDecimalNumber decimalNumberWithString:@"10"] + completion:^(BTThreeDSecureCardNonce * _Nullable threeDSecureCard, NSError * _Nullable error) + { + self.callbackCount++; + [self updateCallbackCount]; + if (error) { + self.progressBlock(error.localizedDescription); + } else if (threeDSecureCard) { + self.completionBlock(threeDSecureCard); + + if (threeDSecureCard.liabilityShiftPossible && threeDSecureCard.liabilityShifted) { + self.progressBlock(@"Liability shift possible and liability shifted"); + } else { + self.progressBlock(@"3D Secure authentication was attempted but liability shift is not possible"); + } + } else { + self.progressBlock(@"Cancelled🎲"); + } + }]; + }]; +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(__unused UIViewController *)viewController { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.h new file mode 100755 index 00000000..dd74f490 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoBaseViewController.h" + +@interface BraintreeDemoDropInLegacyViewController : BraintreeDemoBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.m new file mode 100755 index 00000000..4382775b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In Old/BraintreeDemoDropInLegacyViewController.m @@ -0,0 +1,97 @@ +#import "BraintreeDemoDropInLegacyViewController.h" + +#import +#import +#import +#import +#import "BraintreeDemoSettings.h" + +@interface BraintreeDemoDropInLegacyViewController () + +@property (nonatomic, strong) BTAPIClient *apiClient; + +@end + +@implementation BraintreeDemoDropInLegacyViewController + +- (instancetype)initWithAuthorization:(NSString *)authorization { + if (self = [super initWithAuthorization:authorization]) { + _apiClient = [[BTAPIClient alloc] initWithAuthorization:authorization]; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = @"Drop In (Legacy)"; + + UIButton *dropInButton = [UIButton buttonWithType:UIButtonTypeSystem]; + dropInButton.translatesAutoresizingMaskIntoConstraints = NO; + [dropInButton addTarget:self action:@selector(tappedToShowDropIn) forControlEvents:UIControlEventTouchUpInside]; + [dropInButton setBackgroundColor:[UIColor redColor]]; + [dropInButton setTitleColor:[UIColor whiteColor]forState:UIControlStateNormal]; + dropInButton.layer.cornerRadius = 5.0f; + dropInButton.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 8, 8); + [dropInButton setTitle:@"Buy Now" forState:UIControlStateNormal]; + [dropInButton sizeToFit]; + + [self.view addSubview:dropInButton]; + [dropInButton autoCenterInSuperview]; + + self.progressBlock(@"Ready to present Drop In (Old)"); +} + +- (void)tappedToShowDropIn { + BTPaymentRequest *paymentRequest = [[BTPaymentRequest alloc] init]; + paymentRequest.summaryTitle = @"Our Fancy Magazine"; + paymentRequest.summaryDescription = @"53 Week Subscription"; + paymentRequest.displayAmount = @"$19.00"; + paymentRequest.callToActionText = @"$19 - Subscribe Now"; + paymentRequest.shouldHideCallToAction = NO; + BTDropInViewController *dropIn = [[BTDropInViewController alloc] initWithAPIClient:self.apiClient]; + dropIn.delegate = self; + dropIn.paymentRequest = paymentRequest; + dropIn.title = @"Check Out"; + + if ([BraintreeDemoSettings useModalPresentation]) { + self.progressBlock(@"Presenting Drop In Modally"); + dropIn.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(tappedCancel)]; + UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:dropIn]; + [self presentViewController:nav animated:YES completion:nil]; + } else { + self.progressBlock(@"Pushing Drop In on nav stack"); + [self.navigationController pushViewController:dropIn animated:YES]; + } +} + + +- (void)tappedCancel { + self.progressBlock(@"Dismissing Drop In"); + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - BTDropInViewControllerDelegate + +// Renamed from -dropInViewController:didSucceedWithPaymentMethod: +- (void)dropInViewController:(BTDropInViewController *)viewController didSucceedWithTokenization:(BTPaymentMethodNonce *)paymentMethodNonce { + if ([BraintreeDemoSettings useModalPresentation]) { + [viewController dismissViewControllerAnimated:YES completion:^{ + self.completionBlock(paymentMethodNonce); + }]; + } else { + [self.navigationController popViewControllerAnimated:YES]; + self.completionBlock(paymentMethodNonce); + } +} + +- (void)dropInViewControllerWillComplete:(__unused BTDropInViewController *)viewController { + self.progressBlock(@"Drop In Will Complete"); +} + +- (void)dropInViewControllerDidCancel:(BTDropInViewController *)viewController { + self.progressBlock(@"User Canceled Drop In"); + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.h new file mode 100755 index 00000000..0696111f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoBaseViewController.h" + +@interface BraintreeDemoDropInViewController : BraintreeDemoBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.m new file mode 100755 index 00000000..c0eaf828 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Drop In/BraintreeDemoDropInViewController.m @@ -0,0 +1,328 @@ +#import "BraintreeDemoDropInViewController.h" + +#import +#import +#import +#import +#import "BraintreeUIKit.h" +#import "BraintreeDemoSettings.h" +#import "BTPaymentSelectionViewController.h" +#import + +@interface BraintreeDemoDropInViewController () + +@property (nonatomic, strong) BTUIKPaymentOptionCardView *paymentMethodTypeIcon; +@property (nonatomic, strong) UILabel *paymentMethodTypeLabel; +@property (nonatomic, strong) UILabel *cartLabel; +@property (nonatomic, strong) UILabel *itemLabel; +@property (nonatomic, strong) UILabel *priceLabel; +@property (nonatomic, strong) UILabel *paymentMethodHeaderLabel; +@property (nonatomic, strong) UIButton *dropInButton; +@property (nonatomic, strong) UIButton *purchaseButton; +@property (nonatomic, strong) UISegmentedControl *dropinThemeSwitch; +@property (nonatomic, strong) NSString *authorizationString; +@property (nonatomic) BOOL useApplePay; +@property (nonatomic, strong) BTPaymentMethodNonce *selectedNonce; +@property (nonatomic, strong) NSArray *checkoutConstraints; +@end + +@implementation BraintreeDemoDropInViewController + +- (instancetype)initWithAuthorization:(NSString *)authorization { + if (self = [super initWithAuthorization:authorization]) { + + self.authorizationString = authorization; + } + return self; +} + +- (void) updatePaymentMethod:(BTPaymentMethodNonce*)paymentMethodNonce { + self.paymentMethodTypeLabel.hidden = paymentMethodNonce == nil; + self.paymentMethodTypeIcon.hidden = paymentMethodNonce == nil; + if (paymentMethodNonce != nil) { + BTUIKPaymentOptionType paymentMethodType = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:paymentMethodNonce.type]; + self.paymentMethodTypeIcon.paymentOptionType = paymentMethodType; + [self.paymentMethodTypeLabel setText:paymentMethodNonce.localizedDescription]; + } + [self updatePaymentMethodConstraints]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"Drop-in"; + self.cartLabel = [[UILabel alloc] init]; + [self.cartLabel setText:@"CART"]; + self.cartLabel.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]]; + [self.cartLabel setTextColor:[UIColor grayColor]]; + self.cartLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.cartLabel]; + + self.itemLabel = [[UILabel alloc] init]; + [self.itemLabel setText:@"1 Sock"]; + self.itemLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.itemLabel]; + + self.priceLabel = [[UILabel alloc] init]; + [self.priceLabel setText:@"$100"]; + self.priceLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.priceLabel]; + + self.paymentMethodHeaderLabel = [[UILabel alloc] init]; + [self.paymentMethodHeaderLabel setText:@"PAYMENT METHODS"]; + [self.paymentMethodHeaderLabel setTextColor:[UIColor grayColor]]; + self.paymentMethodHeaderLabel.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]]; + self.paymentMethodHeaderLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.paymentMethodHeaderLabel]; + + self.dropInButton = [[UIButton alloc] init]; + [self.dropInButton setTitle:@"Select Payment Method" forState:UIControlStateNormal]; + [self.dropInButton setTitleColor:self.view.tintColor forState:UIControlStateNormal]; + self.dropInButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.dropInButton addTarget:self action:@selector(tappedToShowDropIn) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.dropInButton]; + + self.purchaseButton = [[UIButton alloc] init]; + [self.purchaseButton setTitle:@"Complete Purchase" forState:UIControlStateNormal]; + [self.purchaseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [self.purchaseButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.8] forState:UIControlStateHighlighted]; + self.purchaseButton.backgroundColor = self.view.tintColor; + self.purchaseButton.translatesAutoresizingMaskIntoConstraints = NO; + + [self.purchaseButton addTarget:self action:@selector(purchaseButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + self.purchaseButton.layer.cornerRadius = 4.0; + [self.view addSubview:self.purchaseButton]; + + self.paymentMethodTypeIcon = [BTUIKPaymentOptionCardView new]; + self.paymentMethodTypeIcon.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.paymentMethodTypeIcon]; + self.paymentMethodTypeIcon.hidden = YES; + + self.paymentMethodTypeLabel = [[UILabel alloc] init]; + self.paymentMethodTypeLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.paymentMethodTypeLabel]; + self.paymentMethodTypeLabel.hidden = YES; + + self.dropinThemeSwitch = [[UISegmentedControl alloc] initWithItems:@[@"Light Theme", @"Dark Theme"]]; + self.dropinThemeSwitch.translatesAutoresizingMaskIntoConstraints = NO; + self.dropinThemeSwitch.selectedSegmentIndex = 0; + [self.view addSubview:self.dropinThemeSwitch]; + + [self updatePaymentMethodConstraints]; + + self.progressBlock(@"Fetching customer's payment methods..."); + self.useApplePay = NO; + + [BTDropInResult fetchDropInResultForAuthorization:self.authorizationString handler:^(BTDropInResult * _Nullable result, NSError * _Nullable error) { + if (error) { + self.progressBlock([NSString stringWithFormat:@"Error: %@", error.localizedDescription]); + NSLog(@"Error: %@", error); + } else { + if (result.paymentOptionType == BTUIKPaymentOptionTypeApplePay) { + self.progressBlock(@"Ready for checkout..."); + [self setupApplePay]; + } else { + self.useApplePay = NO; + self.selectedNonce = result.paymentMethod; + self.progressBlock(@"Ready for checkout..."); + [self updatePaymentMethod:self.selectedNonce]; + } + } + }]; +} + +- (void) setupApplePay { + self.paymentMethodTypeLabel.hidden = NO; + self.paymentMethodTypeIcon.hidden = NO; + self.paymentMethodTypeIcon.paymentOptionType = BTUIKPaymentOptionTypeApplePay; + [self.paymentMethodTypeLabel setText:@"Apple Pay"]; + self.useApplePay = YES; + [self updatePaymentMethodConstraints]; +} + +#pragma mark Constraints + +- (void)updatePaymentMethodConstraints { + if (self.checkoutConstraints) { + [NSLayoutConstraint deactivateConstraints:self.checkoutConstraints]; + } + NSDictionary *viewBindings = @{ + @"view": self, + @"cartLabel": self.cartLabel, + @"itemLabel": self.itemLabel, + @"priceLabel": self.priceLabel, + @"paymentMethodHeaderLabel": self.paymentMethodHeaderLabel, + @"dropInButton": self.dropInButton, + @"paymentMethodTypeIcon": self.paymentMethodTypeIcon, + @"paymentMethodTypeLabel": self.paymentMethodTypeLabel, + @"purchaseButton":self.purchaseButton, + @"dropinThemeSwitch":self.dropinThemeSwitch + }; + + NSMutableArray *newConstraints = [NSMutableArray new]; + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[cartLabel]-|" options:0 metrics:nil views:viewBindings]]; + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[purchaseButton]-|" options:0 metrics:nil views:viewBindings]]; + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(20)-[cartLabel]-[itemLabel]-[paymentMethodHeaderLabel]" options:0 metrics:nil views:viewBindings]]; + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[itemLabel]-[priceLabel]-|" options:NSLayoutFormatAlignAllTop metrics:nil views:viewBindings]]; + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[paymentMethodHeaderLabel]-|" options:0 metrics:nil views:viewBindings]]; + + if (!self.paymentMethodTypeIcon.hidden && !self.paymentMethodTypeLabel.hidden) { + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[paymentMethodHeaderLabel]-[paymentMethodTypeIcon(29)]-[dropInButton]" options:0 metrics:nil views:viewBindings]]; + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[paymentMethodTypeIcon(45)]-[paymentMethodTypeLabel]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:viewBindings]]; + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[dropInButton]-|" options:0 metrics:nil views:viewBindings]]; + [self.dropInButton setTitle:@"Change Payment Method" forState:UIControlStateNormal]; + self.purchaseButton.backgroundColor = self.view.tintColor; + self.purchaseButton.enabled = YES; + } else { + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[paymentMethodHeaderLabel]-[dropInButton]" options:0 metrics:nil views:viewBindings]]; + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[dropInButton]-|" options:0 metrics:nil views:viewBindings]]; + [self.dropInButton setTitle:@"Add Payment Method" forState:UIControlStateNormal]; + self.purchaseButton.backgroundColor = [UIColor lightGrayColor]; + self.purchaseButton.enabled = NO; + } + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[dropInButton]-(20)-[purchaseButton]-(20)-[dropinThemeSwitch]" options:0 metrics:nil views:viewBindings]]; + + [newConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[dropinThemeSwitch]-|" options:0 metrics:nil views:viewBindings]]; + + self.checkoutConstraints = newConstraints; + [self.view addConstraints:self.checkoutConstraints]; +} + +#pragma mark Button Handlers + +- (void)purchaseButtonPressed { + if (self.useApplePay) { + + PKPaymentRequest *paymentRequest = [[PKPaymentRequest alloc] init]; + paymentRequest.paymentSummaryItems = @[ + [PKPaymentSummaryItem summaryItemWithLabel:@"Socks" amount:[NSDecimalNumber decimalNumberWithString:@"100"]] + ]; + paymentRequest.supportedNetworks = @[PKPaymentNetworkVisa, PKPaymentNetworkMasterCard, PKPaymentNetworkAmex, PKPaymentNetworkDiscover]; + paymentRequest.merchantCapabilities = PKMerchantCapability3DS; + paymentRequest.currencyCode = @"USD"; + paymentRequest.countryCode = @"US"; + + switch ([BraintreeDemoSettings currentEnvironment]) { + case BraintreeDemoTransactionServiceEnvironmentSandboxBraintreeSampleMerchant: + paymentRequest.merchantIdentifier = @"merchant.com.braintreepayments.sandbox.Braintree-Demo"; + break; + case BraintreeDemoTransactionServiceEnvironmentProductionExecutiveSampleMerchant: + paymentRequest.merchantIdentifier = @"merchant.com.braintreepayments.Braintree-Demo"; + break; + case BraintreeDemoTransactionServiceEnvironmentCustomMerchant: + self.progressBlock(@"Direct Apple Pay integration does not support custom environments in this Demo App"); + break; + } + + PKPaymentAuthorizationViewController *viewController = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:paymentRequest]; + viewController.delegate = self; + + self.progressBlock(@"Presenting Apple Pay Sheet"); + [self presentViewController:viewController animated:YES completion:nil]; + } else { + self.completionBlock(self.selectedNonce); + self.transactionBlock(); + } +} + +- (void)tappedToShowDropIn { + BTDropInRequest *dropInRequest = [[BTDropInRequest alloc] init]; + // To test 3DS + //dropInRequest.amount = @"10.00"; + //dropInRequest.threeDSecureVerification = YES; + if (self.dropinThemeSwitch.selectedSegmentIndex == 0) { + [BTUIKAppearance lightTheme]; + } else { + [BTUIKAppearance darkTheme]; + } + BTDropInController *dropIn = [[BTDropInController alloc] initWithAuthorization:self.authorizationString request:dropInRequest handler:^(BTDropInController * _Nonnull dropInController, BTDropInResult * _Nullable result, NSError * _Nullable error) { + if (error) { + self.progressBlock([NSString stringWithFormat:@"Error: %@", error.localizedDescription]); + NSLog(@"Error: %@", error); + } else if (result.isCancelled) { + self.progressBlock(@"Cancelled🎲"); + } else { + if (result.paymentOptionType == BTUIKPaymentOptionTypeApplePay) { + self.progressBlock(@"Ready for checkout..."); + [self setupApplePay]; + } else { + self.useApplePay = NO; + self.selectedNonce = result.paymentMethod; + self.progressBlock(@"Ready for checkout..."); + [self updatePaymentMethod:self.selectedNonce]; + } + } + [dropInController dismissViewControllerAnimated:YES completion:nil]; + }]; + + [self presentViewController:dropIn animated:YES completion:nil]; +} + +#pragma mark PKPaymentAuthorizationViewControllerDelegate + +- (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewController *)controller + didSelectShippingMethod:(PKShippingMethod *)shippingMethod + completion:(void (^)(PKPaymentAuthorizationStatus, NSArray * _Nonnull))completion +{ + PKPaymentSummaryItem *testItem = [PKPaymentSummaryItem summaryItemWithLabel:@"SOME ITEM" amount:[NSDecimalNumber decimalNumberWithString:@"10"]]; + if ([shippingMethod.identifier isEqualToString:@"fast"]) { + completion(PKPaymentAuthorizationStatusSuccess, + @[ + testItem, + [PKPaymentSummaryItem summaryItemWithLabel:@"SHIPPING" amount:shippingMethod.amount], + [PKPaymentSummaryItem summaryItemWithLabel:@"BRAINTREE" amount:[testItem.amount decimalNumberByAdding:shippingMethod.amount]], + ]); + } else if ([shippingMethod.identifier isEqualToString:@"fail"]) { + completion(PKPaymentAuthorizationStatusFailure, @[testItem]); + } else { + completion(PKPaymentAuthorizationStatusSuccess, @[testItem]); + } +} + +- (void)paymentAuthorizationViewControllerDidFinish:(__unused PKPaymentAuthorizationViewController *)controller { + [controller dismissViewControllerAnimated:YES completion:nil]; +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 +- (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewController *)controller didAuthorizePayment:(PKPayment *)payment handler:(void (^)(PKPaymentAuthorizationResult * _Nonnull))completion { + self.progressBlock(@"Apple Pay Did Authorize Payment"); + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:self.authorizationString]; + BTApplePayClient *applePayClient = [[BTApplePayClient alloc] initWithAPIClient:client]; + [applePayClient tokenizeApplePayPayment:payment completion:^(BTApplePayCardNonce * _Nullable tokenizedApplePayPayment, NSError * _Nullable error) { + if (error) { + self.progressBlock(error.localizedDescription); + completion([[PKPaymentAuthorizationResult alloc] initWithStatus:PKPaymentAuthorizationStatusFailure errors:nil]); + } else { + self.completionBlock(tokenizedApplePayPayment); + completion([[PKPaymentAuthorizationResult alloc] initWithStatus:PKPaymentAuthorizationStatusSuccess errors:nil]); + } + }]; +} +#endif + +- (void)paymentAuthorizationViewController:(__unused PKPaymentAuthorizationViewController *)controller + didAuthorizePayment:(PKPayment *)payment + completion:(void (^)(PKPaymentAuthorizationStatus status))completion { + self.progressBlock(@"Apple Pay Did Authorize Payment"); + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:self.authorizationString]; + BTApplePayClient *applePayClient = [[BTApplePayClient alloc] initWithAPIClient:client]; + [applePayClient tokenizeApplePayPayment:payment completion:^(BTApplePayCardNonce * _Nullable tokenizedApplePayPayment, NSError * _Nullable error) { + if (error) { + self.progressBlock(error.localizedDescription); + completion(PKPaymentAuthorizationStatusFailure); + } else { + self.completionBlock(tokenizedApplePayPayment); + completion(PKPaymentAuthorizationStatusSuccess); + } + }]; +} + +- (void)paymentAuthorizationViewControllerWillAuthorizePayment:(__unused PKPaymentAuthorizationViewController *)controller { + self.progressBlock(@"Apple Pay will Authorize Payment"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.h new file mode 100755 index 00000000..801bf94d --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoBaseViewController.h" + +@interface BraintreeDemoBTDataCollectorViewController : BraintreeDemoBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.m new file mode 100755 index 00000000..1b2f2b5b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Fraud Protection - BTDataCollector/BraintreeDemoBTDataCollectorViewController.m @@ -0,0 +1,117 @@ +#import "BraintreeDemoBTDataCollectorViewController.h" +#import "BTDataCollector.h" +#import +#import "PPDataCollector.h" +#import + +@interface BraintreeDemoBTDataCollectorViewController () +/// Retain BTDataCollector for entire lifecycle of view controller +@property (nonatomic, strong) BTDataCollector *dataCollector; +@property (nonatomic, strong) UILabel *dataLabel; +@property (nonatomic, strong) BTAPIClient *apiClient; +@end + +@implementation BraintreeDemoBTDataCollectorViewController + +- (instancetype)initWithAuthorization:(NSString *)authorization { + if (self = [super initWithAuthorization:authorization]) { + _apiClient = [[BTAPIClient alloc] initWithAuthorization:authorization]; + } + + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = @"BTDataCollector Protection"; + + UIButton *collectButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [collectButton setTitle:@"Collect All Data" forState:UIControlStateNormal]; + [collectButton addTarget:self action:@selector(tappedCollect) forControlEvents:UIControlEventTouchUpInside]; + + UIButton *collectKountButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [collectKountButton setTitle:@"Collect Kount Data" forState:UIControlStateNormal]; + [collectKountButton addTarget:self action:@selector(tappedCollectKount) forControlEvents:UIControlEventTouchUpInside]; + + UIButton *collectDysonButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [collectDysonButton setTitle:@"Collect PayPal Data" forState:UIControlStateNormal]; + [collectDysonButton addTarget:self action:@selector(tappedCollectDyson) forControlEvents:UIControlEventTouchUpInside]; + + UIButton *obtainLocationPermissionButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [obtainLocationPermissionButton setTitle:@"Obtain Location Permission" forState:UIControlStateNormal]; + [obtainLocationPermissionButton addTarget:self action:@selector(tappedRequestLocationAuthorization:) forControlEvents:UIControlEventTouchUpInside]; + + self.dataLabel = [[UILabel alloc] init]; + self.dataLabel.numberOfLines = 0; + + [self.view addSubview:collectButton]; + [self.view addSubview:collectKountButton]; + [self.view addSubview:collectDysonButton]; + [self.view addSubview:obtainLocationPermissionButton]; + [self.view addSubview:self.dataLabel]; + + [collectButton autoCenterInSuperviewMargins]; + [collectKountButton autoAlignAxis:ALAxisVertical toSameAxisOfView:collectButton]; + [collectDysonButton autoAlignAxis:ALAxisVertical toSameAxisOfView:collectButton]; + [collectKountButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:collectButton]; + [collectDysonButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:collectKountButton]; + + [obtainLocationPermissionButton autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:20]; + [obtainLocationPermissionButton autoAlignAxisToSuperviewMarginAxis:ALAxisVertical]; + + [self.dataLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:collectDysonButton]; + [self.dataLabel autoPinEdgeToSuperviewEdge:ALEdgeLeft]; + [self.dataLabel autoPinEdgeToSuperviewEdge:ALEdgeRight]; + [self.dataLabel autoAlignAxisToSuperviewMarginAxis:ALAxisVertical]; + + self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:self.apiClient]; + self.dataCollector.delegate = self; +} + +- (IBAction)tappedCollect +{ self.progressBlock(@"Started collecting all data..."); + [self.dataCollector collectFraudData:^(NSString * _Nonnull deviceData) { + self.dataLabel.text = deviceData; + }]; +} + +- (IBAction)tappedCollectKount { + self.progressBlock(@"Started collecting Kount data..."); + [self.dataCollector collectCardFraudData:^(NSString * _Nonnull deviceData) { + self.dataLabel.text = deviceData; + }]; +} + +- (IBAction)tappedCollectDyson { + self.dataLabel.text = [PPDataCollector collectPayPalDeviceData]; + self.progressBlock(@"Collected PayPal clientMetadataID!"); +} + +- (IBAction)tappedRequestLocationAuthorization:(__unused id)sender { + CLLocationManager *locationManager = [[CLLocationManager alloc] init]; + [locationManager requestWhenInUseAuthorization]; +} + +#pragma mark - BTDataCollectorDelegate + +/// The collector has started. +- (void)dataCollectorDidStart:(__unused BTDataCollector *)dataCollector { + self.progressBlock(@"Data collector did start..."); +} + +/// The collector finished successfully. +- (void)dataCollectorDidComplete:(__unused BTDataCollector *)dataCollector { + self.progressBlock(@"Data collector did complete."); +} + +/// An error occurred. +/// +/// @param error Triggering error +- (void)dataCollector:(__unused BTDataCollector *)dataCollector didFailWithError:(NSError *)error { + self.progressBlock(@"Error collecting data."); + NSLog(@"Error collecting data. error = %@", error); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.h new file mode 100755 index 00000000..e7ab6251 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.h @@ -0,0 +1,6 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoBTUIPayPalButtonViewController : BraintreeDemoPaymentButtonBaseViewController +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.m new file mode 100755 index 00000000..4afef45b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - BTUIPayPalButton/BraintreeDemoBTUIPayPalButtonViewController.m @@ -0,0 +1,61 @@ +#import "BraintreeDemoBTUIPayPalButtonViewController.h" +#import "BTUIPaymentButtonCollectionViewCell.h" +#import +#import + +@interface BraintreeDemoBTUIPayPalButtonViewController () +@end + +@implementation BraintreeDemoBTUIPayPalButtonViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"BTUIPayPalButton"; + + self.paymentButton.hidden = YES; + [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) { + if (error) { + self.progressBlock(@"Failed to fetch configuration"); + NSLog(@"Failed to fetch configuration: %@", error); + return; + } + + if (!configuration.isPayPalEnabled) { + self.progressBlock(@"canCreatePaymentMethodWithProviderType: returns NO, hiding PayPal button"); + } else { + self.paymentButton.hidden = NO; + } + }]; +} + +- (UIView *)createPaymentButton { + BTUIPayPalButton *payPalButton = [[BTUIPayPalButton alloc] init]; + [payPalButton addTarget:self action:@selector(tappedPayPalButton) forControlEvents:UIControlEventTouchUpInside]; + return payPalButton; +} + +- (void)tappedPayPalButton { + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + payPalDriver.viewControllerPresentingDelegate = self; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + if (tokenizedPayPalAccount) { + self.progressBlock(@"Got a nonce 💎!"); + NSLog(@"%@", [tokenizedPayPalAccount debugDescription]); + self.completionBlock(tokenizedPayPalAccount); + } else if (error) { + self.progressBlock(error.localizedDescription); + } else { + self.progressBlock(@"Canceled 🔰"); + } + }]; +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.h new file mode 100755 index 00000000..e271f916 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.h @@ -0,0 +1,5 @@ +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoPayPalBillingAgreementViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.m new file mode 100755 index 00000000..ffb8087f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Billing Agreement/BraintreeDemoPayPalBillingAgreementViewController.m @@ -0,0 +1,86 @@ +#import "BraintreeDemoPayPalBillingAgreementViewController.h" + +#import + +@interface BraintreeDemoPayPalBillingAgreementViewController () +@property(nonatomic, strong) UITextView *infoTextView; + +@end + +@implementation BraintreeDemoPayPalBillingAgreementViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.infoTextView = [[UITextView alloc] initWithFrame:CGRectMake((self.view.bounds.size.width / 2) - 100, (self.view.bounds.size.width / 8) * 7, 200, 100)]; + [self.view addSubview:self.infoTextView]; + self.infoTextView.backgroundColor = [UIColor clearColor]; +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"Billing Agreement with PayPal" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[UIColor colorWithRed:50.0/255 green:50.0/255 blue:255.0/255 alpha:1.0] forState:UIControlStateHighlighted]; + [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; + [button addTarget:self action:@selector(tappedPayPalCheckout:) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedPayPalCheckout:(UIButton *)sender { + self.progressBlock(@"Tapped PayPal - initiating checkout using BTPayPalDriver"); + self.infoTextView.text = @""; + [sender setTitle:@"Processing..." forState:UIControlStateDisabled]; + [sender setEnabled:NO]; + + BTPayPalDriver *driver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + driver.appSwitchDelegate = self; + driver.viewControllerPresentingDelegate = self; + BTPayPalRequest *checkout = [[BTPayPalRequest alloc] init]; + [driver requestBillingAgreement:checkout completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalCheckout, NSError * _Nullable error) { + [sender setEnabled:YES]; + + if (error) { + self.progressBlock(error.localizedDescription); + } else if (tokenizedPayPalCheckout) { + self.completionBlock(tokenizedPayPalCheckout); + } else { + self.progressBlock(@"Cancelled"); + } + }]; +} + +#pragma mark BTAppSwitchDelegate + +- (void)appSwitcherWillPerformAppSwitch:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillPerformAppSwitch:"); +} + +- (void)appSwitcherWillProcessPaymentInfo:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillProcessPaymentInfo:"); +} + +- (void)appSwitcher:(__unused id)appSwitcher didPerformSwitchToTarget:(BTAppSwitchTarget)target { + switch (target) { + case BTAppSwitchTargetWebBrowser: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: browser"); + break; + case BTAppSwitchTargetNativeApp: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: app"); + break; + case BTAppSwitchTargetUnknown: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: unknown"); + break; + } +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + self.infoTextView.text = @"DismissalOfViewController Called"; + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.h new file mode 100755 index 00000000..e3ce31be --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.h @@ -0,0 +1,5 @@ +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoPayPalOneTimePaymentViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.m new file mode 100755 index 00000000..02a6b4fa --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Checkout/BraintreeDemoPayPalOneTimePaymentViewController.m @@ -0,0 +1,76 @@ +#import "BraintreeDemoPayPalOneTimePaymentViewController.h" + +#import + +@interface BraintreeDemoPayPalOneTimePaymentViewController () + +@end + +@implementation BraintreeDemoPayPalOneTimePaymentViewController + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"PayPal one-time payment" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[UIColor colorWithRed:50.0/255 green:50.0/255 blue:255.0/255 alpha:1.0] forState:UIControlStateHighlighted]; + [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; + [button addTarget:self action:@selector(tappedPayPalOneTimePayment:) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedPayPalOneTimePayment:(UIButton *)sender { + self.progressBlock(@"Tapped PayPal - initiating one-time payment using BTPayPalDriver"); + + [sender setTitle:@"Processing..." forState:UIControlStateDisabled]; + [sender setEnabled:NO]; + + BTPayPalDriver *driver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + driver.appSwitchDelegate = self; + driver.viewControllerPresentingDelegate = self; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"4.30"]; + [driver requestOneTimePayment:request completion:^(BTPayPalAccountNonce * _Nullable payPalAccount, NSError * _Nullable error) { + [sender setEnabled:YES]; + + if (error) { + self.progressBlock(error.localizedDescription); + } else if (payPalAccount) { + self.completionBlock(payPalAccount); + } else { + self.progressBlock(@"Cancelled"); + } + }]; +} + +#pragma mark BTAppSwitchDelegate + +- (void)appSwitcherWillPerformAppSwitch:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillPerformAppSwitch:"); +} + +- (void)appSwitcherWillProcessPaymentInfo:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillProcessPaymentInfo:"); +} + +- (void)appSwitcher:(__unused id)appSwitcher didPerformSwitchToTarget:(BTAppSwitchTarget)target { + switch (target) { + case BTAppSwitchTargetWebBrowser: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: browser"); + break; + case BTAppSwitchTargetNativeApp: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: app"); + break; + case BTAppSwitchTargetUnknown: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: unknown"); + break; + } +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.h new file mode 100755 index 00000000..294b49ec --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.h @@ -0,0 +1,5 @@ +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoPayPalCreditPaymentViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.m new file mode 100755 index 00000000..056ad961 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Credit/BraintreeDemoPayPalCreditPaymentViewController.m @@ -0,0 +1,116 @@ +#import "BraintreeDemoPayPalCreditPaymentViewController.h" + +#import + +@interface BraintreeDemoPayPalCreditPaymentViewController () +@property (nonatomic, strong) UISegmentedControl *paypalTypeSwitch; +@end + +@implementation BraintreeDemoPayPalCreditPaymentViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.paypalTypeSwitch = [[UISegmentedControl alloc] initWithItems:@[@"Checkout", @"Billing Agreement"]]; + self.paypalTypeSwitch.translatesAutoresizingMaskIntoConstraints = NO; + self.paypalTypeSwitch.selectedSegmentIndex = 0; + [self.view addSubview:self.paypalTypeSwitch]; + NSDictionary *viewBindings = @{ + @"view": self, + @"paypalTypeSwitch":self.paypalTypeSwitch + }; + + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[paypalTypeSwitch]-(50)-|" options:0 metrics:nil views:viewBindings]]; + + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[paypalTypeSwitch]-|" options:0 metrics:nil views:viewBindings]]; + +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"PayPal with Credit Offered" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[UIColor colorWithRed:50.0/255 green:50.0/255 blue:255.0/255 alpha:1.0] forState:UIControlStateHighlighted]; + [button setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; + [button addTarget:self action:@selector(tappedPayPalOneTimePayment:) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedPayPalOneTimePayment:(UIButton *)sender { + + if (self.paypalTypeSwitch.selectedSegmentIndex == 0) { + self.progressBlock(@"Tapped - initiating Checkout payment with credit offered"); + } else { + self.progressBlock(@"Tapped - initiating Billing Agreement payment with credit offered"); + } + + [sender setTitle:@"Processing..." forState:UIControlStateDisabled]; + [sender setEnabled:NO]; + + BTPayPalDriver *driver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + driver.appSwitchDelegate = self; + driver.viewControllerPresentingDelegate = self; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"4.30"]; + + request.offerCredit = YES; + + if (self.paypalTypeSwitch.selectedSegmentIndex == 0) { + [driver requestOneTimePayment:request completion:^(BTPayPalAccountNonce * _Nullable payPalAccount, NSError * _Nullable error) { + [sender setEnabled:YES]; + + if (error) { + self.progressBlock(error.localizedDescription); + } else if (payPalAccount) { + self.completionBlock(payPalAccount); + } else { + self.progressBlock(@"Cancelled"); + } + }]; + } else { + [driver requestBillingAgreement:request completion:^(BTPayPalAccountNonce * _Nullable payPalAccount, NSError * _Nullable error) { + [sender setEnabled:YES]; + + if (error) { + self.progressBlock(error.localizedDescription); + } else if (payPalAccount) { + self.completionBlock(payPalAccount); + } else { + self.progressBlock(@"Cancelled"); + } + }]; + } +} + +#pragma mark BTAppSwitchDelegate + +- (void)appSwitcherWillPerformAppSwitch:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillPerformAppSwitch:"); +} + +- (void)appSwitcherWillProcessPaymentInfo:(__unused id)appSwitcher { + self.progressBlock(@"paymentDriverWillProcessPaymentInfo:"); +} + +- (void)appSwitcher:(__unused id)appSwitcher didPerformSwitchToTarget:(BTAppSwitchTarget)target { + switch (target) { + case BTAppSwitchTargetWebBrowser: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: browser"); + break; + case BTAppSwitchTargetNativeApp: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: app"); + break; + case BTAppSwitchTargetUnknown: + self.progressBlock(@"appSwitcher:didPerformSwitchToTarget: unknown"); + break; + } +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.h new file mode 100755 index 00000000..8ef68dce --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoCustomPayPalButtonViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.m new file mode 100755 index 00000000..ac9d1ed7 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Custom Button/BraintreeDemoCustomPayPalButtonViewController.m @@ -0,0 +1,62 @@ +#import "BraintreeDemoCustomPayPalButtonViewController.h" +#import +#import + +@interface BraintreeDemoCustomPayPalButtonViewController () +@end + +@implementation BraintreeDemoCustomPayPalButtonViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"PayPal (custom button)"; + + self.paymentButton.hidden = YES; + [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) { + if (error) { + self.progressBlock(error.localizedDescription); + return; + } + + if (!configuration.isPayPalEnabled) { + self.progressBlock(@"canCreatePaymentMethodWithProviderType: returns NO, hiding custom PayPal button"); + } else { + self.paymentButton.hidden = NO; + } + }]; +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"PayPal (custom button)" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[[UIColor blueColor] bt_adjustedBrightness:0.5] forState:UIControlStateHighlighted]; + [button addTarget:self action:@selector(tappedCustomPayPal) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedCustomPayPal { + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + payPalDriver.viewControllerPresentingDelegate = self; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + if (tokenizedPayPalAccount) { + self.progressBlock(@"Got a nonce 💎!"); + NSLog(@"%@", [tokenizedPayPalAccount debugDescription]); + self.completionBlock(tokenizedPayPalAccount); + } else if (error) { + self.progressBlock(error.localizedDescription); + } else { + self.progressBlock(@"Canceled 🔰"); + } + }]; +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.h new file mode 100755 index 00000000..52e65559 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoPayPalForceFuturePaymentViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.m new file mode 100755 index 00000000..767a3d7f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Force Future Payment/BraintreeDemoPayPalForceFuturePaymentViewController.m @@ -0,0 +1,63 @@ +#import "BraintreeDemoPayPalForceFuturePaymentViewController.h" +#import +#import +#import + +@interface BraintreeDemoPayPalForceFuturePaymentViewController () +@end + +@implementation BraintreeDemoPayPalForceFuturePaymentViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"PayPal (future payment button)"; + + self.paymentButton.hidden = YES; + [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) { + if (error) { + self.progressBlock(error.localizedDescription); + return; + } + + if (!configuration.isPayPalEnabled) { + self.progressBlock(@"canCreatePaymentMethodWithProviderType: returns NO, hiding custom PayPal button"); + } else { + self.paymentButton.hidden = NO; + } + }]; +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"PayPal (future payment button)" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[[UIColor blueColor] bt_adjustedBrightness:0.5] forState:UIControlStateHighlighted]; + [button addTarget:self action:@selector(tappedCustomPayPal) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedCustomPayPal { + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + payPalDriver.viewControllerPresentingDelegate = self; + [payPalDriver authorizeAccountWithAdditionalScopes:[NSSet set] forceFuturePaymentFlow:true completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + if (tokenizedPayPalAccount) { + self.progressBlock(@"Got a nonce 💎!"); + NSLog(@"%@", [tokenizedPayPalAccount debugDescription]); + self.completionBlock(tokenizedPayPalAccount); + } else if (error) { + self.progressBlock(error.localizedDescription); + } else { + self.progressBlock(@"Canceled 🔰"); + } + }]; +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.h new file mode 100755 index 00000000..b516695f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.h @@ -0,0 +1,6 @@ +@import Foundation; + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoPayPalScopesViewController : BraintreeDemoPaymentButtonBaseViewController +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.m new file mode 100755 index 00000000..44b204f0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/PayPal - Scopes/BraintreeDemoPayPalScopesViewController.m @@ -0,0 +1,70 @@ +#import "BraintreeDemoPayPalScopesViewController.h" + +#import +#import + +@interface BraintreeDemoPayPalScopesViewController () +@property(nonatomic, strong) UITextView *addressTextView; +@end + +@implementation BraintreeDemoPayPalScopesViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.addressTextView = [[UITextView alloc] initWithFrame:CGRectMake((self.view.bounds.size.width / 2) - 100, (self.view.bounds.size.width / 8) * 7, 200, 100)]; + [self.view addSubview:self.addressTextView]; + self.addressTextView.backgroundColor = [UIColor clearColor]; + + self.paymentButton.hidden = YES; + [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) { + if (error) { + self.progressBlock(error.localizedDescription); + return; + } + + if (!configuration.isPayPalEnabled) { + self.progressBlock(@"canCreatePaymentMethodWithProviderType: returns NO, hiding custom PayPal button"); + } else { + self.paymentButton.hidden = NO; + } + }]; +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"PayPal (Address Scope)" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [button setTitleColor:[[UIColor blueColor] bt_adjustedBrightness:0.5] forState:UIControlStateHighlighted]; + [button addTarget:self action:@selector(tappedCustomPayPal) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedCustomPayPal { + BTPayPalDriver *driver = [[BTPayPalDriver alloc] initWithAPIClient:self.apiClient]; + driver.viewControllerPresentingDelegate = self; + self.progressBlock(@"Tapped PayPal - initiating authorization using BTPayPalDriver"); + + [driver authorizeAccountWithAdditionalScopes:[NSSet setWithArray:@[@"address"]] completion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + if (error) { + self.progressBlock(error.localizedDescription); + } else if (tokenizedPayPalAccount) { + self.completionBlock(tokenizedPayPalAccount); + + BTPostalAddress *address = tokenizedPayPalAccount.shippingAddress; + self.addressTextView.text = [NSString stringWithFormat:@"Address:\n%@\n%@\n%@ %@\n%@ %@", address.streetAddress, address.extendedAddress, address.locality, address.region, address.postalCode, address.countryCodeAlpha2]; + } else { + self.progressBlock(@"Cancelled"); + } + }]; +} + +- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + [self presentViewController:viewController animated:YES completion:nil]; +} + +- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.h new file mode 100755 index 00000000..b7354024 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.h @@ -0,0 +1,5 @@ +#import + +@interface BraintreeDemoCardHintViewController : UIViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.m new file mode 100755 index 00000000..4923b47c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCardHintViewController.m @@ -0,0 +1,40 @@ +#import "BraintreeDemoCardHintViewController.h" + +#import "BTUICardHint.h" + +@interface BraintreeDemoCardHintViewController () +@property (weak, nonatomic) IBOutlet BTUICardHint *cardHintView; +@property (weak, nonatomic) IBOutlet BTUICardHint *smallCardHintView; +@end + +@implementation BraintreeDemoCardHintViewController + +- (IBAction)selectedCardType:(UISegmentedControl *)sender { + BTUIPaymentOptionType type = BTUIPaymentOptionTypeUnknown; + switch(sender.selectedSegmentIndex) { + case 0: + type = BTUIPaymentOptionTypeUnknown; + break; + case 1: + type = BTUIPaymentOptionTypeVisa; + break; + case 2: + type = BTUIPaymentOptionTypeMasterCard; + break; + case 3: + type = BTUIPaymentOptionTypeAMEX; + break; + case 4: + type = BTUIPaymentOptionTypeDiscover; + break; + } + [self.cardHintView setCardType:type animated:YES]; + [self.smallCardHintView setCardType:type animated:YES]; +} + +- (IBAction)selectedHintMode:(UISegmentedControl *)sender { + [self.cardHintView setDisplayMode:(sender.selectedSegmentIndex == 0 ? BTCardHintDisplayModeCardType : BTCardHintDisplayModeCVVHint) animated:YES]; + [self.smallCardHintView setDisplayMode:(sender.selectedSegmentIndex == 0 ? BTCardHintDisplayModeCardType : BTCardHintDisplayModeCVVHint) animated:YES]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.h new file mode 100755 index 00000000..85b38690 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.h @@ -0,0 +1,7 @@ +#import +#import + +@interface BraintreeDemoCreditCardEntryViewController : UIViewController +@property (weak, nonatomic) IBOutlet BTUICardFormView *cardFormView; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.m new file mode 100755 index 00000000..e7f107ad --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.m @@ -0,0 +1,94 @@ +#import "BraintreeDemoCreditCardEntryViewController.h" +#import + +@interface BraintreeDemoCreditCardEntryViewController () +@property (weak, nonatomic) IBOutlet UITextView *successOutputTextView; + +@end + +@implementation BraintreeDemoCreditCardEntryViewController + +- (void)cardFormViewDidChange:(BTUICardFormView *)cardFormView { + if (cardFormView.valid) { + self.successOutputTextView.text = [NSString stringWithFormat: + @"😍 YOU DID IT \n" + "Number: %@\n" + "Expiration: %@/%@\n" + "CVV: %@\n" + "Postal: %@", + cardFormView.number, + cardFormView.expirationMonth, + cardFormView.expirationYear, + cardFormView.cvv, + cardFormView.postalCode]; + } else { + self.successOutputTextView.text = @"INVALID 🐴"; + } +} +- (IBAction)toggleCVV:(__unused id)sender { + self.cardFormView.optionalFields = self.cardFormView.optionalFields ^ BTUICardFormOptionalFieldsCvv; +} +- (IBAction)togglePostalCode:(__unused id)sender { + self.cardFormView.optionalFields = self.cardFormView.optionalFields ^ BTUICardFormOptionalFieldsPostalCode; +} +- (IBAction)toggleVibrate:(UISwitch *)sender { + self.cardFormView.vibrate = sender.on; +} + +#pragma mark card.io + +- (IBAction)cardIoPressed:(__unused id)sender { + if (![CardIOUtilities canReadCardWithCamera]) { + // Hide your "Scan Card" button, or take other appropriate action... + NSLog(@"can NOT read card with camera"); + + [self addCardFormWithInfo:nil]; + } else { + CardIOPaymentViewController *v = [[CardIOPaymentViewController alloc] initWithPaymentDelegate:self]; + + [self presentViewController:v + animated:YES + completion:nil]; + } +} + +- (void)userDidProvideCreditCardInfo:(CardIOCreditCardInfo *)cardInfo inPaymentViewController:(CardIOPaymentViewController *)paymentViewController { + // The full card number is available as info.cardNumber, but don't log that! + NSLog(@"Received card info. Number: %@, expiry: %02lu/%lu, cvv: %@.", cardInfo.redactedCardNumber, (unsigned long)cardInfo.expiryMonth, (unsigned long)cardInfo.expiryYear, cardInfo.cvv); + // Use the card info... + + [paymentViewController dismissViewControllerAnimated:YES completion:^{ + [self addCardFormWithInfo:cardInfo]; + }]; +} + +- (void)userDidCancelPaymentViewController:(CardIOPaymentViewController *)paymentViewController { + [paymentViewController dismissViewControllerAnimated:YES + completion:nil]; +} + +- (void)addCardFormWithInfo:(CardIOCreditCardInfo *)info { + BTUICardFormView *cardForm = self.cardFormView; + + if (info) { + cardForm.number = info.cardNumber; + + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.month = info.expiryMonth; + dateComponents.year = info.expiryYear; + dateComponents.calendar = [NSCalendar calendarWithIdentifier:NSGregorianCalendar]; + + [cardForm setExpirationDate:dateComponents.date]; + [cardForm setCvv:info.cvv]; + [cardForm setPostalCode:info.postalCode]; + } else { + cardForm.number = @"4111111111111111"; + [cardForm setExpirationDate:[NSDate date]]; + [cardForm setCvv:@"123"]; + [cardForm setPostalCode:@"60606"]; + } + + [self.view addSubview:cardForm]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.xib b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.xib new file mode 100755 index 00000000..f4fbebd0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoCreditCardEntryViewController.xib @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.h new file mode 100755 index 00000000..71efe8d2 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.h @@ -0,0 +1,4 @@ +#import + +@interface BraintreeDemoPaymentsUIComponentsViewController : UIViewController +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.m new file mode 100755 index 00000000..fc0d1206 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoPaymentsUIComponentsViewController.m @@ -0,0 +1,35 @@ +#import "BraintreeDemoBraintreeUIKitComponentsViewController.h" +#import "BTUIPaymentMethodView.h" +#import "BTUICTAControl.h" + +@interface BraintreeDemoPaymentsUIComponentsViewController () +@property (nonatomic, weak) IBOutlet BTUIPaymentMethodView *cardPaymentMethodView; +@property (nonatomic, weak) IBOutlet UISwitch *processingSwitch; +@property (nonatomic, weak) IBOutlet BTUICTAControl *ctaControl; +@end + +@implementation BraintreeDemoPaymentsUIComponentsViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.processingSwitch setOn:self.cardPaymentMethodView.isProcessing]; +} + +- (IBAction)tappedCTAControl:(__unused id)sender { + NSLog(@"Tapped CTA"); +} + +- (IBAction)tappedSwapCardType { + [self.cardPaymentMethodView setType:((self.cardPaymentMethodView.type+1) % (BTUIPaymentOptionTypePayPal+1))]; +} + +- (IBAction)toggledProcessingState:(UISwitch *)sender { + self.cardPaymentMethodView.processing = sender.on; +} +- (IBAction)toggledCTAEnabled:(UISwitch *)sender { + self.ctaControl.enabled = sender.on; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.h new file mode 100755 index 00000000..ee632a2a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoBaseViewController.h" + +@interface BraintreeDemoUIWidgetsViewController : BraintreeDemoBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.m new file mode 100755 index 00000000..8ddf64ee --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/BraintreeDemoUIWidgetsViewController.m @@ -0,0 +1,32 @@ +#import "BraintreeDemoUIWidgetsViewController.h" + + +@interface BraintreeDemoUIWidgetsViewController () +@end + +@implementation BraintreeDemoUIWidgetsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"UI Components"; + + UIStoryboard *uiStoryboard = [UIStoryboard storyboardWithName:@"UI" bundle:nil]; + + UIViewController *v = [uiStoryboard instantiateInitialViewController]; + + [self addChildViewController:v]; + [self.view addSubview:v.view]; + [v didMoveToParentViewController:self]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self.navigationController setToolbarHidden:YES animated:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + [self.navigationController setToolbarHidden:NO animated:animated]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/UI.storyboard b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/UI.storyboard new file mode 100755 index 00000000..6605cf11 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UI Components/UI.storyboard @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.h new file mode 100755 index 00000000..6400d0b4 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.h @@ -0,0 +1,5 @@ +#import "BraintreeDemoBaseViewController.h" + +@interface BraintreeDemoUnionPayViewController : BraintreeDemoBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.m new file mode 100755 index 00000000..a1c46e73 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/UnionPay/BraintreeDemoUnionPayViewController.m @@ -0,0 +1,221 @@ +#import "BraintreeDemoUnionPayViewController.h" +#import +#import "BTUICardFormView.h" + +@interface BraintreeDemoUnionPayViewController () + +@property (nonatomic, strong) IBOutlet UITextField *cardNumberField; +@property (nonatomic, strong) IBOutlet UITextField *expirationMonthField; +@property (nonatomic, strong) IBOutlet UITextField *expirationYearField; +@property (nonatomic, strong) BTUICardFormView *cardForm; +@property (nonatomic, strong) UIButton *submitButton; +@property (nonatomic, strong) UIButton *smsButton; +@property (nonatomic, strong) BTAPIClient *apiClient; +@property (nonatomic, strong) BTCardClient *cardClient; +@property (nonatomic, copy) NSString *lastCardNumber; + +@end + +@implementation BraintreeDemoUnionPayViewController + + +- (instancetype)initWithAuthorization:(NSString *)authorization { + if (self = [super initWithAuthorization:authorization]) { + _apiClient = [[BTAPIClient alloc] initWithAuthorization:authorization]; + _cardClient = [[BTCardClient alloc] initWithAPIClient:_apiClient]; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.title = @"UnionPay"; + self.edgesForExtendedLayout = UIRectEdgeBottom; + + self.cardForm = [[BTUICardFormView alloc] init]; + self.cardForm.optionalFields = BTUICardFormOptionalFieldsCvv; + self.cardForm.translatesAutoresizingMaskIntoConstraints = NO; + self.cardForm.delegate = self; + [self.view addSubview:self.cardForm]; + + self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem]; + self.submitButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.submitButton setTitle:@"Submit" forState:UIControlStateNormal]; + [self.submitButton addTarget:self action:@selector(submit:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.submitButton]; + + self.smsButton = [UIButton buttonWithType:UIButtonTypeSystem]; + self.smsButton.translatesAutoresizingMaskIntoConstraints = NO; + self.smsButton.hidden = YES; + [self.smsButton setTitle:@"Send SMS" forState:UIControlStateNormal]; + [self.smsButton addTarget:self action:@selector(enroll:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.smsButton]; + + [self.view addConstraints:[NSLayoutConstraint + constraintsWithVisualFormat:@"H:|[cardForm]|" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:@{@"cardForm" : self.cardForm}]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.submitButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.smsButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.cardForm + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTop + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.cardForm + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.smsButton + attribute:NSLayoutAttributeTop + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.smsButton + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.submitButton + attribute:NSLayoutAttributeTop + multiplier:1 + constant:0]]; + [self.view addConstraint:[NSLayoutConstraint + constraintWithItem:self.submitButton + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeBottom + multiplier:1 + constant:0]]; +} + +#pragma mark - Actions + +- (void)enroll:(__unused UIButton *)button { + self.progressBlock(@"Enrolling card"); + + BTCard *card = [[BTCard alloc] initWithNumber:self.cardForm.number expirationMonth:self.cardForm.expirationMonth expirationYear:self.cardForm.expirationYear cvv:self.cardForm.cvv]; +// card.shouldValidate = YES; + BTCardRequest *request = [[BTCardRequest alloc] initWithCard:card]; + request.mobileCountryCode = @"62"; + request.mobilePhoneNumber = self.cardForm.phoneNumber; + + [self.cardClient enrollCard:request completion:^(NSString * _Nullable enrollmentID, BOOL smsCodeRequired, NSError * _Nullable error) { + if (error) { + NSMutableString *errorMessage = [NSMutableString stringWithFormat:@"Error enrolling card: %@", error.localizedDescription]; + if (error.localizedFailureReason) { + [errorMessage appendString:[NSString stringWithFormat:@". %@", error.localizedFailureReason]]; + } + self.progressBlock(errorMessage); + return; + } + + request.enrollmentID = enrollmentID; + + if (smsCodeRequired) { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"SMS Auth Code" message:@"An authorization code has been sent to your mobile phone number. Please enter it here" preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:nil]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Submit" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction * _Nonnull action) { + UITextField *codeTextField = [alertController.textFields firstObject]; + NSString *authCode = codeTextField.text; + request.smsCode = authCode; + + self.progressBlock(@"Tokenizing card"); + + [self.cardClient tokenizeCard:request options:nil completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + if (error) { + self.progressBlock([NSString stringWithFormat:@"Error tokenizing card: %@", error.localizedDescription]); + return; + } + + self.completionBlock(tokenizedCard); + }]; + }]]; + + [self presentViewController:alertController animated:YES completion:nil]; + } else { + [self.cardClient tokenizeCard:request options:nil completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + if (error) { + NSMutableString *errorMessage = [NSMutableString stringWithFormat:@"Error tokenizing card: %@", error.localizedDescription]; + if (error.localizedFailureReason) { + [errorMessage appendString:[NSString stringWithFormat:@". %@", error.localizedFailureReason]]; + } + self.progressBlock(errorMessage); + return; + } + + self.completionBlock(tokenizedCard); + }]; + } + }]; +} + +- (void)submit:(__unused UIButton *)button { + self.progressBlock(@"Tokenizing card"); + + BTCard *card = [[BTCard alloc] initWithNumber:self.cardForm.number expirationMonth:self.cardForm.expirationMonth expirationYear:self.cardForm.expirationYear cvv:self.cardForm.cvv]; + [self.cardClient tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + if (error) { + self.progressBlock([NSString stringWithFormat:@"Error tokenizing card: %@", error.localizedDescription]); + return; + } + + self.completionBlock(tokenizedCard); + }]; +} + +#pragma mark - Private methods + +- (void)fetchCapabilities:(NSString *)cardNumber { + [self.cardClient fetchCapabilities:cardNumber completion:^(BTCardCapabilities * _Nullable cardCapabilities, NSError * _Nullable error) { + if (error) { + self.progressBlock([NSString stringWithFormat:@"Error fetching capabilities: %@", error.localizedDescription]); + return; + } + + if (cardCapabilities.isSupported) { + self.cardForm.optionalFields = self.cardForm.optionalFields | BTUICardFormOptionalFieldsPhoneNumber; + self.smsButton.hidden = NO; + self.submitButton.hidden = NO; + } else { + self.progressBlock([NSString stringWithFormat:@"This UnionPay card cannot be processed, please try another card."]); + self.submitButton.hidden = YES; + } + + if (cardCapabilities.isDebit) { + NSLog(@"Debit card"); + } else { + NSLog(@"Credit card"); + } + }]; +} + +#pragma mark - BTUICardFormViewDelegate methods + +- (void)cardFormViewDidEndEditing:(BTUICardFormView *)cardFormView { + if (cardFormView.number && + ![cardFormView.number isEqualToString:self.lastCardNumber]) { + [self fetchCapabilities:cardFormView.number]; + self.lastCardNumber = cardFormView.number; + } +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.h new file mode 100755 index 00000000..66c34c1f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.h @@ -0,0 +1,6 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoBTUIVenmoButtonViewController : BraintreeDemoPaymentButtonBaseViewController +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.m new file mode 100755 index 00000000..c55ebb7c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - BTUIVenmoButton/BraintreeDemoBTUIVenmoButtonViewController.m @@ -0,0 +1,52 @@ +#import "BraintreeDemoBTUIVenmoButtonViewController.h" +#import +#import + +@interface BraintreeDemoBTUIVenmoButtonViewController () +@property (nonatomic, strong) BTUIVenmoButton *venmoButton; +@end + +@implementation BraintreeDemoBTUIVenmoButtonViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.title = @"BTUIVenmoButton"; + self.venmoButton.hidden = YES; + [self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable __unused configuration, NSError * _Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + self.progressBlock(error.localizedDescription); + NSLog(@"Failed to fetch configuration: %@", error); + return; + } + }); + }]; +} + +- (UIView *)createPaymentButton { + if (!self.venmoButton) { + self.venmoButton = [[BTUIVenmoButton alloc] init]; + [self.venmoButton addTarget:self action:@selector(tappedPayPalButton) forControlEvents:UIControlEventTouchUpInside]; + } + return self.venmoButton; +} + +- (void)tappedPayPalButton { + self.progressBlock(@"Tapped Venmo - initiating Venmo auth"); + + BTVenmoDriver *driver = [[BTVenmoDriver alloc] initWithAPIClient:self.apiClient]; + + [driver authorizeAccountAndVault:YES completion:^(BTVenmoAccountNonce * _Nullable venmoAccount, NSError * _Nullable error) { + if (venmoAccount) { + self.progressBlock(@"Got a nonce 💎!"); + NSLog(@"%@", [venmoAccount debugDescription]); + self.completionBlock(venmoAccount); + } else if (error) { + self.progressBlock(error.localizedDescription); + } else { + self.progressBlock(@"Canceled 🔰"); + } + }]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.h new file mode 100755 index 00000000..f17bc9ef --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.h @@ -0,0 +1,7 @@ +#import + +#import "BraintreeDemoPaymentButtonBaseViewController.h" + +@interface BraintreeDemoCustomVenmoButtonViewController : BraintreeDemoPaymentButtonBaseViewController + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.m new file mode 100755 index 00000000..2e922aa0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Features/Venmo - Custom Button/BraintreeDemoCustomVenmoButtonViewController.m @@ -0,0 +1,43 @@ +#import "BraintreeDemoCustomVenmoButtonViewController.h" +#import +#import + + +@interface BraintreeDemoCustomVenmoButtonViewController () +@property (nonatomic, strong) BTVenmoDriver *venmoDriver; +@end + +@implementation BraintreeDemoCustomVenmoButtonViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.venmoDriver = [[BTVenmoDriver alloc] initWithAPIClient:self.apiClient]; + self.title = @"Custom Venmo Button"; + self.paymentButton.hidden = [self.venmoDriver isiOSAppAvailableForAppSwitch]; +} + +- (UIView *)createPaymentButton { + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + [button setTitle:@"Venmo (custom button)" forState:UIControlStateNormal]; + [button setTitleColor:[UIColor bt_colorFromHex:@"3D95CE" alpha:1.0f] forState:UIControlStateNormal]; + [button setTitleColor:[[UIColor bt_colorFromHex:@"3D95CE" alpha:1.0f] bt_adjustedBrightness:0.7] forState:UIControlStateHighlighted]; + [button addTarget:self action:@selector(tappedCustomVenmo) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (void)tappedCustomVenmo { + self.progressBlock(@"Tapped Venmo - initiating Venmo auth"); + [self.venmoDriver authorizeAccountAndVault:NO completion:^(BTVenmoAccountNonce * _Nullable venmoAccount, NSError * _Nullable error) { + if (venmoAccount) { + self.progressBlock(@"Got a nonce 💎!"); + NSLog(@"%@", [venmoAccount debugDescription]); + self.completionBlock(venmoAccount); + } else if (error) { + self.progressBlock(error.localizedDescription); + } else { + self.progressBlock(@"Canceled 🔰"); + } + }]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@1x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@1x.png new file mode 100755 index 00000000..3d059cba Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@1x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@2x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@2x.png new file mode 100755 index 00000000..f3bfe7c0 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad-spotlight@2x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@1x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@1x.png new file mode 100755 index 00000000..c47b7752 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@1x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@2x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@2x.png new file mode 100755 index 00000000..a8c050c1 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-ipad@2x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-spotlight.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-spotlight.png new file mode 100755 index 00000000..f3bfe7c0 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon-spotlight.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon@2x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon@2x.png new file mode 100755 index 00000000..728c48c1 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone App Icon@2x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone Pro App Icon-ipad@2x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone Pro App Icon-ipad@2x.png new file mode 100755 index 00000000..c94ea173 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/120 - iPhone Pro App Icon-ipad@2x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/Contents.json b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 00000000..bc226ef9 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,100 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "120 - iPhone App Icon-spotlight.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "120 - iPhone App Icon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "120 - iPhone App Icon-ipad-spotlight@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "120 - iPhone App Icon-ipad-spotlight@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "120 - iPhone App Icon-ipad@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "120 - iPhone App Icon-ipad@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "120 - iPhone Pro App Icon-ipad@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/Contents.json b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100755 index 00000000..b8710e75 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "LaunchImageR4.png", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "LaunchImage@2x.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImage@2x.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImage@2x.png new file mode 100755 index 00000000..e50e754c Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImage@2x.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImageR4.png b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImageR4.png new file mode 100755 index 00000000..814612e9 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Demo/Images.xcassets/LaunchImage.launchimage/LaunchImageR4.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.h b/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.h new file mode 100755 index 00000000..28007571 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.h @@ -0,0 +1,14 @@ +#import + +extern NSString *BraintreeDemoMerchantAPIEnvironmentDidChangeNotification; + +@interface BraintreeDemoMerchantAPI : NSObject + ++ (instancetype)sharedService; + +- (void)fetchMerchantConfigWithCompletion:(void (^)(NSString *merchantId, NSError *error))completionBlock; +- (void)createCustomerAndFetchClientTokenWithCompletion:(void (^)(NSString *clientToken, NSError *error))completionBlock; +- (void)makeTransactionWithPaymentMethodNonce:(NSString *)paymentMethodNonce completion:(void (^)(NSString *transactionId, NSError *error))completionBlock; +- (void)makeTransactionWithPaymentMethodNonce:(NSString *)paymentMethodNonce merchantAccountId:(NSString *)merchantAccountId completion:(void (^)(NSString *transactionId, NSError *error))completionBlock; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.m new file mode 100755 index 00000000..fe9b8084 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Merchant API Client/BraintreeDemoMerchantAPI.m @@ -0,0 +1,128 @@ +#import "BraintreeDemoMerchantAPI.h" +#import + +#import "BraintreeDemoSettings.h" + +NSString *BraintreeDemoMerchantAPIEnvironmentDidChangeNotification = @"BraintreeDemoTransactionServiceEnvironmentDidChangeNotification"; + +@interface BraintreeDemoMerchantAPI () +@property (nonatomic, strong) AFHTTPRequestOperationManager *sessionManager; +@property (nonatomic, assign) NSString *currentEnvironmentURLString; +@property (nonatomic, assign) BraintreeDemoTransactionServiceThreeDSecureRequiredStatus threeDSecureRequiredStatus; +@end + +@implementation BraintreeDemoMerchantAPI + ++ (instancetype)sharedService { + static BraintreeDemoMerchantAPI *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (id)init { + self = [super init]; + if (self) { + self.threeDSecureRequiredStatus = -1; + [self setupSessionManager:nil]; + + // Use KVO because we don't want to be notified while the user types each character of a Custom URL + [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:BraintreeDemoSettingsEnvironmentDefaultsKey options:NSKeyValueObservingOptionNew context:NULL]; + [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:BraintreeDemoSettingsThreeDSecureRequiredDefaultsKey options:NSKeyValueObservingOptionNew context:NULL]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupSessionManager:) name:UITextFieldTextDidEndEditingNotification object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:BraintreeDemoSettingsEnvironmentDefaultsKey]; + [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:BraintreeDemoSettingsThreeDSecureRequiredDefaultsKey]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:nil]; +} + +- (void)observeValueForKeyPath:(__unused NSString *)keyPath ofObject:(__unused id)object change:(__unused NSDictionary *)change context:(__unused void *)context { + [self setupSessionManager:nil]; +} + +- (void)setupSessionManager:(__unused NSNotification *)notif { + if (![self.currentEnvironmentURLString isEqualToString:[BraintreeDemoSettings currentEnvironmentURLString]] || + self.threeDSecureRequiredStatus != [BraintreeDemoSettings threeDSecureRequiredStatus]) + { + self.currentEnvironmentURLString = [BraintreeDemoSettings currentEnvironmentURLString]; + self.threeDSecureRequiredStatus = [BraintreeDemoSettings threeDSecureRequiredStatus]; + self.sessionManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:[NSURL URLWithString:[BraintreeDemoSettings currentEnvironmentURLString]]]; + [[NSNotificationCenter defaultCenter] postNotificationName:BraintreeDemoMerchantAPIEnvironmentDidChangeNotification object:self]; + } +} + +- (void)fetchMerchantConfigWithCompletion:(void (^)(NSString *merchantId, NSError *error))completionBlock { + [self.sessionManager GET:@"/config/current" + parameters:nil + success:^(__unused AFHTTPRequestOperation *operation, id responseObject) { + if (completionBlock) { + completionBlock(responseObject[@"merchant_id"], nil); + } + } failure:^(__unused AFHTTPRequestOperation *operation, NSError *error) { + completionBlock(nil, error); + }]; +} + +- (void)createCustomerAndFetchClientTokenWithCompletion:(void (^)(NSString *, NSError *))completionBlock { + NSMutableDictionary *parameters = [@{@"version":[BraintreeDemoSettings clientTokenVersion]} mutableCopy]; + if ([BraintreeDemoSettings customerPresent]) { + if ([BraintreeDemoSettings customerIdentifier].length > 0) { + parameters[@"customer_id"] = [BraintreeDemoSettings customerIdentifier]; + } else { + parameters[@"customer_id"] = [[NSUUID UUID] UUIDString]; + } + } + + [self.sessionManager GET:@"/client_token" + parameters:parameters + success:^(__unused AFHTTPRequestOperation *operation, id responseObject) { + completionBlock(responseObject[@"client_token"], nil); + } + failure:^(__unused AFHTTPRequestOperation *operation, NSError *error) { + completionBlock(nil, error); + }]; +} + +- (void)makeTransactionWithPaymentMethodNonce:(NSString *)paymentMethodNonce completion:(void (^)(NSString *transactionId, NSError *error))completionBlock { + [self makeTransactionWithPaymentMethodNonce:paymentMethodNonce + merchantAccountId:nil + completion:completionBlock]; +} + +- (void)makeTransactionWithPaymentMethodNonce:(NSString *)paymentMethodNonce merchantAccountId:(NSString *)merchantAccountId completion:(void (^)(NSString *transactionId, NSError *error))completionBlock { + NSLog(@"Creating a transaction with nonce: %@", paymentMethodNonce); + NSMutableDictionary *parameters; + + switch ([BraintreeDemoSettings threeDSecureRequiredStatus]) { + case BraintreeDemoTransactionServiceThreeDSecureRequiredStatusDefault: + parameters = [@{ @"payment_method_nonce": paymentMethodNonce } mutableCopy]; + break; + case BraintreeDemoTransactionServiceThreeDSecureRequiredStatusRequired: + parameters = [@{ @"payment_method_nonce": paymentMethodNonce, @"three_d_secure_required": @YES, } mutableCopy]; + break; + case BraintreeDemoTransactionServiceThreeDSecureRequiredStatusNotRequired: + parameters = [@{ @"payment_method_nonce": paymentMethodNonce, @"three_d_secure_required": @NO, } mutableCopy]; + break; + } + + if (merchantAccountId != nil) { + [parameters setObject:merchantAccountId forKey:@"merchant_account_id"]; + } + + [self.sessionManager POST:@"/nonce/transaction" + parameters:parameters + success:^(__unused AFHTTPRequestOperation *operation, __unused id responseObject) { + completionBlock(responseObject[@"message"], nil); + } + failure:^(__unused AFHTTPRequestOperation *operation, __unused NSError *error) { + completionBlock(nil, error); + }]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/README.md b/examples/braintree/ios/Frameworks/Braintree/Demo/README.md new file mode 100755 index 00000000..da5a97d4 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/README.md @@ -0,0 +1,43 @@ +# Braintree Demo + +This is a universal iOS app that exercises just about every feature of Braintree iOS. + +You can take a look at the classes under [Features](./Features) to get a sense of how this SDK can be used. + +## Usage + +This app allows you to switch between the different features, or sample integrations, that it showcases. Each integration starts with loading a client token from a sample merchant server. This happens automatically when you open the app. Once the client token is loaded, the current integration is shown. + +You can switch between features using the `Settings` menu. This app will remember which feature you last looked at; the in-app settings are synchronized with the iOS Settings app. + +You can reload the current integration by tapping on the the reload button on the upper left. + +The current status is shown on the bottom toolbar. If you've created a payment method nonce, you tap on the status toolbar to create a transaction. + +### Compatibility + +This app should be compiled with a 8.x Base SDK (Xcode 6.x) and has a deployment target of iOS 7.0. + +## Implementation + +This codebase has three primary sections: + +* **Demo Base** - contains boilerplate code that facilitates switching between demo integrations. +* **Merchant API Client** - contains an API client that might be similar to one found in a real app; note that it consumes a _hypothetical merchant_ API, not Braintree's API. +* **Features** - contains a number of Braintree iOS demo integrations. + +Each demo integration must provide a `BraintreeDemoBaseViewController` subclass. Most importantly, the demo provides a `paymentButton`, which is presented to the user when the demo is selected. + +To add a new demo, you will additionally need to register the demo in the [Settings bundle](./Demo Base/Settings/Settings.bundle/Root.plist), identifying the view controller by class name. + +The most common class of integration, which involves presenting the user with a single button—to trigger whatever type of payment experience you choose—can be powered by another base class, `BraintreeDemoPaymentButtonBaseViewController`. + +Your demo view controller may call its `progressBlock` or `completionBlock` in order to update the rest of the app (and the user) about the payment method creation lifecycle. + +### Steps to Add a New Demo + +1. Create a new `BraintreeDemoBaseViewController` subclass in a new directory under Features. +2. Utilize `self.braintree` to implement a Braintree integration, and call `completionBlock` upon successfully creating a payment method. +3. Register this class in the Settings bundle, by adding new items in the `Integration` multi value item, under `titles` and `values`. + +💸👍🏻 diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Info.plist b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Info.plist new file mode 100755 index 00000000..e26d2ca3 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Info.plist @@ -0,0 +1,110 @@ + + + + + CFBundleDevelopmentRegion + en_US + CFBundleDisplayName + SDK Demo + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + da + de + en_AU + en_CA + en_GB + en + es_ES + es + fr_CA + fr_FR + fr + he + it + nb + nl + pl + pt + ru + sv + tr + zh-Hans + + CFBundleName + Braintree iOS SDK Demo + CFBundlePackageType + APPL + CFBundleShortVersionString + 4.8.4 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.braintreepayments.Demo + CFBundleURLSchemes + + com.braintreepayments.Demo.payments + + + + CFBundleVersion + 4.8.4 + LSApplicationQueriesSchemes + + com.braintreepayments.Demo.payments + com.venmo.touch.v2 + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + Launch Screen + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleLightContent + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Prefix.pch b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Prefix.pch new file mode 100755 index 00000000..3d611af3 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/Braintree-Demo-Prefix.pch @@ -0,0 +1,2 @@ +#import +#import diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/InfoPlist.strings b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/InfoPlist.strings new file mode 100755 index 00000000..477b28ff --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/Main.strings b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/Main.strings new file mode 100755 index 00000000..fdcb4955 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/en.lproj/Main.strings @@ -0,0 +1,63 @@ + +/* Class = "IBUILabel"; text = "[braintree payPalControlWithCompletion:];"; ObjectID = "2E9-9x-5fB"; */ +"2E9-9x-5fB.text" = "[braintree paymentButtonWithDelegate:…]"; + +/* Class = "IBUILabel"; text = "Transmit nonce to your server → 💰"; ObjectID = "2q2-ZJ-xOe"; */ +"2q2-ZJ-xOe.text" = "Transmit nonce to your server → 💰"; + +/* Class = "IBUILabel"; text = "Transaction"; ObjectID = "4hk-mK-yky"; */ +"4hk-mK-yky.text" = "Transaction"; + +/* Class = "IBUILabel"; text = "[braintree dropinViewControllerWithCompletion:];"; ObjectID = "9x8-Nu-myL"; */ +"9x8-Nu-myL.text" = "[braintree dropinViewControllerWithCompletion:…];"; + +/* Class = "IBUITableViewSection"; headerTitle = "Step 2: Create Nonce"; ObjectID = "LJb-7g-SHk"; */ +"LJb-7g-SHk.headerTitle" = "Create a Payment Method"; + +/* Class = "IBUILabel"; text = "One Touch"; ObjectID = "PaT-fr-JHw"; */ +"PaT-fr-JHw.text" = "Payment Buttons & One Touch"; + +/* Class = "IBUILabel"; text = "[braintree tokenizeCardWithNumber:…];"; ObjectID = "Rp4-mk-GPh"; */ +"Rp4-mk-GPh.text" = "[braintree tokenizeCardWithNumber:…];"; + +/* Class = "IBUILabel"; text = "(nil)"; ObjectID = "TF6-8A-TuS"; */ +"TF6-8A-TuS.text" = "(nil)"; + +/* Class = "IBUILabel"; text = "Nonce"; ObjectID = "Wbo-0U-3Vw"; */ +"Wbo-0U-3Vw.text" = "Nonce"; + +/* Class = "IBUILabel"; text = "Make a Transaction"; ObjectID = "Wop-mr-JDF"; */ +"Wop-mr-JDF.text" = "Make a Transaction"; + +/* Class = "IBUINavigationItem"; title = "Braintree SDK for iOS"; ObjectID = "ZEn-QI-hbv"; */ +"ZEn-QI-hbv.title" = "v.zero"; + +/* Class = "IBUILabel"; text = "Braintree"; ObjectID = "cV4-n0-2WW"; */ +"cV4-n0-2WW.text" = "Merchant"; + +/* Class = "IBUILabel"; text = "[Braintree braintreeWithClientToken:];"; ObjectID = "ccE-ej-MeD"; */ +"ccE-ej-MeD.text" = "[Braintree braintreeWithClientToken:];"; + +/* Class = "IBUILabel"; text = "Initialize with a Client Token"; ObjectID = "cdT-q9-6cH"; */ +"cdT-q9-6cH.text" = "Initialize with a Client Token"; + +/* Class = "IBUITableViewSection"; headerTitle = "Step 3: Make a Transaction"; ObjectID = "fYo-pp-MTy"; */ +"fYo-pp-MTy.headerTitle" = "Create a Transaction"; + +/* Class = "IBUILabel"; text = "Card Tokenization"; ObjectID = "g1s-7X-ara"; */ +"g1s-7X-ara.text" = "Card Tokenization"; + +/* Class = "IBUILabel"; text = "Drop-In"; ObjectID = "hWi-9S-SNj"; */ +"hWi-9S-SNj.text" = "Drop-In"; + +/* Class = "IBUITableViewSection"; headerTitle = "Library Version"; ObjectID = "hnV-8l-v2h"; */ +"hnV-8l-v2h.headerTitle" = "Library Version"; + +/* Class = "IBUILabel"; text = "pod \"Braintree\""; ObjectID = "ocb-oW-K0r"; */ +"ocb-oW-K0r.text" = "pod \"Braintree\""; + +/* Class = "IBUILabel"; text = "(nil)"; ObjectID = "pZN-Ur-Ks9"; */ +"pZN-Ur-Ks9.text" = "(nil)"; + +/* Class = "IBUILabel"; text = "(nil)"; ObjectID = "zaP-r6-4Ld"; */ +"zaP-r6-4Ld.text" = "(nil)"; diff --git a/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/main.m b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/main.m new file mode 100755 index 00000000..f4e520f7 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Demo/Supporting Files/main.m @@ -0,0 +1,10 @@ +#import + +#import "BraintreeDemoAppDelegate.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([BraintreeDemoAppDelegate class])); + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-4.0-Migration-Guide.md b/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-4.0-Migration-Guide.md new file mode 100755 index 00000000..a5229199 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-4.0-Migration-Guide.md @@ -0,0 +1 @@ +## This document has moved to [Braintree iOS SDK 4.0 Migration Guide](https://developers.braintreepayments.com/guides/client-sdk/migration/ios/v4) diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-Static-Integration-Guide.md b/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-Static-Integration-Guide.md new file mode 100755 index 00000000..3331c76b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Docs/Braintree-Static-Integration-Guide.md @@ -0,0 +1,52 @@ +Static Library Integration Guide +------------------------------------ + +Please follow these instructions to integrate Braintree iOS into your app using the provided static library. + +> Note: We assume that you are using Xcode 8+ and iOS 9.0+ as your Base SDK. + +1. Add the Braintree iOS SDK code to your repository + - [Download the SDK as a ZIP file from GitHub](https://github.com/braintree/braintree_ios/archive/master.zip) and unzip it into your app's root directory in Finder + +2. Open up your app in Xcode + +3. Add Braintree as a subproject + - Open your project and drag the Braintree.xcodeproj file to your Project Navigator under your project. Be sure *NOT* to have the Braintree.xcodeproj open while doing this step. + + ![Screenshot of adding Braintree as a subproject](bt_static_screenshot_sub_project.png) + +4. Add `Braintree` to your build phases (`[Your App Target]` > `Build Phases`) + - `Target Dependencies` + - Click the `+` and add `Braintree` + + ![Screenshot of adding Braintree to Target Dependencies](bt_static_screenshot_target_dependency.gif) + + - `Link Binary With Libraries` + - Click the `+` and add `libBraintree.a` + + ![Screenshot of adding Braintree to Link Bunary With Libraries](bt_static_screenshot_link_binary.gif) + +5. Add `localized strings` to `Copy Bundle Resources` (`[Your App Target]` > `Build Phases`) + - In the Project Navigator, locate the `UI.strings` (`Braintree.xcodeproj` > `BraintreeUI` > `Localization` > `UI.strings`) + - Drag the `UI.strings` file from the Navigator to the `Copy Bundle Resources` panel and drop it + - Repeat for remaining localized strings + - `Drop-In.strings` (`Braintree.xcodeproj` > `BraintreeUI` > `Drop-In` > `Localization` > `Drop-In.strings`) + - `Three-D-Secure.strings` (`Braintree.xcodeproj` > `Braintree3DSecure` > `Localization` > `Three-D-Secure.strings`) + + ![Screenshot of adding localized strings to Copy Bundle Resources](bt_static_screenshot_strings.gif) + +6. Modify your build settings (`[Your App Target]` > `Build Settings`) + - Update `Header Search Paths` + - Add `$(PROJECT_DIR)/braintree_ios` (or whatever the name of the braintree folder at the top level of your project is) + - Be sure to select recursive from the drop down at the right + + ![Screenshot of updating Header Search Paths](bt_static_screenshot_header_search_paths.png) + + - Update `Other Linker Flags` + - Add `-ObjC` + + ![Screenshot of updating Header Search Paths](bt_static_screenshot_linker_flags.png) + +7. `Build and Run` your app to test out the integration + +8. [Integrate the SDK in your checkout form](https://developers.braintreepayments.com/ios/start/overview) diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/Drop-In-Update.md b/examples/braintree/ios/Frameworks/Braintree/Docs/Drop-In-Update.md new file mode 100755 index 00000000..20040354 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Docs/Drop-In-Update.md @@ -0,0 +1,5 @@ +Drop-In Update (Beta) +------------------------------------ + +Please see the [Braintree iOS Drop-In repository](https://github.com/braintree/braintree-ios-drop-in) for the latest Drop-In code. + diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/Manual Integration.md b/examples/braintree/ios/Frameworks/Braintree/Docs/Manual Integration.md new file mode 100755 index 00000000..56c53ff8 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Docs/Manual Integration.md @@ -0,0 +1,56 @@ +Manual Integration Without CocoaPods +------------------------------------ + +Please follow these instructions to integrate Braintree iOS into your app without CocoaPods. + +> Note: We assume that you are using Xcode 8+ and iOS 9.0+ as your Base SDK. + +1. Add the Braintree iOS SDK code to your repository + - Use git: `git submodule add https://github.com/braintree/braintree_ios.git` + - Alternatively, you can [download the SDK as a ZIP file from GitHub](https://github.com/braintree/braintree_ios/archive/master.zip) and unzip it into your app's root directory in Finder + - Delete the following folders: `Braintree-Demo`, `Docs`, `IntegrationTests`, `Specs`, `Unit Tests` +2. Open up your app in Xcode +3. Create a new framework target called `Braintree` (please use this exact name) + - In Xcode, select `File` > `New` > `Target` + - Select `Framework & Library` > `Cocoa Touch Framework` + - Use the following options + - Product Name: `Braintree` + - Language: `Objective C` + - Embed in Application: `[your app target]` + - You will now see a new `Braintree` Target Dependency in your main app target (in the first section of `Build Phases`). +4. Add the Braintree code to project + - In Xcode, select `File` > `Add Files to [...]...` + - Navigate to `[Your app project root]/braintree-ios` and select the `Braintree` directory + - Under `Add to targets`, make sure your newly-created framework target `Braintree` is checked and that `[your app target]` is unchecked + - Optionally check `Copy items if needed` + - Click `Add` + ![Screenshot of adding the Braintree files to Braintree target](screenshot_add_files.png) +5. Modify the `Braintree` target's build phases (`Project` > `Braintree` > `Build Phases`) + - In `Compile Sources`, delete all `.md` files (tip: filter by `.md`) + - In `Headers` + - Under `Public`, delete `Braintree.h` + - Select all files under `Project` and drag them to `Public` + - In `Link Binary With Libraries` + - Add the following system frameworks: + - `Contacts` + - `CoreLocation` + - `Foundation` + - `MessageUI` + - `PassKit` + - `SystemConfiguration` + - `UIKit` + - Update `Contacts` to be weak linked by changing its status to `Optional`. + - In `Copy Bundle Resources`, remove everything except the `.strings` files. +6. Modify `Braintree` build settings (`Project` > `Braintree` > `Build Settings`) + - Edit `Public Headers Folder Path` by appending `/Braintree` (e.g. `$(CONTENTS_FOLDER_PATH)/Headers/Braintree`) + - Edit `Other Linker Flags` by adding `-lc++ -ObjC` +7. Modify `[your app target]` build settings (`Project` > `[your app]` > `Build Settings`) + - Set `Always Search User Paths` to `Yes` +8. Modify `[your app target]` build phases (select the `[your app]` target, then `Build Phases`) + - In `Copy Bundle Resources`, add `Drop-In.strings`, `UI.strings` and `Three-D-Secure.strings` from the Braintree framework target (tip: filter by `.strings`) + ![Screenshot of copying bundle resources for i18n](screenshot_copy_bundles.png) +9. Remove the `Braintree` scheme + - In Xcode, select `Product` > `Scheme` > `Manage Schemes...` + - Select the `Braintree` scheme and press the `-` button +10. Build and Run your app to test out the integration +11. [Integrate the SDK in your checkout form](https://developers.braintreepayments.com/ios/start/overview) diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_header_search_paths.png b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_header_search_paths.png new file mode 100755 index 00000000..9c459315 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_header_search_paths.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_link_binary.gif b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_link_binary.gif new file mode 100755 index 00000000..380aa99c Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_link_binary.gif differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_linker_flags.png b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_linker_flags.png new file mode 100755 index 00000000..074daee7 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_linker_flags.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_strings.gif b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_strings.gif new file mode 100755 index 00000000..21c81cce Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_strings.gif differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_sub_project.png b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_sub_project.png new file mode 100755 index 00000000..49975c75 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_sub_project.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_target_dependency.gif b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_target_dependency.gif new file mode 100755 index 00000000..2c429db6 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/bt_static_screenshot_target_dependency.gif differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods-dark.png b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods-dark.png new file mode 100755 index 00000000..1d8bff88 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods-dark.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods.png b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods.png new file mode 100755 index 00000000..5369faa9 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-payment-methods.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/saved-paypal-method.png b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-paypal-method.png new file mode 100755 index 00000000..deb6449a Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/saved-paypal-method.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_apple_pay.png b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_apple_pay.png new file mode 100755 index 00000000..45deb384 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_apple_pay.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_files.png b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_files.png new file mode 100755 index 00000000..ae64811b Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_add_files.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_copy_bundles.png b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_copy_bundles.png new file mode 100755 index 00000000..5625a515 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/Docs/screenshot_copy_bundles.png differ diff --git a/examples/braintree/ios/Frameworks/Braintree/Frameworks.markdown b/examples/braintree/ios/Frameworks/Braintree/Frameworks.markdown new file mode 100755 index 00000000..73772298 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Frameworks.markdown @@ -0,0 +1,137 @@ +# Frameworks + +The Braintree iOS SDK is organized into a family of frameworks. + +### Differences from Braintree-iOS 3.x +* Frameworks and Carthage support +* Client key and JWT instead of client token +* `BTAPIClient` instead of `BTClient` +* `BTTokenized` instead of `BTPaymentMethod` +* Refactored tests and added tests in Swift + + +## BraintreeCore + +This is the core set of models and networking needed to use Braintree in an app or extension. All other frameworks depend on this. + +PRIMARY CLASS: +### `BTAPIClient`: Braintree API client +* Authentication with client key / JWT +* Access configuration from gateway +* Analytics +* HTTP methods on Braintree API endpoints + +#### Other Classes + +* `BTAppSwitch`: Class and protocol for authentication via app switch +* `BTJSON`: JSON parser + +## Payment Options + +The Braintree iOS SDK currently supports 6 payment options. + +1. `BraintreeCard`: Credit and debit card + * No dependencies other than `BraintreeCore` +2. `BraintreeApplePay`: Apple Pay + * Depends on `PassKit` +3. `BraintreePayPal`: PayPal + * No dependencies other than `BraintreeCore` + * Use `BTPaymentDriverDelegate` to receive app switch lifecycle events +4. `BraintreeVenmo`: Venmo + * Depends on `BraintreeCard` +5. `Braintree3DSecure`: 3D Secure + * Depends on `BraintreeCard` + * Use `BTViewControllerPresentingDelegate` (required) for cases when a view controller must be presented for buyer verification +6. `BraintreeCoinbase`: Coinbase + * No dependencies other than `BraintreeCore` + + +## BraintreeCard + +Tokenizes credit or debit cards. + +PRIMARY CLASS: +### `BTCardTokenizationClient`: Tokenizes credit and debit card info + +#### Other Classes + +* `BTCardTokenizationRequest`: Raw credit or debit card data provided by the customer +* `BTTokenizedCard`: A tokenized card that contains a payment method nonce + + +## BraintreeUI + +A pre-built payment form and payment button. + +Optionally uses these payment option frameworks, if present: `BraintreeCard`, `BraintreePayPal`, `BraintreeVenmo`, `BraintreeCoinbase`. + +### Features + +* UI + * Card form +* Drop-in + + +## BraintreePayPal + +Accept payments with PayPal app via PayPal One Touch. + +### Features + +* `BTPayPalDriver`: Coordinates paying with PayPal by switching to the PayPal app or the web browser +* **Future payments** via `-authorizeAccount...` + * `BTTokenizedPayPalAccount`: A tokenized PayPal account that contains a payment method nonce +* **Single payments** via `-checkoutWithCheckoutRequest...` + * `BTTokenizedPayPalCheckout`: A tokenized PayPal checkout that contains a payment method nonce +* `BTPayPalCheckoutRequest`: Options for a PayPal checkout flow + + +## BraintreeVenmo + +**Depends on BraintreeCard.** + +Accept payments with a credit or debit card from the Venmo app via Venmo One Touch. + +### Features + +* `BTVenmoDriver`: Coordinates switching to the Venmo app for the buyer to select a card +* `BTVenmoTokenizedCard`: A tokenized card from Venmo that contains a payment method nonce + + +## BraintreeCoinbase + +Accept bitcoin payments via Coinbase. + +## Features + +* `BTCoinbaseDriver`: Coordinates paying with Coinbase by switching to the Coinbase app or the web browser +* `BTTokenizedCoinbaseAccount`: A tokenized Coinbase account that contains a payment method nonce + + +## BraintreeApplePay + +**Depends on `PassKit`.** + +Accept Apple Pay by using Braintree to process payments. + +### Features + +* `BTApplePayTokenizationClient`: Performs tokenization of a `PKPayment` and returns a tokenized Apple Pay payment instrument +* `BTTokenizedApplePayPayment`: A tokenized Apple Pay payment that contains a payment method nonce + + +## Braintree3DSecure + +**Depends on `BraintreeCard`.** + +Perform 3D Secure verification. + +### Features + +* `BTThreeDSecureDriver`: Coordinates 3D Secure verification via in-app web view +* `BTThreeDSecureVerification`: Card/transactions details to be verified +* `BTThreeDSecureTokenizedCard`: A tokenized card that contains a payment method nonce + * `liabilityShifted`: 3D Secure worked and authentication succeeded. This will also be true if the issuing bank does not support 3D Secure, but the payment method does + * `liabilityShiftPossible`: The payment method was eligible for 3D Secure + * These parameters pass through the client-side first and should not be trusted for your server-side risk assessment + diff --git a/examples/braintree/ios/Frameworks/Braintree/Gemfile b/examples/braintree/ios/Frameworks/Braintree/Gemfile new file mode 100755 index 00000000..58c410e4 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gem 'cocoapods' +gem 'rake' +gem 'git-pairing' +gem 'highline', :require => 'highline/import' +gem 'rake_commit' +gem 'shenzhen' +gem 'xcpretty' + diff --git a/examples/braintree/ios/Frameworks/Braintree/Gemfile.lock b/examples/braintree/ios/Frameworks/Braintree/Gemfile.lock new file mode 100755 index 00000000..3a20730f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Gemfile.lock @@ -0,0 +1,128 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (2.3.5) + activesupport (4.2.8) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + awesome_print (1.6.1) + aws-sdk (1.64.0) + aws-sdk-v1 (= 1.64.0) + aws-sdk-v1 (1.64.0) + json (~> 1.4) + nokogiri (>= 1.4.4) + claide (1.0.1) + cocoapods (1.2.0) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.1, < 2.0) + cocoapods-core (= 1.2.0) + cocoapods-deintegrate (>= 1.0.1, < 2.0) + cocoapods-downloader (>= 1.1.3, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.1.2, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored (~> 1.2) + escape (~> 0.0.4) + fourflusher (~> 2.0.1) + gh_inspector (~> 1.0) + molinillo (~> 0.5.5) + nap (~> 1.0) + ruby-macho (~> 0.2.5) + xcodeproj (>= 1.4.1, < 2.0) + cocoapods-core (1.2.0) + activesupport (>= 4.0.2, < 5) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-deintegrate (1.0.1) + cocoapods-downloader (1.1.3) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.0.0) + cocoapods-trunk (1.1.2) + nap (>= 0.8, < 2.0) + netrc (= 0.7.8) + cocoapods-try (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.3.5) + highline (~> 1.7.2) + dotenv (2.0.2) + escape (0.0.4) + faraday (0.8.9) + multipart-post (~> 1.2.0) + faraday_middleware (0.10.0) + faraday (>= 0.7.4, < 0.10) + fourflusher (2.0.1) + fuzzy_match (2.0.4) + gh_inspector (1.0.3) + git-pairing (0.5.3) + awesome_print (>= 1.1.0) + highline (>= 1.6.15) + paint (>= 0.8.5) + trollop (>= 2.0) + highline (1.7.3) + i18n (0.8.1) + json (1.8.6) + mini_portile (0.6.2) + minitest (5.10.1) + molinillo (0.5.7) + multipart-post (1.2.0) + nanaimo (0.2.3) + nap (1.1.0) + net-sftp (2.1.2) + net-ssh (>= 2.6.5) + net-ssh (2.9.2) + netrc (0.7.8) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) + paint (1.0.0) + plist (3.1.0) + rake (10.4.2) + rake_commit (1.2.0) + ruby-macho (0.2.6) + rubyzip (1.1.7) + security (0.1.3) + shenzhen (0.14.2) + aws-sdk (~> 1.0) + commander (~> 4.3) + dotenv (>= 0.7) + faraday (~> 0.8.9) + faraday_middleware (~> 0.9) + highline (>= 1.7.2) + json (~> 1.8) + net-sftp (~> 2.1.2) + plist (~> 3.1.0) + rubyzip (~> 1.1) + security (~> 0.1.3) + terminal-table (~> 1.4.5) + terminal-table (1.4.5) + thread_safe (0.3.6) + trollop (2.1.2) + tzinfo (1.2.3) + thread_safe (~> 0.1) + xcodeproj (1.4.4) + CFPropertyList (~> 2.3.3) + claide (>= 1.0.1, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.3) + xcpretty (0.1.11) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods + git-pairing + highline + rake + rake_commit + shenzhen + xcpretty + +BUNDLED WITH + 1.13.3 diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTAPIClient_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTAPIClient_IntegrationTests.m new file mode 100755 index 00000000..19e9375c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTAPIClient_IntegrationTests.m @@ -0,0 +1,53 @@ +#import +#import +#import + +@interface BTAPIClient_IntegrationTests : XCTestCase +@end + +@implementation BTAPIClient_IntegrationTests + +- (void)testFetchConfiguration_withTokenizationKey_returnsTheConfiguration { + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [client fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertEqualObjects([configuration.json[@"merchantId"] asString], @"dcpspy2brwdjr3qn"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFetchConfiguration_withClientToken_returnsTheConfiguration { + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [client fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + // Note: client token uses a different merchant ID than the merchant whose tokenization key + // we use in the other test + XCTAssertEqualObjects([configuration.json[@"merchantId"] asString], @"348pk9cgf3bgyw2b"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFetchConfiguration_withVersionThreeClientToken_returnsTheConfiguration { + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN_VERSION_3]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [client fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + // Note: client token uses a different merchant ID than the merchant whose tokenization key + // we use in the other test + XCTAssertEqualObjects([configuration.json[@"merchantId"] asString], @"dcpspy2brwdjr3qn"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTHTTPSSLPinning_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTHTTPSSLPinning_IntegrationTests.m new file mode 100755 index 00000000..b9d1b347 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/BTHTTPSSLPinning_IntegrationTests.m @@ -0,0 +1,108 @@ +#import "BTHTTP.h" +#import + +@interface BTHTTPSSLPinning_IntegrationTests : XCTestCase +@end + +@implementation BTHTTPSSLPinning_IntegrationTests + +// Will work when we comply with ATS +- (void)testBTHTTP_whenUsingProductionEnvironmentWithTrustedSSLCertificates_allowsNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"https://api.braintreegateway.com"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"/heartbeat.json" completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) { + XCTAssertEqualObjects([body[@"heartbeat"] asString], @"d2765eaa0dad9b300b971f074-production"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBTHTTP_whenUsingSandboxEnvironmentWithTrustedSSLCertificates_allowsNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"https://api.sandbox.braintreegateway.com"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"/heartbeat.json" completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) { + XCTAssertEqualObjects([body[@"heartbeat"] asString], @"d2765eaa0dad9b300b971f074-sandbox"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBTHTTP_whenUsingAServerWithValidCertificateChainWithARootCAThatWeDoNotExplicitlyTrust_doesNotAllowNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"https://www.digicert.com"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"/heartbeat.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(body); + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, NSURLErrorDomain); + XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - SSL Pinning + +#ifdef RUN_SSL_PINNING_SPECS + +- (void)testBTHTTP_whenUsingTrustedPinnedRootCertificates_allowsNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"https://localhost:9443"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + http.pinnedCertificates = @[[NSData dataWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"good_root_cert" ofType:@"der"]]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBTHTTP_whenUsingUntrustedUnpinnedRootCertificatesFromLegitimateHosts_doesNotallowNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"https://localhost:9444"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + http.pinnedCertificates = @[[NSData dataWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"good_root_cert" ofType:@"der"]]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"heartbeat" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(body); + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, NSURLErrorDomain); + XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBTHTTP_whenUsingNonSSLConnection_allowsNetworkCommunication { + NSURL *url = [NSURL URLWithString:@"http://localhost:9445/"]; + BTHTTP *http = [[BTHTTP alloc] initWithBaseURL:url tokenizationKey:@"development_testing_integration_merchant_id"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [http GET:@"heartbeat" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#endif + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.der b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.der new file mode 100755 index 00000000..45dec5cd Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.der differ diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.pem new file mode 100755 index 00000000..4d33be0a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_cert.pem @@ -0,0 +1,63 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + c4:3f:63:74:d1:c2:3f:8b + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=CA, ST=California, O=All That is Evil, OU=All That is Evil Root CA + Validity + Not Before: Jun 4 17:46:36 2014 GMT + Not After : May 11 17:46:36 2114 GMT + Subject: C=CA, ST=California, O=All That is Evil, OU=All That is Evil Root CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:aa:cc:0d:cf:8c:f6:be:8f:61:af:69:09:d8:b4: + 9c:e3:7f:f1:81:1d:4d:c7:72:8f:cc:00:b4:60:01: + 4f:15:39:14:42:34:7f:f5:7c:76:ec:29:20:17:b1: + 44:e8:b8:ad:3a:8f:e1:6c:0f:4f:5d:82:d2:e0:8c: + 91:18:3d:bd:84:a6:58:06:90:d4:06:d3:93:0a:27: + 65:41:9b:8e:46:2d:28:d8:e4:60:d3:ec:ab:08:d5: + 9b:1e:34:1d:c2:ad:e1:23:9b:23:9f:7d:90:57:b5: + e8:8d:a3:12:84:88:7a:e0:79:7b:a1:18:3d:30:2e: + f0:bf:e8:07:e0:bf:2d:32:47 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + C5:27:18:DE:1E:17:5D:AA:D5:49:75:16:A6:45:E1:A6:49:1A:43:E3 + X509v3 Authority Key Identifier: + keyid:C5:27:18:DE:1E:17:5D:AA:D5:49:75:16:A6:45:E1:A6:49:1A:43:E3 + DirName:/C=CA/ST=California/O=All That is Evil/OU=All That is Evil Root CA + serial:C4:3F:63:74:D1:C2:3F:8B + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + a3:8e:07:86:f8:70:56:dc:df:f2:fa:57:fe:a2:b6:fc:db:6e: + a7:b7:ec:f0:51:3a:f0:77:a3:6e:d7:2f:3b:cf:d8:b7:8a:7f: + fd:d0:50:ca:85:77:b4:ce:d2:bf:f4:d2:70:31:ce:c1:9f:f9: + 66:9a:74:7f:2b:97:c1:aa:3c:d2:96:ff:37:e1:1d:c2:10:31: + f4:8b:05:1e:1b:52:0e:52:66:12:eb:0a:b0:2a:4a:88:25:94: + ed:2a:9c:16:03:90:7b:6c:2b:30:78:ce:65:6b:24:e0:f9:e8: + 87:65:2a:e1:5d:3d:ed:ec:84:db:60:3d:23:ab:0f:7e:80:99: + 60:55 +-----BEGIN CERTIFICATE----- +MIIDBjCCAm+gAwIBAgIJAMQ/Y3TRwj+LMA0GCSqGSIb3DQEBBQUAMGAxCzAJBgNV +BAYTAkNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRkwFwYDVQQKExBBbGwgVGhhdCBp +cyBFdmlsMSEwHwYDVQQLExhBbGwgVGhhdCBpcyBFdmlsIFJvb3QgQ0EwIBcNMTQw +NjA0MTc0NjM2WhgPMjExNDA1MTExNzQ2MzZaMGAxCzAJBgNVBAYTAkNBMRMwEQYD +VQQIEwpDYWxpZm9ybmlhMRkwFwYDVQQKExBBbGwgVGhhdCBpcyBFdmlsMSEwHwYD +VQQLExhBbGwgVGhhdCBpcyBFdmlsIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAKrMDc+M9r6PYa9pCdi0nON/8YEdTcdyj8wAtGABTxU5FEI0f/V8 +duwpIBexROi4rTqP4WwPT12C0uCMkRg9vYSmWAaQ1AbTkwonZUGbjkYtKNjkYNPs +qwjVmx40HcKt4SObI599kFe16I2jEoSIeuB5e6EYPTAu8L/oB+C/LTJHAgMBAAGj +gcUwgcIwHQYDVR0OBBYEFMUnGN4eF12q1Ul1FqZF4aZJGkPjMIGSBgNVHSMEgYow +gYeAFMUnGN4eF12q1Ul1FqZF4aZJGkPjoWSkYjBgMQswCQYDVQQGEwJDQTETMBEG +A1UECBMKQ2FsaWZvcm5pYTEZMBcGA1UEChMQQWxsIFRoYXQgaXMgRXZpbDEhMB8G +A1UECxMYQWxsIFRoYXQgaXMgRXZpbCBSb290IENBggkAxD9jdNHCP4swDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCjjgeG+HBW3N/y+lf+orb8226nt+zw +UTrwd6Nu1y87z9i3in/90FDKhXe0ztK/9NJwMc7Bn/lmmnR/K5fBqjzSlv834R3C +EDH0iwUeG1IOUmYS6wqwKkqIJZTtKpwWA5B7bCsweM5layTg+eiHZSrhXT3t7ITb +YD0jqw9+gJlgVQ== +-----END CERTIFICATE----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_key.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_key.pem new file mode 100755 index 00000000..2c86951d --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_root_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqzA3PjPa+j2GvaQnYtJzjf/GBHU3Hco/MALRgAU8VORRCNH/1 +fHbsKSAXsUTouK06j+FsD09dgtLgjJEYPb2EplgGkNQG05MKJ2VBm45GLSjY5GDT +7KsI1ZseNB3CreEjmyOffZBXteiNoxKEiHrgeXuhGD0wLvC/6Afgvy0yRwIDAQAB +AoGAfdQbEJ9PYRCM7Qe7Y1Wch9ZIe9C07o0t9yNFv7z3IDGPBT9cTeTGUDH0HMBS +fgkgRhaeAlg9Ji0tYpTsiClkJtLx8CRnOJE2ON4DSjV7X24Me4zeEobimWlfNU/6 +R1KGcbHBHlIedf+l0okPdMgveDp/25a/ekZZiv0sYHwI2IECQQDgRzHda1/AMtkH +Wq6Ycemwfu6CYiTDXvK/0ZOLZA/CrOBcWIkSPEAm3Obd5Zkc9XR6og4C7vm86JRo +g4k51honAkEAwvRiWiUGAPDw6htOiSGICBRtjThiwyvTgtQkcqqfilLrMjkLOZ8m +K8dsByfnkpCrv8ZRCTiznBRF43ohtk3a4QJAZsY5Q44AwsKKUaRsfc81l3uTMIxo +7F6GPwB67FVeI4e1CJxJs+GIREbWRLkCARM53TiF0zJPnxG1cG9WYvqJ4QJBAJWg +MRgUkE4KnixfNuCCrr/cxdP8Mbivm08u+KZVE8t7Jm5OX7Ii1o4FKYE4fD/97wNp +9uoH7bndyWH0a4laqaECQFxpY99GqpxsMkcbhsWWDvj4LxocCSfD4vgs8nJJprh5 +mQqau9VfIQFU6ify4C/2I662AG+DkgvFjt1rtow8rMU= +-----END RSA PRIVATE KEY----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_cert.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_cert.pem new file mode 100755 index 00000000..1ddee355 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_cert.pem @@ -0,0 +1,47 @@ +Certificate: + Data: + Version: 1 (0x0) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=CA, ST=California, O=All That is Evil, OU=All That is Evil Root CA + Validity + Not Before: Jun 4 17:46:36 2014 GMT + Not After : May 11 17:46:36 2114 GMT + Subject: C=CA, ST=California, O=All That is Evil, OU=All That is Evil Site, CN=* + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:c4:d2:3f:0a:ad:03:b7:fb:fb:c4:0e:b1:39:78: + 18:02:b5:aa:4f:61:4d:6b:55:2c:01:f8:3d:83:32: + 5c:5d:7c:d6:13:cc:e2:c3:15:0b:ed:92:58:35:90: + 49:47:0a:10:36:dc:36:9c:c4:22:75:64:b7:69:8e: + 10:30:bc:64:ac:d8:cf:a5:5d:eb:27:b2:e7:a5:db: + 2e:47:4f:b2:f5:b6:97:da:1a:0a:6e:15:14:fc:4b: + 86:6a:16:b8:93:63:30:32:7f:dd:87:39:01:cc:17: + 27:6c:4e:cc:be:4f:02:75:44:46:fe:52:58:7b:43: + da:a1:36:5b:ec:83:b3:3f:db + Exponent: 65537 (0x10001) + Signature Algorithm: sha1WithRSAEncryption + a8:fe:44:33:5e:88:2c:e5:b7:ea:6a:04:62:92:d8:47:4c:bc: + fd:2f:c3:33:fb:6a:41:67:c8:f4:ae:87:86:89:a9:08:c4:a2: + 56:54:73:42:25:fa:62:59:b4:4f:39:43:9b:63:97:3b:a0:ee: + c9:bd:ff:1f:00:ae:92:52:82:b0:96:34:c9:bd:7a:ec:57:ef: + ba:cf:72:cf:62:bd:17:db:47:b8:58:b5:5b:e9:e4:25:0e:2f: + 7a:41:7a:68:7d:fd:c7:7d:e1:7b:e0:b9:6c:4a:d9:5c:aa:5e: + a1:61:62:80:09:69:59:16:7d:71:18:2b:8e:8f:2f:d5:94:93: + 3c:29 +-----BEGIN CERTIFICATE----- +MIICOjCCAaMCAQEwDQYJKoZIhvcNAQEFBQAwYDELMAkGA1UEBhMCQ0ExEzARBgNV +BAgTCkNhbGlmb3JuaWExGTAXBgNVBAoTEEFsbCBUaGF0IGlzIEV2aWwxITAfBgNV +BAsTGEFsbCBUaGF0IGlzIEV2aWwgUm9vdCBDQTAgFw0xNDA2MDQxNzQ2MzZaGA8y +MTE0MDUxMTE3NDYzNlowaTELMAkGA1UEBhMCQ0ExEzARBgNVBAgTCkNhbGlmb3Ju +aWExGTAXBgNVBAoTEEFsbCBUaGF0IGlzIEV2aWwxHjAcBgNVBAsTFUFsbCBUaGF0 +IGlzIEV2aWwgU2l0ZTEKMAgGA1UEAxQBKjCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEAxNI/Cq0Dt/v7xA6xOXgYArWqT2FNa1UsAfg9gzJcXXzWE8ziwxUL7ZJY +NZBJRwoQNtw2nMQidWS3aY4QMLxkrNjPpV3rJ7LnpdsuR0+y9baX2hoKbhUU/EuG +aha4k2MwMn/dhzkBzBcnbE7Mvk8CdURG/lJYe0PaoTZb7IOzP9sCAwEAATANBgkq +hkiG9w0BAQUFAAOBgQCo/kQzXogs5bfqagRikthHTLz9L8Mz+2pBZ8j0roeGiakI +xKJWVHNCJfpiWbRPOUObY5c7oO7Jvf8fAK6SUoKwljTJvXrsV++6z3LPYr0X20e4 +WLVb6eQlDi96QXpoff3HfeF74LlsStlcql6hYWKACWlZFn1xGCuOjy/VlJM8KQ== +-----END CERTIFICATE----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_key.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_key.pem new file mode 100755 index 00000000..8373ca6d --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDE0j8KrQO3+/vEDrE5eBgCtapPYU1rVSwB+D2DMlxdfNYTzOLD +FQvtklg1kElHChA23DacxCJ1ZLdpjhAwvGSs2M+lXesnsuel2y5HT7L1tpfaGgpu +FRT8S4ZqFriTYzAyf92HOQHMFydsTsy+TwJ1REb+Ulh7Q9qhNlvsg7M/2wIDAQAB +AoGBAIHDO90GBJWghHTWWvHQw8PFkeuT8z74gHMr/yIoac0ZKOsVAcwsbBjNi2qF +Hkq/z8DbnwXsKevL40CscPAwxQ4oP4G8K66cdifBd/ErdvuCeynIPleMoCnUc6Ql +q+9sy6UopSoeW6hZ8FpBZN3GdQaGDeFab1Znxd4Ey/kOf0kBAkEA47L7dBw6+iGC +6fZ8icnBZuK7QCQIu089sXwVmnnxjaz4PIjce6H+680ItB2sBlSRUCV+fcM1Sk9S +ta2MR8480QJBAN1Ix7SMK8LXdZuSBgBOH0H4QlUivE9Aa90f2G9yUFn5PRTdxlou +m/vbAQ1YqywUz43RipaIwU+whxSx/a11rOsCQHYg1PNX8gDygch/Z/zT/tIxrpOI +Hj+OzKLXjR2nRfoKUn6VQk2hrW8H4AwRmL1wAjNiQE1eiLcUkARRFQXrqQECQQCy +OH7XdBl+uKd5H5eDwWe9aySJiwtdTQZStuZLhCcg//LpDmFFmsp4gv+K70IVo8Ey +eHSFHymKdCOnUF9+yAr3AkAzTmtyM1fd72u87sIIXP/cwDKBkMTiR9LuMTVY/KWb +JmhCbgijP+58ZvHOOHbmWKDSTK81r0AvBI8F3SyVHPm3 +-----END RSA PRIVATE KEY----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_request.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_request.pem new file mode 100755 index 00000000..9fef5f06 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/evil_site_request.pem @@ -0,0 +1,40 @@ +Certificate Request: + Data: + Version: 0 (0x0) + Subject: C=CA, ST=California, O=All That is Evil, OU=All That is Evil Site, CN=* + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:c4:d2:3f:0a:ad:03:b7:fb:fb:c4:0e:b1:39:78: + 18:02:b5:aa:4f:61:4d:6b:55:2c:01:f8:3d:83:32: + 5c:5d:7c:d6:13:cc:e2:c3:15:0b:ed:92:58:35:90: + 49:47:0a:10:36:dc:36:9c:c4:22:75:64:b7:69:8e: + 10:30:bc:64:ac:d8:cf:a5:5d:eb:27:b2:e7:a5:db: + 2e:47:4f:b2:f5:b6:97:da:1a:0a:6e:15:14:fc:4b: + 86:6a:16:b8:93:63:30:32:7f:dd:87:39:01:cc:17: + 27:6c:4e:cc:be:4f:02:75:44:46:fe:52:58:7b:43: + da:a1:36:5b:ec:83:b3:3f:db + Exponent: 65537 (0x10001) + Attributes: + a0:00 + Signature Algorithm: sha1WithRSAEncryption + 35:7e:59:a7:c3:2e:56:7d:3f:8e:e3:63:0d:cc:de:b8:d7:6c: + 3a:d4:10:ab:08:90:ad:bd:af:c4:44:e2:b1:dc:b3:2a:de:1d: + 4c:35:93:fd:4a:95:86:9a:63:64:02:d5:fe:59:57:0a:b1:08: + cc:b5:4a:1b:c8:1d:08:2e:cc:05:98:85:92:5a:69:5f:de:7c: + a2:dc:d0:ef:ad:da:82:a6:a1:7e:0e:72:22:bf:a9:ca:7b:bc: + 3a:11:f7:9e:68:c3:04:40:ad:20:93:fe:d1:32:ab:86:fa:36: + 7c:96:ce:7b:d6:12:ab:3c:e9:a5:62:f6:2c:2d:b3:3d:06:b7: + 3d:f8 +-----BEGIN CERTIFICATE REQUEST----- +MIIBqTCCARICAQAwaTELMAkGA1UEBhMCQ0ExEzARBgNVBAgTCkNhbGlmb3JuaWEx +GTAXBgNVBAoTEEFsbCBUaGF0IGlzIEV2aWwxHjAcBgNVBAsTFUFsbCBUaGF0IGlz +IEV2aWwgU2l0ZTEKMAgGA1UEAxQBKjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC +gYEAxNI/Cq0Dt/v7xA6xOXgYArWqT2FNa1UsAfg9gzJcXXzWE8ziwxUL7ZJYNZBJ +RwoQNtw2nMQidWS3aY4QMLxkrNjPpV3rJ7LnpdsuR0+y9baX2hoKbhUU/EuGaha4 +k2MwMn/dhzkBzBcnbE7Mvk8CdURG/lJYe0PaoTZb7IOzP9sCAwEAAaAAMA0GCSqG +SIb3DQEBBQUAA4GBADV+WafDLlZ9P47jYw3M3rjXbDrUEKsIkK29r8RE4rHcsyre +HUw1k/1KlYaaY2QC1f5ZVwqxCMy1ShvIHQguzAWYhZJaaV/efKLc0O+t2oKmoX4O +ciK/qcp7vDoR955owwRArSCT/tEyq4b6NnyWznvWEqs86aVi9iwtsz0Gtz34 +-----END CERTIFICATE REQUEST----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_cert.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_cert.pem new file mode 100755 index 00000000..21352d83 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_cert.pem @@ -0,0 +1,63 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + b9:6f:f6:ef:ab:b7:31:a9 + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=CA, ST=California, O=All That is Good, OU=All That is Good Root CA + Validity + Not Before: Jun 4 17:46:36 2014 GMT + Not After : May 11 17:46:36 2114 GMT + Subject: C=CA, ST=California, O=All That is Good, OU=All That is Good Root CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:dd:31:54:4a:7d:8f:5c:37:bd:4c:d5:e8:54:fd: + 06:d2:b9:0e:bd:89:d1:1d:22:17:7a:7e:14:0c:f2: + e6:4f:89:b4:41:ed:08:8d:2a:d1:4d:8e:05:92:ae: + 0c:83:f6:59:59:94:58:ae:15:2b:78:12:94:70:89: + 8a:af:9c:c8:a2:e8:c7:fd:e9:2e:d2:b1:7a:f5:bf: + 18:4c:a9:56:0c:b7:3b:8b:00:96:c6:25:02:ab:f7: + 97:94:30:7c:a0:bf:86:2a:55:2b:56:da:e1:d9:eb: + c1:ed:b1:8b:96:08:9e:26:f8:58:cb:ae:4a:cf:d9: + 4c:73:b2:83:4d:db:d7:68:a7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + C2:D0:38:78:7D:3C:A3:CE:48:F2:25:FD:F9:AC:5B:80:DE:4B:F6:8B + X509v3 Authority Key Identifier: + keyid:C2:D0:38:78:7D:3C:A3:CE:48:F2:25:FD:F9:AC:5B:80:DE:4B:F6:8B + DirName:/C=CA/ST=California/O=All That is Good/OU=All That is Good Root CA + serial:B9:6F:F6:EF:AB:B7:31:A9 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + d1:3b:85:b0:7a:cb:cb:10:4d:cb:97:28:ad:7b:ea:a6:d5:1d: + 98:c7:b4:8c:d9:47:06:bc:ba:c3:aa:64:d5:97:65:d8:78:79: + c9:66:87:9f:8c:aa:eb:ac:d6:65:95:c5:8b:ba:bd:f3:15:54: + 01:64:21:0e:82:98:e8:2a:9c:de:80:a3:90:4f:5f:5a:69:7f: + d2:76:f9:f3:74:99:e2:9e:02:46:af:8c:04:e4:ea:5e:56:ee: + ef:93:ff:0a:72:44:4d:6a:b6:96:de:92:17:a9:3b:8f:86:77: + 49:2a:0a:64:0e:2e:8d:26:04:c5:f6:e6:15:ea:ad:86:62:67: + 14:6c +-----BEGIN CERTIFICATE----- +MIIDBjCCAm+gAwIBAgIJALlv9u+rtzGpMA0GCSqGSIb3DQEBBQUAMGAxCzAJBgNV +BAYTAkNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRkwFwYDVQQKExBBbGwgVGhhdCBp +cyBHb29kMSEwHwYDVQQLExhBbGwgVGhhdCBpcyBHb29kIFJvb3QgQ0EwIBcNMTQw +NjA0MTc0NjM2WhgPMjExNDA1MTExNzQ2MzZaMGAxCzAJBgNVBAYTAkNBMRMwEQYD +VQQIEwpDYWxpZm9ybmlhMRkwFwYDVQQKExBBbGwgVGhhdCBpcyBHb29kMSEwHwYD +VQQLExhBbGwgVGhhdCBpcyBHb29kIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAN0xVEp9j1w3vUzV6FT9BtK5Dr2J0R0iF3p+FAzy5k+JtEHtCI0q +0U2OBZKuDIP2WVmUWK4VK3gSlHCJiq+cyKLox/3pLtKxevW/GEypVgy3O4sAlsYl +Aqv3l5QwfKC/hipVK1ba4dnrwe2xi5YInib4WMuuSs/ZTHOyg03b12inAgMBAAGj +gcUwgcIwHQYDVR0OBBYEFMLQOHh9PKPOSPIl/fmsW4DeS/aLMIGSBgNVHSMEgYow +gYeAFMLQOHh9PKPOSPIl/fmsW4DeS/aLoWSkYjBgMQswCQYDVQQGEwJDQTETMBEG +A1UECBMKQ2FsaWZvcm5pYTEZMBcGA1UEChMQQWxsIFRoYXQgaXMgR29vZDEhMB8G +A1UECxMYQWxsIFRoYXQgaXMgR29vZCBSb290IENBggkAuW/276u3MakwDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQDRO4WwesvLEE3Llyite+qm1R2Yx7SM +2UcGvLrDqmTVl2XYeHnJZoefjKrrrNZllcWLur3zFVQBZCEOgpjoKpzegKOQT19a +aX/SdvnzdJningJGr4wE5OpeVu7vk/8KckRNaraW3pIXqTuPhndJKgpkDi6NJgTF +9uYV6q2GYmcUbA== +-----END CERTIFICATE----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_key.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_key.pem new file mode 100755 index 00000000..c9c229a5 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_root_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDdMVRKfY9cN71M1ehU/QbSuQ69idEdIhd6fhQM8uZPibRB7QiN +KtFNjgWSrgyD9llZlFiuFSt4EpRwiYqvnMii6Mf96S7SsXr1vxhMqVYMtzuLAJbG +JQKr95eUMHygv4YqVStW2uHZ68HtsYuWCJ4m+FjLrkrP2UxzsoNN29dopwIDAQAB +AoGBAKmOrOzFP2YTnFsQBp9PrzFNhs0onlJU1eaiS0B52q7SAooe59U/I17uJbuB +DDsEVw3iN/CKbd4HcB6scNGZv/ok3spb0We3kwKgii1+qIzmZ3qwkIqnTAhxISTU +QQ9euvPMoPcG1ZyZscU+2ayFGCy+MXQmArIuANlXFMJFp+SRAkEA7t4n9zUTbBk1 +TmSn8mWOUk7C3ir9pfKuY+qEAB8XdW4UTZbPZH0QaiwS+UkPa+byhWHpY4BG8BPR +LMgXLp55MwJBAO0OpCBJcmwV3WggaCRvwdD2hO56mwhuFwJGszxFBHma6/N6iuiB +KcIV3Wg7SwdgUCXNup6SMf6TzRJ8RjTUWr0CQF1Oak4mbW/MaQY2S2RkRzPfkD84 +i9xG79gXw3hIrOEyHrwwLNMUB1Vx4fd+koeTryhrFr/HW+5rz0mu319WiAECQHvW +Ni8Xr0p/cZY/t6exKhK7dV4PdoXE3Qg3XtKRS3ErWS9sSLyFHQdi3LLipqNH0Rau +jlrgDHXtSCfr+9EFThECQAaOPqp2W5OzrXu004VRnyi7W0/zs3ObptiaJJa9meY8 +5tG8cGFBY4lcdv7OkOx1GEDYPQEIyRa6RuzNUmIXwi4= +-----END RSA PRIVATE KEY----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_cert.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_cert.pem new file mode 100755 index 00000000..4f95c27e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_cert.pem @@ -0,0 +1,48 @@ +Certificate: + Data: + Version: 1 (0x0) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=CA, ST=California, O=All That is Good, OU=All That is Good Root CA + Validity + Not Before: Jun 4 17:46:36 2014 GMT + Not After : May 11 17:46:36 2114 GMT + Subject: C=CA, ST=California, O=All That is Good, OU=All That is Good Site, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:ab:c9:bb:0a:6e:7a:b5:d8:40:a2:fb:83:47:e6: + a5:87:08:47:76:d1:ea:e7:16:70:21:cd:18:79:8b: + 27:40:80:10:1a:3b:b3:f8:2f:7b:88:d3:79:1e:b2: + 54:8f:2e:73:41:83:13:6f:8c:6d:58:f8:80:26:19: + 39:ae:8f:d1:c9:c7:c0:52:45:9b:2c:50:39:13:18: + 34:12:10:22:cb:90:a0:64:85:df:b3:7f:c1:17:94: + 50:c1:6b:ee:83:23:72:95:aa:87:2b:6a:0f:89:96: + 8e:85:b4:fc:d6:00:fe:97:a3:46:69:4c:53:67:0d: + 4a:ab:f5:b5:78:c9:88:34:93 + Exponent: 65537 (0x10001) + Signature Algorithm: sha1WithRSAEncryption + 32:b4:ab:b1:27:e1:72:53:fc:74:82:f7:95:6c:7d:ec:0e:f9: + 43:c5:2b:01:46:2f:f9:fd:ac:39:4c:90:4a:69:81:88:a6:fe: + 63:63:9f:a4:23:21:b5:88:53:c5:d4:e1:cf:90:48:eb:a0:eb: + 4a:6a:26:90:b6:ca:c0:fd:04:cd:66:6e:12:e8:63:91:e8:0f: + 46:a9:ff:a8:4a:c1:cf:b4:c4:61:4d:a2:02:25:77:d7:28:05: + 38:58:5c:8e:d2:91:c8:f1:63:d4:fb:4a:4e:6f:0c:2f:66:4d: + b5:54:91:6e:27:1c:8c:d0:e5:a2:73:4c:ae:f8:8d:5a:63:2d: + 96:be +-----BEGIN CERTIFICATE----- +MIICQjCCAasCAQEwDQYJKoZIhvcNAQEFBQAwYDELMAkGA1UEBhMCQ0ExEzARBgNV +BAgTCkNhbGlmb3JuaWExGTAXBgNVBAoTEEFsbCBUaGF0IGlzIEdvb2QxITAfBgNV +BAsTGEFsbCBUaGF0IGlzIEdvb2QgUm9vdCBDQTAgFw0xNDA2MDQxNzQ2MzZaGA8y +MTE0MDUxMTE3NDYzNlowcTELMAkGA1UEBhMCQ0ExEzARBgNVBAgTCkNhbGlmb3Ju +aWExGTAXBgNVBAoTEEFsbCBUaGF0IGlzIEdvb2QxHjAcBgNVBAsTFUFsbCBUaGF0 +IGlzIEdvb2QgU2l0ZTESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCrybsKbnq12ECi+4NH5qWHCEd20ernFnAhzRh5iydAgBAa +O7P4L3uI03keslSPLnNBgxNvjG1Y+IAmGTmuj9HJx8BSRZssUDkTGDQSECLLkKBk +hd+zf8EXlFDBa+6DI3KVqocrag+Jlo6FtPzWAP6Xo0ZpTFNnDUqr9bV4yYg0kwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBADK0q7En4XJT/HSC95VsfewO+UPFKwFGL/n9 +rDlMkEppgYim/mNjn6QjIbWIU8XU4c+QSOug60pqJpC2ysD9BM1mbhLoY5HoD0ap +/6hKwc+0xGFNogIld9coBThYXI7SkcjxY9T7Sk5vDC9mTbVUkW4nHIzQ5aJzTK74 +jVpjLZa+ +-----END CERTIFICATE----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_key.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_key.pem new file mode 100755 index 00000000..65a6ca55 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXwIBAAKBgQCrybsKbnq12ECi+4NH5qWHCEd20ernFnAhzRh5iydAgBAaO7P4 +L3uI03keslSPLnNBgxNvjG1Y+IAmGTmuj9HJx8BSRZssUDkTGDQSECLLkKBkhd+z +f8EXlFDBa+6DI3KVqocrag+Jlo6FtPzWAP6Xo0ZpTFNnDUqr9bV4yYg0kwIDAQAB +AoGBAKo7zk4YDSIGmoboFsA5n+6gFbF5c/5sDdJxG7/WVZ9lSI+2ejGHXDPK3Eu/ +DGyW60AQVEJGNlXka5lVhgOmIYzjOcd/yEFJm9AvmlPXWVKJ1mpz9geZ1tuT4R+d +zfq15nhJsU226JK1+5Ik0XJoSxEUkct0Mt//VlfDZRlKcE+BAkEA3oIh6hwFDef8 +MbLhjpvELCSXLRPadkBYxxi6pNjsUOI8A0g7IebRKYI9C+rpmTQffOXjYCyKpqep +GqbBkRVOnwJBAMWlM9oD+VZCGnNMQqnXFqSXGCtyftkSSfRFosDZk4cQk/398pVE +v58h0lcQslfH+QC2jNYlpWU5Z4omwiIluY0CQQDePXgGTqo1s4nPUe279JTBymI8 +oeHHzoldgrOZRxjxyKVMWe7F87biELVMm/tqDAePRkYOny51OmzKs9gOQwvdAkEA +tpyU8/KIBXK+DZmAXnwkp54S7tGy8c08Fz3fyl89N6XRlvNzlwcWJWmSdm8u2Hwj +TM1eAt51mrkXOUXmSLaiYQJBAJdjC9KRBorIk0cGS/SKAUn4ysoRgbp+aB3ErHwb +xjMCdohHDaHOJIkL1O8mzt6i3LQMitw+fURcTG2vGvHsS5I= +-----END RSA PRIVATE KEY----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_request.pem b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_request.pem new file mode 100755 index 00000000..a36c7eff --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/good_site_request.pem @@ -0,0 +1,41 @@ +Certificate Request: + Data: + Version: 0 (0x0) + Subject: C=CA, ST=California, O=All That is Good, OU=All That is Good Site, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:ab:c9:bb:0a:6e:7a:b5:d8:40:a2:fb:83:47:e6: + a5:87:08:47:76:d1:ea:e7:16:70:21:cd:18:79:8b: + 27:40:80:10:1a:3b:b3:f8:2f:7b:88:d3:79:1e:b2: + 54:8f:2e:73:41:83:13:6f:8c:6d:58:f8:80:26:19: + 39:ae:8f:d1:c9:c7:c0:52:45:9b:2c:50:39:13:18: + 34:12:10:22:cb:90:a0:64:85:df:b3:7f:c1:17:94: + 50:c1:6b:ee:83:23:72:95:aa:87:2b:6a:0f:89:96: + 8e:85:b4:fc:d6:00:fe:97:a3:46:69:4c:53:67:0d: + 4a:ab:f5:b5:78:c9:88:34:93 + Exponent: 65537 (0x10001) + Attributes: + a0:00 + Signature Algorithm: sha1WithRSAEncryption + 4d:4c:d5:31:ef:1c:04:b0:a9:50:f4:98:e9:31:66:b2:44:f2: + 3c:80:97:a0:9b:20:3c:a1:7a:69:33:7f:5a:23:2a:2f:ac:12: + 15:f4:94:54:15:4f:3c:a0:d7:d2:c2:33:7e:e1:fd:c1:10:39: + 39:f2:28:bd:c7:d6:c2:36:c6:a0:d8:5f:d7:2f:56:92:07:e6: + 9d:8b:52:d3:93:0a:34:34:c8:e7:3a:7c:3d:48:73:6c:63:14: + 1f:a7:d1:bd:38:9f:ca:f8:84:33:db:9c:6b:80:86:a2:e5:4d: + 53:7d:c6:98:b3:7e:3a:f1:87:e1:b1:5c:81:dc:50:68:a9:91: + 19:aa +-----BEGIN CERTIFICATE REQUEST----- +MIIBsTCCARoCAQAwcTELMAkGA1UEBhMCQ0ExEzARBgNVBAgTCkNhbGlmb3JuaWEx +GTAXBgNVBAoTEEFsbCBUaGF0IGlzIEdvb2QxHjAcBgNVBAsTFUFsbCBUaGF0IGlz +IEdvb2QgU2l0ZTESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQCrybsKbnq12ECi+4NH5qWHCEd20ernFnAhzRh5iydAgBAaO7P4 +L3uI03keslSPLnNBgxNvjG1Y+IAmGTmuj9HJx8BSRZssUDkTGDQSECLLkKBkhd+z +f8EXlFDBa+6DI3KVqocrag+Jlo6FtPzWAP6Xo0ZpTFNnDUqr9bV4yYg0kwIDAQAB +oAAwDQYJKoZIhvcNAQEFBQADgYEATUzVMe8cBLCpUPSY6TFmskTyPICXoJsgPKF6 +aTN/WiMqL6wSFfSUVBVPPKDX0sIzfuH9wRA5OfIovcfWwjbGoNhf1y9WkgfmnYtS +05MKNDTI5zp8PUhzbGMUH6fRvTifyviEM9uca4CGouVNU33GmLN+OvGH4bFcgdxQ +aKmRGao= +-----END CERTIFICATE REQUEST----- diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/https_server.rb b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/https_server.rb new file mode 100755 index 00000000..188f7ed0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/https_server.rb @@ -0,0 +1,97 @@ +#!/usr/bin/env ruby +require 'webrick' +require 'webrick/https' +require 'openssl' + +BEGIN { File.write("#{ $0 }.pid", $$) } +END { File.delete("#{ $0 }.pid") } + +private_key_file = "#{__dir__}/good_site_key.pem" +cert_file = "#{__dir__}/good_site_cert.pem" +root_cert_file = "#{__dir__}/good_root_cert.pem" + +pkey = OpenSSL::PKey::RSA.new(File.read(private_key_file)) +cert = OpenSSL::X509::Certificate.new(File.read(cert_file)) +root_cert = OpenSSL::X509::Certificate.new(File.read(root_cert_file)) + +def log message + puts "[https_server] #{message}" +end + +good_server = WEBrick::HTTPServer.new( + :BindAddress => '0.0.0.0', + :Port => 9443, + :Logger => WEBrick::Log::new(nil, WEBrick::Log::ERROR), + :SSLEnable => true, + :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, + :SSLCertificate => cert, + :SSLPrivateKey => pkey, + :SSLExtraChainCert => [root_cert] +) +good_server.mount_proc '/' do |req, res| + res.body = '{ "status": "ok", "server": "good" }' + res.content_type = 'application/json' +end + +evil_private_key_file = "#{__dir__}/evil_site_key.pem" +evil_cert_file = "#{__dir__}/evil_site_cert.pem" +evil_root_cert_file = "#{__dir__}/evil_root_cert.pem" + +pkey = OpenSSL::PKey::RSA.new(File.read(evil_private_key_file)) +cert = OpenSSL::X509::Certificate.new(File.read(evil_cert_file)) +root_cert = OpenSSL::X509::Certificate.new(File.read(evil_root_cert_file)) + +evil_server = WEBrick::HTTPServer.new( + :BindAddress => '0.0.0.0', + :Port => 9444, + :Logger => WEBrick::Log::new(nil, WEBrick::Log::ERROR), + :SSLEnable => true, + :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, + :SSLCertificate => cert, + :SSLPrivateKey => pkey, + :SSLExtraChainCert => [root_cert] +) +evil_server.mount_proc '/' do |req, res| + res.body = '{ "status": "ok", "server": "evil" }' + res.content_type = 'application/json' +end + +http_server = WEBrick::HTTPServer.new( + :BindAddress => '0.0.0.0', + :Port => 9445, + :Logger => WEBrick::Log::new(nil, WEBrick::Log::ERROR), + :SSLEnable => false, +) +http_server.mount_proc '/' do |req, res| + res.body = '{ "status": "ok", "server": "non-ssl" }' + res.content_type = 'application/json' +end + +t1 = Thread.new do + log 'Starting good server on :9443' + $stdout.flush + good_server.start +end + +t2 = Thread.new do + log 'Starting evil server on :9444' + $stdout.flush + evil_server.start +end + +t3 = Thread.new do + log 'Starting http server on :9445' + $stdout.flush + http_server.start +end + +trap("INT") do + log 'Shutting down...' + t1.kill; + t2.kill; + t3.kill +end + +t1.join +t2.join +t3.join diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/make_certs.sh b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/make_certs.sh new file mode 100755 index 00000000..e793d70b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/SSL/make_certs.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env zsh + +set -ve + +# Make Good Root CA with new key +openssl req -new -newkey rsa:1024 -keyout good_root_key.pem -nodes -x509 -days 36500 -out good_root_cert.pem -subj "/C=CA/ST=California/O=All That is Good/OU=All That is Good Root CA" -text + +# Make Good Site Req with new key +openssl req -new -newkey rsa:1024 -keyout good_site_key.pem -nodes -days 36500 -out good_site_request.pem -subj "/C=CA/ST=California/O=All That is Good/OU=All That is Good Site/CN=localhost" -text + +# Sign Good Site Req with new key +openssl x509 -in good_site_request.pem -req -CA good_root_cert.pem -CAkey good_root_key.pem -days 36500 -set_serial 1 -out good_site_cert.pem -text + +# Make Evil Root CA with new key +openssl req -new -newkey rsa:1024 -keyout evil_root_key.pem -nodes -x509 -days 36500 -out evil_root_cert.pem -subj "/C=CA/ST=California/O=All That is Evil/OU=All That is Evil Root CA" -text + +# Make Evil Site Req with new key +openssl req -new -newkey rsa:1024 -keyout evil_site_key.pem -nodes -days 36500 -out evil_site_request.pem -subj "/C=CA/ST=California/O=All That is Evil/OU=All That is Evil Site/CN=*" -text + +# Sign Evil Site Req with new key +openssl x509 -in evil_site_request.pem -req -CA evil_root_cert.pem -CAkey evil_root_key.pem -days 36500 -set_serial 1 -out evil_site_cert.pem -text + +# Encode certificates for iOS Specs +openssl x509 -in good_root_cert.pem -inform pem -out good_root_cert.der -outform der + +set +v +echo '=============================' +echo Verifying newly created certs +echo '=============================' + +# Verify +openssl verify -verbose -purpose sslclient -CAfile good_root_cert.pem good_site_cert.pem +openssl verify -verbose -purpose sslclient -CAfile evil_root_cert.pem good_site_cert.pem + +openssl verify -verbose -purpose sslclient -CAfile good_root_cert.pem evil_site_cert.pem +openssl verify -verbose -purpose sslclient -CAfile evil_root_cert.pem evil_site_cert.pem diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/good_root_cert.der b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/good_root_cert.der new file mode 100755 index 00000000..64417424 Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Braintree-API-Integration-Specs/good_root_cert.der differ diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeApplePay_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeApplePay_IntegrationTests.m new file mode 100755 index 00000000..685e4849 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeApplePay_IntegrationTests.m @@ -0,0 +1,43 @@ +#import +#import "BTIntegrationTestsHelper.h" +#import +#import + +@interface BraintreeApplePay_IntegrationTests : XCTestCase + +@end + +@implementation BraintreeApplePay_IntegrationTests + +- (void)testTokenizeApplePayPayment_whenApplePayEnabledInControlPanel_returnsANonce { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTApplePayClient *client = [[BTApplePayClient alloc] initWithAPIClient:apiClient]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize Apple Pay payment"]; + [client tokenizeApplePayPayment:[[PKPayment alloc] init] + completion:^(BTApplePayCardNonce * _Nullable tokenizedApplePayPayment, NSError * _Nullable error) { + XCTAssertTrue(tokenizedApplePayPayment.nonce.isANonce); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testTokenizeApplePayPayment_whenApplePayDisabledInControlPanel_returnsError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY_APPLE_PAY_DISABLED]; + BTApplePayClient *client = [[BTApplePayClient alloc] initWithAPIClient:apiClient]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize Apple Pay payment"]; + [client tokenizeApplePayPayment:[[PKPayment alloc] init] + completion:^(BTApplePayCardNonce * _Nullable tokenizedApplePayPayment, NSError * _Nullable error) { + XCTAssertEqualObjects(error.domain, BTApplePayErrorDomain); + XCTAssertEqual(error.code, BTApplePayErrorTypeUnsupported); + XCTAssertNil(tokenizedApplePayPayment); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeCard_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeCard_IntegrationTests.m new file mode 100755 index 00000000..b82598bf --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeCard_IntegrationTests.m @@ -0,0 +1,131 @@ +#import "BTIntegrationTestsHelper.h" +#import +#import +#import +#import + +@interface BTCardClient_IntegrationTests : XCTestCase +@end + +@implementation BTCardClient_IntegrationTests + +- (void)testTokenizeCard_whenCardHasValidationDisabledAndCardIsInvalid_tokenizesSuccessfully { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [self invalidCard]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + expect(tokenizedCard.nonce.isANonce).to.beTruthy(); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testTokenizeCard_whenCardIsInvalidAndValidationIsEnabled_failsWithExpectedValidationError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [[BTCard alloc] initWithNumber:@"123" expirationMonth:@"12" expirationYear:@"2020" cvv:nil]; + card.shouldValidate = YES; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + XCTAssertNil(tokenizedCard); + XCTAssertEqualObjects(error.domain, BTCardClientErrorDomain); + XCTAssertEqual(error.code, BTCardClientErrorTypeCustomerInputInvalid); + XCTAssertEqualObjects(error.localizedDescription, @"Credit card is invalid"); + XCTAssertEqualObjects(error.localizedFailureReason, @"Credit card number must be 12-19 digits"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testTokenizeCard_whenCardHasValidationDisabledAndCardIsValid_tokenizesSuccessfully { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [self validCard]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + expect(tokenizedCard.nonce.isANonce).to.beTruthy(); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + + +- (void)testTokenizeCard_whenUsingTokenizationKeyAndCardHasValidationEnabled_failsWithAuthorizationError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [self invalidCard]; + card.shouldValidate = YES; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + XCTAssertNil(tokenizedCard); + expect(error.domain).to.equal(BTHTTPErrorDomain); + expect(error.code).to.equal(BTHTTPErrorCodeClientError); + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)error.userInfo[BTHTTPURLResponseKey]; + expect(httpResponse.statusCode).to.equal(403); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testTokenizeCard_whenUsingClientTokenAndCardHasValidationEnabledAndCardIsValid_tokenizesSuccessfully { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [self validCard]; + card.shouldValidate = YES; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + expect(tokenizedCard.nonce.isANonce).to.beTruthy(); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testTokenizeCard_whenUsingVersionThreeClientTokenAndCardHasValidationEnabledAndCardIsValid_tokenizesSuccessfully { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN_VERSION_3]; + BTCardClient *client = [[BTCardClient alloc] initWithAPIClient:apiClient]; + BTCard *card = [self validCard]; + card.shouldValidate = YES; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenize card"]; + [client tokenizeCard:card completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + expect(tokenizedCard.nonce.isANonce).to.beTruthy(); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Helpers + +- (BTCard *)invalidCard { + BTCard *card = [[BTCard alloc] init]; + card.number = @"INVALID_CARD"; + card.expirationMonth = @"XX"; + card.expirationYear = @"YYYY"; + return card; +} + +- (BTCard *)validCard { + BTCard *card = [[BTCard alloc] init]; + card.number = @"4111111111111111"; + card.expirationMonth = @"12"; + card.expirationYear = @"2018"; + return card; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeDataCollector_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeDataCollector_IntegrationTests.m new file mode 100755 index 00000000..0ca1ac63 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeDataCollector_IntegrationTests.m @@ -0,0 +1,114 @@ +#import "BraintreeDataCollector.h" +#import "KDataCollector.h" +#import +#import + +@interface BraintreeDataCollector_IntegrationTests : XCTestCase +@property (nonatomic, strong) BTDataCollector *dataCollector; +@end + +@implementation BraintreeDataCollector_IntegrationTests + +- (void)setUp { + [super setUp]; + BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:client]; +} + +- (void)tearDown { + [super tearDown]; + self.dataCollector = nil; +} + +#pragma mark - collectFraudData: + +- (void)testCollectFraudData_returnsFraudData { + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + + [self.dataCollector collectFraudData:^(NSString * _Nonnull deviceData) { + XCTAssertTrue([deviceData containsString:@"correlation_id"]); + XCTAssertTrue([deviceData containsString:@"device_session_id"]); + XCTAssertTrue([deviceData containsString:@"fraud_merchant_id"]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +// Test is failing because sandbox test merchant is configured with a Kount merchant ID that causes Kount to error. +- (void)pendCollectFraudDataWithCallback_returnsFraudData { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:apiClient]; + id delegate = OCMProtocolMock(@protocol(BTDataCollectorDelegate)); + self.dataCollector.delegate = delegate; + XCTestExpectation *expectation = [self expectationWithDescription:@"Delegate received completion callback"]; + OCMStub([delegate dataCollectorDidComplete:self.dataCollector]).andDo(^(__unused NSInvocation *invocation) { + [expectation fulfill]; + }); + + XCTestExpectation *callbackExpectation = [self expectationWithDescription:@"Callback invoked"]; + [self.dataCollector collectFraudData:^(NSString * _Nonnull deviceData) { + XCTAssertTrue([deviceData containsString:@"correlation_id"]); + [callbackExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +// Test is failing because sandbox test merchant is configured with a Kount merchant ID that causes Kount to error. +- (void)pendCollectCardFraudDataWithCallback_returnsFraudDataWithNoPayPalFraudData { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + self.dataCollector = [[BTDataCollector alloc] initWithAPIClient:apiClient]; + + id delegate = OCMProtocolMock(@protocol(BTDataCollectorDelegate)); + self.dataCollector.delegate = delegate; + XCTestExpectation *expectation = [self expectationWithDescription:@"Delegate received completion callback"]; + OCMStub([delegate dataCollectorDidComplete:self.dataCollector]).andDo(^(__unused NSInvocation *invocation) { + [expectation fulfill]; + }); + + XCTestExpectation *callbackExpectation = [self expectationWithDescription:@"Callback invoked"]; + [self.dataCollector collectCardFraudData:^(NSString * _Nonnull deviceData) { + XCTAssertNotNil(deviceData); + XCTAssertFalse([deviceData containsString:@"correlation_id"]); + [callbackExpectation fulfill]; + }]; + + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testCollectCardFraudData_returnsFraudDataWithNoPayPalFraudData { + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.dataCollector collectCardFraudData:^(NSString * _Nonnull deviceData) { + XCTAssertNotNil(deviceData); + XCTAssertFalse([deviceData containsString:@"correlation_id"]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +// Test is failing because Kount is no longer async and doesn't return errors +- (void)pendCollectCardFraudData_whenMerchantIDIsInvalid_invokesErrorCallback { + [self.dataCollector setFraudMerchantId:@"-1"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Error callback invoked"]; + + [self.dataCollector collectCardFraudData:^(NSString * _Nonnull deviceData) { + NSLog(@"%@", deviceData); + //XCTAssertEqualObjects(error.localizedDescription, @"Merchant ID formatted incorrectly."); + //XCTAssertEqual(error.code, (NSInteger)KDataCollectorErrorCodeBadParameter); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testCollectPayPalClientMetadataId_returnsClientMetadataId { + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.dataCollector collectFraudData:^(NSString * _Nonnull deviceData) { + XCTAssertTrue([deviceData containsString:@"correlation_id"]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreePayPal_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreePayPal_IntegrationTests.m new file mode 100755 index 00000000..678f80d9 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreePayPal_IntegrationTests.m @@ -0,0 +1,649 @@ +#import +#import +#import +#import "BTIntegrationTestsHelper.h" +#import +#import + +@interface BTAppSwitchTestDelegate : NSObject +@property (nonatomic, strong) XCTestExpectation *willPerform; +@property (nonatomic, strong) XCTestExpectation *didPerform; +@property (nonatomic, strong) XCTestExpectation *willProcess; +@property (nonatomic, strong) id lastAppSwitcher; +@property (nonatomic, assign) BTAppSwitchTarget lastTarget; +@end + +@implementation BTAppSwitchTestDelegate + +- (void)appSwitcherWillPerformAppSwitch:(id)appSwitcher { + self.lastAppSwitcher = appSwitcher; + [self.willPerform fulfill]; +} + +- (void)appSwitcher:(id)appSwitcher didPerformSwitchToTarget:(BTAppSwitchTarget)target { + self.lastTarget = target; + self.lastAppSwitcher = appSwitcher; + [self.didPerform fulfill]; +} + +- (void)appSwitcherWillProcessPaymentInfo:(id)appSwitcher { + self.lastAppSwitcher = appSwitcher; + [self.willProcess fulfill]; +} + +@end + +@interface BTViewControllerPresentingTestDelegate : NSObject +@property (nonatomic, strong) XCTestExpectation *requestsPresentationExpectation; +@property (nonatomic, strong) XCTestExpectation *requestsDismissalExpectation; +@property (nonatomic, strong) id lastDriver; +@property (nonatomic, strong) id lastViewController; +@end + +@implementation BTViewControllerPresentingTestDelegate + +- (void)paymentDriver:(id)driver requestsDismissalOfViewController:(UIViewController *)viewController { + self.lastDriver = driver; + self.lastViewController = viewController; + [self.requestsDismissalExpectation fulfill]; +} + +- (void)paymentDriver:(id)driver requestsPresentationOfViewController:(UIViewController *)viewController { + self.lastDriver = driver; + self.lastViewController = viewController; + [self.requestsPresentationExpectation fulfill]; +} + +@end + +@interface BTPayPalApprovalHandlerTestDelegate : NSObject +@property (nonatomic, strong) XCTestExpectation *handleApprovalExpectation; +@property (nonatomic, strong) NSURL *url; +@property (nonatomic, assign) BOOL cancel; +@end + +@implementation BTPayPalApprovalHandlerTestDelegate + +- (void)handleApproval:(__unused PPOTRequest *)request paypalApprovalDelegate:(id)delegate { + if (self.cancel) { + [delegate onApprovalCancel]; + } else { + [delegate onApprovalComplete:self.url]; + } + [self.handleApprovalExpectation fulfill]; +} + +@end + + +@interface BraintreePayPal_IntegrationTests : XCTestCase +@property (nonatomic, strong) NSNumber *didReceiveCompletionCallback; +// We keep a reference to these stub delegates so they don't get released! +@property (nonatomic, strong) BTViewControllerPresentingTestDelegate *viewControllerPresentingDelegate; +@property (nonatomic, strong) BTAppSwitchTestDelegate *appSwitchDelegate; + +@end + + +@implementation BraintreePayPal_IntegrationTests + +NSString * const OneTouchCoreAppSwitchSuccessURLFixture = @"com.braintreepayments.Demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"; + +#pragma mark - Authorization (Future Payments) + +- (void)testFuturePayments_withTokenizationKey_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + payPalDriver.clientMetadataId = @"fake-client-metadata-id"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenized PayPal Account"]; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFuturePayments_withClientToken_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + id stubDelegate = [[BTViewControllerPresentingTestDelegate alloc] init]; + payPalDriver.viewControllerPresentingDelegate = stubDelegate; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Tokenized PayPal Account"]; + [payPalDriver authorizeAccountWithAdditionalScopes:[NSSet set] forceFuturePaymentFlow:YES completion:^(BTPayPalAccountNonce * _Nonnull tokenizedPayPalAccount, NSError * _Nonnull error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFuturePayments_whenReturnURLSchemeIsMissing_returnsError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + id stubDelegate = [[BTViewControllerPresentingTestDelegate alloc] init]; + payPalDriver.viewControllerPresentingDelegate = stubDelegate; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [payPalDriver authorizeAccountWithAdditionalScopes:[NSSet set] forceFuturePaymentFlow:YES completion:^(BTPayPalAccountNonce * _Nonnull tokenizedPayPalAccount, NSError * _Nonnull error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertEqualObjects(error.domain, BTPayPalDriverErrorDomain); + XCTAssertEqual(error.code, BTPayPalDriverErrorTypeIntegrationReturnURLScheme); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFuturePayments_whenReturnURLSchemeIsInvalid_returnsError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + id stubDelegate = [[BTViewControllerPresentingTestDelegate alloc] init]; + payPalDriver.viewControllerPresentingDelegate = stubDelegate; + [BTAppSwitch sharedInstance].returnURLScheme = @"not-my-app-bundle-id"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [payPalDriver authorizeAccountWithAdditionalScopes:[NSSet set] forceFuturePaymentFlow:YES completion:^(BTPayPalAccountNonce * _Nonnull tokenizedPayPalAccount, NSError * _Nonnull error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertEqualObjects(error.domain, BTPayPalDriverErrorDomain); + XCTAssertEqual(error.code, BTPayPalDriverErrorTypeIntegrationReturnURLScheme); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testFuturePayments_onCancelledAppSwitchAuthorization_callsBackWithNoTokenizedAccountOrError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + id stubDelegate = [[BTViewControllerPresentingTestDelegate alloc] init]; + payPalDriver.viewControllerPresentingDelegate = stubDelegate; + + self.didReceiveCompletionCallback = nil; + [payPalDriver authorizeAccountWithAdditionalScopes:[NSSet set] forceFuturePaymentFlow:YES completion:^(BTPayPalAccountNonce * _Nonnull tokenizedPayPalAccount, NSError * _Nonnull error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNil(error); + self.didReceiveCompletionCallback = @(YES); + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:@"com.braintreepayments.Demo.payments://onetouch/v1/cancel?payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IjQ1QUZEQkE3LUJEQTYtNDNEMi04MUY2LUY4REM1QjZEOTkzQSIsImVudmlyb25tZW50IjoibW9jayJ9&x-source=com.paypal.ppclient.touch.v2"]]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"didReceiveCompletionCallback != nil"]; + [self expectationForPredicate:predicate evaluatedWithObject:self handler:nil]; + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - One-time (Checkout) payments + +- (void)testOneTimePayment_withTokenizationKey_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [self stubDelegatesForPayPalDriver:payPalDriver]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"1.00"]; + [payPalDriver requestOneTimePayment:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize one-time payment"]; + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testOneTimePayment_withClientToken_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [self stubDelegatesForPayPalDriver:payPalDriver]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"1.00"]; + [payPalDriver requestOneTimePayment:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize one-time payment"]; + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testOneTimePayment_withClientToken_tokenizesPayPalAccount_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.url = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"1.00"]; + [payPalDriver requestOneTimePayment:request handler:testApproval completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize one-time payment"]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testOneTimePayment_withClientToken_returnsErrorWithMalformedURL_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.url = [NSURL URLWithString:@"bad://url"]; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"1.00"]; + [payPalDriver requestOneTimePayment:request handler:testApproval completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNotNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testOneTimePayment_withClientToken_cancels_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.cancel = YES; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] initWithAmount:@"1.00"]; + [payPalDriver requestOneTimePayment:request handler:testApproval completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Billing Agreement + +- (void)testBillingAgreement_withTokenizationKey_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [self stubDelegatesForPayPalDriver:payPalDriver]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] init]; + [payPalDriver requestBillingAgreement:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize billing agreement payment"]; + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBillingAgreement_withClientToken_tokenizesPayPalAccount { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [self stubDelegatesForPayPalDriver:payPalDriver]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] init]; + [payPalDriver requestBillingAgreement:request completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize billing agreement payment"]; + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBillingAgreement_withClientToken_tokenizesPayPalAccount_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.url = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + + __block XCTestExpectation *tokenizationExpectation; + BTPayPalRequest *request = [[BTPayPalRequest alloc] init]; + [payPalDriver requestBillingAgreement:request handler:testApproval completion:^(BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount, NSError * _Nullable error) { + XCTAssertTrue(tokenizedPayPalAccount.nonce.isANonce); + XCTAssertNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + tokenizationExpectation = [self expectationWithDescription:@"Tokenize billing agreement payment"]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBillingAgreement_withClientToken_returnsErrorWithMalformedURL_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.url = [NSURL URLWithString:@"bad://url"]; + + __block XCTestExpectation *tokenizationExpectation = [self expectationWithDescription:@"Tokenize billing agreement payment"]; + BTPayPalRequest *request = [[BTPayPalRequest alloc] init]; + [payPalDriver requestBillingAgreement:request handler:testApproval completion:^(__unused BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount,__unused NSError * _Nullable error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNotNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testBillingAgreement_withClientToken_cancels_withCustomHandler { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_CLIENT_TOKEN]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + + __block BTPayPalApprovalHandlerTestDelegate *testApproval = [BTPayPalApprovalHandlerTestDelegate new]; + testApproval.handleApprovalExpectation = [self expectationWithDescription:@"Delegate received handleApproval:paypalApprovalDelegate:"]; + testApproval.cancel = YES; + + __block XCTestExpectation *tokenizationExpectation = [self expectationWithDescription:@"Tokenize billing agreement payment"]; + BTPayPalRequest *request = [[BTPayPalRequest alloc] init]; + [payPalDriver requestBillingAgreement:request handler:testApproval completion:^(__unused BTPayPalAccountNonce * _Nullable tokenizedPayPalAccount,__unused NSError * _Nullable error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNil(error); + XCTAssertNotNil(testApproval); + [tokenizationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Return URL handling + +- (void)testCanHandleAppSwitchReturnURL_forURLsFromBrowserSwitch_returnsYES { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Demo.payments://onetouch/v1/success?payloadEnc=e0yvzQHOOoXyoLjKZvHBI0Rbyad6usxhOz22CjG3V1lOsguMRsuQpEqPxlIlK86VPmTuagb1jJcnDUK9QsWJE8ffe4i9Ms4ggd6r5EoymVM%2BAYgjyjaYtPPOxIgMepNGnvnYt9EKJs2Bd0wbZj0ekxSA6BzRZDPEpZ%2FjhssxJVscjbPvOwCoTnjEhuNxiOamAGSRd6fo7ln%2BishDwRCLz81qlV8cgfXNzlHrRw1P7CbTQ8XhNGn35CHD64ysuHAW97ZjAzPCRdikWbgiw2S%2BDvSePhRRnTR10e2NPDYBeVzGQFzvf6WRklrqcLeFwRcAqoa0ZneOPgMbk5nvylGY716caCCPtJKnoJAflZZK6%2F7iXcA%2F3p9qrQIrszmthu%2FbnA%2FP7dZsWRarUiT%2FZhZg32MsmV3B3fPjQOMbhB7dRv5uomhCjhNhPzXH7nFA54mKOlvAdTm1QOk5P%2Fh3AaHz0qwIKgXAhxIfwxqHgIYxtba53sdwa7OXfx14FRlcfPngrR02IAHeaulkH6vJ24ZAsoUUdNkvRfDmM1O2%2B4424%2FMINTUJJsR0%2FwrYrwzp0gC6fKoAzT%2FgFhL6QVLoUss%3D&payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IkMwQTkwODQ1LTJBRUQtNEZCRC04NzIwLTQzNUU2MkRGNjhFNCIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJsaXZlIiwiZXJyb3IiOm51bGx9&x-source=com.braintree.browserswitch"]; + NSString *source = @"com.apple.mobilesafari"; + + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + + XCTAssertTrue(canHandleAppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_forURLsFromWebSwitch_returnsYES { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + NSURL *returnURL = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + BOOL canHandleV1AppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.apple.mobilesafari"]; + BOOL canHandleV2AppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.apple.safariviewservice"]; + + XCTAssertTrue(canHandleV1AppSwitch); + XCTAssertTrue(canHandleV2AppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_forMalformedURLs_returnsNO { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + // This malformed returnURL is just missing payload + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Demo.payments://onetouch/v1/success?x-source=com.paypal.ppclient.touch.v1-or-v2"]; + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.paypal.ppclient.touch.v1"]; + + XCTAssertFalse(canHandleAppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_forUnsupportedSourceApplication_returnsNO { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + // This malformed returnURL is just missing payload + NSURL *returnURL = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.example.application"]; + + XCTAssertFalse(canHandleAppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_whenNoAppSwitchIsInProgress_returnsNO { + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + NSURL *returnURL = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.malicious.app"]; + + XCTAssertFalse(canHandleAppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_afterHandlingAnAppSwitchAndBeforeInitiatingAnotherAppSwitch_returnsNO { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + self.didReceiveCompletionCallback = nil; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + self.didReceiveCompletionCallback = @(YES); + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + NSURL *returnURL = [NSURL URLWithString:OneTouchCoreAppSwitchSuccessURLFixture]; + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.apple.mobilesafari"]; + XCTAssertTrue(canHandleAppSwitch); + [BTPayPalDriver handleAppSwitchReturnURL:returnURL]; + + // Pause until handleAppSwitchReturnURL has finished + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"didReceiveCompletionCallback != nil"]; + [self expectationForPredicate:predicate evaluatedWithObject:self handler:nil]; + [self waitForExpectationsWithTimeout:5 handler:nil]; + + canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:@"com.apple.mobilesafari"]; + XCTAssertFalse(canHandleAppSwitch); +} + +- (void)testCanHandleAppSwitchReturnURL_whenAppSwitchReturnURLHasMismatchedCase_returnsYES { + // Motivation for this test is because of Safari's habit of downcasing URL schemes + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNotNil(tokenizedPayPalAccount); + if (error) { + XCTFail(@"%@", error); + } + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Demo.payments://onetouch/v1/success?payloadEnc=e0yvzQHOOoXyoLjKZvHBI0Rbyad6usxhOz22CjG3V1lOsguMRsuQpEqPxlIlK86VPmTuagb1jJcnDUK9QsWJE8ffe4i9Ms4ggd6r5EoymVM%2BAYgjyjaYtPPOxIgMepNGnvnYt9EKJs2Bd0wbZj0ekxSA6BzRZDPEpZ%2FjhssxJVscjbPvOwCoTnjEhuNxiOamAGSRd6fo7ln%2BishDwRCLz81qlV8cgfXNzlHrRw1P7CbTQ8XhNGn35CHD64ysuHAW97ZjAzPCRdikWbgiw2S%2BDvSePhRRnTR10e2NPDYBeVzGQFzvf6WRklrqcLeFwRcAqoa0ZneOPgMbk5nvylGY716caCCPtJKnoJAflZZK6%2F7iXcA%2F3p9qrQIrszmthu%2FbnA%2FP7dZsWRarUiT%2FZhZg32MsmV3B3fPjQOMbhB7dRv5uomhCjhNhPzXH7nFA54mKOlvAdTm1QOk5P%2Fh3AaHz0qwIKgXAhxIfwxqHgIYxtba53sdwa7OXfx14FRlcfPngrR02IAHeaulkH6vJ24ZAsoUUdNkvRfDmM1O2%2B4424%2FMINTUJJsR0%2FwrYrwzp0gC6fKoAzT%2FgFhL6QVLoUss%3D&payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IkMwQTkwODQ1LTJBRUQtNEZCRC04NzIwLTQzNUU2MkRGNjhFNCIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJsaXZlIiwiZXJyb3IiOm51bGx9&x-source=com.braintree.browserswitch"]; + NSString *source = @"com.apple.mobilesafari"; + BOOL canHandleAppSwitch = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + + XCTAssertTrue(canHandleAppSwitch); +} + +#pragma mark handleURL + +- (void)testHandleURL_whenURLIsConsideredInvalidByPayPalOTC_returnsError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + self.didReceiveCompletionCallback = nil; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNotNil(error); + self.didReceiveCompletionCallback = @(YES); + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:@"com.braintreepayments.Demo.payments://----invalid----"]]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"didReceiveCompletionCallback != nil"]; + [self expectationForPredicate:predicate evaluatedWithObject:self handler:nil]; + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testHandleURL_whenURLIsMissingHostAndPath_returnsError { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:SANDBOX_TOKENIZATION_KEY]; + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithAPIClient:apiClient]; + [BTAppSwitch sharedInstance].returnURLScheme = @"com.braintreepayments.Demo.payments"; + [self stubDelegatesForPayPalDriver:payPalDriver]; + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); + + self.didReceiveCompletionCallback = nil; + [payPalDriver authorizeAccountWithCompletion:^(BTPayPalAccountNonce *tokenizedPayPalAccount, NSError *error) { + XCTAssertNil(tokenizedPayPalAccount); + XCTAssertNotNil(error); + self.didReceiveCompletionCallback = @(YES); + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:@"com.braintreepayments.Demo.payments://"]]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"didReceiveCompletionCallback != nil"]; + [self expectationForPredicate:predicate evaluatedWithObject:self handler:nil]; + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Helper + +// Stubs the app switch or view controller presenting delegate, depending on which one will be used. +// The main purpose is to wait for delegate callbacks to occur in the app switch lifecycle to ensure +// that the PayPal driver's app switch return block is set and its behavior is ready to be tested. +- (void)stubDelegatesForPayPalDriver:(BTPayPalDriver *)payPalDriver { + if (!self.viewControllerPresentingDelegate) { + self.viewControllerPresentingDelegate = [[BTViewControllerPresentingTestDelegate alloc] init]; + } + if (!self.appSwitchDelegate) { + self.appSwitchDelegate = [[BTAppSwitchTestDelegate alloc] init]; + } + + if (NSClassFromString(@"SFSafariViewController")) { + self.viewControllerPresentingDelegate.requestsPresentationExpectation = [self expectationWithDescription:@"Delegate received requestsPresentation"]; + payPalDriver.viewControllerPresentingDelegate = self.viewControllerPresentingDelegate; + } else { + self.appSwitchDelegate.willPerform = [self expectationWithDescription:@"Delegate received willPerformAppSwitch"]; + self.appSwitchDelegate.didPerform = [self expectationWithDescription:@"Delegate received didPerformAppSwitch"]; + payPalDriver.appSwitchDelegate = self.appSwitchDelegate; + } +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeUnionPay_IntegrationTests.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeUnionPay_IntegrationTests.m new file mode 100755 index 00000000..d92fa1f2 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/BraintreeUnionPay_IntegrationTests.m @@ -0,0 +1,107 @@ +#import +#import "BTIntegrationTestsHelper.h" +#import + +@interface BraintreeUnionPay_IntegrationTests : XCTestCase +@property (nonatomic, strong) BTCardClient *cardClient; +@end + +@implementation BraintreeUnionPay_IntegrationTests + +- (void)setUp { + [super setUp]; + + static NSString *clientToken; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + clientToken = [self fetchClientToken]; + }); + + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:clientToken]; + self.cardClient = [[BTCardClient alloc] initWithAPIClient:apiClient]; +} + +- (void)pendFetchCapabilities_returnsCardCapabilities { + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.cardClient fetchCapabilities:@"6212345678901232" completion:^(BTCardCapabilities * _Nullable cardCapabilities, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertFalse(cardCapabilities.isDebit); + XCTAssertTrue(cardCapabilities.isUnionPay); + XCTAssertTrue(cardCapabilities.isSupported); + XCTAssertTrue(cardCapabilities.supportsTwoStepAuthAndCapture); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)pendEnrollCard_whenSuccessful_returnsEnrollmentID { + BTCardRequest *request = [[BTCardRequest alloc] init]; + request.card = [[BTCard alloc] initWithNumber:@"6222821234560017" expirationMonth:@"12" expirationYear:@"2019" cvv:@"123"]; + request.mobileCountryCode = @"62"; + request.mobilePhoneNumber = @"12345678901"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.cardClient enrollCard:request completion:^(NSString * _Nullable enrollmentID, __unused BOOL smsCodeRequired, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertTrue([enrollmentID isKindOfClass:[NSString class]]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)pendEnrollCard_whenCardDoesNotRequireEnrollment_returnsError { + BTCardRequest *request = [[BTCardRequest alloc] init]; + request.card = [[BTCard alloc] initWithNumber:@"6212345678900085" expirationMonth:@"12" expirationYear:@"2019" cvv:@"123"]; + request.mobileCountryCode = @"62"; + request.mobilePhoneNumber = @"12345678901"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.cardClient enrollCard:request completion:^(NSString * _Nullable enrollmentID, __unused BOOL smsCodeRequired, NSError * _Nullable error) { + XCTAssertNil(enrollmentID); + XCTAssertEqualObjects(error.domain, BTCardClientErrorDomain); + XCTAssertEqual(error.code, BTCardClientErrorTypeCustomerInputInvalid); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)pendTokenizeCard_withEnrolledUnionPayCard_isSuccessful { + BTCardRequest *request = [[BTCardRequest alloc] init]; + request.card = [[BTCard alloc] initWithNumber:@"6212345678901232" expirationMonth:@"12" expirationYear:@"2019" cvv:@"123"]; + request.mobileCountryCode = @"62"; + request.mobilePhoneNumber = @"12345678901"; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.cardClient enrollCard:request completion:^(NSString * _Nullable enrollmentID, __unused BOOL smsCodeRequired, NSError * _Nullable error) { + XCTAssertNil(error); + request.enrollmentID = enrollmentID; + request.smsCode = @"11111"; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + expectation = [self expectationWithDescription:@"Callback invoked"]; + [self.cardClient tokenizeCard:request options:nil completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertTrue([tokenizedCard.nonce isANonce]); + XCTAssertEqual(tokenizedCard.cardNetwork, BTCardNetworkUnionPay); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Helpers + +- (NSString *)fetchClientToken { + NSURL *url = [NSURL URLWithString:@"http://braintree-sample-merchant.herokuapp.com/client_token?merchant_account_id=fake_switch_usd"]; + NSData *data = [NSData dataWithContentsOfURL:url]; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; + return jsonResponse[@"client_token"]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsConstants.h b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsConstants.h new file mode 100755 index 00000000..ba072330 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsConstants.h @@ -0,0 +1,4 @@ +#define SANDBOX_TOKENIZATION_KEY @"sandbox_9dbg82cq_dcpspy2brwdjr3qn" +#define SANDBOX_TOKENIZATION_KEY_APPLE_PAY_DISABLED @"sandbox_g42y39zw_348pk9cgf3bgyw2b" +#define SANDBOX_CLIENT_TOKEN @"eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiI4MTUzNjg2M2ViY2Q2MWUyZTVkYjE1NjJiMGI5ZjkxNzM3YTQ2YjE1OWNmNTdjZTU2ZmVlZmE1OGNhOWEyZGEwfGNyZWF0ZWRfYXQ9MjAxNS0xMi0xNFQxODozMzowOS4wNzAyODE0NDQrMDAwMFx1MDAyNm1lcmNoYW50X2lkPTM0OHBrOWNnZjNiZ3l3MmJcdTAwMjZwdWJsaWNfa2V5PTJuMjQ3ZHY4OWJxOXZtcHIiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvMzQ4cGs5Y2dmM2JneXcyYi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJjaGFsbGVuZ2VzIjpbXSwiZW52aXJvbm1lbnQiOiJzYW5kYm94IiwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzLzM0OHBrOWNnZjNiZ3l3MmIvY2xpZW50X2FwaSIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vY2xpZW50LWFuYWx5dGljcy5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIn0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsInRocmVlRFNlY3VyZSI6eyJsb29rdXBVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvMzQ4cGs5Y2dmM2JneXcyYi90aHJlZV9kX3NlY3VyZS9sb29rdXAifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImRpc3BsYXlOYW1lIjoiQWNtZSBXaWRnZXRzLCBMdGQuIChTYW5kYm94KSIsImNsaWVudElkIjpudWxsLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJhbGxvd0h0dHAiOnRydWUsImVudmlyb25tZW50Tm9OZXR3b3JrIjp0cnVlLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjp0cnVlLCJtZXJjaGFudEFjY291bnRJZCI6ImFjbWV3aWRnZXRzbHRkc2FuZGJveCIsImN1cnJlbmN5SXNvQ29kZSI6IlVTRCJ9LCJjb2luYmFzZUVuYWJsZWQiOmZhbHNlLCJtZXJjaGFudElkIjoiMzQ4cGs5Y2dmM2JneXcyYiIsInZlbm1vIjoib2ZmIn0=" +#define SANDBOX_CLIENT_TOKEN_VERSION_3 @"eyJ2ZXJzaW9uIjozLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiIxYzM5N2E5OGZmZGRkNDQwM2VjNzEzYWRjZTI3NTNiMzJlODc2MzBiY2YyN2M3NmM2OWVmZjlkMTE5MjljOTVkfGNyZWF0ZWRfYXQ9MjAxNy0wNC0wNVQwNjowNzowOC44MTUwOTkzMjUrMDAwMFx1MDAyNm1lcmNoYW50X2lkPWRjcHNweTJicndkanIzcW5cdTAwMjZwdWJsaWNfa2V5PTl3d3J6cWszdnIzdDRuYzgiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24ifQ==" diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.h b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.h new file mode 100755 index 00000000..842509b6 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.h @@ -0,0 +1,5 @@ +#import + +@interface NSString (Nonce) +- (BOOL)isANonce; +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.m b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.m new file mode 100755 index 00000000..34232887 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Helpers/BTIntegrationTestsHelper.m @@ -0,0 +1,20 @@ +#import "BTSpecHelper.h" + +@implementation NSString (Nonce) + +- (BOOL)isANonce { + NSString *nonceRegularExpressionString = @"\\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\Z"; + + NSError *error; + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:nonceRegularExpressionString + options:0 + error:&error]; + if (error) { + NSLog(@"Error parsing regex: %@", error); + return NO; + } + + return [regex numberOfMatchesInString:self options:0 range:NSMakeRange(0, [self length])] > 0; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Info.plist b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Info.plist new file mode 100755 index 00000000..ba72822e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/IntegrationTests.pch b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/IntegrationTests.pch new file mode 100755 index 00000000..633b5b5c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/IntegrationTests/IntegrationTests.pch @@ -0,0 +1 @@ +#import "BTIntegrationTestsConstants.h" diff --git a/examples/braintree/ios/Frameworks/Braintree/LICENSE b/examples/braintree/ios/Frameworks/Braintree/LICENSE new file mode 100755 index 00000000..b50ce6f4 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2016 Braintree, a division of PayPal, Inc. + +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. diff --git a/examples/braintree/ios/Frameworks/Braintree/Podfile b/examples/braintree/ios/Frameworks/Braintree/Podfile new file mode 100755 index 00000000..7fa7b5ae --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Podfile @@ -0,0 +1,37 @@ +source 'https://github.com/CocoaPods/Specs.git' + +platform :ios, '9.0' +workspace 'Braintree.xcworkspace' + +target 'Demo' do + platform :ios, '9.0' + + pod 'AFNetworking', '~> 2.6.0' + pod 'CardIO' + pod 'NSURL+QueryDictionary', '~> 1.0' + pod 'PureLayout' + pod 'FLEX' + pod 'InAppSettingsKit' + pod 'iOS-Slide-Menu' + pod 'BraintreeDropIn', :podspec => 'BraintreeDropIn.podspec' +end + +abstract_target 'Tests' do + pod 'Specta' + pod 'Expecta' + pod 'OCMock' + pod 'OHHTTPStubs' + + target 'UnitTests' + target 'IntegrationTests' +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + if target.name == "BraintreeDropIn" + target.build_configurations.each do |config| + config.build_settings['HEADER_SEARCH_PATHS'] = '${PODS_ROOT}/../BraintreeCore/Public ${PODS_ROOT}/../BraintreeCard/Public ${PODS_ROOT}/../BraintreeUnionPay/Public ${PODS_ROOT}/Headers/Private ${PODS_ROOT}/Headers/Private/BraintreeDropIn ${PODS_ROOT}/Headers/Public' + end + end + end +end diff --git a/examples/braintree/ios/Frameworks/Braintree/Podfile.lock b/examples/braintree/ios/Frameworks/Braintree/Podfile.lock new file mode 100755 index 00000000..a2f8f8ac --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Podfile.lock @@ -0,0 +1,91 @@ +PODS: + - AFNetworking (2.6.3): + - AFNetworking/NSURLConnection (= 2.6.3) + - AFNetworking/NSURLSession (= 2.6.3) + - AFNetworking/Reachability (= 2.6.3) + - AFNetworking/Security (= 2.6.3) + - AFNetworking/Serialization (= 2.6.3) + - AFNetworking/UIKit (= 2.6.3) + - AFNetworking/NSURLConnection (2.6.3): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/NSURLSession (2.6.3): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/Reachability (2.6.3) + - AFNetworking/Security (2.6.3) + - AFNetworking/Serialization (2.6.3) + - AFNetworking/UIKit (2.6.3): + - AFNetworking/NSURLConnection + - AFNetworking/NSURLSession + - BraintreeDropIn (99.99.99-github-master): + - BraintreeDropIn/DropIn (= 99.99.99-github-master) + - BraintreeDropIn/UIKit (= 99.99.99-github-master) + - BraintreeDropIn/DropIn (99.99.99-github-master): + - BraintreeDropIn/UIKit + - BraintreeDropIn/UIKit (99.99.99-github-master) + - CardIO (5.4.1) + - Expecta (1.0.5) + - FLEX (2.4.0) + - InAppSettingsKit (2.8.1) + - iOS-Slide-Menu (1.5) + - NSURL+QueryDictionary (1.2.0) + - OCMock (3.4) + - OHHTTPStubs (6.0.0): + - OHHTTPStubs/Default (= 6.0.0) + - OHHTTPStubs/Core (6.0.0) + - OHHTTPStubs/Default (6.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (6.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (6.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (6.0.0) + - PureLayout (3.0.2) + - Specta (1.0.6) + +DEPENDENCIES: + - AFNetworking (~> 2.6.0) + - BraintreeDropIn (from `BraintreeDropIn.podspec`) + - CardIO + - Expecta + - FLEX + - InAppSettingsKit + - iOS-Slide-Menu + - NSURL+QueryDictionary (~> 1.0) + - OCMock + - OHHTTPStubs + - PureLayout + - Specta + +EXTERNAL SOURCES: + BraintreeDropIn: + :podspec: BraintreeDropIn.podspec + +CHECKOUT OPTIONS: + BraintreeDropIn: + :commit: ca87c76f9ce266e93ce3c05ffca36499c17320df + :git: https://github.com/braintree/braintree-ios-drop-in.git + +SPEC CHECKSUMS: + AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60 + BraintreeDropIn: 3dd4539753a417a8325f580730d39848b758e505 + CardIO: 56983b39b62f495fc6dae9ad7cf875143df06443 + Expecta: e1c022fcd33910b6be89c291d2775b3fe27a89fe + FLEX: bd1a39e55b56bb413b6f1b34b3c10a0dc44ef079 + InAppSettingsKit: 94d2fba3ccd700fc4e32c6d09fabcc0af2426744 + iOS-Slide-Menu: 40b0cbd8916c42afc755f0869793cd09e7a32102 + NSURL+QueryDictionary: bae616404e2adf6409d3d5c02a093cbf44c8a236 + OCMock: 35ae71d6a8fcc1b59434d561d1520b9dd4f15765 + OHHTTPStubs: 752f9b11fd810a15162d50f11c06ff94f8e012eb + PureLayout: 4d550abe49a94f24c2808b9b95db9131685fe4cd + Specta: f506f3a8361de16bc0dcf3b17b75e269072ba465 + +PODFILE CHECKSUM: 5c0f6d7472cce11fd35b3c4a01b851c0799076c5 + +COCOAPODS: 1.2.0 diff --git a/examples/braintree/ios/Frameworks/Braintree/README.md b/examples/braintree/ios/Frameworks/Braintree/README.md new file mode 100755 index 00000000..f3296747 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/README.md @@ -0,0 +1,69 @@ +# Braintree iOS SDK + +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Travis CI build status](https://travis-ci.org/braintree/braintree_ios.svg?branch=master)](https://travis-ci.org/braintree/braintree_ios) + +Welcome to Braintree's iOS SDK. This library will help you accept card and alternative payments in your iOS app. + +**The Braintree iOS SDK requires Xcode 8+ and a Base SDK of iOS 9+**. It permits a Deployment Target of iOS 7.0 or higher. + +## Supported Payment Methods + +- [Credit Cards](https://developers.braintreepayments.com/guides/credit-cards/overview) +- [PayPal](https://developers.braintreepayments.com/guides/paypal/overview) +- [Pay with Venmo](https://developers.braintreepayments.com/guides/venmo/overview) +- [Apple Pay](https://developers.braintreepayments.com/guides/apple-pay/overview) +- [ThreeDSecure](https://developers.braintreepayments.com/guides/3d-secure/overview) +- [Visa Checkout](https://developers.braintreepayments.com/guides/visa-checkout/overview) + +## Installation + +We recommend using either [CocoaPods](https://github.com/CocoaPods/CocoaPods) or [Carthage](https://github.com/Carthage/Carthage) to integrate the Braintree SDK with your project. + +#### CocoaPods +``` +# Includes Cards and PayPal +pod 'Braintree' + +# Optionally include additional Pods +pod 'Braintree/DataCollector' +pod 'Braintree/Venmo' +``` + +#### Carthage +Add `github "braintree/braintree_ios"` to your `Cartfile`, and [add the frameworks to your project](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). + +## Documentation + +Start with [**'Hello, Client!'**](https://developers.braintreepayments.com/ios/start/hello-client) for instructions on basic setup and usage. + +Next, read the [**full documentation**](https://developers.braintreepayments.com/ios/sdk/client) for information about integration options, such as Drop-In UI, PayPal, and credit card tokenization. + +## Demo + +A demo app is included in the project. To run it, run `pod install` and then open `Braintree.xcworkspace` in Xcode. + +## Feedback + +The Braintree iOS SDK is in active development, we welcome your feedback! + +Here are a few ways to get in touch: + +* [GitHub Issues](https://github.com/braintree/braintree_ios/issues) - For generally applicable issues and feedback +* [Braintree Support](https://articles.braintreepayments.com/) / support@braintreepayments.com - for personal support at any phase of integration + +## Help + +* Read the headers +* [Read the docs](https://developers.braintreepayments.com/ios/sdk/client) +* Find a bug? [Open an issue](https://github.com/braintree/braintree_ios/issues) +* Want to contribute? [Check out contributing guidelines](CONTRIBUTING.md) and [submit a pull request](https://help.github.com/articles/creating-a-pull-request). + +## Releases + +Subscribe to our [Google Group](https://groups.google.com/forum/#!forum/braintree-sdk-announce) to +be notified when SDK releases go out. + +### License + +The Braintree iOS SDK is open source and available under the MIT license. See the [LICENSE](LICENSE) file for more info. \ No newline at end of file diff --git a/examples/braintree/ios/Frameworks/Braintree/Rakefile b/examples/braintree/ios/Frameworks/Braintree/Rakefile new file mode 100755 index 00000000..c4f5f3ef --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Rakefile @@ -0,0 +1,354 @@ +require 'tempfile' +require 'fileutils' +require 'shellwords' +require 'bundler' +Bundler.require +HighLine.color_scheme = HighLine::SampleColorScheme.new + +task :default => %w[sanity_checks spec] + +desc "Run default set of tasks" +task :spec => %w[spec:all] + +desc "Run internal release process, pushing to internal GitHub Enterprise only" +task :release => %w[release:assumptions sanity_checks release:check_working_directory release:bump_version release:test release:lint_podspec release:tag release:push_private] + +desc "Publish code and pod to public github.com" +task :publish => %w[publish:push publish:push_pod publish:cocoadocs] + +desc "Distribute app, in its current state, to HockeyApp" +task :distribute => %w[distribute:build distribute:hockeyapp] + +SEMVER = /\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?/ +PODSPEC = "Braintree.podspec" +BRAINTREE_VERSION_FILE = "BraintreeCore/Braintree-Version.h" +PAYPAL_ONE_TOUCH_VERSION_FILE = "BraintreePayPal/PayPalUtils/Public/PPOTVersion.h" +DEMO_PLIST = "Demo/Supporting Files/Braintree-Demo-Info.plist" +FRAMEWORKS_PLIST = "BraintreeCore/Info.plist" +PUBLIC_REMOTE_NAME = "public" + +class << self + def run cmd + say(HighLine.color("$ #{cmd}", :debug)) + File.popen(cmd) { |file| + if block_given? + result = '' + result << file.gets until file.eof? + yield result + else + puts file.gets until file.eof? + end + } + $? == 0 + end + + def run! cmd + run(cmd) or fail("Command failed with non-zero exit status #{$?}:\n$ #{cmd}") + end + + def current_version + File.read(PODSPEC)[SEMVER] + end + + def current_version_with_sha + %x{git describe}.strip + end + + def current_branch + %x{git rev-parse --abbrev-ref HEAD}.strip + end + + def xcodebuild(scheme, command, configuration, ios_version, options={}) + default_options = { + :build_settings => {} + } + ios_version_specifier = ",OS=#{ios_version}" if !ios_version.nil? + options = default_options.merge(options) + build_settings = options[:build_settings].map{|k,v| "#{k}='#{v}'"}.join(" ") + return "set -o pipefail && xcodebuild -workspace 'Braintree.xcworkspace' -sdk 'iphonesimulator' -configuration '#{configuration}' -scheme '#{scheme}' -destination 'name=iPhone 6,platform=iOS Simulator#{ios_version_specifier}' #{build_settings} #{command} | xcpretty -c -r junit" + end + +end + +namespace :spec do + def run_test_scheme! scheme, ios_version = nil + run! xcodebuild(scheme, 'test', 'Release', ios_version) + end + + desc 'Run unit tests' + task :unit, [:ios_version] do |t, args| + if args[:ios_version] + run_test_scheme! 'UnitTests', args[:ios_version] + else + run_test_scheme! 'UnitTests' + end + end + + desc 'Run UI tests' + task :ui do + run_test_scheme! 'UITests' + end + + namespace :api do + def with_https_server &block + begin + pid = Process.spawn('ruby ./IntegrationTests/Braintree-API-Integration-Specs/SSL/https_server.rb') + puts "Started server (#{pid})" + yield + puts "Killing server (#{pid})" + ensure + Process.kill("INT", pid) + end + end + + desc 'Run integration tests' + task :integration do + with_https_server do + run! xcodebuild('IntegrationTests', 'test', 'Release', nil, :build_settings => {'GCC_PREPROCESSOR_DEFINITIONS' => '$GCC_PREPROCESSOR_DEFINITIONS RUN_SSL_PINNING_SPECS=1'}) + end + end + end + + desc 'Run all spec schemes' + task :all => %w[spec:unit spec:api:integration spec:ui] +end + +namespace :demo do + desc 'Verify that the demo app builds successfully' + task :build do + run! xcodebuild('Demo', 'build', 'Release', nil) + end +end + +desc 'Run Carthage update' +namespace :carthage do + def generate_cartfile + File.write("./Cartfile", "git \"file://#{Dir.pwd}\" \"#{current_branch}\"") + end + + task :generate do + generate_cartfile + end + + task :clean do + run! 'rm -rf Carthage && rm Cartfile && rm Cartfile.resolved && rm -rf ~/Library/Developers/Xcode/DerivedData' + end + + task :test do + generate_cartfile + run! "carthage update" + run! "xcodebuild -project 'Demo/CarthageTest/CarthageTest.xcodeproj' -scheme 'CarthageTest' clean build" + end +end + +desc 'Run all sanity checks' +task :sanity_checks => %w[sanity_checks:pending_specs sanity_checks:build_demo sanity_checks:carthage_test] + +namespace :sanity_checks do + desc 'Check for pending tests' + task :pending_specs do + # ack returns 1 if no match is found, which is our success case + run! "which -s ack && ! ack 'fit\\(|fdescribe\\(' Specs" or fail "Please do not commit pending specs." + end + + desc 'Verify that all demo apps Build successfully' + task :build_demo => 'demo:build' + + desc 'Verify that Carthage builds successfully' + task :carthage_test => %w[carthage:test carthage:clean] +end + + + +def apple_doc_command + %W[/usr/local/bin/appledoc + -o appledocs + --project-name Braintree + --project-version '#{current_version_with_sha}' + --project-company Braintree + --docset-bundle-id '%COMPANYID' + --docset-bundle-name Braintree + --docset-desc 'Braintree iOS SDK (%VERSION)' + --index-desc README.md + --include LICENSE + --include CHANGELOG.md + --print-information-block-titles + --company-id com.braintreepayments + --prefix-merged-sections + --no-merge-categories + --warn-missing-company-id + --warn-undocumented-object + --warn-undocumented-member + --warn-empty-description + --warn-unknown-directive + --warn-invalid-crossref + --warn-missing-arg + --no-repeat-first-par + ].join(' ') +end + +def apple_doc_files + %x{find Braintree -name "*.h"}.split("\n").reject { |name| name =~ /mSDK/}.map { |name| name.gsub(' ', '\\ ')}.join(' ') +end + +desc "Generate documentation via appledoc" +task :docs => 'docs:generate' + +namespace :appledoc do + task :check do + unless File.exists?('/usr/local/bin/appledoc') + puts "appledoc not found at /usr/local/bin/appledoc: Install via homebrew and try again: `brew install --HEAD appledoc`" + exit 1 + end + end +end + +namespace :docs do + desc "Generate apple docs as html" + task :generate => 'appledoc:check' do + command = apple_doc_command << " --no-create-docset --keep-intermediate-files --create-html #{apple_doc_files}" + run(command) + puts "Generated HTML documentationa at appledocs/html" + end + + desc "Check that documentation can be built from the source code via appledoc successfully." + task :check => 'appledoc:check' do + command = apple_doc_command << " --no-create-html --verbose 5 #{apple_doc_files}" + exitstatus = run(command) + if exitstatus == 0 + puts "appledoc generation completed successfully!" + elsif exitstatus == 1 + puts "appledoc generation produced warnings" + elsif exitstatus == 2 + puts "! appledoc generation encountered an error" + exit(exitstatus) + else + puts "!! appledoc generation failed with a fatal error" + end + exit(exitstatus) + end + + desc "Generate & install a docset into Xcode from the current sources" + task :install => 'appledoc:check' do + command = apple_doc_command << " --install-docset #{apple_doc_files}" + run(command) + end +end + + +namespace :release do + desc "Print out pre-release checklist" + task :assumptions do + say "Release Assumptions" + say "* [ ] You have pulled and reconciled origin (internal GitHub Enterprise) vs public (github.com)." + say "* [ ] You are on the branch and commit you want to release." + say "* [ ] You have already merged hotfixes and pulled changes." + say "* [ ] You have already reviewed the diff between the current release and the last tag, noting breaking changes in the semver and CHANGELOG." + say "* [ ] Tests are passing, manual verifications complete." + say "* [ ] iOS Simulator has hardware keyboard disabled" + say "* [ ] Email is composed and ready to send to braintree-sdk-announce@googlegroups.com" + + abort(1) unless ask "Ready to release? " + end + + desc "Check that working directory is clean" + task :check_working_directory do + run! "echo 'Checking for uncommitted changes' && git diff --exit-code" + end + + desc "Bump version in Podspec" + task :bump_version do + say "Current version in Podspec: #{current_version}" + n = 10 + say "Previous #{n} versions in Git:" + run "git tag -l | tail -n #{n}" + version = ask("What version are you releasing?") { |q| q.validate = /\A#{SEMVER}\Z/ } + + podspec = File.read(PODSPEC) + podspec.gsub!(/(s\.version\s*=\s*)"#{SEMVER}"/, "\\1\"#{version}\"") + File.open(PODSPEC, "w") { |f| f.puts podspec } + + version_header = File.read(BRAINTREE_VERSION_FILE) + version_header.gsub!(SEMVER, version) + File.open(BRAINTREE_VERSION_FILE, "w") { |f| f.puts version_header } + + version_header = File.read(PAYPAL_ONE_TOUCH_VERSION_FILE) + version_header.gsub!(SEMVER, version) + File.open(PAYPAL_ONE_TOUCH_VERSION_FILE, "w") { |f| f.puts version_header } + + [DEMO_PLIST, FRAMEWORKS_PLIST].each do |plist| + run! "plutil -replace CFBundleVersion -string #{current_version} -- '#{plist}'" + run! "plutil -replace CFBundleShortVersionString -string #{current_version} -- '#{plist}'" + end + run "git commit -m 'Bump pod version to #{version}' -- #{PODSPEC} Podfile.lock '#{DEMO_PLIST}' '#{FRAMEWORKS_PLIST}' #{BRAINTREE_VERSION_FILE} #{PAYPAL_ONE_TOUCH_VERSION_FILE}" + end + + desc "Test." + task :test => 'spec:all' + + desc "Lint podspec." + task :lint_podspec do + run! "pod lib lint Braintree.podspec --allow-warnings" + end + + desc "Tag." + task :tag do + run! "git tag #{current_version} -a -m 'Release #{current_version}'" + end + + desc "Push tag to ghe." + task :push_private do + run! "git push origin HEAD #{current_version}" + end + +end + +namespace :publish do + + desc "Push code and tag to github.com" + task :push do + run! "git push #{PUBLIC_REMOTE_NAME} HEAD #{current_version}" + end + + desc "Pod push." + task :push_pod do + run! "pod trunk push --allow-warnings Braintree.podspec" + end + + desc "Force CocoaDocs reparse" + task :cocoadocs do + run! "curl --silent --show-error http://api.cocoadocs.org:4567/redeploy/Braintree/latest" + end + +end + +namespace :distribute do + task :build do + destination = File.expand_path("~/Desktop/Braintree-Demo-#{current_version_with_sha}") + run! "ipa build --scheme Demo --destination '#{destination}' --embed EverybodyVenmo.mobileprovision --identity 'iPhone Distribution: Venmo Inc.'" + say "Archived Demo (#{current_version}) to: #{destination}" + end + + task :hockeyapp do + destination = File.expand_path("~/Desktop/Braintree-Demo-#{current_version_with_sha}") + changes = File.read("CHANGELOG.md")[/(## #{current_version}.*?)^## /m, 1].strip + run! "ipa distribute:hockeyapp --token '#{File.read(".hockeyapp").strip}' --identifier '7134982f3df6419a0eb52b16e7d6d175' --file '#{destination}/Braintree-Demo.ipa' --dsym '#{destination}/Braintree-Demo.app.dSYM.zip' --markdown --notes #{Shellwords.shellescape("#{changes}\n\n#{current_version_with_sha}")}" + say "Uploaded Demo (#{current_version_with_sha}) to HockeyApp!" + end +end + +desc "Generate code for pinned certificates. (Copies *.crt -> BTAPIPinnedCertificates.{h,m})" +task :generate_pinned_certificates_code do + run! "cd #{File.join(File.dirname(__FILE__), "Braintree/API/Networking/Certificates")} && ./codify_certificates.sh" +end + +namespace :gen do + task :strings do + ["Drop-In", "UI"].each do |subspec| + run! "genstrings -o Braintree/#{subspec}/Localization/en.lproj Braintree/#{subspec}/**/*.m && " + + "iconv -f utf-16 -t utf-8 Braintree/#{subspec}/Localization/en.lproj/Localizable.strings > Braintree/#{subspec}/Localization/en.lproj/#{subspec}.strings && " + + "rm -f Braintree/#{subspec}/Localization/en.lproj/Localizable.strings" + end + end +end + diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTLoggerSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTLoggerSpec.m new file mode 100755 index 00000000..1059fec6 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTLoggerSpec.m @@ -0,0 +1,78 @@ +#import "BTLogger_Internal.h" + +SpecBegin(BTLogger) + +describe(@"sharedLogger", ^{ + it(@"returns the singleton logger", ^{ + BTLogger *logger1 = [BTLogger sharedLogger]; + BTLogger *logger2 = [BTLogger sharedLogger]; + expect(logger1).to.beKindOf([BTLogger class]); + expect(logger1).to.equal(logger2); + }); +}); + +SpecEnd + +SpecBegin(BTLogger_Internal) + +describe(@"logger", ^{ + + __block BTLogger *logger; + + beforeEach(^{ + logger = [[BTLogger alloc] init]; + }); + + describe(@"log", ^{ + it(@"sends log message to NSLog", ^{ + [logger log:@"BTLogger probably works!"]; + // Can't mock NSLog + }); + + it(@"sends log message to logBlock if defined", ^{ + waitUntil(^(DoneCallback done) { + NSString *messageLogged = @"BTLogger logBlock works!"; + logger.logBlock = ^(BTLogLevel level, NSString *messageReceived) { + expect(level).to.equal(BTLogLevelInfo); + expect(messageReceived).to.equal(messageLogged); + done(); + }; + [logger log:messageLogged]; + }); + }); + }); + + describe(@"level", ^{ + it(@"defaults to 'info'", ^{ + expect(logger.level).to.equal(BTLogLevelInfo); + }); + + it(@"allows logging if logged at or below level", ^{ + + for (int level = BTLogLevelNone; level <= BTLogLevelDebug; level++) { + NSString *message = [NSString stringWithFormat:@"test %d", level]; + NSMutableArray *messagesLogged = [NSMutableArray array]; + __block BTLogLevel maxLevel = level; + waitUntil(^(DoneCallback done) { + logger.logBlock = ^(BTLogLevel actualLevel, NSString *messageReceived) { + expect(actualLevel).to.beLessThanOrEqualTo(maxLevel); + [messagesLogged addObject:messageReceived]; + }; + + logger.level = level; + [logger critical:message]; + [logger error:message]; + [logger warning:message]; + [logger info:message]; + [logger debug:message]; + done(); + }); + expect(messagesLogged.count).to.equal(level); + } + }); + + }); + +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTThreeDSecureLookupSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTThreeDSecureLookupSpec.m new file mode 100755 index 00000000..2df5811a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTThreeDSecureLookupSpec.m @@ -0,0 +1,28 @@ +#import "BTThreeDSecureLookupResult.h" + +// TODO: Reenable this spec when 3D Secure stuff is added back into the new BTThreeDSecureDriver + +SpecBegin(BTThreeDSecureLookupResult) + +describe(@"requiresUserAuthentication", ^{ + it(@"returns YES when the acs url is present", ^{ + BTThreeDSecureLookupResult *lookup = [[BTThreeDSecureLookupResult alloc] init]; + lookup.acsURL = [NSURL URLWithString:@"http://example.com"]; + lookup.termURL = [NSURL URLWithString:@"http://example.com"]; + lookup.MD = @"an-md"; + lookup.PAReq = @"a-PAReq"; + + expect(lookup.requiresUserAuthentication).to.beTruthy(); + }); + it(@"returns NO when the acs url is not present", ^{ + BTThreeDSecureLookupResult *lookup = [[BTThreeDSecureLookupResult alloc] init]; + lookup.acsURL = nil; + lookup.termURL = [NSURL URLWithString:@"http://example.com"]; + lookup.MD = @"an-md"; + lookup.PAReq = @"a-PAReq"; + + expect(lookup.requiresUserAuthentication).to.beFalsy(); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTURLUtilsSpecs.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTURLUtilsSpecs.m new file mode 100755 index 00000000..31186ef7 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTURLUtilsSpecs.m @@ -0,0 +1,65 @@ +#import "BTURLUtils.h" + +SpecBegin(BTURLUtils) + +describe(@"URLfromURL:withAppendedQueryDictionary:", ^{ + it(@"appends a dictionary to a url as a query string", ^{ + expect([BTURLUtils URLfromURL:[NSURL URLWithString:@"http://example.com:80/path/to/file"] withAppendedQueryDictionary:@{ @"key": @"value" }]).to.equal([NSURL URLWithString:@"http://example.com:80/path/to/file?key=value"]); + }); + + it(@"accepts a nil dictionary", ^{ + expect([BTURLUtils URLfromURL:[NSURL URLWithString:@"http://example.com"] withAppendedQueryDictionary:nil]).to.equal([NSURL URLWithString:@"http://example.com?"]); + }); + + it(@"precent escapes the query parameters", ^{ + expect([BTURLUtils URLfromURL:[NSURL URLWithString:@"http://example.com"] withAppendedQueryDictionary:@{ @"space ": @"sym&bol=" }]).to.equal([NSURL URLWithString:@"http://example.com?space%20=sym%26bol%3D"]); + }); + + it(@"passes a nil URL", ^{ + expect([BTURLUtils URLfromURL:nil withAppendedQueryDictionary:@{ @"space ": @"sym&bol=" }]).to.beNil(); + }); + + it(@"accepts relative URLs", ^{ + expect([BTURLUtils URLfromURL:[NSURL URLWithString:@"/relative/path"] withAppendedQueryDictionary:@{ @"key": @"value" }]).to.equal([NSURL URLWithString:@"/relative/path?key=value"]); + }); +}); + +describe(@"dictionaryForQueryString:", ^{ + it(@"returns an empty dictionary for a nil query string", ^{ + expect([BTURLUtils dictionaryForQueryString:nil]).to.equal(@{}); + }); + + it(@"returns an empty dictionary for an empty query string", ^{ + expect([BTURLUtils dictionaryForQueryString:@""]).to.equal(@{}); + }); + + it(@"returns a dictionary containing items from the query string", ^{ + expect([BTURLUtils dictionaryForQueryString:@"foo=bar&baz=quux"]).to.equal(@{ @"foo": @"bar", @"baz": @"quux" }); + }); + + it(@"URL decodes entities from query string keys and values", ^{ + expect([BTURLUtils dictionaryForQueryString:@"IHaveEquals%3D=IHaveComma%2C"]).to.equal(@{ @"IHaveEquals=": @"IHaveComma," }); + }); + + it(@"URL decodes entities from query string keys and values", ^{ + expect([BTURLUtils dictionaryForQueryString:@"key+with%20spaces=value"]).to.equal(@{ @"key with spaces": @"value" }); + }); + + it(@"returns a dictionary with [NSNull null] values for keys that don't have values", ^{ + expect([BTURLUtils dictionaryForQueryString:@"key"]).to.equal(@{ @"key": [NSNull null] }); + }); + + it(@"returns a dictionary with empty string values for key=", ^{ + expect([BTURLUtils dictionaryForQueryString:@"key="]).to.equal(@{ @"key": @"" }); + }); + + it(@"represents empty keys with the empty string", ^{ + expect([BTURLUtils dictionaryForQueryString:@"&=asdf&"]).to.equal(@{ @"": @"asdf" }); + }); + + it(@"keeps the right-most value for duplicate keys", ^{ + expect([BTURLUtils dictionaryForQueryString:@"key=value1&key=value2"]).to.equal(@{ @"key": @"value2" }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTVersionSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTVersionSpec.m new file mode 100755 index 00000000..20700a86 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-API-Specs/BTVersionSpec.m @@ -0,0 +1,9 @@ +#import "Braintree-Version.h" + +SpecBegin(BTVersion) + +it(@"returns the current version", ^{ + expect(BRAINTREE_VERSION).to.match(@"\\d+\\.\\d+\\.\\d+"); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAcceptanceSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAcceptanceSpec.m new file mode 100755 index 00000000..770b1394 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAcceptanceSpec.m @@ -0,0 +1,199 @@ +#import "BTThreeDSecureDriver.h" +#import "BTClient+Testing.h" + +SpecBegin(BTThreeDSecure) + +describe(@"verifyCardWithNonce:amount:", ^{ + __block BTClient *client; + __block id delegate; + __block NSString *nonce; + + beforeEach(^{ + waitUntil(^(DoneCallback done) { + [BTClient testClientWithConfiguration:@{ BTClientTestConfigurationKeyMerchantIdentifier:@"integration_merchant_id", + BTClientTestConfigurationKeyPublicKey:@"integration_public_key", + BTClientTestConfigurationKeyCustomer:@YES, + BTClientTestConfigurationKeyClientTokenVersion: @2, + BTClientTestConfigurationKeyMerchantAccountIdentifier: @"three_d_secure_merchant_account", } + async:YES + completion:^(BTClient *aClient) { + client = aClient; + BTClientCardRequest *r = [[BTClientCardRequest alloc] init]; + r.number = @"4000000000000002"; + r.expirationMonth = @"12"; + r.expirationYear = @"2020"; + r.shouldValidate = NO; + [client saveCardWithRequest:r + success:^(BTCardPaymentMethod *card) { + nonce = card.nonce; + done(); + } failure:nil]; + }]; + }); + + delegate = [OCMockObject mockForProtocol:@protocol(BTPaymentMethodCreationDelegate)]; + }); + + describe(@"for a card that requires authentication", ^{ + it(@"returns the nonce on authentication completion", ^{ + BTThreeDSecureDriver *threeDSecure = [[BTThreeDSecureDriver alloc] initWithClient:client delegate:delegate]; + + id delegateRequestPresentationExpectation = [(OCMockObject *)delegate expect]; + __block UIViewController *threeDSecureViewController; + [delegateRequestPresentationExpectation andDo:^(NSInvocation *invocation) { + [invocation retainArguments]; + [invocation getArgument:&threeDSecureViewController atIndex:3]; + [system presentViewController:threeDSecureViewController +withinNavigationControllerWithNavigationBarClass:nil + toolbarClass:nil + configurationBlock:nil]; + }]; + [delegateRequestPresentationExpectation paymentMethodCreator:threeDSecure requestsPresentationOfViewController:[OCMArg isNotNil]]; + + [threeDSecure verifyCardWithNonce:nonce amount:[NSDecimalNumber decimalNumberWithString:@"1"]]; + + [(OCMockObject *)delegate verifyWithDelay:30]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(threeDSecureViewController != nil, error, @"Did not present 3D Secure authentication flow"); + return KIFTestStepResultSuccess; + }]; + + [[(OCMockObject *)delegate expect] paymentMethodCreator:threeDSecure didCreatePaymentMethod:[OCMArg checkWithBlock:^BOOL(id obj) { + waitUntil(^(DoneCallback done) { + BTPaymentMethod *paymentMethod = obj; + [client fetchNonceThreeDSecureVerificationInfo:paymentMethod.nonce + success:^(NSDictionary *nonceInfo) { + expect(nonceInfo[@"reportStatus"]).to.equal(@"authenticate_successful"); + done(); + } failure:nil]; + }); + return YES; + }]]; + + [[(OCMockObject *)delegate expect] paymentMethodCreator:threeDSecure requestsDismissalOfViewController:[OCMArg isNotNil]]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:1.5]; + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + + [(OCMockObject *)delegate verifyWithDelay:30]; + }); + }); + + describe(@"for a issuer that is not enrolled", ^{ + __block NSString *unenrolledNonce; + + beforeEach(^{ + waitUntil(^(DoneCallback done) { + BTClientCardRequest *r = [[BTClientCardRequest alloc] init]; + r.number = @"4000000000000051"; + r.expirationMonth = @"12"; + r.expirationYear = @"2020"; + r.shouldValidate = NO; + [client saveCardWithRequest:r + success:^(BTCardPaymentMethod *card) { + unenrolledNonce = card.nonce; + done(); + } failure:nil]; + }); + }); + + it(@"returns a nonce without user authentication", ^{ + BTThreeDSecureDriver *threeDSecure = [[BTThreeDSecureDriver alloc] initWithClient:client delegate:delegate]; + + [[(OCMockObject *)delegate expect] paymentMethodCreator:threeDSecure didCreatePaymentMethod:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isKindOfClass:[BTCardPaymentMethod class]]; + }]]; + + [threeDSecure verifyCardWithNonce:unenrolledNonce + amount:[NSDecimalNumber decimalNumberWithString:@"1"]]; + + [(OCMockObject *)delegate verifyWithDelay:30]; + }); + }); + + describe(@"for an unsupported card type", ^{ + __block NSString *unsupportedNonce; + + beforeEach(^{ + + waitUntil(^(DoneCallback done) { + BTClientCardRequest *r = [[BTClientCardRequest alloc] init]; + r.number = @"6011111111111117"; + r.expirationMonth = @"12"; + r.expirationYear = @"2020"; + r.shouldValidate = NO; + [client saveCardWithRequest:r + success:^(BTCardPaymentMethod *card) { + unsupportedNonce = card.nonce; + done(); + } failure:nil]; + + }); + }); + + it(@"returns a card with a new nonce and appropriate threeDSecureInfo", ^{ + BTThreeDSecureDriver *threeDSecure = [[BTThreeDSecureDriver alloc] initWithClient:client delegate:delegate]; + + [[(OCMockObject *)delegate expect] paymentMethodCreator:threeDSecure + didCreatePaymentMethod:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[BTCardPaymentMethod class]]) { + return NO; + } + BTCardPaymentMethod *card = (BTCardPaymentMethod *)obj; + if ([card.nonce isEqualToString:unsupportedNonce] || !card.nonce || [card.nonce isEqualToString:@""]) { + return NO; + } + if (card.threeDSecureInfo.liabilityShiftPossible || card.threeDSecureInfo.liabilityShifted) { + return NO; + } + return YES; + }]]; + + [threeDSecure verifyCardWithNonce:unsupportedNonce + amount:[NSDecimalNumber decimalNumberWithString:@"1"]]; + + [(OCMockObject *)delegate verifyWithDelay:30]; + }); + }); + + describe(@"when the user taps cancel", ^{ + it(@"requests dismissal and notifies the delegate of cancelation", ^{ + BTThreeDSecureDriver *threeDSecure = [[BTThreeDSecureDriver alloc] initWithClient:client delegate:delegate]; + + id delegateRequestPresentationExpectation = [(OCMockObject *)delegate expect]; + __block UIViewController *threeDSecureViewController; + [delegateRequestPresentationExpectation andDo:^(NSInvocation *invocation) { + [invocation retainArguments]; + [invocation getArgument:&threeDSecureViewController atIndex:3]; + [system presentViewController:threeDSecureViewController + withinNavigationControllerWithNavigationBarClass:nil + toolbarClass:nil + configurationBlock:nil]; + }]; + + [delegateRequestPresentationExpectation paymentMethodCreator:threeDSecure requestsPresentationOfViewController:[OCMArg isNotNil]]; + + [threeDSecure verifyCardWithNonce:nonce amount:[NSDecimalNumber decimalNumberWithString:@"1"]]; + + [(OCMockObject *)delegate verifyWithDelay:10]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(threeDSecureViewController != nil, error, @"Did not present 3D Secure authentication flow"); + return KIFTestStepResultSuccess; + }]; + + [[(OCMockObject *)delegate expect] paymentMethodCreator:threeDSecure requestsDismissalOfViewController:[OCMArg isNotNil]]; + [[(OCMockObject *)delegate expect] paymentMethodCreatorDidCancel:threeDSecure]; + + [tester tapViewWithAccessibilityLabel:@"Cancel"]; + + [(OCMockObject *)delegate verifyWithDelay:30]; + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAuthenticationViewControllerAcceptanceSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAuthenticationViewControllerAcceptanceSpec.m new file mode 100755 index 00000000..8348907d --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/BTThreeDSecureAuthenticationViewControllerAcceptanceSpec.m @@ -0,0 +1,543 @@ +#import "BTThreeDSecureAuthenticationViewController.h" +#import "BTClient+Testing.h" +#import "BTClient_Internal.h" + +#import "KIFUITestActor+BTWebView.h" +#import "EXPMatchers+BTBeANonce.h" + +#define TIME_TO_WAIT_FOR_KEYBOARD 1.5 + +@interface BTThreeDSecureAuthenticationViewController_AcceptanceSpecHelper : NSObject + +@property (nonatomic, strong) BTClient *client; +@property (nonatomic, strong) BTThreeDSecureAuthenticationViewController *threeDSecureViewController; +@property (nonatomic, strong) BTThreeDSecureLookupResult *lookupResult; +@property (nonatomic, copy) NSString *originalNonce; + +@property (nonatomic, copy) void (^authenticateBlock)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus)); +@property (nonatomic, copy) void (^finishBlock)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController); +@property (nonatomic, copy) void (^failureBlock)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error); +@end + +@implementation BTThreeDSecureAuthenticationViewController_AcceptanceSpecHelper + ++ (instancetype)helper { + BTThreeDSecureAuthenticationViewController_AcceptanceSpecHelper *helper = [[self alloc] init]; + waitUntil(^(DoneCallback done) { + [BTClient testClientWithConfiguration:@{ BTClientTestConfigurationKeyMerchantIdentifier:@"integration_merchant_id", + BTClientTestConfigurationKeyPublicKey:@"integration_public_key", + BTClientTestConfigurationKeyCustomer:@YES, + BTClientTestConfigurationKeyClientTokenVersion: @2, + BTClientTestConfigurationKeyMerchantAccountIdentifier: @"three_d_secure_merchant_account", } + async:YES + completion:^(BTClient *client) { + helper.client = client; + done(); + }]; + }); + + return helper; +} + +- (void)lookupCard:(NSString *)number completion:(void (^)(BTThreeDSecureLookupResult *))completion { + BTClientCardRequest *request = [[BTClientCardRequest alloc] init]; + request.number = number; + request.expirationMonth = @"12"; + request.expirationYear = @"2020"; + request.shouldValidate = YES; + + [self.client saveCardWithRequest:request + success:^(BTPaymentMethod *card) { + NSString *originalNonce = card.nonce; + self.originalNonce = originalNonce; + [self.client lookupNonceForThreeDSecure:originalNonce + transactionAmount:[NSDecimalNumber decimalNumberWithString:@"1"] + success:^(BTThreeDSecureLookupResult *threeDSecureLookup) { + completion(threeDSecureLookup); + } failure:^(NSError *error) { + completion(nil); + }]; + } failure:^(__unused NSError *error) { + completion(nil); + }]; +} + +- (void)fetchThreeDSecureVerificationInfo:(NSString *)nonce completion:(void (^)(NSDictionary *response))completion { + [self.client fetchNonceThreeDSecureVerificationInfo:nonce + success:^(NSDictionary *threeDSecureVerificationInfo){ + completion(threeDSecureVerificationInfo); + } failure:^(__unused NSError *error){ + completion(nil); + }]; +} + +- (void)lookupNumber:(NSString *)number + andDo:(void (^)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController))testBlock + didAuthenticate:(void (^)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus)))authenticateBlock + didFail:(void (^)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error))failureBlock + didFinish:(void (^)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController))finishBlock { + + waitUntil(^(DoneCallback done) { + [self lookupCard:number + completion:^(BTThreeDSecureLookupResult *threeDSecureLookup){ + self.lookupResult = threeDSecureLookup; + done(); + }]; + }); + + self.threeDSecureViewController = [[BTThreeDSecureAuthenticationViewController alloc] initWithLookupResult:self.lookupResult]; + + self.authenticateBlock = authenticateBlock; + self.finishBlock = finishBlock; + self.failureBlock = failureBlock; + + self.threeDSecureViewController.delegate = self; + + if (testBlock) { + testBlock(self.threeDSecureViewController); + } +} + +#pragma mark ThreeDSecureViewControllerDelegate + +- (void)threeDSecureViewController:(BTThreeDSecureAuthenticationViewController *)viewController + didAuthenticateCard:(BTCardPaymentMethod *)card + completion:(void (^)(BTThreeDSecureViewControllerCompletionStatus))completionBlock { + if (self.authenticateBlock) { + self.authenticateBlock(viewController, card, completionBlock); + } else { + [[NSException exceptionWithName:NSInternalInconsistencyException + reason:@"BTThreeDSecureViewController_AcceptanceSpecHelper received an unexpected call to threeDSecureViewController:didAuthenticateNonce:completion:" + userInfo:nil] raise]; + } +} + +- (void)threeDSecureViewController:(BTThreeDSecureAuthenticationViewController *)viewController didFailWithError:(NSError *)error { + if (self.failureBlock) { + self.failureBlock(viewController, error); + } else { + [[NSException exceptionWithName:NSInternalInconsistencyException + reason:@"BTThreeDSecureViewController_AcceptanceSpecHelper received an unexpected call to threeDSecureViewController:didFailWithError:" + userInfo:nil] raise]; + } +} + +- (void)threeDSecureViewControllerDidFinish:(BTThreeDSecureAuthenticationViewController *)viewController { + if (self.finishBlock) { + self.finishBlock(viewController); + } else { + [[NSException exceptionWithName:NSInternalInconsistencyException + reason:@"BTThreeDSecureViewController_AcceptanceSpecHelper received an unexpected call to threeDSecureViewControllerDidFinish:" + userInfo:nil] raise]; + } +} + +- (void)lookupHappyPathAndDo:(void (^)(BTThreeDSecureAuthenticationViewController *threeDSecureViewController))completion { + [self lookupNumber:@"4000000000000002" andDo:completion didAuthenticate:nil didFail:nil didFinish:nil]; +} + +@end + +SpecBegin(BTThreeDSecureAuthenticationViewController_Acceptance) + +describe(@"3D Secure View Controller", ^{ + __block BTThreeDSecureAuthenticationViewController_AcceptanceSpecHelper *helper; + beforeEach(^{ + helper = [BTThreeDSecureAuthenticationViewController_AcceptanceSpecHelper helper]; + }); + + describe(@"developer perspective - delegate messages", ^{ + it(@"fails to load a view controller when lookup fails", ^{ + BTThreeDSecureLookupResult *lookupResult = nil; + BTThreeDSecureAuthenticationViewController *threeDSecureViewController = [[BTThreeDSecureAuthenticationViewController alloc] initWithLookupResult:lookupResult]; + + expect(threeDSecureViewController).to.beNil(); + }); + + it(@"fails to load a view controller when lookup does not require a user flow", ^{ + BTThreeDSecureLookupResult *lookupResult = [[BTThreeDSecureLookupResult alloc] init]; + BTThreeDSecureAuthenticationViewController *threeDSecureViewController = [[BTThreeDSecureAuthenticationViewController alloc] initWithLookupResult:lookupResult]; + + expect(lookupResult.requiresUserAuthentication).to.beFalsy(); + expect(threeDSecureViewController).to.beNil(); + }); + + it(@"calls didAuthenticate with the upgraded nonce (consuming the original nonce)", ^{ + __block BOOL calledDidAuthenticate = NO; + [helper lookupNumber:@"4000000000000002" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus)) { + calledDidAuthenticate = YES; + expect(card.nonce).to.beANonce(); + } didFail:nil + didFinish:nil]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidAuthenticate, error, @"Did not call didAuthenticate"); + return KIFTestStepResultSuccess; + }]; + }); + + it(@"calls didFinish after didAuthenticate calls its completion with success", ^{ + __block BOOL calledDidAuthenticate = NO; + __block BOOL calledDidFinish = NO; + [helper lookupNumber:@"4000000000000002" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus)) { + calledDidAuthenticate = YES; + expect(calledDidFinish).to.beFalsy(); + completion(BTThreeDSecureViewControllerCompletionStatusSuccess); + } didFail:nil + didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + calledDidFinish = YES; + expect(calledDidAuthenticate).to.beTruthy(); + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidAuthenticate, error, @"Did not call didAuthenticate"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + + it(@"calls didFail when authentication fails (leaving the original nonce transactable)", ^{ + __block BOOL calledDidFail = NO; + __block BOOL calledDidFinish = NO; + + [helper lookupNumber:@"4000000000000010" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:nil + didFail:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error) { + expect(error.domain).to.equal(BTThreeDSecureErrorDomain); + expect(error.code).to.equal(BTThreeDSecureFailedAuthenticationErrorCode); + expect(error.localizedDescription).to.equal(@"Failed to authenticate, please try a different form of payment"); + expect(error.userInfo[BTThreeDSecureInfoKey]).to.equal(@{ @"liabilityShifted": @NO, @"liabilityShiftPossible": @YES, }); + calledDidFail = YES; + } + didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + expect(calledDidFail).to.beTruthy(); + calledDidFinish = YES; + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidFail, error, @"Did not call didFail"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + describe(@"user flows - 3DS Statuses (enrolled, authenticated, signature verified)", ^{ + context(@"cardholder enrolled, successful authentication, successful signature verification - Y,Y,Y", ^{ + it(@"successfully authenticates a user when they enter their password", ^{ + __block BOOL checkedNonce = NO; + [helper lookupNumber:@"4000000000000002" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus)) { + [helper fetchThreeDSecureVerificationInfo:card.nonce + completion:^(NSDictionary *response) { + expect(response[@"reportStatus"]).to.equal(@"authenticate_successful"); + checkedNonce = YES; + }]; + } didFail:nil + didFinish:nil]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(checkedNonce, error, @"Did not check nonce"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"issuer not enrolled - N", ^{ + it(@"bypasses the entire authentication experience", ^{ + [helper lookupNumber:@"4000000000000051" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + expect(threeDSecureViewController).to.beNil(); + } didAuthenticate:nil didFail:nil didFinish:nil]; + }); + }); + + context(@"simulated cardinal error on lookup - error", ^{ + it(@"bypasses the entire authentication experience", ^{ + [helper lookupNumber:@"4000000000000077" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + expect(threeDSecureViewController).to.beNil(); + } didAuthenticate:nil + didFail:nil + didFinish:nil]; + }); + }); + + context(@"User enters incorrect password - Y,N,Y", ^{ + it(@"it presents the failure to the user and fails to authenticate the nonce", ^{ + __block BOOL calledDidFail; + __block BOOL calledDidFinish; + + [helper lookupNumber:@"4000000000000028" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester enterTextIntoCurrentFirstResponder:@"bad"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + [tester waitForViewWithAccessibilityLabel:@"Account Authentication Blocked"]; + [tester tapViewWithAccessibilityLabel:@"Continue"]; + } didAuthenticate:nil + didFail:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error) { + expect(error.domain).to.equal(BTThreeDSecureErrorDomain); + expect(error.code).to.equal(BTThreeDSecureFailedAuthenticationErrorCode); + expect(error.localizedDescription).to.equal(@"Failed to authenticate, please try a different form of payment"); + expect(error.userInfo[BTThreeDSecureInfoKey]).to.equal(@{ @"liabilityShifted": @NO, @"liabilityShiftPossible": @YES}); + calledDidFail = YES; + } didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + calledDidFinish = YES; + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidFail, error, @"Did not call didFail"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"User attempted to enter a password - Y,A,Y", ^{ + it(@"displays a loading indication to the user and successfully authenticates the nonce", ^{ + __block BOOL checkedNonce; + + [helper lookupNumber:@"4000000000000101" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + } didAuthenticate:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, BTCardPaymentMethod *card, void (^completion)(BTThreeDSecureViewControllerCompletionStatus status)) { + [helper fetchThreeDSecureVerificationInfo:card.nonce + completion:^(NSDictionary *response) { + expect(response[@"reportStatus"]).to.equal(@"authenticate_attempt_successful"); + checkedNonce = YES; + }]; + } didFail:nil + didFinish:nil]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(checkedNonce, error, @"Did not check nonce"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"Signature verification fails - Y,Y,N", ^{ + it(@"accepts a password but resuts in an failed verification", ^{ + __block BOOL calledDidFail = NO; + __block BOOL calledDidFinish = NO; + + [helper lookupNumber:@"4000000000000010" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:nil + didFail:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error) { + expect(error.domain).to.equal(BTThreeDSecureErrorDomain); + expect(error.code).to.equal(BTThreeDSecureFailedAuthenticationErrorCode); + expect(error.localizedDescription).to.equal(@"Failed to authenticate, please try a different form of payment"); + expect(error.userInfo[BTThreeDSecureInfoKey]).to.equal(@{ @"liabilityShifted": @NO, @"liabilityShiftPossible": @YES, }); + calledDidFail = YES; + } + didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + calledDidFinish = YES; + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidFail, error, @"Did not call didFail"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"issuer is down - Y,U", ^{ + it(@"returns a nonce without asking user for authentication", ^{ + __block BOOL calledDidFail = NO; + __block BOOL calledDidFinish = NO; + + [helper lookupNumber:@"4000000000000036" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"System Error" traits:UIAccessibilityTraitStaticText]; + [tester tapViewWithAccessibilityLabel:@"Continue"]; + } didAuthenticate:nil + didFail:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error) { + calledDidFail = YES; + + expect(error.domain).to.equal(BTThreeDSecureErrorDomain); + expect(error.code).to.equal(BTThreeDSecureFailedAuthenticationErrorCode); + expect(error.userInfo[BTThreeDSecureInfoKey]).to.equal(@{ @"liabilityShifted": @NO, @"liabilityShiftPossible": @YES, }); + } didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + expect(calledDidFail).to.beTruthy(); + calledDidFinish = YES; + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidFail, error, @"Did not call didFail"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"Early termination due to cardinal error - Y, Error", ^{ + it(@"accepts a password but fails to authenticate the nonce", ^{ + __block BOOL calledDidFail = NO; + __block BOOL calledDidFinish = NO; + + [helper lookupNumber:@"4000000000000093" + andDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + [tester tapUIWebviewXPathElement:@"//input[@name=\"external.field.password\"]"]; + [tester waitForTimeInterval:TIME_TO_WAIT_FOR_KEYBOARD]; + + [tester enterTextIntoCurrentFirstResponder:@"1234"]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + } didAuthenticate:nil + didFail:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController, NSError *error) { + calledDidFail = YES; + expect(error.domain).to.equal(BTThreeDSecureErrorDomain); + expect(error.code).to.equal(BTThreeDSecureFailedAuthenticationErrorCode); + expect(error.userInfo[BTThreeDSecureInfoKey]).to.equal(@{ @"liabilityShiftPossible": @YES, @"liabilityShifted": @NO, }); + } didFinish:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + calledDidFinish = YES; + expect(calledDidFail).to.beTruthy(); + }]; + + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition(calledDidFail, error, @"Did not call didFail"); + KIFTestWaitCondition(calledDidFinish, error, @"Did not call didFinish"); + return KIFTestStepResultSuccess; + }]; + }); + }); + + context(@"The ACS Frame fails to load", ^{ + it(@"accepts a password but fails to authenticate the nonce", ^{ + id stub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:@"acs.example.com"]; + } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:123 userInfo:@{ NSLocalizedDescriptionKey: @"Something bad happened" }]]; + }]; + + BTThreeDSecureLookupResult *lookupResult = [[BTThreeDSecureLookupResult alloc] init]; + lookupResult.acsURL = [NSURL URLWithString:@"https://acs.example.com/"]; + lookupResult.PAReq = @"pareq"; + lookupResult.termURL = [NSURL URLWithString:@"https://example.com/term"]; + lookupResult.MD = @"md"; + + BTThreeDSecureAuthenticationViewController *threeDSecureViewController = [[BTThreeDSecureAuthenticationViewController alloc] initWithLookupResult:lookupResult]; + + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Something bad happened"]; + [tester tapViewWithAccessibilityLabel:@"OK"]; + [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Something bad happened"]; + + [OHHTTPStubs removeStub:stub]; + }); + }); + }); + + describe(@"web view interaction details", ^{ + xit(@"displays a loading indicator during page loads", ^{ + [helper lookupHappyPathAndDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [system waitForApplicationToSetNetworkActivityIndicatorVisible:YES]; + [system waitForApplicationToSetNetworkActivityIndicatorVisible:NO]; + [system waitForTimeInterval:1]; + [tester tapViewWithAccessibilityLabel:@"Submit"]; + [system waitForApplicationToSetNetworkActivityIndicatorVisible:YES]; + [system waitForTimeInterval:1]; + [tester waitForViewWithAccessibilityLabel:@"Incorrect, Please try again"]; + [system waitForApplicationToSetNetworkActivityIndicatorVisible:NO]; + }]; + }); + + it(@"closes the popup when the user taps Cancel in the nav bar", ^{ + [helper lookupHappyPathAndDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester tapViewWithAccessibilityLabel:@"Help"]; + [tester waitForViewWithAccessibilityLabel:@"Social Security Number"]; + [tester tapViewWithAccessibilityLabel:@"Cancel"]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + }]; + }); + + it(@"closes the popup when the user taps a close link", ^{ + [helper lookupHappyPathAndDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester tapViewWithAccessibilityLabel:@"Help"]; + [tester waitForViewWithAccessibilityLabel:@"Social Security Number"]; + [tester tapUIWebviewXPathElement:@"//a[text()=\"Social Security Number\"]"]; + [tester tapUIWebviewXPathElement:@"(//a[contains(text(), \"Return\")])[last()]"]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + }]; + }); + + it(@"uses the html title tag for the view controllers title", ^{ + [helper lookupHappyPathAndDo:^(BTThreeDSecureAuthenticationViewController *threeDSecureViewController) { + [system presentViewController:threeDSecureViewController withinNavigationControllerWithNavigationBarClass:nil toolbarClass:nil configurationBlock:nil]; + + [tester waitForViewWithAccessibilityLabel:@"Please submit your Verified by Visa password." traits:UIAccessibilityTraitStaticText]; + + expect(threeDSecureViewController.title).to.equal(@"Authentication"); + }]; + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.h b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.h new file mode 100755 index 00000000..5fa7d22e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.h @@ -0,0 +1,7 @@ +#import "KIFSystemTestActor.h" + +@interface KIFSystemTestActor (BTNetworkActivity) + +- (void)waitForApplicationToSetNetworkActivityIndicatorVisible:(BOOL)visible; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.m new file mode 100755 index 00000000..7eab576c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFSystemTestActor+BTNetworkActivity.m @@ -0,0 +1,12 @@ +#import "KIFSystemTestActor+BTNetworkActivity.h" + +@implementation KIFSystemTestActor (BTNetworkActivity) + +- (void)waitForApplicationToSetNetworkActivityIndicatorVisible:(BOOL)visible { + [system runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestWaitCondition([[UIApplication sharedApplication] isNetworkActivityIndicatorVisible] == visible, error, @"Network activity indicator visiblity was not %@", (visible ? @"YES" : @"NO")); + return KIFTestStepResultSuccess; + }]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.h b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.h new file mode 100755 index 00000000..08bd3d60 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.h @@ -0,0 +1,8 @@ +#import "KIFUITestActor.h" + +@interface KIFUITestActor (BTWebView) + +- (void) waitForUIWebviewXPathElement:(NSString*)xpath; +- (void) tapUIWebviewXPathElement:(NSString*)xpath; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.m new file mode 100755 index 00000000..c278394d --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Acceptance-Specs/KIFUITestActor+BTWebView.m @@ -0,0 +1,66 @@ +#import "KIFUITestActor+BTWebView.h" + +@implementation KIFUITestActor (BTWebView) + +- (UIWebView *)_getWebViewOnCurrentScreen:(NSArray *)views +{ + for (UIView *view in views) { + if ([NSStringFromClass([view class]) isEqual:@"UIWebView"]) { + return (UIWebView*) view; + } + UIWebView* found = [self _getWebViewOnCurrentScreen:view.subviews]; + if (found != nil) + return found; + } + return nil; +} + +- (UIWebView *)getWebViewOnCurrentScreen { + return [self _getWebViewOnCurrentScreen:[[UIApplication sharedApplication] windows]]; +} + + +// If the send in xpath doesn't find any element the return CGPoint will be -1,-1 +- (CGPoint)webViewElementCoordinates:(NSString *)xpath { + UIWebView *currentWebView = [self getWebViewOnCurrentScreen]; + if (currentWebView) { + [currentWebView stringByEvaluatingJavaScriptFromString:@"var script = document.createElement('script');" + "script.type = 'text/javascript';" + "script.text = \"function findPos(obj) {var curtop = 0; if (obj.offsetParent) { do { curtop += obj.offsetTop; } while (obj = obj.offsetParent); return [curtop]; }}; function getElementsByXPath(xpath, contextNode) { try { if(contextNode === undefined) { var xpathResult = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); } else { var xpathResult = contextNode.evaluate(xpath, contextNode, null, XPathResult.ANY_TYPE, null); } var array = []; var element; element = xpathResult.iterateNext(); while(element) { array[array.length] = element; element = xpathResult.iterateNext(); } if (array.length >= 0) { var element = array[0]; window.scroll(0,findPos(element)); var rect = element.getBoundingClientRect(); var elementLeft,elementTop; var scrollTop = document.documentElement.scrollTop?document.documentElement.scrollTop:document.body.scrollTop; var scrollLeft = document.documentElement.scrollLeft? document.documentElement.scrollLeft:document.body.scrollLeft; elementTop = rect.top+scrollTop; elementLeft = rect.left+scrollLeft; return elementLeft + ',' + elementTop + ',' + document.documentElement.scrollTop + ',' + document.body.scrollTop; } else { return ''; } } catch(err) { return 'xpath not found';} };\";" + "document.getElementsByTagName('head')[0].appendChild(script);"]; + NSString *message = [currentWebView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"%@%@%@", @"getElementsByXPath('", xpath, @"');"]]; + // This sleep is to allow scrolling to the element happening + [self waitForTimeInterval:0.1]; + if (![message isEqualToString:@""] && ![message isEqualToString:@"xpath not found"]) { + NSArray *list = [message componentsSeparatedByString:@","]; + CGPoint domCoordinates = CGPointMake([list[0] floatValue], [list[1] floatValue]); + CGPoint windowCoordinates = [currentWebView.scrollView convertPoint:domCoordinates + toView:[[UIApplication sharedApplication] keyWindow]]; + + return windowCoordinates; + } + } + return CGPointMake(-1, -1); +} + +- (void)waitForUIWebviewXPathElement:(NSString *)xpath { + + [self runBlock:^KIFTestStepResult(NSError **error) { + CGPoint point = [self webViewElementCoordinates:xpath]; + KIFTestWaitCondition(point.x != -1 && point.y != -1, error, @"Cannot find element with xpath \"%@\"", xpath); + return KIFTestStepResultSuccess; + } timeout:10.0]; + +} + +- (void)tapUIWebviewXPathElement:(NSString *)xpath { + [self runBlock:^KIFTestStepResult(NSError **error) { + CGPoint point = [self webViewElementCoordinates:xpath]; + KIFTestWaitCondition(point.x != -1 && point.y != -1, error, @"Cannot find element with xpath \"%@\"", xpath); + [self tapScreenAtPoint:point]; + return KIFTestStepResultSuccess; + } timeout:10.0]; + +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Integration-Specs/BTPayPalDriverSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Integration-Specs/BTPayPalDriverSpec.m new file mode 100755 index 00000000..a7035b2f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Integration-Specs/BTPayPalDriverSpec.m @@ -0,0 +1,917 @@ +#import "BTPayPalDriver.h" + +#import "Braintree.h" +#import "BTClient_Internal.h" +#import "PayPalOneTouchCore.h" +#import "PayPalOneTouchRequest.h" +#import "BTAppSwitchErrors.h" +#import "BTPayPalDriver_Compatibility.h" + +@interface BTPayPalDriverSpecHelper : NSObject +@end + +@implementation BTPayPalDriverSpecHelper + ++ (void)setupSpec:(void (^)(NSString *returnURLScheme, id mockClient, id mockApplication))setupBlock { + id configuration = [OCMockObject mockForClass:[BTConfiguration class]]; + [[[configuration stub] andReturnValue:@YES] payPalEnabled]; + [[[configuration stub] andReturn:[NSURL URLWithString:@"https://example.com/privacy"]] payPalPrivacyPolicyURL]; + [[[configuration stub] andReturn:[NSURL URLWithString:@"https://example.com/tos"]] payPalMerchantUserAgreementURL]; + [[[configuration stub] andReturn:@"offline"] payPalEnvironment]; + [[[configuration stub] andReturn:@"client-id"] payPalClientId]; + [[[configuration stub] andReturnValue:@NO] payPalUseBillingAgreement]; + + id clientToken = [OCMockObject mockForClass:[BTClientToken class]]; + [[[clientToken stub] andReturn:@"client-token"] originalValue]; + + id client = [OCMockObject mockForClass:[BTClient class]]; + [[[client stub] andReturn:client] copyWithMetadata:OCMOCK_ANY]; + [[[client stub] andReturn:clientToken] clientToken]; + [[[client stub] andReturn:configuration] configuration]; + + NSString *returnURLScheme = @"com.braintreepayments.Braintree-Demo.payments"; + + id bundle = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[bundle stub] andReturn:@[@{ @"CFBundleURLSchemes": @[returnURLScheme] }]] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + + id application = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[[application stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", returnURLScheme)]; + + setupBlock(returnURLScheme, client, application); +} + +@end + +SpecBegin(BTPayPalDriver) + +describe(@"PayPal One Touch Core", ^{ + describe(@"future payments", ^{ + describe(@"performing app switches", ^{ + it(@"performs an app switch to PayPal when the PayPal app is installed", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"Perform App Switch"]; + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(__unused NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockApplication verify]; + }]; + }); + + it(@"performs an app switch to Safari when the PayPal app is not installed", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"Perform App Switch"]; + + [[[mockApplication stub] andReturnValue:@NO] canOpenURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"https")]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", @"https")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockApplication verify]; + }]; + }); + + it(@"fails to initialize if the returnURLScheme is not valid", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:@"invalid-return-url-scheme"]; + + expect(payPalDriver).to.beNil(); + }]; + }); + }); + + describe(@"handling app switch returns", ^{ + it(@"receives a payment method on app switch return success", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + BTPaymentMethod *fakePaymentMethod = [OCMockObject mockForClass:[BTPaymentMethod class]]; + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockClient expect] andDo:^(NSInvocation *invocation) { + void (^successBlock)(BTPaymentMethod *paymentMethod); + [invocation getArgument:&successBlock atIndex:4]; + successBlock(fakePaymentMethod); + }] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC expect] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeSuccess)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[result stub] response]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + expect(paymentMethod).to.equal(fakePaymentMethod); + expect(error).to.beNil(); + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + [mockOTC verify]; + }]; + }); + + it(@"receives the error passed through directly on failure", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + NSError *fakeError = [OCMockObject mockForClass:[NSError class]]; + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC expect] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeError)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturn:fakeError] error]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + expect(paymentMethod).to.beNil(); + expect(error).to.equal(fakeError); + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + [mockOTC verify]; + }]; + }); + + it(@"receives neither a payment method nor an error on cancelation", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC expect] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeCancel)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[result stub] error]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + expect(paymentMethod).to.beNil(); + expect(error).to.beNil(); + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + [mockOTC verify]; + }]; + }); + }); + + describe(@"scopes", ^{ + it(@"includes email and future payments", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"opened URL"]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", @"https")]; + + id otcStub = [OCMockObject mockForClass:[PayPalOneTouchAuthorizationRequest class]]; + [[[[otcStub expect] classMethod] andForwardToRealObject] requestWithScopeValues:HC_containsInAnyOrder(@"email", @"https://uri.paypal.com/services/payments/futurepayments", nil) + privacyURL:OCMOCK_ANY + agreementURL:OCMOCK_ANY + clientID:OCMOCK_ANY + environment:OCMOCK_ANY + callbackURLScheme:OCMOCK_ANY]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + [payPalDriver startAuthorizationWithCompletion:nil]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [otcStub verify]; + }]; + }); + }); + + describe(@"analytics", ^{ + it(@"posts an analytics event for a successful app switch to the PayPal app", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"Perform App Switch"]; + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(__unused NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.appswitch.initiate.started"]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"posts an analytics event for a successful app switch to the Browser", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"Perform App Switch"]; + [[[mockApplication stub] andReturnValue:@NO] canOpenURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"https")]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(__unused NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", @"https")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.webswitch.initiate.started"]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"posts an analytics event for a failed app switch", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + XCTestExpectation *appSwitchExpectation = [self expectationWithDescription:@"Perform App Switch"]; + [[[mockApplication stub] andReturnValue:@NO] canOpenURL:HC_hasProperty(@"scheme", HC_startsWith(@"com.paypal"))]; + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"https")]; + [[[[mockApplication expect] andReturnValue:@YES] andDo:^(__unused NSInvocation *invocation) { + [appSwitchExpectation fulfill]; + }] openURL:HC_hasProperty(@"scheme", @"https")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.webswitch.initiate.started"]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"posts analytics events when preflight checks fail", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-otc.preflight.invalid-return-url-scheme"]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:@"invalid-return-url-scheme"]; + expect(payPalDriver).to.beNil(); + + [mockClient verify]; + }]; + }); + + it(@"post an analytics event to indicate handling the one touch core response ", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC stub] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeCancel)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[result stub] error]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.unknown.canceled"]; + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"posts an anlaytics event to indicate tokenization success", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockClient stub] andDo:^(NSInvocation *invocation) { + void (^successBlock)(BTPaymentMethod *paymentMethod); + [invocation getArgument:&successBlock atIndex:4]; + successBlock(nil); + }] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC stub] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeSuccess)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[result stub] response]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.tokenize.succeeded"]; + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"posts an anlaytics event to indicate tokenization failure", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + NSURL *fakeReturnURL = [OCMockObject mockForClass:[NSURL class]]; + + [[[mockClient stub] andDo:^(NSInvocation *invocation) { + void (^failureBlock)(BTPaymentMethod *paymentMethod); + [invocation getArgument:&failureBlock atIndex:5]; + failureBlock(nil); + }] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:OCMOCK_ANY]; + [[[mockApplication stub] andReturnValue:@YES] openURL:OCMOCK_ANY]; + + id mockOTC = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[[mockOTC stub] classMethod] andDo:^(NSInvocation *invocation) { + void (^stubOTCCompletionBlock)(PayPalOneTouchCoreResult *result); + [invocation getArgument:&stubOTCCompletionBlock atIndex:3]; + id result = [OCMockObject mockForClass:[PayPalOneTouchCoreResult class]]; + [(PayPalOneTouchCoreResult *)[[result stub] andReturnValue:OCMOCK_VALUE(PayPalOneTouchResultTypeSuccess)] type]; + [(PayPalOneTouchCoreResult *)[result stub] target]; + [(PayPalOneTouchCoreResult *)[result stub] response]; + stubOTCCompletionBlock(result); + }] parseResponseURL:fakeReturnURL completionBlock:[OCMArg isNotNil]]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [[mockClient expect] postAnalyticsEvent:@"ios.paypal-future-payments.tokenize.failed"]; + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Received call to completion block"]; + [payPalDriver startAuthorizationWithCompletion:^void(BTPayPalPaymentMethod *paymentMethod, NSError *error) { + [completionExpectation fulfill]; + }]; + + [BTPayPalDriver handleAppSwitchReturnURL:fakeReturnURL]; + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + }); + + describe(@"delegate notifications", ^{ + }); + + describe(@"isAvailable", ^{ + + + it(@"returns YES when PayPal is enabled in configuration and One Touch Core is ready", ^{ + + id configuration = [OCMockObject mockForClass:[BTConfiguration class]]; + [[[configuration stub] andReturnValue:@YES] payPalEnabled]; + [[[configuration stub] andReturn:[NSURL URLWithString:@"https://example.com/privacy"]] payPalPrivacyPolicyURL]; + [[[configuration stub] andReturn:[NSURL URLWithString:@"https://example.com/tos"]] payPalMerchantUserAgreementURL]; + [[[configuration stub] andReturn:@"offline"] payPalEnvironment]; + [[[configuration stub] andReturn:@"client-id"] payPalClientId]; + + id clientToken = [OCMockObject mockForClass:[BTClientToken class]]; + [[[clientToken stub] andReturn:@"client-token"] originalValue]; + + id client = [OCMockObject mockForClass:[BTClient class]]; + [[[client stub] andReturn:client] copyWithMetadata:OCMOCK_ANY]; + [[[client stub] andReturn:clientToken] clientToken]; + [[[client stub] andReturn:configuration] configuration]; + + NSString *returnURLScheme = @"com.braintreepayments.Braintree-Demo.bt-payments"; + + id bundle = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[bundle stub] andReturn:@[@{ @"CFBundleURLSchemes": @[returnURLScheme] }]] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + + id application = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[[application stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", returnURLScheme)]; + + NSError *error; + BOOL isAvailable = [BTPayPalDriver verifyAppSwitchConfigurationForClient:client returnURLScheme:returnURLScheme error:&error]; + expect(isAvailable).to.beTruthy(); + expect(error).to.beNil(); + + + }); + + it(@"returns NO when PayPal is not enabled in configuration", ^{ + + id configuration = [OCMockObject mockForClass:[BTConfiguration class]]; + [[[configuration stub] andReturnValue:@NO] payPalEnabled]; + + [[[configuration stub] andReturn:@"offline"] payPalEnvironment]; + [[[configuration stub] andReturn:@"client-id"] payPalClientId]; + + id clientToken = [OCMockObject mockForClass:[BTClientToken class]]; + [[[clientToken stub] andReturn:@"client-token"] originalValue]; + + id client = [OCMockObject mockForClass:[BTClient class]]; + [[[client stub] andReturn:client] copyWithMetadata:OCMOCK_ANY]; + [[[client stub] andReturn:clientToken] clientToken]; + [[[client stub] andReturn:configuration] configuration]; + + NSString *returnURLScheme = @"com.braintreepayments.Braintree-Demo.bt-payments"; + + id bundle = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[bundle stub] andReturn:@[@{ @"CFBundleURLSchemes": @[returnURLScheme] }]] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + + id application = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[[application stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", returnURLScheme)]; + + [[client expect] postAnalyticsEvent:@"ios.paypal-otc.preflight.disabled"]; + + NSError *error; + BOOL isAvailable = [BTPayPalDriver verifyAppSwitchConfigurationForClient:client returnURLScheme:returnURLScheme error:&error]; + expect(isAvailable).to.beFalsy(); + expect(error).notTo.beNil(); + + }); + + it(@"returns NO when the URL scheme has not been setup", ^{ + + id configuration = [OCMockObject mockForClass:[BTConfiguration class]]; + [[[configuration stub] andReturnValue:@YES] payPalEnabled]; + + [[[configuration stub] andReturn:@"offline"] payPalEnvironment]; + [[[configuration stub] andReturn:@"client-id"] payPalClientId]; + + id clientToken = [OCMockObject mockForClass:[BTClientToken class]]; + [[[clientToken stub] andReturn:@"client-token"] originalValue]; + + id client = [OCMockObject mockForClass:[BTClient class]]; + [[[client stub] andReturn:client] copyWithMetadata:OCMOCK_ANY]; + [[[client stub] andReturn:clientToken] clientToken]; + [[[client stub] andReturn:configuration] configuration]; + + NSString *returnURLScheme = @"com.braintreepayments.Braintree-Demo.bt-payments"; + + id application = [OCMockObject partialMockForObject:[UIApplication sharedApplication]]; + [[[application stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", returnURLScheme)]; + + [[client expect] postAnalyticsEvent:@"ios.paypal-otc.preflight.invalid-return-url-scheme"]; + + NSError *error; + BOOL isAvailable = [BTPayPalDriver verifyAppSwitchConfigurationForClient:client returnURLScheme:returnURLScheme error:&error]; + expect(isAvailable).to.beFalsy(); + expect(error).notTo.beNil(); + + }); + + it(@"returns NO when the return URL scheme has not been registered with UIApplication", ^{ + + id configuration = [OCMockObject mockForClass:[BTConfiguration class]]; + [[[configuration stub] andReturnValue:@YES] payPalEnabled]; + + [[[configuration stub] andReturn:@"offline"] payPalEnvironment]; + [[[configuration stub] andReturn:@"client-id"] payPalClientId]; + + id clientToken = [OCMockObject mockForClass:[BTClientToken class]]; + [[[clientToken stub] andReturn:@"client-token"] originalValue]; + + id client = [OCMockObject mockForClass:[BTClient class]]; + [[[client stub] andReturn:client] copyWithMetadata:OCMOCK_ANY]; + [[[client stub] andReturn:clientToken] clientToken]; + [[[client stub] andReturn:configuration] configuration]; + + NSString *returnURLScheme = @"com.braintreepayments.Braintree-Demo.bt-payments"; + + id bundle = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[bundle stub] andReturn:@[@{ @"CFBundleURLSchemes": @[returnURLScheme] }]] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + + [[client expect] postAnalyticsEvent:@"ios.paypal-otc.preflight.invalid-return-url-scheme"]; + + NSError *error; + BOOL isAvailable = [BTPayPalDriver verifyAppSwitchConfigurationForClient:client returnURLScheme:returnURLScheme error:&error]; + expect(isAvailable).to.beFalsy(); + expect(error).notTo.beNil(); + + }); + }); + + }); + + describe(@"classifying app switch returns", ^{ + + afterEach(^{ + // Reset state of BTPayPalDriver after each test (execute BTPayPalHandleURLContinuation if set) + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:@""]]; + }); + + it(@"accepts return URLs from the browser", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC. + // Pretend that no wallet is installed. + [[[mockApplication stub] andReturnValue:@NO] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v1")]; + [[[mockApplication stub] andReturnValue:@NO] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v1")]; + [[[mockApplication stub] andReturnValue:@NO] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@NO] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.payments://onetouch/v1/success?payloadEnc=e0yvzQHOOoXyoLjKZvHBI0Rbyad6usxhOz22CjG3V1lOsguMRsuQpEqPxlIlK86VPmTuagb1jJcnDUK9QsWJE8ffe4i9Ms4ggd6r5EoymVM%2BAYgjyjaYtPPOxIgMepNGnvnYt9EKJs2Bd0wbZj0ekxSA6BzRZDPEpZ%2FjhssxJVscjbPvOwCoTnjEhuNxiOamAGSRd6fo7ln%2BishDwRCLz81qlV8cgfXNzlHrRw1P7CbTQ8XhNGn35CHD64ysuHAW97ZjAzPCRdikWbgiw2S%2BDvSePhRRnTR10e2NPDYBeVzGQFzvf6WRklrqcLeFwRcAqoa0ZneOPgMbk5nvylGY716caCCPtJKnoJAflZZK6%2F7iXcA%2F3p9qrQIrszmthu%2FbnA%2FP7dZsWRarUiT%2FZhZg32MsmV3B3fPjQOMbhB7dRv5uomhCjhNhPzXH7nFA54mKOlvAdTm1QOk5P%2Fh3AaHz0qwIKgXAhxIfwxqHgIYxtba53sdwa7OXfx14FRlcfPngrR02IAHeaulkH6vJ24ZAsoUUdNkvRfDmM1O2%2B4424%2FMINTUJJsR0%2FwrYrwzp0gC6fKoAzT%2FgFhL6QVLoUss%3D&payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IkMwQTkwODQ1LTJBRUQtNEZCRC04NzIwLTQzNUU2MkRGNjhFNCIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJsaXZlIiwiZXJyb3IiOm51bGx9&x-source=com.braintree.browserswitch"]; + NSString *source = @"com.apple.mobilesafari"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beTruthy(); + + [mockApplication verify]; + }]; + + }); + + it(@"accepts return URLs from the app", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Braintree-Demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + NSString *source = @"com.paypal.ppclient.touch.v1"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beTruthy(); + + source = @"com.paypal.ppclient.touch.v2"; + + canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beTruthy(); + + [mockApplication verify]; + }]; + + }); + + it(@"rejects return URLs that did not come from browser or app", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Braintree-Demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + NSString *source = @"com.something.else"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beFalsy(); + + [mockApplication verify]; + }]; + + }); + + it(@"rejects other malformed URLs", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + // This malformed returnURL is just missing payload + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Braintree-Demo.payments://onetouch/v1/success?x-source=com.paypal.ppclient.touch.v1-or-v2"]; + + NSString *source = @"com.paypal.ppclient.touch.v2"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beFalsy(); + + + [mockApplication verify]; + }]; + + }); + + it(@"rejects returns when there is no app switch in progress", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.Braintree-Demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + + NSString *source = @"com.paypal.ppclient.touch.v2"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beFalsy(); + + + [mockApplication verify]; + }]; + + }); + + it(@"ignores the case of the URL Scheme to account for Safari's habit of downcasing URL schemes", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.PaYmEnTs://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + NSString *source = @"com.paypal.ppclient.touch.v2"; + + BOOL canHandle = [BTPayPalDriver canHandleAppSwitchReturnURL:returnURL sourceApplication:source]; + expect(canHandle).to.beTruthy(); + + [mockApplication verify]; + }]; + }); + }); + + describe(@"handling app switch returns", ^{ + + afterEach(^{ + // Reset state of BTPayPalDriver after each test (execute BTPayPalHandleURLContinuation if set) + [BTPayPalDriver handleAppSwitchReturnURL:[NSURL URLWithString:@""]]; + }); + + it(@"ignores an irrelevant or malformed URL", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.PaYmEnTs://----malformed----"]; + + [[mockClient reject] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [BTPayPalDriver handleAppSwitchReturnURL:returnURL]; + + [mockClient verify]; + }]; + + }); + + it(@"accepts a success app switch return", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + + [[mockClient expect] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [BTPayPalDriver handleAppSwitchReturnURL:returnURL]; + + [mockClient verify]; + }]; + }); + + it(@"accepts a failure app switch return", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.PaYmEnTs://onetouch/v1/failure?error=some+message"]; + + XCTestExpectation *parseOtcExpectation = [self expectationWithDescription:@"Parse otc response"]; + + [PayPalOneTouchCore parseResponseURL:returnURL + completionBlock:^(PayPalOneTouchCoreResult *result) { + expect(result.type).to.equal(PayPalOneTouchResultTypeError); + [parseOtcExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + + + + }); + + it(@"accepts a cancelation app switch return", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + [payPalDriver startAuthorizationWithCompletion:nil]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.payments://onetouch/v1/cancel?payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IjQ1QUZEQkE3LUJEQTYtNDNEMi04MUY2LUY4REM1QjZEOTkzQSIsImVudmlyb25tZW50IjoibW9jayJ9&x-source=com.paypal.ppclient.touch.v2"]; + + XCTestExpectation *parseOtcExpectation = [self expectationWithDescription:@"Parse otc response"]; + + [PayPalOneTouchCore parseResponseURL:returnURL + completionBlock:^(PayPalOneTouchCoreResult *result) { + expect(result.type).to.equal(PayPalOneTouchResultTypeCancel); + [parseOtcExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [mockClient verify]; + }]; + }); + + it(@"tokenizes a success response, returning the payment method nonce to the developer", ^{ + + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication) { + + id ppOtcMock = [OCMockObject mockForClass:[PayPalOneTouchCore class]]; + [[[ppOtcMock stub] andReturnValue:@YES] canParseURL:OCMOCK_ANY sourceApplication:OCMOCK_ANY]; + + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"authorization completion callback"]; + [payPalDriver startAuthorizationWithCompletion:^(BTPayPalPaymentMethod *payPalPaymentMethod, NSError *error) { + NSLog(@"startAuthorizationWithCompletion %@ %@", payPalPaymentMethod, error); + [completionExpectation fulfill]; + }]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.payments://onetouch/v1/success?payloadEnc=IiRZ%2FKEnD6RQ8UeUmOFO8Ofh1RqQcWFycpO6pB9Yzl7fLb5szdaHanap7gwpmKsq4MJ2KGRJ0MzZBPvmoL%2BxkSH7%2FC%2F4WqeeVeGYvCpAvsPpkg%2BY8PID54FqVDpP1EXKS3Vx%2F6XmqbDplNLUUNzXZ4P%2FNcaXiEZXoHv6odjm7rxP3Ric%2Fsal9oiCDGDeFOAwTkiklA%2BA5nsASGopzrMHeIVBtcA01yae%2BDrgwPhHWNy6hffL2yVPVREtpVRBLrXK0jzn9IGUKMbBSMg%2F8BZ14ijhU%2F4cFlqi51NARQEFXMJcSba%2FscQTV1%2Fzj7D6B9W4pUYk9WY7eygmwMs%2BTYkTYnKRJjHTPWzMScdesYjj161c6DdWBFFtCVcanwvdk5rp1YCaElOmYV5WZSGKkSORCNMNKVKe8AkXMVO%2BPc41&payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6IkNCNkY1Q0IwLUY4NEYtNEZEMC1BNzQ1LTdCMDE0MDQ0OUQyRSIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXJyb3IiOm51bGx9&x-source=com.braintree.browserswitch"]; + + // We must call +[Braintree handleOpenURL:sourceApplication:] (not [BTPayPalDriver handleAppSwitchReturnURL:returnURL]) + // in order to test that the returnURL is passed through to BTPayPalDriver. + // This was a real bug that shipped in 4.0.0-pre2 and was fixed in 625ae947ee92561934dfb1a3a2bf387d8890b91f. + // Also, sourceApplication is verified by OTC. + [Braintree handleOpenURL:returnURL sourceApplication:@"com.apple.mobilesafari"]; + + // Note: -savePaypalAccount:clientMetadataID:success:failure: isn't actually called here. + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [mockClient verify]; + + [ppOtcMock stopMocking]; + }]; + }); + + it(@"returns tokenization failures to the developer", ^{ //Todo + }); + + it(@"returns a failure to the developer", ^{ //Todo + }); + + it(@"returns a cancelation to the developer", ^{ //Todo + }); + + it(@"rejects returns when there is no app switch in progress", ^{ + [BTPayPalDriverSpecHelper setupSpec:^(NSString *returnURLScheme, id mockClient, id mockApplication){ + [[mockClient stub] postAnalyticsEvent:OCMOCK_ANY]; + + // Both -canOpenURL: and -openURL: are checked by OTC + [[[mockApplication stub] andReturnValue:@YES] canOpenURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + [[[mockApplication stub] andReturnValue:@YES] openURL:HC_hasProperty(@"scheme", @"com.paypal.ppclient.touch.v2")]; + + __unused BTPayPalDriver *payPalDriver = [[BTPayPalDriver alloc] initWithClient:mockClient returnURLScheme:returnURLScheme]; + + NSURL *returnURL = [NSURL URLWithString:@"com.braintreepayments.braintree-demo.payments://onetouch/v1/success?payload=eyJ2ZXJzaW9uIjoyLCJhY2NvdW50X2NvdW50cnkiOiJVUyIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXhwaXJlc19pbiI6LTEsImRpc3BsYXlfbmFtZSI6Im1vY2tEaXNwbGF5TmFtZSIsInNjb3BlIjoiaHR0cHM6XC9cL3VyaS5wYXlwYWwuY29tXC9zZXJ2aWNlc1wvcGF5bWVudHNcL2Z1dHVyZXBheW1lbnRzIiwiZW1haWwiOiJtb2NrZW1haWxhZGRyZXNzQG1vY2suY29tIiwiYXV0aG9yaXphdGlvbl9jb2RlIjoibW9ja1RoaXJkUGFydHlBdXRob3JpemF0aW9uQ29kZSJ9&x-source=com.paypal.ppclient.touch.v1-or-v2"]; + + [[mockClient reject] savePaypalAccount:OCMOCK_ANY clientMetadataID:OCMOCK_ANY success:OCMOCK_ANY failure:OCMOCK_ANY]; + + [BTPayPalDriver handleAppSwitchReturnURL:returnURL]; + + [mockClient verify]; + }]; + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Specs/BTClient+BTPayPalSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Specs/BTClient+BTPayPalSpec.m new file mode 100755 index 00000000..ada230c6 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-PayPal-Specs/BTClient+BTPayPalSpec.m @@ -0,0 +1,155 @@ +#import "BTClientToken.h" +#import "BTClient+BTPayPal.h" +#import "PayPalMobile.h" +#import "BTErrors+BTPayPal.h" +#import "BTTestClientTokenFactory.h" +#import "BTConfiguration.h" +#import "BTClient+Offline.h" + +#import "BTClient_Internal.h" +#import "BTClientSpecHelper.h" + +SharedExamplesBegin(BTClient_BTPayPalSpec) + +sharedExamplesFor(@"a BTClient", ^(NSDictionary *data) { + + __block BOOL asyncClient = [data[@"asyncClient"] boolValue]; + __block NSMutableDictionary *mutableClaims; + + beforeEach(^{ + + NSDictionary *paypalClaims = @{ + BTConfigurationKeyPayPalMerchantName: @"PayPal Merchant", + BTConfigurationKeyPayPalMerchantPrivacyPolicyUrl: @"http://merchant.example.com/privacy", + BTConfigurationKeyPayPalMerchantUserAgreementUrl: @"http://merchant.example.com/tos", + BTConfigurationKeyPayPalClientId: @"PayPal-Test-Merchant-ClientId", + BTConfigurationKeyPayPalDirectBaseUrl: @"http://api.paypal.example.com" + }; + + NSDictionary *baseClaims = @{ + BTConfigurationKeyClientApiURL: @"http://gateway.example.com/client_api", + BTConfigurationKeyPayPalEnabled: @YES, + BTConfigurationKeyPayPal: [paypalClaims mutableCopy] }; + + + mutableClaims = [baseClaims mutableCopy]; + }); + + describe(@"btPayPal_preparePayPalMobileWithError", ^{ + + describe(@"in Live PayPal environment", ^{ + describe(@"btPayPal_payPalEnvironment", ^{ + it(@"returns PayPal mSDK notion of Live", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentLive; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + expect([client btPayPal_environment]).to.equal(PayPalEnvironmentProduction); + }); + }); + }); + + describe(@"with custom PayPal environment", ^{ + it(@"does not return an error with the valid set of claims", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentCustom; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSError *error; + BOOL success = [client btPayPal_preparePayPalMobileWithError:&error]; + expect(error).to.beNil(); + expect(success).to.beTruthy(); + }); + + it(@"returns an error if the client ID is present but the Base URL is missing", ^{ + mutableClaims[@"paypal"][@"directBaseUrl"] = [NSNull null]; + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentCustom; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSError *error; + BOOL success = [client btPayPal_preparePayPalMobileWithError:&error]; + expect(error.code).to.equal(BTMerchantIntegrationErrorPayPalConfiguration); + expect(error.userInfo).notTo.beNil(); + expect(success).to.beFalsy(); + }); + + it(@"returns an error if the PayPal Base URL is present but the client ID is missing", ^{ + mutableClaims[@"paypal"][@"clientId"] = [NSNull null]; + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentCustom; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSError *error; + [client btPayPal_preparePayPalMobileWithError:&error]; + expect(error.code).to.equal(BTMerchantIntegrationErrorPayPalConfiguration); + expect(error.userInfo).notTo.beNil(); + }); + + describe(@"btPayPal_payPalEnvironment", ^{ + it(@"returns a pretty custom environment name", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentCustom; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + expect([client btPayPal_environment]).to.equal(BTClientPayPalMobileEnvironmentName); + }); + }); + }); + + describe(@"when the environment is not production", ^{ + describe(@"if the merchant privacy policy URL, merchant agreement URL, merchant name, and client ID are missing", ^{ + it(@"does not return an error", ^{ + mutableClaims[@"paypal"][BTConfigurationKeyPayPalMerchantPrivacyPolicyUrl] = [NSNull null]; + mutableClaims[@"paypal"][BTConfigurationKeyPayPalMerchantUserAgreementUrl] = [NSNull null]; + mutableClaims[@"paypal"][BTConfigurationKeyPayPalMerchantName] = [NSNull null]; + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentCustom; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSError *error; + [client btPayPal_preparePayPalMobileWithError:&error]; + expect(error).to.beNil(); + }); + }); + + }); + }); + + describe(@"scopes", ^{ + it(@"includes email and future payments", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentLive; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSSet *scopes = [client btPayPal_scopes]; + expect(scopes).to.contain(@"email"); + expect(scopes).to.contain(@"https://uri.paypal.com/services/payments/futurepayments"); + }); + + it(@"does not contain address scope by default", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentLive; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + NSSet *scopes = [client btPayPal_scopes]; + expect(scopes).toNot.contain(@"address"); + }); + + it(@"includes additional scopes and default scopes (email and future payments)", ^{ + mutableClaims[@"paypal"][@"environment"] = BTConfigurationPayPalEnvironmentLive; + BTClient * client = [BTClientSpecHelper clientForTestCase:self withOverrides:mutableClaims async:asyncClient]; + client.additionalPayPalScopes = [NSSet setWithObjects:@"address", nil]; + NSSet *scopes = [client btPayPal_scopes]; + expect(scopes).to.contain(@"address"); + expect(scopes).to.contain(@"email"); + expect(scopes).to.contain(@"https://uri.paypal.com/services/payments/futurepayments"); + }); + + }); + +}); + +SharedExamplesEnd + +SpecBegin(DeprecatedBTClient) + +describe(@"shared initialization behavior", ^{ + NSDictionary* data = @{@"asyncClient": @NO}; + itShouldBehaveLike(@"a BTClient", data); +}); + +SpecEnd + +SpecBegin(AsyncBTClient) + +describe(@"shared initialization behavior", ^{ + NSDictionary* data = @{@"asyncClient": @YES}; + itShouldBehaveLike(@"a BTClient", data); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Specs/BTDropInErrorStateSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Specs/BTDropInErrorStateSpec.m new file mode 100755 index 00000000..ada25e44 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-Specs/BTDropInErrorStateSpec.m @@ -0,0 +1,109 @@ +#import "BTDropInErrorState.h" +#import "BTErrors.h" +#import "BTUICardFormView.h" + +SpecBegin(BTDropInErrorState) + +describe(@"errorTitle", ^{ + it(@"returns an error title based on the NSError's top level error message", ^{ + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ + @"field": @"cvv", + @"message": @"CVV is required" } + ] + }]}; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTBraintreeAPIErrorDomain + code:BTCustomerInputErrorInvalid + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + expect(state.errorTitle).to.equal(@"Credit Card is Invalid"); + }); + + it(@"returns an error title based on the NSError's top level error message, even when there are no field errors associated", ^{ + NSDictionary *validationErrors = @{ @"error": @{ @"message": @"Everything is Invalid" } }; + + + + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTBraintreeAPIErrorDomain + code:BTCustomerInputErrorInvalid + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + expect(state.errorTitle).to.equal(@"Everything is Invalid"); + }); +}); + +describe(@"highlighted fields", ^{ + it(@"returns a set of fields with validation errors associated", ^{ + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ @"field": @"cvv", + @"message": @"CVV is required" }, + @{ @"field": @"billingAddress", + @"fieldErrors": @[@{ @"field": @"postalCode", + @"message": @"Postal Code is required" }], + }, + @{ @"field": @"number", + @"message": @"Number is required" }, + @{ @"field": @"expirationDate", + @"message": @"Expiration date is required" }, + ] + }]}; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTBraintreeAPIErrorDomain + code:BTCustomerInputErrorInvalid + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + expect(state.highlightedFields).to.haveCountOf(4); + expect(state.highlightedFields).to.contain(BTUICardFormFieldNumber);\ + expect(state.highlightedFields).to.contain(BTUICardFormFieldExpiration); + expect(state.highlightedFields).to.contain(BTUICardFormFieldCvv); + expect(state.highlightedFields).to.contain(BTUICardFormFieldPostalCode); + }); + + it(@"returns the empty set when no fields needs to be highlighted", ^{ + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ @"field": @"paymentMethodNonce", + @"message": @"Payment method nonces cannot be used to update an existing card." }, + ] + }]}; + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTBraintreeAPIErrorDomain + code:BTCustomerInputErrorInvalid + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + expect(state.highlightedFields).to.haveCountOf(0); + }); + + it(@"ignores unknown fields", ^{ + NSDictionary *validationErrors = @{@"error": @{ @"message": @"Everything is invalid" } }; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTBraintreeAPIErrorDomain + code:BTCustomerInputErrorInvalid + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + expect(state.highlightedFields).to.haveCountOf(0); + }); + +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUICardFormViewSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUICardFormViewSpec.m new file mode 100755 index 00000000..7c3d44e4 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUICardFormViewSpec.m @@ -0,0 +1,208 @@ +#import +#import "BTUICardType.h" +#import "BTUIFormField.h" +#import +#import +#import + +@interface BTUICardFormViewSpecCardEntryViewController : UIViewController +@property (nonatomic, strong) BTUICardFormView *cardFormView; +@end + +@implementation BTUICardFormViewSpecCardEntryViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.cardFormView = [[BTUICardFormView alloc] initWithFrame:self.view.frame]; + + [self.view addSubview:self.cardFormView]; + + [self.cardFormView autoPinEdgeToSuperviewMargin:ALEdgeLeading]; + [self.cardFormView autoPinEdgeToSuperviewMargin:ALEdgeTrailing]; + [self.cardFormView autoPinToTopLayoutGuideOfViewController:self withInset:10]; +} + +@end + +SpecBegin(BTUICardFormView) + +describe(@"Card Form", ^{ + describe(@"accepting and validating credit card details", ^{ + it(@"accepts a number, an expiry, a cvv and a postal code", ^{ + BTUICardFormViewSpecCardEntryViewController *viewController = [[BTUICardFormViewSpecCardEntryViewController alloc] init]; + + [system presentViewController:viewController]; + + [[tester usingTimeout:1] enterText:@"4111111111111111" intoViewWithAccessibilityLabel:@"Card Number"]; + [[tester usingTimeout:1] tapViewWithAccessibilityLabel:@"MM/YY"]; + [[tester usingTimeout:1] enterTextIntoCurrentFirstResponder:@"122018"]; + [[tester usingTimeout:1] enterText:@"100" intoViewWithAccessibilityLabel:@"CVV"]; + [[tester usingTimeout:1] enterText:@"60606" intoViewWithAccessibilityLabel:@"Postal Code"]; + + expect(viewController.cardFormView.valid).to.beTruthy(); + }); + }); + + describe(@"auto advancing", ^{ + it(@"auto advances from field to field", ^{ + [system presentViewController:[[BTUICardFormViewSpecCardEntryViewController alloc] init]]; + [[tester usingTimeout:1] tapViewWithAccessibilityLabel:@"Card Number"]; + [[tester usingTimeout:1] enterTextIntoCurrentFirstResponder:@"4111111111111111"]; + [[tester usingTimeout:1] waitForFirstResponderWithAccessibilityLabel:@"MM/YY"]; + }); + }); + + describe(@"retreat on backspace", ^{ + it(@"retreats on backspace and deletes one digit", ^{ + [system presentViewController:[[BTUICardFormViewSpecCardEntryViewController alloc] init]]; + [[tester usingTimeout:1] tapViewWithAccessibilityLabel:@"Card Number"]; + [[tester usingTimeout:1] enterTextIntoCurrentFirstResponder:@"4111111111111111"]; + [[tester usingTimeout:1] enterTextIntoCurrentFirstResponder:@"\b"]; + [[tester usingTimeout:1] waitForFirstResponderWithAccessibilityLabel:@"Card Number"]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"411111111111111" traits:0]; + }); + }); + + describe(@"setting the form programmatically", ^{ + __block BTUICardFormView *cardFormView; + + beforeEach(^{ + cardFormView = [[BTUICardFormView alloc] init]; + }); + + describe(@"card number field", ^{ + it(@"sets the field text", ^{ + cardFormView.number = @"411111"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"411111" traits:0]; + }); + + describe(@"number of digits", ^{ + context(@"unknown card type", ^{ + it(@"allows max digits", ^{ + NSString *cardNumber = [@"" stringByPaddingToLength:[BTUICardType maxNumberLength] withString:@"0" startingAtIndex:0]; + cardFormView.number = cardNumber; + [system presentView:cardFormView]; + [[[tester usingTimeout:1] usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:cardNumber traits:0]; + }); + + it(@"doesn't set field if max digits exceeded", ^{ + cardFormView.number = @"1234"; + cardFormView.number = [@"" stringByPaddingToLength:[BTUICardType maxNumberLength]+1 withString:@"0" startingAtIndex:0];; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"1234" traits:0]; + }); + }); + + context(@"known card type", ^{ + it(@"allows max digits", ^{ + cardFormView.number = @"4111111111111111"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"4111111111111111" traits:0]; + }); + + it(@"doesn't set field if max digits exceeded", ^{ + cardFormView.number = @"41111111111111111"; + cardFormView.number = @"1234"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"1234" traits:0]; + }); + }); + }); + + it(@"doesn't allow invalid characters", ^{ + cardFormView.number = @"1234"; + cardFormView.number = @"4111 1111-1111"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Card Number" value:@"411111111111" traits:0]; + }); + }); + + describe(@"expiry field", ^{ + it(@"accepts a date and displays as valid", ^{ + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.month = 1; + dateComponents.year = 2016; + dateComponents.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + + NSDate *date = [dateComponents date]; + [cardFormView setExpirationDate:date]; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"01/2016"]; + + UIAccessibilityElement *element = [[[UIApplication sharedApplication] keyWindow] accessibilityElementWithLabel:@"01/2016"]; + BTUIFormField *expiryField = (BTUIFormField *)([UIAccessibilityElement viewContainingAccessibilityElement:element].superview.superview); + expect(expiryField.displayAsValid).to.beTruthy(); + }); + + it(@"accepts a well-formed invalid date and displays as invalid", ^{ + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.month = 2; + dateComponents.year = 2000; + dateComponents.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + + NSDate *date = [dateComponents date]; + [cardFormView setExpirationDate:date]; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"02/2000"]; + + UIAccessibilityElement *element = [[[UIApplication sharedApplication] keyWindow] accessibilityElementWithLabel:@"02/2000"]; + BTUIFormField *expiryField = (BTUIFormField *)([UIAccessibilityElement viewContainingAccessibilityElement:element].superview.superview); + expect(expiryField.displayAsValid).to.beFalsy(); + }); + + it(@"can be set when visible", ^{ + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.month = 1; + dateComponents.year = 2016; + dateComponents.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + NSDate *date = [dateComponents date]; + + [system presentView:cardFormView]; + [cardFormView setExpirationDate:date]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"01/2016"]; + }); + }); + + describe(@"CVV field", ^{ + it(@"accepts a CVV number", ^{ + cardFormView.cvv = @"123"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"123"]; + }); + + it(@"doesn't set field if max digits exceeded", ^{ + cardFormView.cvv = @"543"; + cardFormView.cvv = @"12345"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"543"]; + }); + }); + + describe(@"Postal code field", ^{ + it(@"accepts a zipcode number", ^{ + cardFormView.postalCode = @"12345"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"12345"]; + }); + + it(@"accepts a postal code string", ^{ + cardFormView.postalCode = @"WC2E 9RZ"; + [system presentView:cardFormView]; + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"WC2E 9RZ"]; + }); + + it(@"won't accept an alphanumeric string if alphanumericPostalCode is NO", ^{ + cardFormView.postalCode = @"123"; + cardFormView.alphaNumericPostalCode = NO; + cardFormView.postalCode = @"WC2E 9RZ"; + + [system presentView:cardFormView]; + + [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"123"]; + }); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUITextFieldSpec.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUITextFieldSpec.m new file mode 100755 index 00000000..dde57569 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Braintree-UI-Specs/BTUITextFieldSpec.m @@ -0,0 +1,131 @@ +#import "BTUITextField.h" +#import + +/// A custom mock outside of OCMock for spying on the BTUITextField text value at various +/// points in the editDelegate lifecycle +@interface BTUITextFieldSpecMockEditDelegate : NSObject +@property (copy, nonatomic) NSString *textAtTimeOfWillInsertText; +@property (copy, nonatomic) NSString *textAtTimeOfDidInsertText; +@property (copy, nonatomic) NSString *textAtTimeOfWillDeleteBackward; +@property (copy, nonatomic) NSString *textAtTimeOfDidDeleteBackward; +@end + +@implementation BTUITextFieldSpecMockEditDelegate + +- (void)textField:(BTUITextField *)textField willInsertText:(NSString *)text { + self.textAtTimeOfWillInsertText = textField.text; +} +- (void)textField:(BTUITextField *)textField didInsertText:(NSString *)text { + self.textAtTimeOfDidInsertText = textField.text; +} +- (void)textFieldWillDeleteBackward:(BTUITextField *)textField { + self.textAtTimeOfWillDeleteBackward = textField.text; +} +- (void)textFieldDidDeleteBackward:(BTUITextField *)textField originalText:(NSString *)originalText { + self.textAtTimeOfDidDeleteBackward = textField.text; +} + +@end + +SpecBegin(BTUITextField) + +describe(@"text field behavior", ^{ + it(@"can accept user input", ^{ + BTUITextField *textField = [[BTUITextField alloc] init]; + textField.backgroundColor = [UIColor whiteColor]; + textField.accessibilityLabel = @"Some Field"; + [system presentView:textField]; + // The text field's initial frame is based on the intrinsic size. We want the view to be + // big enough to see. + [textField autoSetDimension:ALDimensionWidth toSize:300]; + [textField autoSetDimension:ALDimensionHeight toSize:44]; + [tester tapViewWithAccessibilityLabel:@"Some Field"]; + [tester enterTextIntoCurrentFirstResponder:@"Hello, World!"]; + + expect(textField.text).to.equal(@"Hello, World!"); + }); +}); + +describe(@"editDelegate", ^{ + __block BTUITextField *textField; + __block BTUITextFieldSpecMockEditDelegate *editDelegate; + + beforeEach(^{ + editDelegate = OCMPartialMock([[BTUITextFieldSpecMockEditDelegate alloc] init]); + textField = [[BTUITextField alloc] init]; + textField.editDelegate = editDelegate; + textField.backgroundColor = [UIColor whiteColor]; + textField.text = @"Some text"; + textField.accessibilityLabel = @"Some Field"; + [system presentView:textField]; + [tester tapViewWithAccessibilityLabel:@"Some Field"]; + [tester waitForTimeInterval:2]; + }); + + describe(@"delegate method protocol", ^{ + it(@"textField:willInsertText: and textField:didInsertText: are called when user enters text", ^{ + [(OCMockObject *)editDelegate setExpectationOrderMatters:YES]; + OCMExpect([editDelegate textField:textField willInsertText:@"a"]).andForwardToRealObject(); + OCMExpect([editDelegate textField:textField didInsertText:@"a"]).andForwardToRealObject(); + + [tester enterTextIntoCurrentFirstResponder:@"a"]; + + OCMVerify(editDelegate); + // Note: the behavior of `insertText:` in iOS 9 is buggy. We have implemented + // a workaround that makes the card expiry field functional, but the workaround + // causes willInsertText to be called *after* the text has already been inserted. + if ([UIDevice currentDevice].systemVersion.intValue < 9) { + expect(editDelegate.textAtTimeOfWillInsertText).to.equal(@"Some text"); + } else { + expect(editDelegate.textAtTimeOfWillInsertText).to.equal(@"Some texta"); + } + expect(editDelegate.textAtTimeOfDidInsertText).to.equal(@"Some texta"); + }); + + it(@"textField:willDeleteBackward: and textField:didDeleteBackward:originalText: are called when user backspaces", ^{ + [(OCMockObject *)editDelegate setExpectationOrderMatters:YES]; + OCMExpect([editDelegate textFieldWillDeleteBackward:textField]).andForwardToRealObject(); + OCMExpect([editDelegate textFieldDidDeleteBackward:textField originalText:@"Some text"]).andForwardToRealObject(); + + [tester enterTextIntoCurrentFirstResponder:@"\b"]; + + OCMVerify(editDelegate); + expect(editDelegate.textAtTimeOfWillDeleteBackward).to.equal(@"Some text"); + expect(editDelegate.textAtTimeOfDidDeleteBackward).to.equal(@"Some tex"); + }); + + it(@"textField:willDeleteBackward: and textField:didDeleteBackward:originalText: are called when user backspaces beyond the first character", ^{ + textField.text = @"AB"; + + [(OCMockObject *)editDelegate setExpectationOrderMatters:YES]; + OCMExpect([editDelegate textFieldWillDeleteBackward:textField]).andForwardToRealObject(); + OCMExpect([editDelegate textFieldDidDeleteBackward:textField originalText:@"AB"]).andForwardToRealObject(); + + // Delete "B" + [tester enterTextIntoCurrentFirstResponder:@"\b"]; + + OCMVerify(editDelegate); + expect(editDelegate.textAtTimeOfWillDeleteBackward).to.equal(@"AB"); + + // Reduce likelihood of failure: expected: A, got: nil/null + [tester waitForTimeInterval:0.1]; + + expect(editDelegate.textAtTimeOfDidDeleteBackward).to.equal(@"A"); + + // Delete "A" + [tester enterTextIntoCurrentFirstResponder:@"\b"]; + + OCMExpect([editDelegate textFieldWillDeleteBackward:textField]).andForwardToRealObject(); + OCMExpect([editDelegate textFieldDidDeleteBackward:textField originalText:@""]).andForwardToRealObject(); + + // Backspace, nothing to delete + [tester enterTextIntoCurrentFirstResponder:@"\b"]; + + OCMVerify(editDelegate); + expect(editDelegate.textAtTimeOfWillDeleteBackward).to.equal(@""); + expect(editDelegate.textAtTimeOfDidDeleteBackward).to.equal(@""); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.h b/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.h new file mode 100755 index 00000000..817a8a49 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.h @@ -0,0 +1,17 @@ +#import "KIFSystemTestActor.h" + +/// Provides convenience methods for presenting view controllers and views for KIF tests +@interface KIFSystemTestActor (BTViewPresentation) + +/// Present a view controller in the app +/// +/// @param viewController The view controller to show. Cannot be `nil`. +- (void)presentViewController:(UIViewController *)viewController; + +/// Present a view by adding it as a subview of a new view controller, then presenting the view controller +/// in the app. +/// +/// @param view The view to display. +- (void)presentView:(UIView *)view; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.m b/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.m new file mode 100755 index 00000000..7c6869c1 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/Specs/Shared Spec Utilities/KIFSystemTestActor+BTViewPresentation.m @@ -0,0 +1,38 @@ +#import "KIFSystemTestActor+BTViewPresentation.h" +#import + +@implementation KIFSystemTestActor (BTViewPresentation) + +- (void)presentViewController:(UIViewController *)viewController { + [self runBlock:^KIFTestStepResult(NSError **error) { + UIViewController *viewControllerToPresent = viewController; + KIFTestCondition(viewControllerToPresent != nil, error, @"Expected a view controller, but got nil"); + + UINavigationController *navigationController; + if ([viewControllerToPresent isKindOfClass:[UINavigationController class]]) { + navigationController = (UINavigationController *)viewControllerToPresent; + } else { + navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + } + [UIApplication sharedApplication].keyWindow.rootViewController = navigationController; + + return KIFTestStepResultSuccess; + }]; +} + +- (void)presentView:(UIView *)view { + [self runBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { + KIFTestCondition(view != nil, error, @"Expected a view, but got nil"); + + UIViewController *viewController = [[UIViewController alloc] init]; + [viewController.view addSubview:view]; + [view autoCenterInSuperview]; + [view autoMatchDimension:ALDimensionHeight toDimension:ALDimensionHeight ofView:viewController.view withOffset:0 relation:NSLayoutRelationLessThanOrEqual]; + [view autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:viewController.view withOffset:0 relation:NSLayoutRelationLessThanOrEqual]; + [self presentViewController:viewController]; + + return KIFTestStepResultSuccess; + }]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeDropInLegacy_UITests.swift b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeDropInLegacy_UITests.swift new file mode 100755 index 00000000..c6af02cd --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeDropInLegacy_UITests.swift @@ -0,0 +1,186 @@ +/* + IMPORTANT + Hardware keyboard should be disabled on simulator for tests to run reliably. + */ + +import XCTest + +class BraintreeDropInLegacy_TokenizationKey_CardForm_UITests: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-TokenizationKey") + app.launchArguments.append("-Integration:BraintreeDemoDropInLegacyViewController") + app.launch() + sleep(1) + self.waitForElementToBeHittable(app.buttons["Buy Now"]) + app.buttons["Buy Now"].tap() + + } + + func testDropInLegacy_cardInput_receivesNonce() { + + let elementsQuery = app.scrollViews.otherElements + let cardNumberTextField = elementsQuery.textFields["Card Number"] + let expiryTextField = elementsQuery.textFields["MM/YY"] + let postalCodeTextField = elementsQuery.textFields["Postal Code"] + let cvvTextField = elementsQuery.textFields["CVV"] + + self.waitForElementToBeHittable(cardNumberTextField) + + cardNumberTextField.forceTapElement() + cardNumberTextField.typeText("4111111111111111") + expiryTextField.typeText("1119") + + let postalCodeField = elementsQuery.textFields["Postal Code"] + self.waitForElementToBeHittable(postalCodeField) + postalCodeField.forceTapElement() + postalCodeField.typeText("12345") + + let securityCodeField = elementsQuery.textFields["CVV"] + self.waitForElementToBeHittable(securityCodeField) + securityCodeField.forceTapElement() + securityCodeField.typeText("123") + + elementsQuery.buttons["$19 - Subscribe Now"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Got a nonce. Tap to make a transaction."]) + + XCTAssertTrue(app.buttons["Got a nonce. Tap to make a transaction."].exists); + } + + func testDropInLegacy_cardInput_showsInvalidState_withInvalidCardNumber() { + + let elementsQuery = app.scrollViews.otherElements + let cardNumberTextField = elementsQuery.textFields["Card Number"] + + self.waitForElementToBeHittable(cardNumberTextField) + + cardNumberTextField.forceTapElement() + cardNumberTextField.typeText("4141414141414141") + + self.waitForElementToAppear(elementsQuery.textFields["Invalid: Card Number"]) + } + + func testDropInLegacy_cardInput_showsInvalidState_withInvalidExpirationDate() { + + let elementsQuery = app.scrollViews.otherElements + let expiryTextField = elementsQuery.textFields["MM/YY"] + self.waitForElementToBeHittable(expiryTextField) + + expiryTextField.forceTapElement() + expiryTextField.typeText("1111") + + self.waitForElementToAppear(elementsQuery.textFields["Invalid: MM/YY"]) + } + + func testDropInLegacy_cardInput_hidesInvalidCardNumberState_withDeletion() { + + let elementsQuery = app.scrollViews.otherElements + let cardNumberTextField = elementsQuery.textFields["Card Number"] + self.waitForElementToBeHittable(cardNumberTextField) + + cardNumberTextField.forceTapElement() + cardNumberTextField.typeText("4141414141414141") + + self.waitForElementToAppear(elementsQuery.textFields["Invalid: Card Number"]) + + cardNumberTextField.typeText("\u{8}") + + XCTAssertFalse(elementsQuery.textFields["Invalid: Card Number"].exists); + } + + func testDropInLegacy_cardInput_hidesInvalidExpirationState_withDeletion() { + + let elementsQuery = app.scrollViews.otherElements + let expirationField = elementsQuery.textFields["MM/YY"] + self.waitForElementToBeHittable(expirationField) + + expirationField.forceTapElement() + expirationField.typeText("1111") + + self.waitForElementToAppear(elementsQuery.textFields["Invalid: MM/YY"]) + + expirationField.typeText("\u{8}") + + XCTAssertFalse(elementsQuery.textFields["Invalid: MM/YY"].exists); + } +} + +class BraintreeDropInLegacy_ClientToken_CardForm_UITests: XCTestCase { + + // var app: XCUIApplication! + // + // override func setUp() { + // super.setUp() + // continueAfterFailure = false + // app = XCUIApplication() + // app.launchArguments.append("-EnvironmentSandbox") + // app.launchArguments.append("-ClientToken") + // app.launchArguments.append("-Integration:BraintreeDemoDropInLegacyViewController") + // app.launch() + // self.waitForElementToAppear(app.buttons["Buy Now"]) + // app.buttons["Buy Now"].forceTapElement() + // } + // + // // This test card number is now valid + // // Is this related to Union Pay? + // func pendDropInLegacy_cardInput_displaysErrorForFailedValidation() { + // + // let elementsQuery = app.scrollViews.otherElements + // let cardNumberTextField = elementsQuery.textFields["Card Number"] + // let expirationField = elementsQuery.textFields["MM/YY"] + // + // cardNumberTextField.forceTapElement() + // cardNumberTextField.typeText("5105105105105100") + // expirationField.typeText("1119") + // + // elementsQuery.buttons["$19 - Subscribe Now"].forceTapElement() + // + // self.waitForElementToAppear(app.alerts.staticTexts["Credit card verification failed"]) + // + // XCTAssertTrue(app.alerts.staticTexts["Credit card verification failed"].exists); + // } +} + + +class BraintreeDropInLegacy_PayPal_UITests: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-TokenizationKey") + app.launchArguments.append("-Integration:BraintreeDemoDropInLegacyViewController") + app.launch() + sleep(1) + self.waitForElementToBeHittable(app.buttons["Buy Now"]) + app.buttons["Buy Now"].forceTapElement() + } + + func testDropInLegacy_paypal_receivesNonce() { + + let elementsQuery = app.collectionViews["Payment Options"].cells + let paypalButton = elementsQuery.element(boundBy: 0) + paypalButton.forceTapElement() + sleep(3) + + let webviewElementsQuery = app.webViews.element.otherElements + + self.waitForElementToBeHittable(webviewElementsQuery.links["Proceed with Sandbox Purchase"]) + + webviewElementsQuery.links["Proceed with Sandbox Purchase"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Got a nonce. Tap to make a transaction."]) + + XCTAssertTrue(app.buttons["Got a nonce. Tap to make a transaction."].exists); + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreePayPal_UITests.swift b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreePayPal_UITests.swift new file mode 100755 index 00000000..ff4f8575 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreePayPal_UITests.swift @@ -0,0 +1,160 @@ +/* + IMPORTRANT + Hardware keyboard should be disabled on simulator for tests to run reliably. + */ + +import XCTest + +class BraintreePayPal_FuturePayment_UITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-TokenizationKey") + app.launchArguments.append("-Integration:BraintreeDemoPayPalForceFuturePaymentViewController") + app.launch() + sleep(1) + self.waitForElementToBeHittable(app.buttons["PayPal (future payment button)"]) + app.buttons["PayPal (future payment button)"].tap() + sleep(2) + } + + func testPayPal_futurePayment_receivesNonce() { + let webviewElementsQuery = app.webViews.element.otherElements + let emailTextField = webviewElementsQuery.textFields["Email"] + + self.waitForElementToAppear(emailTextField) + emailTextField.forceTapElement() + emailTextField.typeText("test@paypal.com") + + let passwordTextField = webviewElementsQuery.secureTextFields["Password"] + passwordTextField.forceTapElement() + passwordTextField.typeText("1234") + + webviewElementsQuery.buttons["Log In"].forceTapElement() + + self.waitForElementToAppear(webviewElementsQuery.buttons["Agree"]) + + webviewElementsQuery.buttons["Agree"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Got a nonce. Tap to make a transaction."]) + + XCTAssertTrue(app.buttons["Got a nonce. Tap to make a transaction."].exists); + } + + func testPayPal_futurePayment_cancelsSuccessfully() { + let webviewElementsQuery = app.webViews.element.otherElements + let emailTextField = webviewElementsQuery.textFields["Email"] + + self.waitForElementToAppear(emailTextField) + + // Close button has no accessibility helper + // Purposely don't use the webviewElementsQuery variable + // Reevaluate the elements query after the page load to get the close button + app.webViews.buttons.element(boundBy: 0).forceTapElement() + + self.waitForElementToAppear(app.buttons["PayPal (future payment button)"]) + + XCTAssertTrue(app.buttons["Canceled 🔰"].exists); + } +} + +class BraintreePayPal_SinglePayment_UITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-TokenizationKey") + app.launchArguments.append("-Integration:BraintreeDemoPayPalOneTimePaymentViewController") + app.launch() + sleep(1) + self.waitForElementToBeHittable(app.buttons["PayPal one-time payment"]) + app.buttons["PayPal one-time payment"].tap() + sleep(2) + } + + func testPayPal_singlePayment_receivesNonce() { + let webviewElementsQuery = app.webViews.element.otherElements + + self.waitForElementToAppear(webviewElementsQuery.links["Proceed with Sandbox Purchase"]) + + webviewElementsQuery.links["Proceed with Sandbox Purchase"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Got a nonce. Tap to make a transaction."]) + + XCTAssertTrue(app.buttons["Got a nonce. Tap to make a transaction."].exists); + } + + func testPayPal_singlePayment_cancelsSuccessfully() { + let webviewElementsQuery = app.webViews.element.otherElements + + self.waitForElementToAppear(webviewElementsQuery.links["Cancel Sandbox Purchase"]) + + webviewElementsQuery.links["Cancel Sandbox Purchase"].forceTapElement() + + self.waitForElementToAppear(app.buttons["PayPal one-time payment"]) + + XCTAssertTrue(app.buttons["Cancelled"].exists); + } +} + +class BraintreePayPal_BillingAgreement_UITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-TokenizationKey") + app.launchArguments.append("-Integration:BraintreeDemoPayPalBillingAgreementViewController") + app.launch() + sleep(1) + self.waitForElementToBeHittable(app.buttons["Billing Agreement with PayPal"]) + app.buttons["Billing Agreement with PayPal"].tap() + sleep(2) + } + + func testPayPal_billingAgreement_receivesNonce() { + let webviewElementsQuery = app.webViews.element.otherElements + + self.waitForElementToAppear(webviewElementsQuery.links["Proceed with Sandbox Purchase"]) + + webviewElementsQuery.links["Proceed with Sandbox Purchase"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Got a nonce. Tap to make a transaction."]) + + XCTAssertTrue(app.textViews["DismissalOfViewController Called"].exists); + XCTAssertTrue(app.buttons["Got a nonce. Tap to make a transaction."].exists); + } + + func testPayPal_billingAgreement_cancelsSuccessfully() { + let webviewElementsQuery = app.webViews.element.otherElements + + self.waitForElementToAppear(webviewElementsQuery.links["Cancel Sandbox Purchase"]) + + webviewElementsQuery.links["Cancel Sandbox Purchase"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Billing Agreement with PayPal"]) + + XCTAssertTrue(app.textViews["DismissalOfViewController Called"].exists); + XCTAssertTrue(app.buttons["Cancelled"].exists); + } + + func testPayPal_billingAgreement_cancelsSuccessfully_whenTappingSFSafariViewControllerDoneButton() { + self.waitForElementToAppear(app.buttons["Done"]) + + app.buttons["Done"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Billing Agreement with PayPal"]) + + XCTAssertTrue(app.textViews["DismissalOfViewController Called"].exists); + XCTAssertTrue(app.buttons["Cancelled"].exists); + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeThreeDSecure_UITests.swift b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeThreeDSecure_UITests.swift new file mode 100755 index 00000000..4de73a20 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UITests/BraintreeThreeDSecure_UITests.swift @@ -0,0 +1,265 @@ +/* + IMPORTRANT + Hardware keyboard should be disabled on simulator for tests to run reliably. + */ + +import XCTest + +class BraintreeThreeDSecure_UITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + super.setUp() + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments.append("-EnvironmentSandbox") + app.launchArguments.append("-ClientToken") + app.launchArguments.append("-Integration:BraintreeDemoThreeDSecureViewController") + self.app.launch() + sleep(1) + self.waitForElementToBeHittable(app.textFields["Card Number"]) + sleep(2) + } + + func testThreeDSecure_completesAuthentication_receivesNonce() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000002") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + let elementsQuery = app.otherElements["Authentication"] + let passwordTextField = elementsQuery.children(matching: .other).children(matching: .secureTextField).element + + passwordTextField.tap() + sleep(1) + passwordTextField.typeText("1234") + + elementsQuery.buttons["Submit"].tap() + + self.waitForElementToAppear(app.buttons["Liability shift possible and liability shifted"]) + + XCTAssertTrue(app.buttons["Liability shift possible and liability shifted"].exists); + } + + func testThreeDSecure_failsAuthentication() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000010") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + let elementsQuery = app.otherElements["Authentication"] + let passwordTextField = elementsQuery.children(matching: .other).children(matching: .secureTextField).element + + passwordTextField.tap() + sleep(1) + passwordTextField.typeText("1234") + + elementsQuery.buttons["Submit"].tap() + + self.waitForElementToAppear(app.buttons["Failed to authenticate, please try a different form of payment."]) + + XCTAssertTrue(app.buttons["Failed to authenticate, please try a different form of payment."].exists); + } + + func testThreeDSecure_bypassesAuthentication_notEnrolled() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000051") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + + func testThreeDSecure_bypassesAuthentication_lookupFailed() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000077") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + + func testThreeDSecure_incorrectPassword_callsBackWithError_exactlyOnce() { + let app = XCUIApplication() + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000028") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + let elementsQuery = app.otherElements["Authentication"] + let passwordTextField = elementsQuery.children(matching: .other).children(matching: .secureTextField).element + + passwordTextField.tap() + sleep(1) + passwordTextField.typeText("1234") + + elementsQuery.buttons["Submit"].tap() + + self.waitForElementToAppear(app.buttons["Failed to authenticate, please try a different form of payment."]) + + XCTAssertTrue(app.buttons["Failed to authenticate, please try a different form of payment."].exists); + + sleep(2) + + self.waitForElementToAppear(app.staticTexts["Callback Count: 1"]) + + XCTAssertTrue(app.staticTexts["Callback Count: 1"].exists); + } + + func testThreeDSecure_passiveAuthentication_notPromptedForAuthentication() { + let app = XCUIApplication() + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000101") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["Liability shift possible and liability shifted"]) + + XCTAssertTrue(app.buttons["Liability shift possible and liability shifted"].exists); + } + + func testThreeDSecure_returnsNonce_whenIssuerDown() { + let app = XCUIApplication() + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000036") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + let elementsQuery = app.otherElements["Authentication"] + let passwordTextField = elementsQuery.children(matching: .other).children(matching: .secureTextField).element + + passwordTextField.tap() + sleep(1) + passwordTextField.typeText("1234") + + elementsQuery.buttons["Submit"].tap() + + + self.waitForElementToAppear(app.buttons["An unexpected error occurred"]) + + XCTAssertTrue(app.buttons["An unexpected error occurred"].exists); + } + + func testThreeDSecure_acceptsPassword_failsToAuthenticateNonce_dueToCardinalError() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000093") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + let elementsQuery = app.otherElements["Authentication"] + let passwordTextField = elementsQuery.children(matching: .other).children(matching: .secureTextField).element + + passwordTextField.tap() + sleep(1) + passwordTextField.typeText("1234") + + elementsQuery.buttons["Submit"].tap() + + self.waitForElementToAppear(app.buttons["An unexpected error occurred"]) + + XCTAssertTrue(app.buttons["An unexpected error occurred"].exists); + } + + func testThreeDSecure_returnsToApp_whenCancelTapped() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000002") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.navigationBars["Authentication"]) + + app.navigationBars["Authentication"].buttons["Cancel"].forceTapElement() + + self.waitForElementToAppear(app.buttons["Cancelled🎲"]) + + XCTAssertTrue(app.buttons["Cancelled🎲"].exists); + } + + func testThreeDSecure_bypassedAuthentication() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000990000000004") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + + func testThreeDSecure_lookupError() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000085") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + + func testThreeDSecure_unavailable() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000069") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(2) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + + func testThreeDSecure_timeout() { + self.waitForElementToAppear(app.textFields["Card Number"]) + let cardNumberTextField = app.textFields["Card Number"] + cardNumberTextField.tap() + cardNumberTextField.typeText("4000000000000044") + app.textFields["MM/YY"].typeText("012020") + app.buttons["Tokenize and Verify New Card"].tap() + sleep(5) + + self.waitForElementToAppear(app.buttons["3D Secure authentication was attempted but liability shift is not possible"]) + + XCTAssertTrue(app.buttons["3D Secure authentication was attempted but liability shift is not possible"].exists); + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UITests/Helpers/BTUITest.swift b/examples/braintree/ios/Frameworks/Braintree/UITests/Helpers/BTUITest.swift new file mode 100755 index 00000000..fb1e8088 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UITests/Helpers/BTUITest.swift @@ -0,0 +1,42 @@ +import XCTest + +extension XCTestCase { + func waitForElementToAppear(_ element: XCUIElement, timeout: TimeInterval = 10, file: String = #file, line: UInt = #line) { + let existsPredicate = NSPredicate(format: "exists == true") + + expectation(for: existsPredicate, + evaluatedWith: element, handler: nil) + + waitForExpectations(timeout: timeout) { (error) -> Void in + if (error != nil) { + let message = "Failed to find \(element) after \(timeout) seconds." + self.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) + } + } + } + + func waitForElementToBeHittable(_ element: XCUIElement, timeout: TimeInterval = 10, file: String = #file, line: UInt = #line) { + let existsPredicate = NSPredicate(format: "exists == true && hittable == true") + + expectation(for: existsPredicate, + evaluatedWith: element, handler: nil) + + waitForExpectations(timeout: timeout) { (error) -> Void in + if (error != nil) { + let message = "Failed to find \(element) after \(timeout) seconds." + self.recordFailure(withDescription: message, inFile: file, atLine: line, expected: true) + } + } + } +} + +extension XCUIElement { + func forceTapElement() { + if self.isHittable { + self.tap() + } else { + let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx: 0.0, dy: 0.0)) + coordinate.tap() + } + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UITests/Info.plist b/examples/braintree/ios/Frameworks/Braintree/UITests/Info.plist new file mode 100755 index 00000000..ba72822e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_SwiftTests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_SwiftTests.swift new file mode 100755 index 00000000..56c6af13 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_SwiftTests.swift @@ -0,0 +1,248 @@ +import XCTest + +class BTAPIClient_SwiftTests: XCTestCase { + + // MARK: - Initialization + + func testAPIClientInitialization_withValidTokenizationKey_returnsClientWithTokenizationKey() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + XCTAssertEqual(apiClient.tokenizationKey, "development_testing_integration_merchant_id") + } + + func testAPIClientInitialization_withInvalidTokenizationKey_returnsNil() { + XCTAssertNil(BTAPIClient(authorization: "invalid")) + } + + func testAPIClientInitialization_withEmptyTokenizationKey_returnsNil() { + XCTAssertNil(BTAPIClient(authorization: "")) + } + + func testAPIClientInitialization_withValidClientToken_returnsClientWithClientToken() { + let clientToken = BTTestClientTokenFactory.token(withVersion: 2) + let apiClient = BTAPIClient(authorization: clientToken!) + XCTAssertEqual(apiClient?.clientToken?.originalValue, clientToken) + } + + func testAPIClientInitialization_withVersionThreeClientToken_returnsClientWithClientToken() { + let clientToken = BTTestClientTokenFactory.token(withVersion: 3) + let apiClient = BTAPIClient(authorization: clientToken!) + XCTAssertEqual(apiClient?.clientToken?.originalValue, clientToken) + } + + func testAPIClientInitialization_withValidClientToken_performanceMeetsExpectations() { + let clientToken = BTTestClientTokenFactory.token(withVersion: 2) + self.measure() { + _ = BTAPIClient(authorization: clientToken!) + } + } + + // MARK: - Copy + + func testCopyWithSource_whenUsingClientToken_usesSameClientToken() { + let clientToken = BTTestClientTokenFactory.token(withVersion: 2) + let apiClient = BTAPIClient(authorization: clientToken!) + + let copiedApiClient = apiClient?.copy(with: .unknown, integration: .unknown) + + XCTAssertEqual(copiedApiClient?.clientToken?.originalValue, clientToken) + } + + func testCopyWithSource_whenUsingTokenizationKey_usesSameTokenizationKey() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id") + let copiedApiClient = apiClient?.copy(with: .unknown, integration: .unknown) + XCTAssertEqual(copiedApiClient?.tokenizationKey, "development_testing_integration_merchant_id") + } + + func testCopyWithSource_setsMetadataSourceAndIntegration() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id") + let copiedApiClient = apiClient?.copy(with: .payPalBrowser, integration: .dropIn) + XCTAssertEqual(copiedApiClient?.metadata.source, .payPalBrowser) + XCTAssertEqual(copiedApiClient?.metadata.integration, .dropIn) + } + + func testCopyWithSource_copiesHTTP() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id") + let copiedApiClient = apiClient?.copy(with: .payPalBrowser, integration: .dropIn) + XCTAssertTrue(copiedApiClient !== apiClient) + } + + // MARK: - fetchOrReturnRemoteConfiguration + + func testFetchOrReturnRemoteConfiguration_performsGETWithCorrectPayload() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id", sendAnalyticsEvent: false)! + let mockHTTP = BTFakeHTTP()! + mockHTTP.stubRequest("GET", toEndpoint: "/v1/configuration", respondWith: [], statusCode: 200) + apiClient.configurationHTTP = mockHTTP + + let expectation = self.expectation(description: "Callback invoked") + apiClient.fetchOrReturnRemoteConfiguration() { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/configuration") + XCTAssertEqual(mockHTTP.lastRequestParameters?["configVersion"] as? String, "3") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - fetchPaymentMethods + + func testFetchPaymentMethods_performsGETWithCorrectParameter() { + let apiClient = BTAPIClient(authorization: BTValidTestClientToken, sendAnalyticsEvent: false)! + let mockHTTP = BTFakeHTTP()! + mockHTTP.stubRequest("GET", toEndpoint: "/client_api/v1/payment_methods", respondWith: [], statusCode: 200) + apiClient.http = mockHTTP + + var expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces() { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertFalse(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + XCTAssertEqual(mockHTTP.lastRequestParameters!["session_id"] as? String, apiClient.metadata.sessionId) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + + expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces(true) { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertTrue(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + + expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces(false) { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertFalse(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchPaymentMethods_returnsPaymentMethodNonces() { + let apiClient = BTAPIClient(authorization: BTValidTestClientToken, sendAnalyticsEvent: false)! + let stubHTTP = BTFakeHTTP()! + let stubbedResponse = [ + "paymentMethods": [ + [ + "default" : true, + "description": "ending in 05", + "details": [ + "cardType": "American Express", + "lastTwo": "05" + ], + "nonce": "fake-nonce", + "type": "CreditCard" + ], + [ + "default" : false, + "description": "jane.doe@example.com", + "details": [], + "nonce": "fake-nonce", + "type": "PayPalAccount" + ] + ] ] + stubHTTP.stubRequest("GET", toEndpoint: "/client_api/v1/payment_methods", respondWith: stubbedResponse, statusCode: 200) + apiClient.http = stubHTTP + + let expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces() { (paymentMethodNonces, error) in + guard let paymentMethodNonces = paymentMethodNonces else { + XCTFail() + return + } + + XCTAssertNil(error) + XCTAssertEqual(paymentMethodNonces.count, 2) + + guard let cardNonce = paymentMethodNonces[0] as? BTCardNonce else { + XCTFail() + return + } + guard let paypalNonce = paymentMethodNonces[1] as? BTPayPalAccountNonce else { + XCTFail() + return + } + + XCTAssertEqual(cardNonce.nonce, "fake-nonce") + XCTAssertEqual(cardNonce.localizedDescription, "ending in 05") + XCTAssertEqual(cardNonce.lastTwo, "05") + XCTAssertTrue(cardNonce.cardNetwork == BTCardNetwork.AMEX) + XCTAssertTrue(cardNonce.isDefault) + + XCTAssertEqual(paypalNonce.nonce, "fake-nonce") + XCTAssertEqual(paypalNonce.localizedDescription, "jane.doe@example.com") + XCTAssertFalse(paypalNonce.isDefault) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchPaymentMethods_withTokenizationKey_returnsError() { + let apiClient = BTAPIClient(authorization: "development_tokenization_key", sendAnalyticsEvent: false)! + + let expectation = self.expectation(description: "Error returned") + apiClient.fetchPaymentMethodNonces() { (paymentMethodNonces, error) -> Void in + XCTAssertNil(paymentMethodNonces); + guard let error = error as? NSError else {return} + XCTAssertEqual(error._domain, BTAPIClientErrorDomain); + XCTAssertEqual(error._code, BTAPIClientErrorType.notAuthorized.rawValue); + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - V3 Client Token + + func testFetchPaymentMethods_performsGETWithCorrectParameter_withVersionThreeClientToken() { + let clientToken = BTTestClientTokenFactory.token(withVersion: 3) + let apiClient = BTAPIClient(authorization: clientToken!, sendAnalyticsEvent: false)! + let mockHTTP = BTFakeHTTP()! + mockHTTP.stubRequest("GET", toEndpoint: "/client_api/v1/payment_methods", respondWith: [], statusCode: 200) + apiClient.http = mockHTTP + + XCTAssertEqual((apiClient.clientToken!.json["version"] as! BTJSON).asIntegerOrZero(), 3) + + var expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces() { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertFalse(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + XCTAssertEqual(mockHTTP.lastRequestParameters!["session_id"] as? String, apiClient.metadata.sessionId) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + + expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces(true) { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertTrue(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + + expectation = self.expectation(description: "Callback invoked") + apiClient.fetchPaymentMethodNonces(false) { _ in + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods") + XCTAssertFalse(mockHTTP.lastRequestParameters!["default_first"] as! Bool) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - Analytics + + func testAnalyticsService_byDefault_isASingleton() { + let firstAPIClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let secondAPIClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + XCTAssertTrue(firstAPIClient.analyticsService === secondAPIClient.analyticsService) + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_Tests.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_Tests.m new file mode 100755 index 00000000..1d770fd7 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAPIClient_Tests.m @@ -0,0 +1,362 @@ +#import +#import +#import +#import +#import +#import "BTAnalyticsService.h" +#import "BTAPIClient_Internal.h" +#import "BTFakeHTTP.h" +#import "BTHTTP.h" +#import "BTHTTPTestProtocol.h" +#import "BTSpecHelper.h" + +@interface StubBTClientMetadata : BTClientMetadata +@property (nonatomic, assign) BTClientMetadataIntegrationType integration; +@property (nonatomic, assign) BTClientMetadataSourceType source; +@property (nonatomic, copy) NSString *sessionId; +@end + +@implementation StubBTClientMetadata +@synthesize integration = _integration; +@synthesize source = _source; +@synthesize sessionId = _sessionId; +@end + +@interface BTFakeAnalyticsService : BTAnalyticsService +@property (nonatomic, copy) NSString *lastEvent; +@end + +@implementation BTFakeAnalyticsService + +- (void)sendAnalyticsEvent:(NSString *)eventKind { + self.lastEvent = eventKind; +} + +- (void)sendAnalyticsEvent:(NSString *)eventKind completion:(__unused void (^)(NSError *))completionBlock { + self.lastEvent = eventKind; +} + +@end + +@interface BTAPIClient_Tests : XCTestCase +@end + +@implementation BTAPIClient_Tests + +#pragma mark - Initialization + +- (void)testInitialization_withValidTokenizationKey_setsTokenizationKey { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + XCTAssertEqualObjects(apiClient.tokenizationKey, @"development_tokenization_key"); +} + +- (void)testInitialization_withInvalidTokenizationKey_returnsNil { + XCTAssertNil([[BTAPIClient alloc] initWithAuthorization:@"not_a_valid_tokenization_key" sendAnalyticsEvent:NO]); +} + +- (void)testInitialization_withValidClientToken_setsClientToken { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:BTValidTestClientToken sendAnalyticsEvent:NO]; + XCTAssertEqualObjects(apiClient.clientToken.originalValue, BTValidTestClientToken); +} + +- (void)testInitialization_withInvalidClientToken_returnsNil { + XCTAssertNil([[BTAPIClient alloc] initWithAuthorization:@"invalidclienttoken" sendAnalyticsEvent:NO]); +} + +#pragma mark - Environment Base URL + +- (void)testBaseURL_isDeterminedByTokenizationKey { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + XCTAssertEqualObjects(apiClient.configurationHTTP.baseURL.absoluteString, @"http://localhost:3000/merchants/key/client_api"); + + apiClient = [[BTAPIClient alloc] initWithAuthorization:@"sandbox_tokenization_key" sendAnalyticsEvent:NO]; + XCTAssertEqualObjects(apiClient.configurationHTTP.baseURL.absoluteString, @"https://sandbox.braintreegateway.com/merchants/key/client_api"); + + apiClient = [[BTAPIClient alloc] initWithAuthorization:@"production_tokenization_key" sendAnalyticsEvent:NO]; + XCTAssertEqualObjects(apiClient.configurationHTTP.baseURL.absoluteString, @"https://api.braintreegateway.com:443/merchants/key/client_api"); +} + +#pragma mark - Configuration + +- (void)testAPIClient_canGetRemoteConfiguration { + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"test": @YES }]; + BTFakeHTTP *mockConfigurationHTTP = (BTFakeHTTP *)apiClient.configurationHTTP; + + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNotNil(configuration); + XCTAssertNil(error); + + XCTAssertEqual(mockConfigurationHTTP.GETRequestCount, (NSUInteger)1); + XCTAssertTrue([configuration.json[@"test"] isTrue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testConfiguration_whenServerRespondsWithNon200StatusCode_returnsAPIClientError { + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + + BTFakeHTTP *fake = [BTFakeHTTP fakeHTTP]; + [fake stubRequest:@"GET" toEndpoint:@"/client_api/v1/configuration" respondWith:@{ @"error_message": @"Something bad happened" } statusCode:503]; + apiClient.configurationHTTP = fake; + + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + // Note: GETRequestCount will be 1 or 2 depending on whether the analytics event for the API client initialization + // has failed yet + XCTAssertNil(configuration); + XCTAssertEqualObjects(error.domain, BTAPIClientErrorDomain); + XCTAssertEqual(error.code, BTAPIClientErrorTypeConfigurationUnavailable); + XCTAssertEqualObjects(error.localizedFailureReason, @"Unable to fetch remote configuration from Braintree API at this time."); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testConfiguration_whenNetworkHasError_returnsNetworkErrorInCallback { + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + + BTFakeHTTP *fake = [BTFakeHTTP fakeHTTP]; + NSError *anError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorCannotConnectToHost + userInfo:nil]; + [fake stubRequest:@"GET" toEndpoint:@"/client_api/v1/configuration" respondWithError:anError]; + apiClient.configurationHTTP = fake; + + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + // BTAPIClient fetches the config when initialized so there can potentially be 2 requests here + XCTAssertLessThanOrEqual(fake.GETRequestCount, (NSUInteger)2); + XCTAssertNil(configuration); + XCTAssertEqual(error, anError); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testConfigurationHTTP_byDefault_usesAnInMemoryCache { + // We don't want configuration to cache configuration responses past the lifetime of the app + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + NSURLCache *cache = apiClient.configurationHTTP.session.configuration.URLCache; + + XCTAssertTrue(cache.diskCapacity == 0); + XCTAssertTrue(cache.memoryCapacity > 0); +} + +#pragma mark - Dispatch Queue + +- (void)testCallbacks_useMainDispatchQueue { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + BTFakeHTTP *fake = [[BTFakeHTTP alloc] initWithBaseURL:apiClient.http.baseURL authorizationFingerprint:@""]; + // Override apiClient.http so that requests don't fail + apiClient.configurationHTTP = fake; + apiClient.http = fake; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(__unused BTConfiguration *configuration, __unused NSError *error) { + XCTAssert([NSThread isMainThread]); + [expectation1 fulfill]; + }]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"GET request"]; + [apiClient GET:@"" parameters:@{} completion:^(__unused BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(response); + XCTAssertNil(error); + + XCTAssert([NSThread isMainThread]); + [expectation2 fulfill]; + }]; + XCTestExpectation *expectation3 = [self expectationWithDescription:@"POST request"]; + [apiClient POST:@"" parameters:@{} completion:^(__unused BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(response); + XCTAssertNil(error); + + XCTAssert([NSThread isMainThread]); + [expectation3 fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark - Payment option categories + +- (void)testIsVenmoEnabledIsFalse_withoutAccessToken { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"payWithVenmo": @{}}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertFalse(configuration.isVenmoEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsVenmoEnabledIsTrue_withoutAccessToken { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"payWithVenmo": @{}}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertFalse(configuration.isVenmoEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsPayPalEnabled_whenEnabled_returnsTrue { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"paypalEnabled": @(YES) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertTrue(configuration.isPayPalEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsPayPalEnabled_whenDisabled_returnsFalse { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"paypalEnabled": @(NO) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertFalse(configuration.isPayPalEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsApplePayEnabled_whenEnabled_returnsTrue { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"applePay": @{ @"status": @"production" } }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertTrue(configuration.isApplePayEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsApplePayEnabled_whenDisabled_returnsFalse { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"applePay": @{ @"status": @"off" } }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertFalse(configuration.isApplePayEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testIsUnionPayEnabled_whenGatewayReturnsFalse_isFalse { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"unionPayEnabled": @(NO) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertFalse(configuration.isUnionPayEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testIsUnionPayEnabled_whenGatewayReturnsTrue_isTrue { + BTAPIClient *apiClient = [self clientThatReturnsConfiguration:@{ @"unionPay": @{@"enabled": @(YES) } }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Fetch configuration"]; + [apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, NSError *error) { + XCTAssertNil(error); + + XCTAssertTrue(configuration.isUnionPayEnabled); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +//#pragma mark - Analytics tests + +- (void)testAnalyticsService_isCreatedDuringInitialization { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + XCTAssertTrue([apiClient.analyticsService isKindOfClass:[BTAnalyticsService class]]); +} + +- (void)testSendAnalyticsEvent_whenCalled_callsAnalyticsService { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + BTFakeAnalyticsService *mockAnalyticsService = [[BTFakeAnalyticsService alloc] init]; + apiClient.analyticsService = mockAnalyticsService; + + [apiClient sendAnalyticsEvent:@"blahblah"]; + + XCTAssertEqualObjects(mockAnalyticsService.lastEvent, @"blahblah"); +} + +- (void)testPOST_usesMetadataSourceAndIntegration { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + apiClient = [apiClient copyWithSource:BTClientMetadataSourcePayPalApp integration:BTClientMetadataIntegrationDropIn]; + BTFakeHTTP *mockHTTP = [BTFakeHTTP fakeHTTP]; + apiClient.http = mockHTTP; + [mockHTTP stubRequest:@"GET" + toEndpoint:@"/client_api/v1/configuration" + respondWith:@{ + @"analytics" : @{ + @"url" : @"test://do-not-send.url" + } } + statusCode:200]; + BTClientMetadata *metadata = apiClient.metadata; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends analytics event"]; + [apiClient POST:@"/" parameters:@{} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + XCTAssertEqualObjects(mockHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqual(apiClient.metadata.source, BTClientMetadataSourcePayPalApp); + XCTAssertEqual(apiClient.metadata.integration, BTClientMetadataIntegrationDropIn); + XCTAssertEqualObjects(mockHTTP.lastRequestParameters[@"_meta"][@"integration"], metadata.integrationString); + XCTAssertEqualObjects(mockHTTP.lastRequestParameters[@"_meta"][@"source"], metadata.sourceString); + XCTAssertEqualObjects(mockHTTP.lastRequestParameters[@"_meta"][@"sessionId"], metadata.sessionId); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +#pragma mark - Helpers + +- (BTAPIClient *)clientThatReturnsConfiguration:(NSDictionary *)configurationDictionary { + BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:@"development_tokenization_key" sendAnalyticsEvent:NO]; + BTFakeHTTP *fake = [BTFakeHTTP fakeHTTP]; + fake.cannedConfiguration = [[BTJSON alloc] initWithValue:configurationDictionary]; + fake.cannedStatusCode = 200; + apiClient.configurationHTTP = fake; + + return apiClient; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsMetadataSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsMetadataSpec.m new file mode 100755 index 00000000..5f81cdb5 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsMetadataSpec.m @@ -0,0 +1,152 @@ +#import "BTAnalyticsMetadata.h" + +#import +#import "BTSpecDependencies.h" + +SpecBegin(BTAnalyticsMetadata) + +describe(@"metadata", ^{ + it(@"returns a dictionary of analytics metadata", ^{ + expect([BTAnalyticsMetadata metadata]).to.beKindOf([NSDictionary class]); + expect([[BTAnalyticsMetadata metadata] allKeys]).to.contain(@"platform"); + }); + + describe(@"platform", ^{ + it(@"returns \"iOS\"", ^{ + expect([BTAnalyticsMetadata metadata][@"platform"]).to.equal(@"iOS"); + }); + }); + + describe(@"platformVersion", ^{ + it(@"returns the iOS version, e.g. 7.0", ^{ + expect([BTAnalyticsMetadata metadata][@"platformVersion"]).to.match(@"^\\d+\\.\\d+(\\.\\d+)?$"); + }); + }); + + describe(@"sdkVersion", ^{ + it(@"returns Braintree sdk version", ^{ + expect([BTAnalyticsMetadata metadata][@"sdkVersion"]).to.match(@"^\\d+\\.\\d+\\.\\d+(-[0-9a-zA-Z-]+)?$"); + }); + }); + describe(@"merchantAppId", ^{ + it(@"returns app bundle identifier", ^{ + OCMockObject *mock = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[mock stub] andReturn:@{ (__bridge NSString *)kCFBundleIdentifierKey: @"com.braintree.Braintree-Demo" }] infoDictionary]; + + expect([BTAnalyticsMetadata metadata][@"merchantAppId"]).to.equal(@"com.braintree.Braintree-Demo"); + + [mock stopMocking]; + }); + }); + describe(@"merchantAppName", ^{ + it(@"returns the merchant's app version", ^{ + OCMockObject *mock = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[mock stub] andReturn:@{ (__bridge NSString *)kCFBundleNameKey:@"Braintree Demo" }] infoDictionary]; + + expect([BTAnalyticsMetadata metadata][@"merchantAppName"]).to.equal(@"Braintree Demo"); + + [mock stopMocking]; + }); + }); + describe(@"merchantAppVersion", ^{ + it(@"returns the merchant's app version", ^{ + OCMockObject *mock = [OCMockObject partialMockForObject:[NSBundle mainBundle]]; + [[[mock stub] andReturn:@{ (__bridge NSString *)kCFBundleVersionKey:@"2.3.4" }] infoDictionary]; + + expect([BTAnalyticsMetadata metadata][@"merchantAppVersion"]).to.match(@"2.3.4"); + + [mock stopMocking]; + }); + }); + describe(@"deviceRooted", ^{ + it(@"returns true iff the device has been jailbroken", ^{ + expect([[BTAnalyticsMetadata metadata][@"deviceRooted"] boolValue]).to.beFalsy(); + }); + }); + describe(@"deviceManufacturer", ^{ + it(@"returns \"Apple\"", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceManufacturer"]).to.equal(@"Apple"); + }); + }); + describe(@"deviceModel", ^{ + it(@"returns the device model", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceModel"]).to.match(@"iPhone\\d,\\d|i386|x86_64"); + }); + }); + + describe(@"deviceAppGeneratedPersistentUuid", ^{ + it(@"returns a UUID", ^{ + NSString *deviceAppGeneratedPersistentUuid = [BTAnalyticsMetadata metadata][@"deviceAppGeneratedPersistentUuid"]; + if (deviceAppGeneratedPersistentUuid) { + expect(deviceAppGeneratedPersistentUuid).to.match(@"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"); + } + }); + + it(@"returns a consistent value", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceAppGeneratedPersistentUuid"]).to.equal([BTAnalyticsMetadata metadata][@"deviceAppGeneratedPersistentUuid"]); + }); + }); + + it(@"deviceNetworkType", ^{ + it(@"returns whether we're on cellular or wifi", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceNetworkType"]).to.equal(@"wifi"); + }); + }); + describe(@"deviceLocationLatitude", ^{ + it(@"returns the devices location if already available", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceLocationLatitude"]).to.beNil(); + }); + }); + describe(@"deviceLocationLongitude", ^{ + it(@"returns the device location if already available", ^{ + expect([BTAnalyticsMetadata metadata][@"deviceLocationLongitude"]).to.beNil(); + }); + }); + describe(@"iosIdentifierForVendor", ^{ + it(@"returns the identifierForVendor", ^{ + NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:@"00000000-0000-0000-0000-000000000000"]; + OCMockObject *mock = [OCMockObject partialMockForObject:[UIDevice currentDevice]]; + [[[mock stub] andReturn:uuid] identifierForVendor]; + + expect([BTAnalyticsMetadata metadata][@"iosIdentifierForVendor"]).to.equal(uuid.UUIDString); + + [mock stopMocking]; + }); + }); + describe(@"iosIsCocoaPods", ^{ + it(@"is present", ^{ + expect([BTAnalyticsMetadata metadata][@"iosIsCocoapods"]).to.beKindOf([NSNumber class]); + }); + }); + describe(@"isSimulator", ^{ + it(@"returns true for ios simulators", ^{ + expect([BTAnalyticsMetadata metadata][@"isSimulator"]).to.beTruthy(); + }); + }); + describe(@"deviceScreenOrientation", ^{ + it(@"returns the screen orientation, e.g. Portrait or FaceUp", ^{ + id mockDevice = OCMPartialMock([UIDevice currentDevice]); + OCMStub([mockDevice orientation]).andReturn(UIDeviceOrientationFaceUp); + expect([BTAnalyticsMetadata metadata][@"deviceScreenOrientation"]).to.equal(@"FaceUp"); + [mockDevice stopMocking]; + }); + it(@"returns AppExtension when running in an App Extension", ^{ + id stubMainBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([stubMainBundle infoDictionary]).andReturn(@{@"NSExtension": @{}}); + expect([BTAnalyticsMetadata metadata][@"deviceScreenOrientation"]).to.equal(@"AppExtension"); + [stubMainBundle stopMocking]; + }); + }); + describe(@"userInterfaceOrientation", ^{ + it(@"returns the user interface orientation, e.g. Portrait or Landscape", ^{ +#ifdef __IPHONE_8_0 + expect([BTAnalyticsMetadata metadata][@"userInterfaceOrientation"]).to.beNil(); +#else + expect([BTAnalyticsMetadata metadata][@"userInterfaceOrientation"]).to.equal(@"Unknown"); +#endif + + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsService_Tests.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsService_Tests.m new file mode 100755 index 00000000..630de020 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAnalyticsService_Tests.m @@ -0,0 +1,278 @@ +#import "UnitTests-Swift.h" +#import "BTAnalyticsService.h" +#import "BTKeychain.h" +#import "Braintree-Version.h" +#import "BTFakeHTTP.h" +#import +#import +#import + +@interface BTAnalyticsService_Tests : XCTestCase + +@end + +@implementation BTAnalyticsService_Tests + +#pragma mark - Analytics tests + +- (void)testSendAnalyticsEvent_whenRemoteConfigurationHasNoAnalyticsURL_returnsError { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:nil]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends analytics event"]; + [analyticsService sendAnalyticsEvent:@"any.analytics.event" completion:^(NSError *error) { + XCTAssertEqual(error.domain, BTAnalyticsServiceErrorDomain); + XCTAssertEqual(error.code, (NSInteger)BTAnalyticsServiceErrorTypeMissingAnalyticsURL); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testSendAnalyticsEvent_whenRemoteConfigurationHasAnalyticsURL_setsUpAnalyticsHTTPToUseBaseURL { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends analytics event"]; + [analyticsService sendAnalyticsEvent:@"any.analytics.event" completion:^(NSError *error) { + XCTAssertEqualObjects(analyticsService.http.baseURL.absoluteString, @"test://do-not-send.url"); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testSendAnalyticsEvent_whenNumberOfQueuedEventsMeetsThreshold_sendsAnalyticsEvent { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 1; + analyticsService.http = mockAnalyticsHTTP; + + [analyticsService sendAnalyticsEvent:@"an.analytics.event"]; + // Pause briefly to allow analytics service to dispatch async blocks + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][0][@"kind"], @"an.analytics.event"); + [self validateMetaParameters:mockAnalyticsHTTP.lastRequestParameters[@"_meta"]]; +} + +- (void)testSendAnalyticsEvent_whenFlushThresholdIsGreaterThanNumberOfBatchedEvents_doesNotSendAnalyticsEvent { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 2; + analyticsService.http = mockAnalyticsHTTP; + + [analyticsService sendAnalyticsEvent:@"an.analytics.event"]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 0); +} + +- (void)testSendAnalyticsEventCompletion_whenCalled_sendsAllEvents { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 5; + analyticsService.http = mockAnalyticsHTTP; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends batched request"]; + [analyticsService sendAnalyticsEvent:@"an.analytics.event"]; + [analyticsService sendAnalyticsEvent:@"another.analytics.event" completion:^(NSError *error) { + XCTAssertNil(error); + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 1); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][0][@"kind"], @"an.analytics.event"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][1][@"kind"], @"another.analytics.event"); + [self validateMetaParameters:mockAnalyticsHTTP.lastRequestParameters[@"_meta"]]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testFlush_whenCalled_sendsAllQueuedEvents { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 5; + analyticsService.http = mockAnalyticsHTTP; + + [analyticsService sendAnalyticsEvent:@"an.analytics.event"]; + [analyticsService sendAnalyticsEvent:@"another.analytics.event"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends batched request"]; + [analyticsService flush:^(NSError *error) { + XCTAssertNil(error); + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 1); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][0][@"kind"], @"an.analytics.event"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][1][@"kind"], @"another.analytics.event"); + [self validateMetaParameters:mockAnalyticsHTTP.lastRequestParameters[@"_meta"]]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testFlush_whenThereAreNoQueuedEvents_doesNotPOST { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 5; + analyticsService.http = mockAnalyticsHTTP; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Sends batched request"]; + [analyticsService flush:^(NSError *error) { + XCTAssertNil(error); + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testAnalyticsService_whenAPIClientConfigurationFails_returnsError { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + NSError *stubbedError = [NSError errorWithDomain:@"SomeError" code:1 userInfo:nil]; + stubAPIClient.cannedConfigurationResponseError = stubbedError; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.http = mockAnalyticsHTTP; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked with error"]; + [analyticsService sendAnalyticsEvent:@"an.analytics.event" completion:^(NSError *error) { + XCTAssertEqualObjects(error, stubbedError); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; + + expectation = [self expectationWithDescription:@"Callback invoked with error"]; + [analyticsService flush:^(NSError *error) { + XCTAssertEqualObjects(error, stubbedError); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testAnalyticsService_afterConfigurationError_maintainsQueuedEventsUntilConfigurationIsSuccessful { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + NSError *stubbedError = [NSError errorWithDomain:@"SomeError" code:1 userInfo:nil]; + stubAPIClient.cannedConfigurationResponseError = stubbedError; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.http = mockAnalyticsHTTP; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback invoked with error"]; + [analyticsService sendAnalyticsEvent:@"an.analytics.event" completion:^(NSError *error) { + XCTAssertEqualObjects(error, stubbedError); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; + + stubAPIClient.cannedConfigurationResponseError = nil; + + expectation = [self expectationWithDescription:@"Callback invoked with error"]; + [analyticsService sendAnalyticsEvent:@"an.analytics.event" completion:^(NSError *error) { + XCTAssertNil(error); + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 1); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][0][@"kind"], @"an.analytics.event"); + [self validateMetaParameters:mockAnalyticsHTTP.lastRequestParameters[@"_meta"]]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testAnalyticsService_whenAppIsBackgrounded_sendsQueuedAnalyticsEvents { + MockAPIClient *stubAPIClient = [self stubbedAPIClientWithAnalyticsURL:@"test://do-not-send.url"]; + BTFakeHTTP *mockAnalyticsHTTP = [BTFakeHTTP fakeHTTP]; + BTAnalyticsService *analyticsService = [[BTAnalyticsService alloc] initWithAPIClient:stubAPIClient]; + analyticsService.flushThreshold = 5; + analyticsService.http = mockAnalyticsHTTP; + + [analyticsService sendAnalyticsEvent:@"an.analytics.event"]; + [analyticsService sendAnalyticsEvent:@"another.analytics.event"]; + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillResignActiveNotification object:nil]; + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + XCTAssertTrue(mockAnalyticsHTTP.POSTRequestCount == 1); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestEndpoint, @"/"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][0][@"kind"], @"an.analytics.event"); + XCTAssertEqualObjects(mockAnalyticsHTTP.lastRequestParameters[@"analytics"][1][@"kind"], @"another.analytics.event"); + [self validateMetaParameters:mockAnalyticsHTTP.lastRequestParameters[@"_meta"]]; +} + +#pragma mark - Helpers + +- (MockAPIClient *)stubbedAPIClientWithAnalyticsURL:(NSString *)analyticsURL { + MockAPIClient *stubAPIClient = [[MockAPIClient alloc] initWithAuthorization:@"development_tokenization_key"]; + if (analyticsURL) { + stubAPIClient.cannedConfigurationResponseBody = [[BTJSON alloc] initWithValue:@{ @"analytics" : @{ @"url" : analyticsURL } }]; + } else { + stubAPIClient.cannedConfigurationResponseBody = [[BTJSON alloc] initWithValue:@{}]; + } + return stubAPIClient; +} + +- (void)validateMetaParameters:(NSDictionary *)metaParameters { + NSString *unitTestDeploymentTargetVersion = [@(__IPHONE_OS_VERSION_MIN_REQUIRED) stringValue]; + NSString *unitTestBaseSDKVersion = [@(__IPHONE_OS_VERSION_MAX_ALLOWED) stringValue]; + + XCTAssertEqualObjects(metaParameters[@"deviceManufacturer"], @"Apple"); + XCTAssertEqualObjects(metaParameters[@"deviceModel"], [self deviceModel]); + XCTAssertEqualObjects(metaParameters[@"deviceAppGeneratedPersistentUuid"], [self deviceAppGeneratedPersistentUuid]); + XCTAssertEqualObjects(metaParameters[@"deviceScreenOrientation"], @"Portrait"); + XCTAssertEqualObjects(metaParameters[@"integration"], @"custom"); + XCTAssertEqualObjects(metaParameters[@"iosBaseSDK"], unitTestBaseSDKVersion); + XCTAssertEqualObjects(metaParameters[@"iosDeploymentTarget"], unitTestDeploymentTargetVersion); + XCTAssertEqualObjects(metaParameters[@"iosDeviceName"], [[UIDevice currentDevice] name]); + XCTAssertTrue((BOOL)metaParameters[@"isSimulator"] == TARGET_IPHONE_SIMULATOR); + XCTAssertEqualObjects(metaParameters[@"merchantAppId"], @"com.braintreepayments.Demo"); + XCTAssertEqualObjects(metaParameters[@"merchantAppName"], @"Braintree iOS SDK Demo"); + XCTAssertEqualObjects(metaParameters[@"sdkVersion"], BRAINTREE_VERSION); + XCTAssertEqualObjects(metaParameters[@"platform"], @"iOS"); + XCTAssertEqualObjects(metaParameters[@"platformVersion"], [[UIDevice currentDevice] systemVersion]); + XCTAssertNotNil(metaParameters[@"sessionId"]); + XCTAssertEqualObjects(metaParameters[@"source"], @"unknown"); + XCTAssertTrue([metaParameters[@"venmoInstalled"] isKindOfClass:[NSNumber class]]); +} + +// Ripped from BTAnalyticsMetadata +- (NSString *)deviceModel { + struct utsname systemInfo; + + uname(&systemInfo); + + NSString* code = [NSString stringWithCString:systemInfo.machine + encoding:NSUTF8StringEncoding]; + return code; +} + +// Ripped from BTAnalyticsMetadata +- (NSString *)deviceAppGeneratedPersistentUuid { + @try { + static NSString *deviceAppGeneratedPersistentUuidKeychainKey = @"deviceAppGeneratedPersistentUuid"; + NSString *savedIdentifier = [BTKeychain stringForKey:deviceAppGeneratedPersistentUuidKeychainKey]; + if (savedIdentifier.length == 0) { + savedIdentifier = [[NSUUID UUID] UUIDString]; + BOOL setDidSucceed = [BTKeychain setString:savedIdentifier + forKey:deviceAppGeneratedPersistentUuidKeychainKey]; + if (!setDidSucceed) { + return nil; + } + } + return savedIdentifier; + } @catch (NSException *exception) { + return nil; + } +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAppSwitch_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAppSwitch_Tests.swift new file mode 100755 index 00000000..15d0e54a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTAppSwitch_Tests.swift @@ -0,0 +1,86 @@ +import XCTest + +class BTAppSwitch_Tests: XCTestCase { + + var appSwitch = BTAppSwitch.sharedInstance() + + override func setUp() { + super.setUp() + appSwitch = BTAppSwitch.sharedInstance() + } + + override func tearDown() { + MockAppSwitchHandler.cannedCanHandle = false + MockAppSwitchHandler.lastCanHandleURL = nil + MockAppSwitchHandler.lastCanHandleSourceApplication = nil + MockAppSwitchHandler.lastHandleAppSwitchReturnURL = nil + super.tearDown() + } + + func testHandleOpenURL_whenHandlerIsRegistered_invokesCanHandleAppSwitchReturnURL() { + appSwitch.register(MockAppSwitchHandler.self) + let expectedURL = URL(string: "fake://url")! + let expectedSourceApplication = "fakeSourceApplication" + + BTAppSwitch.handleOpen(expectedURL, sourceApplication: expectedSourceApplication) + + XCTAssertEqual(MockAppSwitchHandler.lastCanHandleURL!, expectedURL) + XCTAssertEqual(MockAppSwitchHandler.lastCanHandleSourceApplication!, expectedSourceApplication) + } + + func testHandleOpenURL_whenHandlerCanHandleOpenURL_invokesHandleAppSwitchReturnURL() { + appSwitch.register(MockAppSwitchHandler.self) + MockAppSwitchHandler.cannedCanHandle = true + let expectedURL = URL(string: "fake://url")! + + let handled = BTAppSwitch.handleOpen(expectedURL, sourceApplication: "not important") + + XCTAssert(handled) + XCTAssertEqual(MockAppSwitchHandler.lastHandleAppSwitchReturnURL!, expectedURL) + } + + func testHandleOpenURL_whenHandlerCantHandleOpenURL_doesNotInvokeHandleAppSwitchReturnURL() { + appSwitch.register(MockAppSwitchHandler.self) + MockAppSwitchHandler.cannedCanHandle = false + + BTAppSwitch.handleOpen(URL(string: "fake://url")!, sourceApplication: "not important") + + XCTAssertNil(MockAppSwitchHandler.lastHandleAppSwitchReturnURL) + } + + func testHandleOpenURL_whenHandlerCantHandleOpenURL_returnsFalse() { + appSwitch.register(MockAppSwitchHandler.self) + MockAppSwitchHandler.cannedCanHandle = false + + XCTAssertFalse(BTAppSwitch.handleOpen(URL(string: "fake://url")!, sourceApplication: "not important")) + } + + func testHandleOpenURL_acceptsOptionalSourceApplication() { + // This doesn't assert any behavior about nil source application. It only checks that the code will compile! + let sourceApplication : String? = nil + BTAppSwitch.handleOpen(URL(string: "fake://url")!, sourceApplication: sourceApplication) + } + + func testHandleOpenURL_withNoAppSwitching() { + let handled = BTAppSwitch.handleOpen(URL(string: "scheme://")!, sourceApplication: "com.yourcompany.hi") + XCTAssertFalse(handled) + } + +} + +class MockAppSwitchHandler: BTAppSwitchHandler { + static var cannedCanHandle = false + static var lastCanHandleURL : URL? = nil + static var lastCanHandleSourceApplication : String? = nil + static var lastHandleAppSwitchReturnURL : URL? = nil + + @objc static func canHandleAppSwitchReturn(_ url: URL, sourceApplication: String) -> Bool { + lastCanHandleURL = url + lastCanHandleSourceApplication = sourceApplication + return cannedCanHandle + } + + @objc static func handleAppSwitchReturn(_ url: URL) { + lastHandleAppSwitchReturnURL = url + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTApplePay_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTApplePay_Tests.swift new file mode 100755 index 00000000..79f70f66 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTApplePay_Tests.swift @@ -0,0 +1,329 @@ +import PassKit +import XCTest + +@available(iOS 8.0, *) + +class BTApplePay_Tests: XCTestCase { + + var mockClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + + override func setUp() { + super.setUp() + mockClient = MockAPIClient(authorization: "development_tokenization_key")! + } + + // MARK: - Payment Request + + func testPaymentRequest_whenConfiguredOff_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "off" + ] + ]) + let applePayClient = BTApplePayClient(apiClient: mockClient) + + let expectation = self.expectation(description: "Callback invoked") + applePayClient.paymentRequest { (paymentRequest, error) in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.unsupported.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testPaymentRequest_whenConfigurationIsMissingApplePayStatus_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [:]) + let applePayClient = BTApplePayClient(apiClient: mockClient) + + let expectation = self.expectation(description: "Callback invoked") + applePayClient.paymentRequest { (paymentRequest, error) in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.unsupported.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testPaymentRequest_whenAPIClientIsNil_callsBackWithError() { + let applePayClient = BTApplePayClient(apiClient: mockClient) + applePayClient.apiClient = nil + + let expectation = self.expectation(description: "Callback invoked") + applePayClient.paymentRequest { (paymentRequest, error) in + XCTAssertNil(paymentRequest) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.integration.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testPaymentRequest_returnsPaymentRequestUsingConfiguration() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production", + "countryCode": "BT", + "currencyCode": "BTB", + "merchantIdentifier": "merchant.com.braintree-unit-tests", + "supportedNetworks": ["visa", "mastercard", "amex"] + ] ]) + let applePayClient = BTApplePayClient(apiClient: mockClient) + + let expectation = self.expectation(description: "Callback invoked") + applePayClient.paymentRequest { (paymentRequest, error) in + guard let paymentRequest = paymentRequest else { + XCTFail() + return + } + + XCTAssertNil(error) + XCTAssertEqual(paymentRequest.countryCode, "BT") + XCTAssertEqual(paymentRequest.currencyCode, "BTB") + XCTAssertEqual(paymentRequest.merchantIdentifier, "merchant.com.braintree-unit-tests") + XCTAssertEqual(paymentRequest.supportedNetworks, [PKPaymentNetwork.visa, PKPaymentNetwork.masterCard, PKPaymentNetwork.amex]) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testPaymentRequest_whenConfigurationIsMissingValues_returnsPaymentRequestWithValuesUndefined() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production" + ] ]) + let applePayClient = BTApplePayClient(apiClient: mockClient) + + let expectation = self.expectation(description: "Callback invoked") + applePayClient.paymentRequest { (paymentRequest, error) in + guard let paymentRequest = paymentRequest else { + XCTFail() + return + } + + XCTAssertNil(error) + XCTAssertEqual(paymentRequest.countryCode, "") + XCTAssertEqual(paymentRequest.currencyCode, "") + XCTAssertEqual(paymentRequest.merchantIdentifier, "") + XCTAssertEqual(paymentRequest.supportedNetworks, []) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + + // MARK: - Tokenization + + func testTokenization_whenConfiguredOff_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "off" + ] + ]) + let expectation = self.expectation(description: "Unsuccessful tokenization") + + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.unsupported.rawValue) + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenConfigurationIsMissingApplePayStatus_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [:]) + let expectation = self.expectation(description: "Unsuccessful tokenization") + + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.unsupported.rawValue) + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenAPIClientIsNil_callsBackWithError() { + let client = BTApplePayClient(apiClient: mockClient) + client.apiClient = nil + + let expectation = self.expectation(description: "Callback invoked") + client.tokenizeApplePay(MockPKPayment()) { (tokenizedPayment, error) -> Void in + XCTAssertNil(tokenizedPayment) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTApplePayErrorDomain) + XCTAssertEqual(error.code, BTApplePayErrorType.integration.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenConfigurationFetchErrorOccurs_callsBackWithError() { + mockClient.cannedConfigurationResponseError = NSError(domain: "MyError", code: 1, userInfo: nil) + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + let expectation = self.expectation(description: "tokenization error") + + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, "MyError") + XCTAssertEqual(error.code, 1) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenTokenizationErrorOccurs_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production" + ] + ]) + mockClient.cannedHTTPURLResponse = HTTPURLResponse(url: URL(string: "any")!, statusCode: 503, httpVersion: nil, headerFields: nil) + mockClient.cannedResponseError = NSError(domain: "foo", code: 100, userInfo: nil) + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + let expectation = self.expectation(description: "tokenization failure") + + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockClient.cannedResponseError!) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenTokenizationFailureOccurs_callsBackWithError() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production" + ] + ]) + mockClient.cannedResponseError = NSError(domain: "MyError", code: 1, userInfo: nil) + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + let expectation = self.expectation(description: "tokenization failure") + + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, "MyError") + XCTAssertEqual(error.code, 1) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenSuccessfulTokenizationInProduction_callsBackWithTokenizedPayment() { + mockClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production" + ] + ]) + mockClient.cannedResponseBody = BTJSON(value: [ + "applePayCards": [ + [ + "nonce" : "an-apple-pay-nonce", + "description": "a description", + ] + ] + ]) + let expectation = self.expectation(description: "successful tokenization") + + let client = BTApplePayClient(apiClient: mockClient) + let payment = MockPKPayment() + client.tokenizeApplePay(payment) { (tokenizedPayment, error) -> Void in + XCTAssertNil(error) + XCTAssertEqual(tokenizedPayment!.localizedDescription, "a description") + XCTAssertEqual(tokenizedPayment!.nonce, "an-apple-pay-nonce") + expectation.fulfill() + } + + XCTAssertEqual(mockClient.lastPOSTPath, "v1/payment_methods/apple_payment_tokens") + + waitForExpectations(timeout: 2, handler: nil) + } + + // MARK: - Metadata + + func testMetaParameter_whenTokenizationIsSuccessful_isPOSTedToServer() { + let mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "applePay" : [ + "status" : "production" + ] + ]) + let applePayClient = BTApplePayClient(apiClient: mockAPIClient) + let payment = MockPKPayment() + + let expectation = self.expectation(description: "Tokenized card") + applePayClient.tokenizeApplePay(payment) { _ -> Void in + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "v1/payment_methods/apple_payment_tokens") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "unknown") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } + + class MockPKPaymentToken : PKPaymentToken { + override var paymentData : Data { + get { + return Data() + } + } + override var transactionIdentifier : String { + get { + return "transaction-id" + } + } + override var paymentInstrumentName : String { + get { + return "payment-instrument-name" + } + } + override var paymentNetwork : String { + get { + return "payment-network" + } + } + } + + class MockPKPayment : PKPayment { + var overrideToken = MockPKPaymentToken() + override var token : PKPaymentToken { + get { + return overrideToken + } + } + } + +} + + + + + + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_Tests.swift new file mode 100755 index 00000000..38720a30 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_Tests.swift @@ -0,0 +1,226 @@ +import XCTest + +class BTCardClient_Tests: XCTestCase { + + func testTokenization_postsCardDataToClientAPI() { + let expectation = self.expectation(description: "Tokenize Card") + let fakeHTTP = FakeHTTP.fakeHTTP() + let apiClient = BTAPIClient(authorization: "development_tokenization_key")! + apiClient.http = fakeHTTP + let cardClient = BTCardClient(apiClient: apiClient) + + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "1234") + card.cardholderName = "Brian Tree" + + cardClient.tokenizeCard(card) { (tokenizedCard, error) -> Void in + XCTAssertEqual(fakeHTTP.lastRequest!.endpoint, "v1/payment_methods/credit_cards") + XCTAssertEqual(fakeHTTP.lastRequest!.method, "POST") + + if let cardParameters = fakeHTTP.lastRequest!.parameters["credit_card"] as? [String:AnyObject] { + XCTAssertEqual(cardParameters["number"] as? String, "4111111111111111") + XCTAssertEqual(cardParameters["expiration_date"] as? String, "12/2038") + XCTAssertEqual(cardParameters["cvv"] as? String, "1234") + XCTAssertEqual(cardParameters["cardholder_name"] as? String, "Brian Tree") + } else { + XCTFail() + } + expectation.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testTokenization_whenAPIClientSucceeds_returnsTokenizedCard() { + let expectation = self.expectation(description: "Tokenize Card") + let apiClient = BTAPIClient(authorization: "development_tokenization_key")! + apiClient.http = FakeHTTP.fakeHTTP() + let cardClient = BTCardClient(apiClient: apiClient) + + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + + cardClient.tokenizeCard(card) { (tokenizedCard, error) -> Void in + guard let tokenizedCard = tokenizedCard else { + XCTFail("Received an error: \(error)") + return + } + + XCTAssertEqual(tokenizedCard.nonce, FakeHTTP.fakeNonce) + XCTAssertEqual(tokenizedCard.localizedDescription, "Visa ending in 11") + XCTAssertEqual(tokenizedCard.lastTwo!, "11") + XCTAssertEqual(tokenizedCard.cardNetwork, BTCardNetwork.visa) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testTokenization_whenAPIClientFails_returnsError() { + let expectation = self.expectation(description: "Tokenize Card") + let apiClient = BTAPIClient(authorization: "development_tokenization_key")! + apiClient.http = ErrorHTTP.fakeHTTP() + let cardClient = BTCardClient(apiClient: apiClient) + + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + + cardClient.tokenizeCard(card) { (tokenizedCard, error) -> Void in + XCTAssertNil(tokenizedCard) + XCTAssertEqual(error! as NSError, ErrorHTTP.error) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testTokenization_whenTokenizationEndpointReturns422_callCompletionWithValidationError() { + let stubAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + let stubJSONResponse = BTJSON(value: [ + "error" : [ + "message" : "Credit card is invalid" + ], + "fieldErrors" : [ + [ + "field" : "creditCard", + "fieldErrors" : [ + [ + "field" : "number", + "message" : "Credit card number must be 12-19 digits", + "code" : "81716" + ] + ] + ] + ] + ]) + let stubError = NSError(domain: BTHTTPErrorDomain, code: BTHTTPErrorCode.clientError.rawValue, userInfo: [ + BTHTTPURLResponseKey: HTTPURLResponse(url: URL(string: "http://fake")!, statusCode: 422, httpVersion: nil, headerFields: nil)!, + BTHTTPJSONResponseBodyKey: stubJSONResponse + ]) + stubAPIClient.cannedResponseError = stubError + let cardClient = BTCardClient(apiClient: stubAPIClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + + let expectation = self.expectation(description: "Callback invoked with error") + cardClient.tokenizeCard(request, options: nil) { (cardNonce, error) -> Void in + XCTAssertNil(cardNonce) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTCardClientErrorDomain) + XCTAssertEqual(error.code, BTCardClientErrorType.customerInputInvalid.rawValue) + if let json = (error.userInfo as NSDictionary)[BTCustomerInputBraintreeValidationErrorsKey] as? NSDictionary { + XCTAssertEqual(json, (stubJSONResponse as BTJSON).asDictionary()! as NSDictionary) + } else { + XCTFail("Expected JSON response in userInfo[BTCustomInputBraintreeValidationErrorsKey]") + } + XCTAssertEqual(error.localizedDescription, "Credit card is invalid") + XCTAssertEqual((error as NSError).localizedFailureReason, "Credit card number must be 12-19 digits") + + + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenization_whenTokenizationEndpointReturnsAnyNon422Error_callCompletionWithError() { + let stubAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + stubAPIClient.cannedResponseError = NSError(domain: BTHTTPErrorDomain, code: BTHTTPErrorCode.clientError.rawValue, userInfo: nil) + let cardClient = BTCardClient(apiClient: stubAPIClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + request.smsCode = "12345" + request.enrollmentID = "fake-enrollment-id" + + let expectation = self.expectation(description: "Callback invoked with error") + cardClient.tokenizeCard(request, options: nil) { (cardNonce, error) -> Void in + XCTAssertNil(cardNonce) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTHTTPErrorDomain) + XCTAssertEqual(error.code, BTHTTPErrorCode.clientError.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + // MARK: - _meta parameter + + func testMetaParameter_whenTokenizationIsSuccessful_isPOSTedToServer() { + let mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + let cardClient = BTCardClient(apiClient: mockAPIClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + + let expectation = self.expectation(description: "Tokenized card") + cardClient.tokenizeCard(card) { _ -> Void in + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "v1/payment_methods/credit_cards") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "unknown") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } +} + +// MARK: - Helpers + +class FakeHTTP : BTHTTP { + struct Request { + let endpoint : String + let method : String + let parameters : [AnyHashable: Any] + } + + static let fakeNonce = "fake-nonce" + var lastRequest : Request? + + class func fakeHTTP() -> FakeHTTP { + return FakeHTTP(baseURL: URL(string: "fake://fake")!, authorizationFingerprint: "") + } + + override func post(_ path: String, parameters: [AnyHashable : Any]?, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + self.lastRequest = Request(endpoint: path, method: "POST", parameters: parameters!) + + let response = HTTPURLResponse(url: URL(string: path)!, statusCode: 202, httpVersion: nil, headerFields: nil)! + + guard let completionBlock = completionBlock else { + return + } + completionBlock(BTJSON(value: [ + "creditCards": [ + [ + "nonce": FakeHTTP.fakeNonce, + "description": "Visa ending in 11", + "details": [ + "lastTwo" : "11", + "cardType": "visa"] ] ] ]), response, nil) + } +} + +class ErrorHTTP : BTHTTP { + static let error = NSError(domain: "TestErrorDomain", code: 1, userInfo: nil) + + class func fakeHTTP() -> ErrorHTTP { + let fakeURL = URL(string: "fake://fake") + return ErrorHTTP(baseURL: fakeURL!, authorizationFingerprint: "") + } + + override func get(_ path: String, parameters: [String : String]?, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + guard let completionBlock = completionBlock else { + return + } + completionBlock(nil, nil, ErrorHTTP.error) + } + + override func post(_ path: String, parameters: [AnyHashable : Any]?, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + guard let completionBlock = completionBlock else { + return + } + completionBlock(nil, nil, ErrorHTTP.error) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_UnionPayTests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_UnionPayTests.swift new file mode 100755 index 00000000..bf9a61aa --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardClient_UnionPayTests.swift @@ -0,0 +1,578 @@ +import XCTest +import BraintreeUnionPay + +class BTCardClient_UnionPayTests: XCTestCase { + + var apiClient: BTAPIClient! + + override func setUp() { + super.setUp() + apiClient = clientWithUnionPayEnabled(true) + } + + // MARK: - Fetch capabilities + + func testFetchCapabilities_whenConfigurationFetchFails_returnsError() { + let stubConfigurationHTTP = BTFakeHTTP()! + stubConfigurationHTTP.cannedError = NSError(domain: "FakeDomain", code: 2, userInfo: nil) + apiClient.configurationHTTP = stubConfigurationHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (cardNonce, error) -> Void in + guard let error = error else { + XCTFail() + return + } + + XCTAssertNil(cardNonce) + XCTAssertEqual(error as NSError, stubConfigurationHTTP.cannedError as! NSError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchCapabilities_whenCallToCapabilitiesEndpointReturnsError_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: ["unionPay": ["enabled": true]]) + mockAPIClient.cannedResponseError = NSError(domain: "FakeError", code: 0, userInfo: nil) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (_, _) -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.capabilities-failed") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchCapabilities_whenUnionPayIsNotEnabledForMerchant_returnsError() { + apiClient = clientWithUnionPayEnabled(false) + let cardClient = BTCardClient(apiClient: apiClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (cardNonce, error) -> Void in + XCTAssertNil(cardNonce) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTCardClientErrorDomain) + XCTAssertEqual(error.code, BTCardClientErrorType.paymentOptionNotEnabled.rawValue) + XCTAssertEqual(error.localizedDescription, "UnionPay is not enabled for this merchant") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchCapabilities_whenUnionPayIsEnabledForMerchant_sendsGETRequestToCapabilitiesEndpointWithExpectedPayload() { + let mockHTTP = BTFakeHTTP()! + apiClient.http = mockHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (_, _) -> Void in + expectation.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + + XCTAssertEqual(mockHTTP.lastRequestMethod, "GET") + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods/credit_cards/capabilities") + guard let lastRequestParameters = mockHTTP.lastRequestParameters else { + XCTFail() + return + } + guard let cardNumberInPayload = lastRequestParameters["credit_card[number]"] as? String else { + XCTFail() + return + } + XCTAssertEqual(cardNumberInPayload, cardNumber) + } + + func testFetchCapabilities_whenSuccessful_parsesCardCapabilitiesFromJSONResponse() { + let stubHTTP = BTFakeHTTP()! + stubHTTP.stubRequest("GET", toEndpoint: "v1/payment_methods/credit_cards/capabilities", respondWith: [ + "isUnionPay": true, + "isDebit": false, + "unionPay": [ + "supportsTwoStepAuthAndCapture": true, + "isSupported": true + ] + ], statusCode: 201) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (cardCapabilities, error) -> Void in + guard let cardCapabilities = cardCapabilities else { + XCTFail("Expected union pay capabilities") + return + } + + XCTAssertNil(error) + XCTAssertEqual(true, cardCapabilities.isUnionPay) + XCTAssertEqual(false, cardCapabilities.isDebit) + XCTAssertEqual(true, cardCapabilities.supportsTwoStepAuthAndCapture) + XCTAssertEqual(true, cardCapabilities.isSupported) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testFetchCapabilities_whenSuccessful_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: ["unionPay": ["enabled": true]]) + mockAPIClient.cannedResponseBody = BTJSON(value:[ + "isUnionPay": true, + "isDebit": false, + "unionPay": [ + "supportsTwoStepAuthAndCapture": true, + "isSupported": true + ] + ]) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (cardCapabilities, error) -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.capabilities-received") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testFetchCapabilities_whenFailure_returnsError() { + let stubHTTP = BTFakeHTTP()! + let stubbedError = NSError(domain: "FakeError", code: 1, userInfo: nil) + stubHTTP.stubRequest("GET", toEndpoint: "v1/credit_cards/capabilities", respondWithError: stubbedError) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let cardNumber = "411111111111111" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.fetchCapabilities(cardNumber) { (cardCapabilities, error) -> Void in + XCTAssertNil(cardCapabilities) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, stubbedError.domain) + XCTAssertEqual(error.code, stubbedError.code) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + // MARK: - Enrollment + + func testEnroll_whenConfigurationFetchFails_returnsError() { + let stubConfigurationHTTP = BTFakeHTTP()! + stubConfigurationHTTP.cannedError = NSError(domain: "FakeDomain", code: 2, userInfo: nil) + apiClient.configurationHTTP = stubConfigurationHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + let request = BTCardRequest(card: card) + request.mobileCountryCode = "123" + request.mobilePhoneNumber = "321" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { (enrollmentID, smsCodeRequired, error) -> Void in + guard let error = error else { + XCTFail() + return + } + + XCTAssertNil(enrollmentID) + XCTAssertEqual(error as NSError, stubConfigurationHTTP.cannedError! as NSError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testEnrollment_whenUnionPayIsNotEnabledForMerchant_returnsError() { + apiClient = clientWithUnionPayEnabled(false) + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + let request = BTCardRequest(card: card) + request.mobileCountryCode = "123" + request.mobilePhoneNumber = "321" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { (enrollmentID, smsCodeRequired, error) -> Void in + XCTAssertNil(enrollmentID) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTCardClientErrorDomain) + XCTAssertEqual(error.code, BTCardClientErrorType.paymentOptionNotEnabled.rawValue) + XCTAssertEqual(error.localizedDescription, "UnionPay is not enabled for this merchant") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testEnrollment_whenUnionPayIsEnabledForMerchant_sendsPOSTRequestToEnrollmentEndpointWithExpectedPayload() { + let mockHTTP = BTFakeHTTP()! + apiClient.http = mockHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + let request = BTCardRequest(card: card) + request.mobileCountryCode = "123" + request.mobilePhoneNumber = "321" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + expectation.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + + XCTAssertEqual(mockHTTP.lastRequestMethod, "POST") + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/union_pay_enrollments") + guard let parameters = mockHTTP.lastRequestParameters as? [String:AnyObject] else { + XCTFail() + return + } + guard let enrollment = parameters["union_pay_enrollment"] as? [String:AnyObject] else { + XCTFail() + return + } + + XCTAssertEqual(enrollment["number"] as? String, card.number!) + XCTAssertEqual(enrollment["expiration_month"] as? String, card.expirationMonth!) + XCTAssertEqual(enrollment["expiration_year"] as? String, card.expirationYear!) + XCTAssertEqual(enrollment["mobile_country_code"] as? String, request.mobileCountryCode!) + XCTAssertEqual(enrollment["mobile_number"] as? String, request.mobilePhoneNumber!) + } + + func testEnrollmentPayload_doesNotContainCVV() { + let mockHTTP = BTFakeHTTP()! + apiClient.http = mockHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + let request = BTCardRequest(card: card) + request.mobileCountryCode = "123" + request.mobilePhoneNumber = "321" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + expectation.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + + guard let parameters = mockHTTP.lastRequestParameters as? [String:AnyObject] else { + XCTFail() + return + } + guard let enrollment = parameters["union_pay_enrollment"] as? [String:AnyObject] else { + XCTFail() + return + } + + XCTAssertNil(enrollment["cvv"] as? String) + } + + func testEnrollCard_whenSuccessful_returnsEnrollmentIDAndSmsCodeRequiredFromJSONResponse() { + let stubHTTP = BTFakeHTTP()! + stubHTTP.stubRequest("POST", toEndpoint: "v1/union_pay_enrollments", respondWith: [ + "unionPayEnrollmentId": "fake-enrollment-id", + "smsCodeRequired": true + ], statusCode: 201) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { (enrollmentID, smsCodeRequired, error) -> Void in + guard let enrollmentID = enrollmentID else { + XCTFail("Expected UnionPay enrollment") + return + } + XCTAssertNil(error) + XCTAssertEqual(enrollmentID, "fake-enrollment-id") + XCTAssertTrue(smsCodeRequired) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnrollCard_when422Failure_returnsValidationError() { + let stubHTTP = BTFakeHTTP()! + let stubbed422HTTPResponse = HTTPURLResponse(url: URL(string: "someendpoint")!, statusCode: 422, httpVersion: nil, headerFields: nil)! + let stubbed422ResponseBody = BTJSON(value: ["some": "thing"]) + let stubbedError = NSError(domain: BTHTTPErrorDomain, code: BTHTTPErrorCode.clientError.rawValue, userInfo: [ + BTHTTPURLResponseKey: stubbed422HTTPResponse, + BTHTTPJSONResponseBodyKey: stubbed422ResponseBody]) + stubHTTP.stubRequest("POST", toEndpoint: "v1/union_pay_enrollments", respondWithError:stubbedError) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { (enrollmentID, smsCodeRequired, error) -> Void in + XCTAssertNil(enrollmentID) + XCTAssertFalse(smsCodeRequired) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTCardClientErrorDomain) + XCTAssertEqual(error.code, BTCardClientErrorType.customerInputInvalid.rawValue) + + guard let inputErrors = (error._userInfo as! NSDictionary)[BTCustomerInputBraintreeValidationErrorsKey] as? NSDictionary else { + XCTFail("Expected error userInfo to contain validation errors") + return + } + XCTAssertEqual(inputErrors["some"] as! String, "thing") + + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnrollCard_onError_invokesCallbackOnMainThread() { + let stubHTTP = BTFakeHTTP()! + stubHTTP.stubRequest("POST", toEndpoint: "v1/union_pay_enrollments", respondWithError: NSError(domain: "CannedError", code: 0, userInfo: nil)) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnrollCard_whenEnrollmentEndpointReturnsError_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: ["unionPay": ["enabled": true]]) + mockAPIClient.cannedResponseError = NSError(domain: "FakeError", code: 0, userInfo: nil) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.enrollment-failed") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testEnrollCard_onSuccess_invokesCallbackOnMainThread() { + let stubHTTP = BTFakeHTTP()! + stubHTTP.stubRequest("POST", toEndpoint: "v1/union_pay_enrollments", respondWith: [ + "unionPayEnrollmentId": "fake-enrollment-id" + ], statusCode: 201) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + XCTAssertTrue(Thread.isMainThread) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testEnrollCard_onSuccess_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: ["unionPay": ["enabled": true]]) + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "unionPayEnrollmentId": "fake-enrollment-id", + "smsCodeRequired": true + ]) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { _ -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.enrollment-succeeded") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testEnrollCard_whenOtherFailure_returnsError() { + let stubHTTP = BTFakeHTTP()! + let stubbedError = NSError(domain: "FakeError", code: 1, userInfo: nil) + stubHTTP.stubRequest("POST", toEndpoint: "v1/union_pay_enrollments", respondWithError:stubbedError) + apiClient.http = stubHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + let request = BTCardRequest(card: card) + + let expectation = self.expectation(description: "Callback invoked") + cardClient.enrollCard(request) { (enrollmentID, smsCodeRequired, error) -> Void in + XCTAssertNil(enrollmentID) + XCTAssertFalse(smsCodeRequired) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, "FakeError") + XCTAssertEqual(error.code, 1) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + // MARK: - Tokenization + + func testTokenization_POSTsToTokenizationEndpoint() { + let mockHTTP = BTFakeHTTP()! + apiClient.http = mockHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + request.smsCode = "12345" + // This is an internal-only property, but we want to verify that it gets sent when hitting the tokenization endpoint + request.enrollmentID = "enrollment-id" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.tokenizeCard(request, options: nil) { (_, _) -> Void in + expectation.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + + XCTAssertEqual(mockHTTP.lastRequestMethod, "POST") + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods/credit_cards") + if let parameters = mockHTTP.lastRequestParameters as? [String: AnyObject] { + guard let cardParameters = parameters["credit_card"] as? [String: AnyObject] else { + XCTFail("Card should be in parameters") + return + } + XCTAssertEqual(cardParameters["number"] as? String, "4111111111111111") + XCTAssertEqual(cardParameters["expiration_date"] as? String, "12/2038") + XCTAssertEqual(cardParameters["cvv"] as? String, "123") + + guard let tokenizationOptionsParameters = cardParameters["options"] as? [String: AnyObject] else { + XCTFail("Tokenization options should be present") + return + } + + guard let unionPayEnrollmentParameters = tokenizationOptionsParameters["union_pay_enrollment"] as? [String: AnyObject] else { + XCTFail("UnionPay enrollment should be present") + return + } + + XCTAssertEqual(unionPayEnrollmentParameters["sms_code"] as? String, "12345") + XCTAssertEqual(unionPayEnrollmentParameters["id"] as? String, "enrollment-id") + } else { + XCTFail() + } + } + + func testTokenization_withEnrollmentIDAndNoSMSCode_sendsUnionPayEnrollment() { + let mockHTTP = BTFakeHTTP()! + apiClient.http = mockHTTP + let cardClient = BTCardClient(apiClient: apiClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + // This is an internal-only property, but we want to verify that it gets sent when hitting the tokenization endpoint + request.enrollmentID = "enrollment-id" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.tokenizeCard(request, options: nil) { (_, _) -> Void in + expectation.fulfill() + } + waitForExpectations(timeout: 1, handler: nil) + + XCTAssertEqual(mockHTTP.lastRequestMethod, "POST") + XCTAssertEqual(mockHTTP.lastRequestEndpoint, "v1/payment_methods/credit_cards") + if let parameters = mockHTTP.lastRequestParameters as? [String: AnyObject] { + guard let cardParameters = parameters["credit_card"] as? [String: AnyObject] else { + XCTFail("Card should be in parameters") + return + } + XCTAssertEqual(cardParameters["number"] as? String, "4111111111111111") + XCTAssertEqual(cardParameters["expiration_date"] as? String, "12/2038") + XCTAssertEqual(cardParameters["cvv"] as? String, "123") + + guard let tokenizationOptionsParameters = cardParameters["options"] as? [String: AnyObject] else { + XCTFail("Tokenization options should be present") + return + } + + guard let unionPayEnrollmentParameters = tokenizationOptionsParameters["union_pay_enrollment"] as? [String: AnyObject] else { + XCTFail("UnionPay enrollment should be present") + return + } + + XCTAssertNil(unionPayEnrollmentParameters["sms_code"]) + XCTAssertEqual(unionPayEnrollmentParameters["id"] as? String, "enrollment-id") + } else { + XCTFail() + } + } + + func testTokenization_whenTokenizingUnionPayEnrolledCardSucceeds_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "creditCards": [ + [ + "nonce": "fake-nonce", + "description": "UnionPay ending in 11", + "details": [ + "lastTwo" : "11", + "cardType": "unionpay"] ] ] ] ) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + request.smsCode = "12345" + request.enrollmentID = "enrollment-id" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.tokenizeCard(request, options: nil) { (_, _) -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.nonce-received") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testTokenization_whenTokenizingUnionPayEnrolledCardFails_sendsAnalyticsEvent() { + let mockAPIClient = MockAPIClient(authorization: BTValidTestClientToken)! + mockAPIClient.cannedResponseError = NSError(domain: "FakeError", code: 0, userInfo: nil) + let cardClient = BTCardClient(apiClient: mockAPIClient) + let request = BTCardRequest() + request.card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: "123") + request.smsCode = "12345" + request.enrollmentID = "enrollment-id" + + let expectation = self.expectation(description: "Callback invoked") + cardClient.tokenizeCard(request, options: nil) { (_, _) -> Void in + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.custom.unionpay.nonce-failed") + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + // MARK: - Helpers + + func clientWithUnionPayEnabled(_ unionPayEnabled: Bool) -> BTAPIClient { + let apiClient = BTAPIClient(authorization: BTValidTestClientToken, sendAnalyticsEvent: false)! + let stubbedConfigurationHTTP = BTFakeHTTP()! + stubbedConfigurationHTTP.cannedConfiguration = BTJSON(value: ["unionPay": [ + "enabled": unionPayEnabled + ] ]) + stubbedConfigurationHTTP.cannedStatusCode = 200 + apiClient.configurationHTTP = stubbedConfigurationHTTP + return apiClient + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardNonce_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardNonce_Tests.swift new file mode 100755 index 00000000..ecd31941 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCardNonce_Tests.swift @@ -0,0 +1,104 @@ +import XCTest + +class BTCardNonce_Tests: XCTestCase { + + override func setUp() { + super.setUp() + } + + func testCardWithJSON_createsCardWithExpectedValues() { + let cardNonce = BTCardNonce(json: BTJSON(value: [ + "description": "Visa ending in 11", + "details": [ + "cardType": "Visa", + "lastTwo": "11", + ], + "nonce": "fake-nonce", + ])) + + XCTAssertEqual(cardNonce.localizedDescription, "Visa ending in 11") + XCTAssertEqual(cardNonce.cardNetwork, BTCardNetwork.visa) + XCTAssertEqual(cardNonce.lastTwo, "11") + XCTAssertEqual(cardNonce.nonce, "fake-nonce") + XCTAssertEqual(cardNonce.type, "Visa") + } + + func testCardWithJSON_ignoresCaseWhenParsingCardType() { + let cardNonce = BTCardNonce(json: BTJSON(value: [ + "description": "Visa ending in 11", + "details": [ + "cardType": "vIsA", + "lastTwo": "11", + ], + "nonce": "fake-nonce", + ])) + + XCTAssertEqual(cardNonce.localizedDescription, "Visa ending in 11") + XCTAssertEqual(cardNonce.cardNetwork, BTCardNetwork.visa) + XCTAssertEqual(cardNonce.lastTwo, "11") + XCTAssertEqual(cardNonce.nonce, "fake-nonce") + XCTAssertEqual(cardNonce.type, "Visa") + } + + func testCardWithJSON_parsesAllCardTypesCorrectly() { + let cardNetworks = [ + BTCardNetwork.unknown, + BTCardNetwork.AMEX, + BTCardNetwork.dinersClub, + BTCardNetwork.discover, + BTCardNetwork.maestro, + BTCardNetwork.masterCard, + BTCardNetwork.JCB, + BTCardNetwork.laser, + BTCardNetwork.solo, + BTCardNetwork.switch, + BTCardNetwork.unionPay, + BTCardNetwork.ukMaestro, + BTCardNetwork.visa, + ] + let cardTypeJSONValues = [ + "some unrecognized type", + "american express", + "diners club", + "discover", + "maestro", + "mastercard", + "jcb", + "laser", + "solo", + "switch", + "unionpay", + "uk maestro", + "visa", + ] + let cardTypes = [ + "Unknown", + "AMEX", + "DinersClub", + "Discover", + "Maestro", + "MasterCard", + "JCB", + "Laser", + "Solo", + "Switch", + "UnionPay", + "UKMaestro", + "Visa", + ] + for i in 0.. +#import "BTCard_Internal.h" + +// See also BTCard_Tests +@interface BTCard_Internal_Tests : XCTestCase + +@end + +@implementation BTCard_Internal_Tests + +- (void)testParameters_standardProperties { + BTCard *card = [[BTCard alloc] initWithNumber:@"4111111111111111" + expirationMonth:@"12" + expirationYear:@"2038" + cvv:@"123"]; + BTJSON *parameters = [[BTJSON alloc] initWithValue:card.parameters]; + XCTAssertEqualObjects([parameters[@"number"] asString], @"4111111111111111"); + XCTAssertEqualObjects([parameters[@"expiration_date"] asString], @"12/2038"); + XCTAssertEqualObjects([parameters[@"cvv"] asString], @"123"); + XCTAssertTrue([parameters[@"options"][@"validate"] isFalse]); +} + +- (void)testParameters_whenShouldValidateIsTrue_encodesParametersCorrectly { + BTCard *card = [[BTCard alloc] init]; + card.shouldValidate = YES; + BTJSON *parameters = [[BTJSON alloc] initWithValue:card.parameters]; + XCTAssertTrue([parameters[@"options"][@"validate"] isTrue]); +} + +- (void)testParameters_whenShouldValidateIsTrueInParameters_encodesParametersCorrectly { + BTCard *card = [[BTCard alloc] initWithParameters:@{@"options": @{@"validate": @YES}}]; + BTJSON *parameters = [[BTJSON alloc] initWithValue:card.parameters]; + XCTAssertTrue([parameters[@"options"][@"validate"] isTrue]); +} + +- (void)testParameters_encodesAllParametersIncludingAdditionalParameters { + BTCard *card = + [[BTCard alloc] initWithParameters:@{}]; + + card.number = @"4111111111111111"; + card.expirationMonth = @"12"; + card.expirationYear = @"2038"; + card.postalCode = @"40404"; + card.streetAddress = @"724 Evergreen Terrace"; + card.locality = @"some locality"; + card.region = @"some region"; + card.countryName = @"some country name"; + card.countryCodeAlpha2 = @"US"; + + BTJSON *parameters = [[BTJSON alloc] initWithValue:card.parameters]; + XCTAssertEqualObjects([parameters[@"number"] asString], @"4111111111111111"); + XCTAssertEqualObjects([parameters[@"expiration_date"] asString], @"12/2038"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"postal_code"] asString], @"40404"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"street_address"] asString], @"724 Evergreen Terrace"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"locality"] asString], @"some locality"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"region"] asString], @"some region"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"country_name"] asString], @"some country name"); + XCTAssertEqualObjects([parameters[@"billing_address"][@"country_code_alpha2"] asString], @"US"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCard_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCard_Tests.swift new file mode 100755 index 00000000..61bc4454 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCard_Tests.swift @@ -0,0 +1,156 @@ +import XCTest + +// See also BTCard_Internal_Tests +class BTCard_Tests: XCTestCase { + func testInitialization_savesStandardProperties() { + let card = BTCard(number: "4111111111111111", expirationMonth:"12", expirationYear:"2038", cvv: "123") + + XCTAssertEqual(card.number!, "4111111111111111") + XCTAssertEqual(card.expirationMonth!, "12") + XCTAssertEqual(card.expirationYear!, "2038") + XCTAssertNil(card.postalCode) + XCTAssertEqual(card.cvv!, "123") + } + + func testInitialization_acceptsNilCvv() { + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2038", cvv: nil) + XCTAssertNil(card.cvv) + } + + func testInitialization_withoutParameters() { + let card = BTCard() + + card.number = "4111111111111111" + card.expirationMonth = "12" + card.expirationYear = "2038" + card.cvv = "123" + + XCTAssertEqual(card.number!, "4111111111111111") + XCTAssertEqual(card.expirationMonth!, "12") + XCTAssertEqual(card.expirationYear!, "2038") + XCTAssertNil(card.postalCode) + XCTAssertEqual(card.cvv!, "123") + } + + func testInitWithParameters_withAllValuesPresent_setsAllProperties() { + let card = BTCard(parameters: [ + "number": "4111111111111111", + "expiration_date": "12/20", + "cvv": "123", + "billing_address": [ + "street_address": "123 Townsend St", + "locality": "San Francisco", + "region": "CA", + "country_name": "United States of America", + "country_code_alpha2": "US", + "postal_code": "94107" + ], + "options": ["validate": true], + "cardholder_name": "Brian Tree" + ]) + + XCTAssertEqual(card.number, "4111111111111111") + XCTAssertEqual(card.expirationMonth, "12") + XCTAssertEqual(card.expirationYear, "20") + XCTAssertEqual(card.postalCode, "94107") + XCTAssertEqual(card.cvv, "123") + XCTAssertTrue(card.shouldValidate) + XCTAssertEqual(card.cardholderName, "Brian Tree") + XCTAssertEqual(card.streetAddress, "123 Townsend St") + XCTAssertEqual(card.locality, "San Francisco") + XCTAssertEqual(card.region, "CA") + XCTAssertEqual(card.countryName, "United States of America") + XCTAssertEqual(card.countryCodeAlpha2, "US") + XCTAssertEqual(card.postalCode, "94107") + } + + func testInitWithParameters_withEmptyParameters_setsPropertiesToExpectedValues() { + let card = BTCard(parameters: [:]) + + XCTAssertNil(card.number) + XCTAssertNil(card.expirationMonth) + XCTAssertNil(card.expirationYear) + XCTAssertNil(card.postalCode) + XCTAssertNil(card.cvv) + XCTAssertNil(card.cardholderName) + XCTAssertFalse(card.shouldValidate) + XCTAssertNil(card.streetAddress) + XCTAssertNil(card.locality) + XCTAssertNil(card.region) + XCTAssertNil(card.countryName) + XCTAssertNil(card.countryCodeAlpha2) + } + + func testInitWithParameters_withCVVAndPostalCode_setsPropertiesToExpectedValues() { + let card = BTCard(parameters: [ + "cvv": "123", + "billing_address": ["postal_code": "94949"], + ]) + + XCTAssertNil(card.number) + XCTAssertNil(card.expirationMonth) + XCTAssertNil(card.expirationYear) + XCTAssertEqual(card.postalCode, "94949") + XCTAssertEqual(card.cvv, "123") + XCTAssertFalse(card.shouldValidate) + } + + func testParameters_whenInitializedWithInitWithParameters_returnsExpectedValues() { + let card = BTCard(parameters: [ + "number": "4111111111111111", + "expiration_date": "12/20", + "cvv": "123", + "billing_address": [ + "street_address": "123 Townsend St", + "locality": "San Francisco", + "region": "CA", + "country_name": "United States of America", + "country_code_alpha2": "US", + "postal_code": "94107" + ], + "options": ["validate": false], + "cardholder_name": "Brian Tree" + ]) + + XCTAssertEqual(card.parameters() as NSObject, [ + "number": "4111111111111111", + "expiration_date": "12/20", + "cvv": "123", + "billing_address": [ + "street_address": "123 Townsend St", + "locality": "San Francisco", + "region": "CA", + "country_name": "United States of America", + "country_code_alpha2": "US", + "postal_code": "94107" + ], + "options": ["validate": false], + "cardholder_name": "Brian Tree" + ] as NSObject) + } + + func testParameters_whenInitializedWithCustomParameters_returnsExpectedValues() { + let card = BTCard(parameters: [ + "cvv": "123", + "billing_address": ["postal_code": "94949"], + "options": ["foo": "bar"], + ]) + + XCTAssertEqual(card.parameters() as NSObject, [ + "cvv": "123", + "billing_address": ["postal_code": "94949"], + "options": [ + "foo": "bar", + "validate": false, + ], + ] as NSObject) + } + + func testParameters_whenShouldValidateIsSetToNewValue_returnsExpectedValues() { + let card = BTCard(parameters: ["options": ["validate": false]]) + card.shouldValidate = true + XCTAssertEqual(card.parameters() as NSObject, [ + "options": [ "validate": true ], + ] as NSObject) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCheckoutRequest_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCheckoutRequest_Tests.swift new file mode 100755 index 00000000..fa951e6c --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTCheckoutRequest_Tests.swift @@ -0,0 +1,70 @@ +import XCTest + +class BTPaymentRequest_Tests: XCTestCase { + + func testPaymentRequest_initializesAndCopiesCorrectly() { + let paymentRequest = BTPaymentRequest() + XCTAssertNil(paymentRequest.summaryTitle) + XCTAssertNil(paymentRequest.summaryDescription) + XCTAssertEqual("", paymentRequest.displayAmount) + XCTAssertEqual("Pay", paymentRequest.callToActionText) + XCTAssertFalse(paymentRequest.shouldHideCallToAction) + XCTAssertNil(paymentRequest.amount) + XCTAssertNil(paymentRequest.currencyCode) + XCTAssertFalse(paymentRequest.noShipping) + XCTAssertFalse(paymentRequest.presentViewControllersFromTop) + XCTAssertNil(paymentRequest.shippingAddress) + XCTAssertFalse(paymentRequest.showDefaultPaymentMethodNonceFirst) + + let paymentRequestCopy = paymentRequest.copy() as! BTPaymentRequest + XCTAssertNil(paymentRequestCopy.summaryTitle) + XCTAssertNil(paymentRequestCopy.summaryDescription) + XCTAssertEqual("", paymentRequestCopy.displayAmount) + XCTAssertEqual("Pay", paymentRequestCopy.callToActionText) + XCTAssertFalse(paymentRequestCopy.shouldHideCallToAction) + XCTAssertNil(paymentRequestCopy.amount) + XCTAssertNil(paymentRequestCopy.currencyCode) + XCTAssertFalse(paymentRequestCopy.noShipping) + XCTAssertFalse(paymentRequestCopy.presentViewControllersFromTop) + XCTAssertNil(paymentRequestCopy.shippingAddress) + XCTAssertFalse(paymentRequest.showDefaultPaymentMethodNonceFirst) + } + + func testPaymentRequest_valuesAreSetAndCopiedCorrectly() { + let paymentRequest = BTPaymentRequest() + paymentRequest.summaryTitle = "My Summary Title" + paymentRequest.summaryDescription = "My Summary Description" + paymentRequest.displayAmount = "$123.45" + paymentRequest.callToActionText = "My Call To Action" + paymentRequest.shouldHideCallToAction = true + paymentRequest.amount = "123.45" + paymentRequest.currencyCode = "USD" + paymentRequest.noShipping = true + paymentRequest.presentViewControllersFromTop = true + let shippingAddress = BTPostalAddress() + paymentRequest.shippingAddress = shippingAddress + + XCTAssertEqual("My Summary Title", paymentRequest.summaryTitle) + XCTAssertEqual("My Summary Description", paymentRequest.summaryDescription) + XCTAssertEqual("$123.45", paymentRequest.displayAmount) + XCTAssertEqual("My Call To Action", paymentRequest.callToActionText) + XCTAssertTrue(paymentRequest.shouldHideCallToAction) + XCTAssertEqual("123.45", paymentRequest.amount) + XCTAssertEqual("USD", paymentRequest.currencyCode) + XCTAssertTrue(paymentRequest.noShipping) + XCTAssertTrue(paymentRequest.presentViewControllersFromTop) + XCTAssertEqual(shippingAddress, paymentRequest.shippingAddress) + + let paymentRequestCopy = paymentRequest.copy() as! BTPaymentRequest + XCTAssertEqual("My Summary Title", paymentRequestCopy.summaryTitle) + XCTAssertEqual("My Summary Description", paymentRequestCopy.summaryDescription) + XCTAssertEqual("$123.45", paymentRequestCopy.displayAmount) + XCTAssertEqual("My Call To Action", paymentRequestCopy.callToActionText) + XCTAssertTrue(paymentRequestCopy.shouldHideCallToAction) + XCTAssertEqual("123.45", paymentRequestCopy.amount) + XCTAssertEqual("USD", paymentRequestCopy.currencyCode) + XCTAssertTrue(paymentRequestCopy.noShipping) + XCTAssertTrue(paymentRequestCopy.presentViewControllersFromTop) + XCTAssertEqual(shippingAddress, paymentRequestCopy.shippingAddress) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientMetadataSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientMetadataSpec.m new file mode 100755 index 00000000..ddaacd57 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientMetadataSpec.m @@ -0,0 +1,205 @@ +#import "BTClientMetadata.h" +#import "BTSpecDependencies.h" + +SpecBegin(BTClientMetadata) + +describe(@"string values", ^{ + + BTMutableClientMetadata *m = [[BTMutableClientMetadata alloc] init]; + + it(@"source returns expected strings", ^{ + NSDictionary *sources = @{ + @(BTClientMetadataSourceUnknown) : @"unknown", + @(BTClientMetadataSourceForm) : @"form", + @(BTClientMetadataSourcePayPalApp) : @"paypal-app", + @(BTClientMetadataSourcePayPalBrowser) : @"paypal-browser", + @(BTClientMetadataSourceVenmoApp) : @"venmo-app", + }; + + for (NSNumber *sourceNumber in sources) { + m.source = (BTClientMetadataSourceType)sourceNumber.integerValue; + XCTAssertEqualObjects(m.sourceString, sources[sourceNumber]); + } + }); + + it(@"integration returns expected strings", ^{ + m.integration = BTClientMetadataIntegrationDropIn; + expect(m.integrationString).to.equal(@"dropin"); + + m.integration = BTClientMetadataIntegrationDropIn2; + expect(m.integrationString).to.equal(@"dropin2"); + + m.integration = BTClientMetadataIntegrationCustom; + expect(m.integrationString).to.equal(@"custom"); + + m.integration = BTClientMetadataIntegrationUnknown; + expect(m.integrationString).to.equal(@"unknown"); + }); + + it(@"sessionId returns a 32 character UUID string", ^{ + expect(m.sessionId.length).to.equal(32); + }); + + it(@"sessionId should be different than a different instance's sessionId", ^{ + BTMutableClientMetadata *m2 = [BTMutableClientMetadata new]; + expect(m.sessionId).notTo.equal(m2.sessionId); + }); + +}); + +sharedExamplesFor(@"a copied metadata instance", ^(NSDictionary *data) { + __block BTClientMetadata *original, *copied; + + beforeEach(^{ + original = data[@"original"]; + copied = data[@"copy"]; + }); + + it(@"has the same values", ^{ + expect(copied.integration).to.equal(original.integration); + expect(copied.source).to.equal(original.source); + expect(copied.sessionId).to.equal(original.sessionId); + }); +}); + + +describe(@"mutableMetadata", ^{ + + __block BTMutableClientMetadata *mutableMetadata; + + beforeEach(^{ + mutableMetadata = [[BTMutableClientMetadata alloc] init]; + }); + + describe(@"init", ^{ + it(@"has expected default values", ^{ + expect(mutableMetadata.integration).to.equal(BTClientMetadataIntegrationCustom); + expect(mutableMetadata.source).to.equal(BTClientMetadataSourceUnknown); + }); + }); + + context(@"with non-default values", ^{ + beforeEach(^{ + mutableMetadata.integration = BTClientMetadataIntegrationDropIn; + mutableMetadata.source = BTClientMetadataSourcePayPalApp; + }); + + describe(@"copy", ^{ + __block BTClientMetadata *copied; + beforeEach(^{ + copied = [mutableMetadata copy]; + }); + + itBehavesLike(@"a copied metadata instance", ^{ + return @{@"original" : mutableMetadata, + @"copy" : copied}; + }); + + it(@"returns a different, immutable instance", ^{ + expect(mutableMetadata).toNot.beIdenticalTo(copied); + expect([copied isKindOfClass:[BTClientMetadata class]]).to.beTruthy(); + expect([copied isKindOfClass:[BTMutableClientMetadata class]]).to.beFalsy(); + }); + }); + + describe(@"mutableCopy", ^{ + __block BTMutableClientMetadata *copied; + beforeEach(^{ + copied = [mutableMetadata mutableCopy]; + }); + + itBehavesLike(@"a copied metadata instance", ^{ + return @{@"original" : mutableMetadata, + @"copy" : copied}; + }); + + it(@"returns a different, immutable instance", ^{ + expect(mutableMetadata).toNot.beIdenticalTo(copied); + expect([copied isKindOfClass:[BTClientMetadata class]]).to.beTruthy(); + expect([copied isKindOfClass:[BTMutableClientMetadata class]]).to.beTruthy(); + }); + }); + }); +}); + +describe(@"metadata", ^{ + + __block BTClientMetadata *metadata; + + beforeEach(^{ + metadata = [[BTClientMetadata alloc] init]; + }); + + describe(@"init", ^{ + it(@"has expected default values", ^{ + expect(metadata.integration).to.equal(BTClientMetadataIntegrationCustom); + expect(metadata.source).to.equal(BTClientMetadataSourceUnknown); + }); + }); + + context(@"with non-default values", ^{ + beforeEach(^{ + metadata = ({ + BTMutableClientMetadata *mutableMetadata = [[BTMutableClientMetadata alloc] init]; + mutableMetadata.integration = BTClientMetadataIntegrationDropIn; + mutableMetadata.source = BTClientMetadataSourcePayPalApp; + [mutableMetadata copy]; + }); + }); + + describe(@"copy", ^{ + __block BTClientMetadata *copied; + beforeEach(^{ + copied = [metadata copy]; + }); + + itBehavesLike(@"a copied metadata instance", ^{ + return @{@"original" : metadata, + @"copy" : copied}; + }); + + it(@"returns a different, immutable instance", ^{ + expect(metadata).toNot.beIdenticalTo(copied); + expect([copied isKindOfClass:[BTClientMetadata class]]).to.beTruthy(); + expect([copied isKindOfClass:[BTMutableClientMetadata class]]).to.beFalsy(); + }); + }); + + describe(@"mutableCopy", ^{ + __block BTMutableClientMetadata *copied; + beforeEach(^{ + copied = [metadata mutableCopy]; + }); + + itBehavesLike(@"a copied metadata instance", ^{ + return @{@"original" : metadata, + @"copy" : copied}; + }); + + it(@"returns a different, immutable instance", ^{ + expect(copied).toNot.beIdenticalTo(metadata); + expect([copied isKindOfClass:[BTClientMetadata class]]).to.beTruthy(); + expect([copied isKindOfClass:[BTMutableClientMetadata class]]).to.beTruthy(); + }); + }); + }); +}); + +SpecEnd + +@interface BTClientMetadata_Tests : XCTestCase +@end + +@implementation BTClientMetadata_Tests + +- (void)testParameters_ReturnsTheMetadataMetaParametersForPosting { + BTClientMetadata *metadata = [[BTClientMetadata alloc] init]; + NSDictionary *parameters = metadata.parameters; + expect(parameters).to.equal( + @{@"integration": metadata.integrationString, + @"source": metadata.sourceString, + @"sessionId": metadata.sessionId, + }); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientTokenSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientTokenSpec.m new file mode 100755 index 00000000..428b298e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTClientTokenSpec.m @@ -0,0 +1,149 @@ +#import "BTClientToken.h" +#import "BTTestClientTokenFactory.h" +#import +#import + +@interface BTClientToken_Tests : XCTestCase +@end + +@implementation BTClientToken_Tests + +- (void)testInitialization_whenVersionIsUnsupported_returnsError { + NSError *error; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:[BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ @"version": @0 }] error:&error]; + XCTAssertNil(clientToken); + XCTAssertEqualObjects(error.domain, BTClientTokenErrorDomain); + XCTAssertEqual(error.code, BTClientTokenErrorUnsupportedVersion); +} + +- (void)testInitialization_withV1RawJSONClientTokens_isSuccessful { + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:[BTTestClientTokenFactory tokenWithVersion:1 overrides:@{ BTClientTokenKeyConfigURL: @"https://api.example.com:443/merchants/a_merchant_id/client_api/v1/configuration"}] error:NULL]; + XCTAssertEqualObjects(clientToken.authorizationFingerprint, @"an_authorization_fingerprint"); + XCTAssertEqualObjects(clientToken.configURL, [NSURL URLWithString:@"https://api.example.com:443/merchants/a_merchant_id/client_api/v1/configuration"]); +} + +- (void)testInitialization_withV2Base64EncodedClientTokens_isSuccessful { + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:[BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyConfigURL: @"https://api.example.com:443/merchants/a_merchant_id/client_api/v1/configuration" }] error:NULL]; + XCTAssertEqualObjects(clientToken.authorizationFingerprint, @"an_authorization_fingerprint"); + XCTAssertEqualObjects(clientToken.configURL, [NSURL URLWithString:@"https://api.example.com:443/merchants/a_merchant_id/client_api/v1/configuration"]); +} + +- (void)testInitialization_withInvalidJSON_returnsError { + NSError *error; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:@"definitely_not_a_client_token" error:&error]; + + XCTAssertNil(clientToken); + XCTAssertEqualObjects(error.domain, BTClientTokenErrorDomain); + XCTAssertEqual(error.code, BTClientTokenErrorInvalid); + XCTAssertEqualObjects([error.userInfo[NSUnderlyingErrorKey] domain], NSCocoaErrorDomain); +} + +#pragma mark - Edge cases + +- (void)testInitialization_whenConfigURLIsBlank_returnsError { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyConfigURL: @"" }]; + NSError *error; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:&error]; + + XCTAssertNil(clientToken); + XCTAssertEqualObjects(error.domain, BTClientTokenErrorDomain); + XCTAssertEqual(error.code, BTClientTokenErrorInvalid); + expect([error localizedDescription]).to.contain(@"config url"); +} + +- (void)testInitialization_whenAuthorizationFingerprintIsOmitted_returnsError { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyAuthorizationFingerprint: NSNull.null }]; + NSError *error; + + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:&error]; + + XCTAssertNil(clientToken); + XCTAssertEqualObjects(error.domain, BTClientTokenErrorDomain); + XCTAssertEqual(error.code, BTClientTokenErrorInvalid); + expect([error localizedDescription]).to.contain(@"Invalid client token."); + expect([error localizedFailureReason]).to.contain(@"Authorization fingerprint"); +} + +- (void)testInitialization_whenAuthorizationFingerprintIsBlank_returnsError { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyAuthorizationFingerprint: @"" }]; + NSError *error; + + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:&error]; + + XCTAssertNil(clientToken); + XCTAssertEqualObjects(error.domain, BTClientTokenErrorDomain); + XCTAssertEqual(error.code, BTClientTokenErrorInvalid); + expect([error localizedDescription]).to.contain(@"Invalid client token."); + expect([error localizedFailureReason]).to.contain(@"Authorization fingerprint"); +} + +#pragma mark - NSCoding + +- (void)testNSCoding_afterEncodingAndDecodingClientToken_preservesClientTokenDataIntegrity { + NSString *clientTokenEncodedJSON = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ + BTClientTokenKeyConfigURL: @"https://api.example.com/client_api/v1/configuration", + BTClientTokenKeyAuthorizationFingerprint: @"an_authorization_fingerprint|created_at=2014-02-12T18:02:30+0000&customer_id=1234567&public_key=integration_public_key" }]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenEncodedJSON error:NULL]; + + NSMutableData *data = [NSMutableData data]; + NSKeyedArchiver *coder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; + [clientToken encodeWithCoder:coder]; + [coder finishEncoding]; + + NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; + BTClientToken *returnedClientToken = [[BTClientToken alloc] initWithCoder:decoder]; + [decoder finishDecoding]; + + expect(returnedClientToken.configURL).to.equal([NSURL URLWithString:@"https://api.example.com/client_api/v1/configuration"]); + expect(returnedClientToken.authorizationFingerprint).to.equal(@"an_authorization_fingerprint|created_at=2014-02-12T18:02:30+0000&customer_id=1234567&public_key=integration_public_key"); +} + +#pragma mark - isEqual + +- (void)testIsEqual_whenTokensContainTheSameValues_returnsTrue { + NSString *clientTokenEncodedJSON = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyAuthorizationFingerprint: @"abcd" }]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenEncodedJSON error:NULL]; + BTClientToken *clientToken2 = [[BTClientToken alloc] initWithClientToken:clientTokenEncodedJSON error:NULL]; + + XCTAssertNotNil(clientToken); + XCTAssertTrue([clientToken isEqual:clientToken2]); +} + +- (void)testIsEqual_whenTokensDoNotContainTheSameValues_returnsFalse { + NSString *clientTokenString1 = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyAuthorizationFingerprint: @"one_auth_fingerprint" }]; + NSString *clientTokenString2 = [BTTestClientTokenFactory tokenWithVersion:2 overrides:@{ BTClientTokenKeyAuthorizationFingerprint: @"different_auth_fingerprint" }]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenString1 error:nil]; + BTClientToken *clientToken2 = [[BTClientToken alloc] initWithClientToken:clientTokenString2 error:nil]; + + XCTAssertNotNil(clientToken); + XCTAssertFalse([clientToken isEqual:clientToken2]); +} + +#pragma mark - NSCopying + +- (void)testCopy_returnsADifferentInstance { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:NULL]; + + XCTAssertTrue([clientToken copy] != clientToken); +} + +- (void)testCopy_returnsAnEquivalentInstance { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:NULL]; + + XCTAssertEqualObjects([clientToken copy], clientToken); +} + +- (void)testCopy_returnsAnInstanceWithEqualValues { + NSString *clientTokenRawJSON = [BTTestClientTokenFactory tokenWithVersion:2]; + BTClientToken *clientToken = [[BTClientToken alloc] initWithClientToken:clientTokenRawJSON error:NULL]; + BTClientToken *copiedClientToken = [clientToken copy]; + + XCTAssertEqualObjects(copiedClientToken.configURL, clientToken.configURL); + XCTAssertEqualObjects(copiedClientToken.json.asDictionary, clientToken.json.asDictionary); + XCTAssertEqualObjects(copiedClientToken.authorizationFingerprint, clientToken.authorizationFingerprint); + XCTAssertEqualObjects(copiedClientToken.originalValue, clientToken.originalValue); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTConfiguration_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTConfiguration_Tests.swift new file mode 100755 index 00000000..8ec8c8ae --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTConfiguration_Tests.swift @@ -0,0 +1,213 @@ +import XCTest +import PassKit + +class BTConfiguration_Tests: XCTestCase { + + override func tearDown() { + BTConfiguration.setBetaPaymentOption("venmo", isEnabled: false) + } + + func testInitWithJSON_setsJSON() { + let json = BTJSON(value: [ + "some": "things", + "number": 1, + "array": [1, 2, 3]]) + let configuration = BTConfiguration(json: json) + + XCTAssertEqual(configuration.json, json) + } + + // MARK: - Beta enabled payment option + + func testIsBetaEnabledPaymentOption_returnsFalse() { + XCTAssertFalse(BTConfiguration.isBetaEnabledPaymentOption("venmo")) + } + + // MARK: - Venmo category methods + + func testIsVenmoEnabled_whenBetaVenmoIsEnabledAndAccessTokenIsPresent_returnsTrue() { + let configurationJSON = BTJSON(value: [ + "payWithVenmo": [ "accessToken": "some access token" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertTrue(configuration.isVenmoEnabled) + } + + func testIsVenmoEnabled_whenBetaVenmoIsEnabledAndAccessTokenNotPresent_returnsFalse() { + let configurationJSON = BTJSON(value: [ + "payWithVenmo": [] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertFalse(configuration.isVenmoEnabled) + } + + func testVenmoAccessToken_returnsVenmoAccessToken() { + let configurationJSON = BTJSON(value: [ + "payWithVenmo": [ "accessToken": "some access token" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.venmoAccessToken, "some access token") + } + + func testEnableVenmo_whenDisabled_setsVenmoBetaPaymentOptionToFalse() { + BTConfiguration.enableVenmo(false) + XCTAssertFalse(BTConfiguration.isBetaEnabledPaymentOption("venmo")) + } + + // MARK: - PayPal category methods + + func testIsPayPalEnabled_returnsPayPalEnabledStatusFromConfigurationJSON() { + for isPayPalEnabled in [true, false] { + let configurationJSON = BTJSON(value: [ "paypalEnabled": isPayPalEnabled ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertTrue(configuration.isPayPalEnabled == isPayPalEnabled) + } + } + + func testIsPayPalEnabled_whenPayPalEnabledStatusNotPresentInConfigurationJSON_returnsFalse() { + let configuration = BTConfiguration(json: BTJSON(value: [])) + XCTAssertFalse(configuration.isPayPalEnabled) + } + + func testIsBillingAgreementsEnabled_returnsBillingAgreementsStatusFromConfigurationJSON() { + for isBillingAgreementsEnabled in [true, false] { + let configurationJSON = BTJSON(value: [ + "paypal": [ "billingAgreementsEnabled": isBillingAgreementsEnabled] + ]) + let configuration = BTConfiguration(json: configurationJSON) + XCTAssertTrue(configuration.isBillingAgreementsEnabled == isBillingAgreementsEnabled) + } + } + + // MARK: - Apple Pay category methods + + func testIsApplePayEnabled_whenApplePayStatusFromConfigurationJSONIsAString_returnsTrue() { + for applePayStatus in ["mock", "production", "asdfasdf"] { + let configurationJSON = BTJSON(value: [ + "applePay": [ "status": applePayStatus ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertTrue(configuration.isApplePayEnabled) + } + } + + func testIsApplePayEnabled_whenApplePayStatusFromConfigurationJSONIsGarbage_returnsFalse() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "status": 3.14 ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertFalse(configuration.isApplePayEnabled) + } + + func testIsApplePayEnabled_whenApplePayStatusFromConfigurationJSONIsOff_returnsFalse() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "status": "off" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertFalse(configuration.isApplePayEnabled) + } + + func testApplePayCountryCode_returnsCountryCode() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "countryCode": "US" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePayCountryCode!, "US") + } + + func testApplePayCurrencyCode_returnsCurrencyCode() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "currencyCode": "USD" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePayCurrencyCode!, "USD") + } + + func testApplePayMerchantIdentifier_returnsMerchantIdentifier() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "merchantIdentifier": "com.merchant.braintree-unit-tests" ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePayMerchantIdentifier!, "com.merchant.braintree-unit-tests") + } + + func testApplePaySupportedNetworks_returnsSupportedNetworks() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "supportedNetworks": ["visa", "mastercard", "amex"] ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePaySupportedNetworks!, [PKPaymentNetwork.visa, PKPaymentNetwork.masterCard, PKPaymentNetwork.amex]) + } + + func testApplePaySupportedNetworks_whenRunningBelowiOS9_doesNotReturnDiscover() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "supportedNetworks": ["discover"] ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + guard #available(iOS 9, *) else { + XCTAssertEqual(configuration.applePaySupportedNetworks!, []) + return + } + } + + @available(iOS 9.0, *) + func testApplePaySupportedNetworks_whenSupportedNetworksIncludesDiscover_returnsSupportedNetworks() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "supportedNetworks": ["discover"] ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePaySupportedNetworks!, [PKPaymentNetwork.discover]) + } + + func testApplePaySupportedNetworks_doesNotPassesThroughUnknownValuesFromConfiguration() { + let configurationJSON = BTJSON(value: [ + "applePay": [ "supportedNetworks": ["ChinaUnionPay", "Interac", "PrivateLabel"] ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertEqual(configuration.applePaySupportedNetworks!, []) + + } + + // MARK: - UnionPay category methods + + func testIsUnionPayEnabled_whenUnionPayEnabledFromConfigurationJSONIsTrue_returnsTrue() { + let configurationJSON = BTJSON(value: [ + "unionPay": [ "enabled": true ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertTrue(configuration.isUnionPayEnabled) + } + + func testIsUnionPayEnabled_whenUnionPayEnabledFromConfigurationJSONIsFalse_returnsFalse() { + let configurationJSON = BTJSON(value: [ + "unionPay": [ "enabled": false ] + ]) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertFalse(configuration.isUnionPayEnabled) + + } + + func testIsUnionPayEnabled_whenUnionPayEnabledFromConfigurationJSONIsMissing_returnsFalse() { + let configurationJSON = BTJSON(value: []) + let configuration = BTConfiguration(json: configurationJSON) + + XCTAssertFalse(configuration.isUnionPayEnabled) + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDataCollector_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDataCollector_Tests.swift new file mode 100755 index 00000000..3a7b0d58 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDataCollector_Tests.swift @@ -0,0 +1,272 @@ +import XCTest +import PayPalDataCollector +// FIXME: comparison operators with optionals were removed from the Swift Standard Libary. +// Consider refactoring the code to use the non-optional operators. +fileprivate func < (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l < r + case (nil, _?): + return true + default: + return false + } +} + +// FIXME: comparison operators with optionals were removed from the Swift Standard Libary. +// Consider refactoring the code to use the non-optional operators. +fileprivate func >= (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l >= r + default: + return !(lhs < rhs) + } +} + +// FIXME: comparison operators with optionals were removed from the Swift Standard Libary. +// Consider refactoring the code to use the non-optional operators. +fileprivate func > (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l > r + default: + return rhs < lhs + } +} + + +class BTDataCollector_Tests: XCTestCase { + + var testDelegate: TestDelegateForBTDataCollector? + + /// We check the delegate because it's the only exposed property of the dataCollector + func testInitsWithNilDelegate() { + let dataCollector = BTDataCollector(environment: BTDataCollectorEnvironment.sandbox) + XCTAssertNil(dataCollector.delegate) + } + + func testSuccessfullyCollectsCardDataAndCallsDelegateMethods() { + let dataCollector = BTDataCollector(environment: .sandbox) + testDelegate = TestDelegateForBTDataCollector(didStartExpectation: expectation(description: "didStart"), didCompleteExpectation: expectation(description: "didComplete")) + dataCollector.delegate = testDelegate + let stubKount = FakeDeviceCollectorSDK() + dataCollector.kount = stubKount + + let jsonString = dataCollector.collectCardFraudData() + + let data = jsonString.data(using: String.Encoding.utf8) + let dictionary = try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! Dictionary + XCTAssert((dictionary["device_session_id"] as! String).characters.count >= 32) + XCTAssertEqual(dictionary["fraud_merchant_id"] as? String, "600000") // BTDataCollectorSharedMerchantId + waitForExpectations(timeout: 10, handler: nil) + } + + /// Ensure that both Kount and PayPal data can be collected together + func testCollectFraudData() { + let dataCollector = BTDataCollector(environment: .sandbox) + testDelegate = TestDelegateForBTDataCollector(didStartExpectation: expectation(description: "didStart"), didCompleteExpectation: expectation(description: "didComplete")) + dataCollector.delegate = testDelegate + let stubKount = FakeDeviceCollectorSDK() + dataCollector.kount = stubKount + BTDataCollector.setPayPalDataCollectorClass(FakePPDataCollector.self) + + let jsonString = dataCollector.collectFraudData() + + let data = jsonString.data(using: String.Encoding.utf8) + let dictionary = try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! Dictionary + XCTAssert((dictionary["device_session_id"] as! String).characters.count >= 32) + XCTAssertEqual(dictionary["fraud_merchant_id"] as? String, "600000") // BTDataCollectorSharedMerchantId + + // Ensure correlation_id (clientMetadataId) is not nil and has a length of at least 12. + // This is just a guess of a reasonable id length. In practice, the id + // typically has a length of 32. + XCTAssertEqual(dictionary["correlation_id"] as? String, "fakeclientmetadataid") + + waitForExpectations(timeout: 2, handler: nil) + } + + func testCollectCardFraudData_doesNotReturnCorrelationId() { + let config = [ + "environment":"development" as AnyObject, + "kount": [ + "enabled": true, + "kountMerchantId": "500000" + ] + ] as [String : Any] + let apiClient = clientThatReturnsConfiguration(config as [String : AnyObject]) + + let dataCollector = BTDataCollector(apiClient: apiClient) + let expectation = self.expectation(description: "Returns fraud data") + + dataCollector.collectCardFraudData { (fraudData: String) in + let json = BTJSON(data: fraudData.data(using: String.Encoding.utf8)!) + XCTAssertNil(json["correlation_id"] as? String) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testOverrideMerchantId_usesMerchantProvidedId() { + let config = [ + "environment":"development", + "kount": [ + "enabled": true, + "kountMerchantId": "500000" + ] + ] as [String : Any] + + let apiClient = clientThatReturnsConfiguration(config as [String : AnyObject]) + + let dataCollector = BTDataCollector(apiClient: apiClient) + dataCollector.setFraudMerchantId("500001") + let expectation = self.expectation(description: "Returns fraud data") + + dataCollector.collectFraudData { (fraudData: String) in + let json = BTJSON(data: fraudData.data(using: String.Encoding.utf8)!) + XCTAssertEqual((json["fraud_merchant_id"] as AnyObject).asString(), "500001") + XCTAssert((json["device_session_id"] as AnyObject).asString()?.characters.count >= 32) + XCTAssert((json["correlation_id"] as AnyObject).asString()?.characters.count > 0) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testCollectFraudDataWithCompletionBlock_whenMerchantHasKountConfiguration_usesConfiguration() { + let config = [ + "environment": "development" as AnyObject, + "kount": [ + "enabled": true, + "kountMerchantId": "500000" + ] + ] as [String : Any] + let apiClient = clientThatReturnsConfiguration(config as [String : AnyObject]) + let dataCollector = BTDataCollector(apiClient: apiClient) + + let expectation = self.expectation(description: "Returns fraud data") + dataCollector.collectFraudData { fraudData in + let json = BTJSON(data: fraudData.data(using: String.Encoding.utf8)!) + XCTAssertEqual((json["fraud_merchant_id"] as AnyObject).asString(), "500000") + XCTAssert((json["device_session_id"] as AnyObject).asString()!.characters.count >= 32) + XCTAssert((json["correlation_id"] as AnyObject).asString()!.characters.count > 0) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testCollectFraudDataWithCompletionBlock_whenMerchantHasKountConfiguration_setsMerchantIDOnKount() { + let config = [ + "environment": "sandbox", + "kount": [ + "enabled": true, + "kountMerchantId": "500000" + ] + ] as [String : Any] + let apiClient = clientThatReturnsConfiguration(config as [String : AnyObject]) + let dataCollector = BTDataCollector(apiClient: apiClient) + let stubKount = FakeDeviceCollectorSDK() + dataCollector.kount = stubKount + + let expectation = self.expectation(description: "Returns fraud data") + dataCollector.collectFraudData { fraudData in + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertEqual(500000, stubKount.merchantID) + XCTAssertEqual(KEnvironment.test, stubKount.environment) + } + + func testCollectFraudData_doesNotCollectKountDataIfDisabledInConfiguration() { + let apiClient = clientThatReturnsConfiguration([ + "environment":"development" as AnyObject + ]) + + let dataCollector = BTDataCollector(apiClient: apiClient) + let expectation = self.expectation(description: "Returns fraud data") + dataCollector.collectFraudData { fraudData in + let json = BTJSON(data: fraudData.data(using: String.Encoding.utf8)!) + XCTAssertNil(json["fraud_merchant_id"] as? String) + XCTAssertNil(json["device_session_id"] as? String) + XCTAssert((json["correlation_id"] as AnyObject).asString()?.characters.count > 0) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } +} + +func clientThatReturnsConfiguration(_ configuration: [String:AnyObject]) -> BTAPIClient { + let apiClient = BTAPIClient(authorization: "development_tokenization_key", sendAnalyticsEvent: false)! + let fakeHttp = BTFakeHTTP()! + let cannedConfig = BTJSON(value: configuration) + fakeHttp.cannedConfiguration = cannedConfig + fakeHttp.cannedStatusCode = 200 + apiClient.configurationHTTP = fakeHttp + + return apiClient +} + +class TestDelegateForBTDataCollector: NSObject, BTDataCollectorDelegate { + + var didStartExpectation: XCTestExpectation? + var didCompleteExpectation: XCTestExpectation? + + var didFailExpectation: XCTestExpectation? + var error: NSError? + + init(didStartExpectation: XCTestExpectation, didCompleteExpectation: XCTestExpectation) { + self.didStartExpectation = didStartExpectation + self.didCompleteExpectation = didCompleteExpectation + } + + init(didFailExpectation: XCTestExpectation) { + self.didFailExpectation = didFailExpectation + } + + func dataCollectorDidStart(_ dataCollector: BTDataCollector) { + didStartExpectation?.fulfill() + } + + func dataCollectorDidComplete(_ dataCollector: BTDataCollector) { + didCompleteExpectation?.fulfill() + } + + func dataCollector(_ dataCollector: BTDataCollector, didFailWithError error: Error) { + self.error = error as NSError + self.didFailExpectation?.fulfill() + } +} + +class FakeDeviceCollectorSDK: KDataCollector { + + var lastCollectSessionID: String? + var forceError = false + + override func collect(forSession sessionID: String, completion completionBlock: ((String, Bool, Error?) -> Void)? = nil) { + lastCollectSessionID = sessionID + if forceError { + completionBlock?("1981", false, NSError(domain: "Fake", code: 1981, userInfo: nil)) + } else { + completionBlock?(sessionID, true, nil) + } + } +} + +class FakePPDataCollector: PPDataCollector { + + static var didGetClientMetadataID = false + + override class func generateClientMetadataID() -> String { + return generateClientMetadataID(nil) + } + + override class func generateClientMetadataID(_ pairingID: String?) -> String { + didGetClientMetadataID = true + return "fakeclientmetadataid" + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInErrorState_Tests.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInErrorState_Tests.m new file mode 100755 index 00000000..4bca235a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInErrorState_Tests.m @@ -0,0 +1,122 @@ +#import +#import "BTHTTP.h" +#import "BTDropInErrorState.h" +#import "BTErrors.h" +#import "BTUICardFormView.h" + +@interface BTDropInErrorState_Tests : XCTestCase + +@end + +@implementation BTDropInErrorState_Tests + +- (void)testErrorTitle_returnsErrorTitleBasedOnNSErrorTopLevelErrorMessage { + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ + @"field": @"cvv", + @"message": @"CVV is required" } + ] + }]}; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTHTTPErrorDomain + code:BTHTTPErrorCodeClientError + userInfo:userInfo]; + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + XCTAssertEqualObjects(state.errorTitle, @"Credit Card is Invalid"); +} + +- (void)testErrorTitle_whenThereAreNoFieldErrorsAssociated_returnsErrorTitleBasedOnNSErrorTopLevelErrorMessage { + NSDictionary *validationErrors = @{ @"error": @{ @"message": @"Everything is Invalid" } }; + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTHTTPErrorDomain + code:BTHTTPErrorCodeClientError + userInfo:userInfo]; + + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + XCTAssertEqualObjects(state.errorTitle, @"Everything is Invalid"); +} + +- (void)testHighlightedFields_whenErrorUserInfoHasFieldErrors_returnsSetOfFieldsWithValidationErrorsAssociated { + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ @"field": @"cvv", + @"message": @"CVV is required" }, + @{ @"field": @"billingAddress", + @"fieldErrors": @[@{ @"field": @"postalCode", + @"message": @"Postal Code is required" }], + }, + @{ @"field": @"number", + @"message": @"Number is required" }, + @{ @"field": @"expirationDate", + @"message": @"Expiration date is required" }, + ] + }]}; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTHTTPErrorDomain + code:BTHTTPErrorCodeClientError + userInfo:userInfo]; + + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + + XCTAssertTrue(state.highlightedFields.count == 4); + XCTAssertTrue([state.highlightedFields containsObject:@(BTUICardFormFieldNumber)]); + XCTAssertTrue([state.highlightedFields containsObject:@(BTUICardFormFieldExpiration)]); + XCTAssertTrue([state.highlightedFields containsObject:@(BTUICardFormFieldCvv)]); + XCTAssertTrue([state.highlightedFields containsObject:@(BTUICardFormFieldPostalCode)]); +} + +- (void)testHighlightedFields_whenErrorUserInfoHasNoFieldErrors_returnsEmptySet { + NSDictionary *validationErrors = @{@"error": @{ + @"message": @"Credit Card is Invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ @"field": @"paymentMethodNonce", + @"message": @"Payment method nonces cannot be used to update an existing card." }, + ] + }]}; + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTHTTPErrorDomain + code:BTHTTPErrorCodeClientError + userInfo:userInfo]; + + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + + XCTAssertTrue(state.highlightedFields.count == 0); +} + +- (void)testHighlightedFields_whenErrorContainsUnknownFields_ignoresThem { + NSDictionary *validationErrors = @{@"error": @{ @"message": @"Everything is invalid" }, + @"fieldErrors": @[ + @{ + @"field": @"creditCard", + @"fieldErrors": @[ + @{ @"field": @"unknownField", + @"message": @"You can't highlight what you can't understand!" }, + ] + }] + }; + + NSDictionary *userInfo = @{BTCustomerInputBraintreeValidationErrorsKey: validationErrors}; + NSError *error = [[NSError alloc] initWithDomain:BTHTTPErrorDomain + code:BTHTTPErrorCodeClientError + userInfo:userInfo]; + + BTDropInErrorState *state = [[BTDropInErrorState alloc] initWithError:error]; + + XCTAssertTrue(state.highlightedFields.count == 0); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInUtil_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInUtil_Tests.swift new file mode 100755 index 00000000..ccaced2a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInUtil_Tests.swift @@ -0,0 +1,17 @@ +import XCTest + +class BTDropInUtil_Tests: XCTestCase { + func testBTDropInUtil_topViewControllerReturnsViewController() { + let topInitialTopController = BTDropInUtil.topViewController() + XCTAssertNotNil(topInitialTopController, "Top UIViewController should not be nil") + + let windowRootController = UIViewController() + let secondWindow = UIWindow(frame: UIScreen.main.bounds) + secondWindow.rootViewController = windowRootController + secondWindow.makeKeyAndVisible() + secondWindow.windowLevel = 100 + let topSecondTopController = BTDropInUtil.topViewController() + XCTAssertNotEqual(topInitialTopController, topSecondTopController) + XCTAssertEqual(windowRootController, topSecondTopController) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInViewController_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInViewController_Tests.swift new file mode 100755 index 00000000..81f4b4e5 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTDropInViewController_Tests.swift @@ -0,0 +1,215 @@ +import XCTest + +class BTDropInViewController_Tests: XCTestCase { + + class BTDropInViewControllerTestDelegate : NSObject, BTDropInViewControllerDelegate { + var didLoadExpectation: XCTestExpectation + + init(didLoadExpectation: XCTestExpectation) { + self.didLoadExpectation = didLoadExpectation + } + + @objc func drop(_ viewController: BTDropInViewController, didSucceedWithTokenization paymentMethodNonce: BTPaymentMethodNonce) {} + + @objc func drop(inViewControllerDidCancel viewController: BTDropInViewController) {} + + @objc func drop(inViewControllerDidLoad viewController: BTDropInViewController) { + didLoadExpectation.fulfill() + } + } + + var window : UIWindow! + var viewController : UIViewController! + let ValidClientToken = "eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiI3ODJhZmFlNDJlZTNiNTA4NWUxNmMzYjhkZTY3OGQxNTJhODFlYzk5MTBmZDNhY2YyYWU4MzA2OGI4NzE4YWZhfGNyZWF0ZWRfYXQ9MjAxNS0wOC0yMFQwMjoxMTo1Ni4yMTY1NDEwNjErMDAwMFx1MDAyNmN1c3RvbWVyX2lkPTM3OTU5QTE5LThCMjktNDVBNC1CNTA3LTRFQUNBM0VBOEM4Nlx1MDAyNm1lcmNoYW50X2lkPWRjcHNweTJicndkanIzcW5cdTAwMjZwdWJsaWNfa2V5PTl3d3J6cWszdnIzdDRuYzgiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJjaGFsbGVuZ2VzIjpbXSwiZW52aXJvbm1lbnQiOiJzYW5kYm94IiwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzL2RjcHNweTJicndkanIzcW4vY2xpZW50X2FwaSIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vY2xpZW50LWFuYWx5dGljcy5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIn0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsInRocmVlRFNlY3VyZSI6eyJsb29rdXBVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi90aHJlZV9kX3NlY3VyZS9sb29rdXAifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImRpc3BsYXlOYW1lIjoiQWNtZSBXaWRnZXRzLCBMdGQuIChTYW5kYm94KSIsImNsaWVudElkIjpudWxsLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJhbGxvd0h0dHAiOnRydWUsImVudmlyb25tZW50Tm9OZXR3b3JrIjp0cnVlLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjpmYWxzZSwibWVyY2hhbnRBY2NvdW50SWQiOiJzdGNoMm5mZGZ3c3p5dHc1IiwiY3VycmVuY3lJc29Db2RlIjoiVVNEIn0sImNvaW5iYXNlRW5hYmxlZCI6dHJ1ZSwiY29pbmJhc2UiOnsiY2xpZW50SWQiOiIxMWQyNzIyOWJhNThiNTZkN2UzYzAxYTA1MjdmNGQ1YjQ0NmQ0ZjY4NDgxN2NiNjIzZDI1NWI1NzNhZGRjNTliIiwibWVyY2hhbnRBY2NvdW50IjoiY29pbmJhc2UtZGV2ZWxvcG1lbnQtbWVyY2hhbnRAZ2V0YnJhaW50cmVlLmNvbSIsInNjb3BlcyI6ImF1dGhvcml6YXRpb25zOmJyYWludHJlZSB1c2VyIiwicmVkaXJlY3RVcmwiOiJodHRwczovL2Fzc2V0cy5icmFpbnRyZWVnYXRld2F5LmNvbS9jb2luYmFzZS9vYXV0aC9yZWRpcmVjdC1sYW5kaW5nLmh0bWwiLCJlbnZpcm9ubWVudCI6Im1vY2sifSwibWVyY2hhbnRJZCI6ImRjcHNweTJicndkanIzcW4iLCJ2ZW5tbyI6Im9mZmxpbmUiLCJhcHBsZVBheSI6eyJzdGF0dXMiOiJtb2NrIiwiY291bnRyeUNvZGUiOiJVUyIsImN1cnJlbmN5Q29kZSI6IlVTRCIsIm1lcmNoYW50SWRlbnRpZmllciI6Im1lcmNoYW50LmNvbS5icmFpbnRyZWVwYXltZW50cy5zYW5kYm94LkJyYWludHJlZS1EZW1vIiwic3VwcG9ydGVkTmV0d29ya3MiOlsidmlzYSIsIm1hc3RlcmNhcmQiLCJhbWV4Il19fQ==" + + override func setUp() { + super.setUp() + + viewController = UIApplication.shared.windows[0].rootViewController + } + + override func tearDown() { + if viewController.presentedViewController != nil { + viewController.dismiss(animated: false, completion: nil) + } + + super.tearDown() + } + + func testInitializesWithCheckoutRequestCorrectly() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let request = BTPaymentRequest() + let dropInViewController = BTDropInViewController(apiClient: apiClient) + dropInViewController.paymentRequest = request + XCTAssertEqual(request, dropInViewController.paymentRequest) + XCTAssertEqual(apiClient.tokenizationKey, dropInViewController.apiClient.tokenizationKey) + + // By default, Drop-in does not set any bar button items. The developer should embed Drop-in in a navigation controller + // as seen in BraintreeDemoDropInViewController, or provide some other way to dismiss Drop-in. + XCTAssertNil(dropInViewController.navigationItem.leftBarButtonItem) + XCTAssertNil(dropInViewController.navigationItem.rightBarButtonItem) + + let didLoadExpectation = self.expectation(description: "Drop-in did finish loading") + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) // for strong reference + dropInViewController.delegate = testDelegate + + DispatchQueue.main.async { () -> Void in + self.viewController.present(dropInViewController, animated: false, completion: nil) + } + + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testInitializesWithoutCheckoutRequestCorrectly() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let request = BTPaymentRequest() + + // When this is true, the call to action control will be hidden from Drop-in's content view. Instead, a submit button will be + // added as a navigation bar button item. The default value is false. + request.shouldHideCallToAction = true + + let dropInViewController = BTDropInViewController(apiClient: apiClient) + dropInViewController.paymentRequest = request + + XCTAssertEqual(request, dropInViewController.paymentRequest) + XCTAssertEqual(apiClient.tokenizationKey, dropInViewController.apiClient.tokenizationKey) + XCTAssertNil(dropInViewController.navigationItem.leftBarButtonItem) + + // There will be a rightBarButtonItem instead of a call to action control because it has been set to hide. + XCTAssertNotNil(dropInViewController.navigationItem.rightBarButtonItem) + + let didLoadExpectation = self.expectation(description: "Drop-in did finish loading") + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) // for strong reference + dropInViewController.delegate = testDelegate + + DispatchQueue.main.async { () -> Void in + self.viewController.present(dropInViewController, animated: false, completion: nil) + } + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testDropIn_canSetNewCheckoutRequestAfterPresentation() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let request = BTPaymentRequest() + let dropInViewController = BTDropInViewController(apiClient: apiClient) + dropInViewController.paymentRequest = request + XCTAssertEqual(request, dropInViewController.paymentRequest) + XCTAssertEqual(apiClient.tokenizationKey, dropInViewController.apiClient.tokenizationKey) + + // By default, Drop-in does not set any bar button items. The developer should embed Drop-in in a navigation controller + // as seen in BraintreeDemoDropInViewController, or provide some other way to dismiss Drop-in. + XCTAssertNil(dropInViewController.navigationItem.leftBarButtonItem) + XCTAssertNil(dropInViewController.navigationItem.rightBarButtonItem) + + let didLoadExpectation = self.expectation(description: "Drop-in did finish loading") + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) // for strong reference + dropInViewController.delegate = testDelegate + + DispatchQueue.main.async { () -> Void in + self.viewController.present(dropInViewController, animated: false, completion: nil) + } + self.waitForExpectations(timeout: 5, handler: nil) + + let newRequest = BTPaymentRequest() + newRequest.shouldHideCallToAction = true + dropInViewController.paymentRequest = newRequest + XCTAssertNil(dropInViewController.navigationItem.leftBarButtonItem) + + // There will now be a rightBarButtonItem because shouldHideCallToAction = true; this button is the replacement + // of the call to action control. + XCTAssertNotNil(dropInViewController.navigationItem.rightBarButtonItem) + } + + func testDropIn_addPaymentMethodViewController_hidesCTA() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let dropInViewController = BTDropInViewController(apiClient: apiClient) + let addPaymentMethodDropInViewController = dropInViewController.addPaymentMethod() + XCTAssertTrue((addPaymentMethodDropInViewController?.paymentRequest!.shouldHideCallToAction)!) + XCTAssertNotNil(addPaymentMethodDropInViewController?.navigationItem.rightBarButtonItem) + + let didLoadExpectation = self.expectation(description: "Add payment method view controller did finish loading") + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) // for strong reference + addPaymentMethodDropInViewController?.delegate = testDelegate + + DispatchQueue.main.async { () -> Void in + self.viewController.present(addPaymentMethodDropInViewController!, animated: false, completion: nil) + } + + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testDropIn_whenPresentViewControllersFromTopIsTrue_presentsViewControllersFromTopViewController() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let dropInViewController = BTDropInViewController(apiClient: apiClient) + let paymentRequest = BTPaymentRequest() + paymentRequest.presentViewControllersFromTop = true + dropInViewController.paymentRequest = paymentRequest + let mockViewController = UIViewController() + let windowRootController = UIViewController() + let secondWindow = UIWindow(frame: UIScreen.main.bounds) + secondWindow.rootViewController = windowRootController + secondWindow.makeKeyAndVisible() + secondWindow.windowLevel = 100 + let topSecondTopController = BTDropInUtil.topViewController() + + dropInViewController.paymentDriver(nil, requestsPresentationOf: mockViewController) + + let expectation = self.expectation(description: "Sleeping for presentation") + DispatchQueue.global(qos: .background).async { + sleep(1) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertEqual(mockViewController.presentingViewController, topSecondTopController) + } + + // MARK: - Metadata + + func testAPIClientMetadata_afterInstantiation_hasIntegrationSetToDropIn() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let dropIn = BTDropInViewController(apiClient: apiClient) + + XCTAssertEqual(dropIn.apiClient.metadata.integration, BTClientMetadataIntegrationType.dropIn) + } + + func testAPIClientMetadata_afterInstantiation_hasSourceSetToOriginalAPIClientMetadataSource() { + var apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + apiClient = apiClient.copy(with: BTClientMetadataSourceType.unknown, integration: BTClientMetadataIntegrationType.custom) + let dropIn = BTDropInViewController(apiClient: apiClient) + + XCTAssertEqual(dropIn.apiClient.metadata.source, BTClientMetadataSourceType.unknown) + } + + // MARK: - Payment method fetching + + func testFetchPaymentMethods_byDefault_doesNotCallAPIClientWithDefaultSortedFirst() { + let mockAPIClient = MockAPIClient(authorization: ValidClientToken)! + let dropIn = BTDropInViewController(apiClient: mockAPIClient) + + let expectation = self.expectation(description: "Callback invoked") + dropIn.fetchPaymentMethods { () -> Void in + XCTAssertTrue(mockAPIClient.didFetchPaymentMethods(sorted: false)) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testFetchPaymentMethods_sortDefaultFirstOverriden_callsAPIClientWithDefaultSortedFirst() { + let mockAPIClient = MockAPIClient(authorization: ValidClientToken)! + let paymentRequest = BTPaymentRequest() + paymentRequest.showDefaultPaymentMethodNonceFirst = false + let dropIn = BTDropInViewController(apiClient: mockAPIClient) + dropIn.paymentRequest = paymentRequest + + let expectation = self.expectation(description: "Callback invoked") + dropIn.fetchPaymentMethods { () -> Void in + XCTAssertTrue(mockAPIClient.didFetchPaymentMethods(sorted: false)) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTHTTPSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTHTTPSpec.m new file mode 100755 index 00000000..0997e915 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTHTTPSpec.m @@ -0,0 +1,1011 @@ +#import "BTHTTP.h" +#import "BTHTTPTestProtocol.h" +#import "BTSpecHelper.h" +#import +#import + +NSURL *validDataURL() { + NSDictionary *validObject = @{@"clientId":@"a-client-id", @"nest": @{@"nested":@"nested-value"}}; + NSError *jsonSerializationError; + NSData *configurationData = [NSJSONSerialization dataWithJSONObject:validObject + options:0 + error:&jsonSerializationError]; + NSString *base64EncodedConfigurationData = [configurationData base64EncodedStringWithOptions:0]; + NSString *dataURLString = [NSString stringWithFormat:@"data:application/json;base64,%@", base64EncodedConfigurationData]; + return [NSURL URLWithString:dataURLString]; +} + +NSDictionary *parameterDictionary() { + return @{@"stringParameter": @"value", + @"crazyStringParameter[]": @"crazy%20and&value", + @"numericParameter": @42, + @"trueBooleanParameter": @YES, + @"falseBooleanParameter": @NO, + @"dictionaryParameter": @{ @"dictionaryKey": @"dictionaryValue" }, + @"arrayParameter": @[@"arrayItem1", @"arrayItem2"] + }; +} + +void withStub(void (^block)(void (^removeStub)(void))) { + id stub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + NSData *jsonResponse = [NSJSONSerialization dataWithJSONObject:@{@"requestHeaders": [request allHTTPHeaderFields]} options:NSJSONWritingPrettyPrinted error:nil]; + return [OHHTTPStubsResponse responseWithData:jsonResponse statusCode:200 headers:@{@"Content-Type": @"application/json"}]; + }]; + + block(^{ + [OHHTTPStubs removeStub:stub]; + }); +} + +NSURLSession *testURLSession() { + NSURLSessionConfiguration *testConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + [testConfiguration setProtocolClasses:@[[BTHTTPTestProtocol class]]]; + return [NSURLSession sessionWithConfiguration:testConfiguration]; +} + +@interface BTHTTPSpec : XCTestCase +@end + +@implementation BTHTTPSpec { + BTHTTP *http; + id stubDescriptor; +} + +#pragma mark - performing a request + +- (void)setUp { + [super setUp]; + + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] authorizationFingerprint:@"test-authorization-fingerprint"]; + http.session = testURLSession(); +} + +- (void)tearDown { + [OHHTTPStubs removeAllStubs]; + + [super tearDown]; +} + +#pragma mark - base URL + +- (void)testRequests_useTheSpecifiedURLScheme { + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + + [http GET:@"200.json" completion:^(BTJSON *body, __unused NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + XCTAssertEqualObjects(httpRequest.URL.scheme, @"bt-http-test"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testRequests_useTheHostAtTheBaseURL { + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + expect(httpRequest.URL.absoluteString).to.startWith(@"bt-http-test://base.example.com:1234/base/path/200.json"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testItAppendsThePathToTheBaseURL { + waitUntil(^(DoneCallback done){ + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + expect(httpRequest.URL.path).to.equal(@"/base/path/200.json"); + done(); + }]; + }); +} + +- (void)test_whenThePathIsNil_itHitsTheBaseURL { + waitUntil(^(DoneCallback done){ + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + expect(httpRequest.URL.path).to.equal(@"/base/path"); + done(); + }]; + }); + + pending(@"returns a json serialization error if the parameters cannot be serialized"); + pending(@"appends the authorization fingerprint to all requests"); +} + +#pragma mark - data base URLs + +- (void)testReturnsTheData { + waitUntil(^(DoneCallback done) { + http = [[BTHTTP alloc] initWithBaseURL:validDataURL() authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + expect([body[@"clientId"] asString]).to.equal(@"a-client-id"); + expect([body[@"nest"][@"nested"] asString]).to.equal(@"nested-value"); + done(); + }]; + }); +} + +- (void)testIgnoresPOSTData { + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + http = [[BTHTTP alloc] initWithBaseURL:validDataURL() authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http POST:@"/" parameters:@{@"a-post-param":@"POST"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + expect(response.statusCode).to.equal(200); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testIgnoresGETParameters { + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + http = [[BTHTTP alloc] initWithBaseURL:validDataURL() authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http GET:@"/" parameters:@{@"a-get-param": @"GET"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + + expect(response.statusCode).to.equal(200); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} +- (void)testIgnoresTheSpecifiedPath { + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + http = [[BTHTTP alloc] initWithBaseURL:validDataURL() authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http GET:@"/resource" completion:^(__unused BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + + expect(response.statusCode).to.equal(200); + expect(error).to.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testSetsTheContentTypeHeader { + NSURL *dataURL = [NSURL URLWithString:@"data:text/plain;base64,SGVsbG8sIFdvcmxkIQo="]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + http = [[BTHTTP alloc] initWithBaseURL:dataURL authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(body); + XCTAssertNil(response); + expect(error.domain).to.equal(BTHTTPErrorDomain); + expect(error.code).to.equal(BTHTTPErrorCodeResponseContentTypeNotAcceptable); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testSetsTheResponseStatusCode { + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + http = [[BTHTTP alloc] initWithBaseURL:validDataURL() authorizationFingerprint:@"test-authorization-fingerprint"]; + + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + expect(response.statusCode).notTo.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testFailsLikeAnHTTP500WhenTheBase64EncodedDataIsInvalid { + XCTestExpectation *expectation = [self expectationWithDescription:@"Perform request"]; + + NSString *dataURLString = [NSString stringWithFormat:@"data:application/json;base64,%@", @"BAD-BASE-64-STRING"]; + + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:dataURLString] authorizationFingerprint:@"test-authorization-fingerprint"]; + [http GET:@"/" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(body); + XCTAssertNil(response); + XCTAssertNotNil(error); + + expect(response).to.beNil(); + expect(error).notTo.beNil(); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +#pragma mark - HTTP methods + +- (void)testSendsGETRequest { + waitUntil(^(DoneCallback done){ + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"/200.json$"); + expect(httpRequest.HTTPMethod).to.equal(@"GET"); + expect(httpRequest.HTTPBody).to.beNil(); + done(); + }]; + }); +} + +- (void)testSendsGETRequestWithParameters { + waitUntil(^(DoneCallback done){ + [http GET:@"200.json" parameters:@{@"param": @"value"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"/200.json$"); + expect(httpRequest.URL.query).to.contain(@"param=value"); + expect(httpRequest.HTTPMethod).to.equal(@"GET"); + expect(httpRequest.HTTPBody).to.beNil(); + done(); + }]; + }); +} + +- (void)testSendsPOSTRequest { + waitUntil(^(DoneCallback done) { + [http POST:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"/200.json$"); + expect(httpRequest.HTTPBody).to.beNil(); + expect(httpRequest.HTTPMethod).to.equal(@"POST"); + expect(httpRequest.URL.query).to.beNil(); + done(); + }]; + }); +} + +- (void)testSendsPOSTRequestWithParameters { + waitUntil(^(DoneCallback done) { + [http POST:@"200.json" parameters:@{@"param": @"value"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSString *httpRequestBody = [BTHTTPTestProtocol parseRequestBodyFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"/200.json$"); + BTJSON *json = [[BTJSON alloc] initWithData:[httpRequestBody dataUsingEncoding:NSUTF8StringEncoding]]; + expect([json[@"param"] asString]).to.equal(@"value"); + expect(httpRequest.HTTPMethod).to.equal(@"POST"); + expect(httpRequest.URL.query).to.beNil(); + done(); + }]; + }); +} + +- (void)testSendsPUTRequest { + waitUntil(^(DoneCallback done) { + [http PUT:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"200.json$"); + expect(httpRequest.HTTPBody).to.beNil(); + expect(httpRequest.HTTPMethod).to.equal(@"PUT"); + expect(httpRequest.URL.query).to.beNil(); + done(); + }]; + }); +} + +- (void)testSendsPUTRequestWithParameters { + waitUntil(^(DoneCallback done) { + [http PUT:@"200.json" parameters:@{@"param": @"value"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSString *httpRequestBody = [BTHTTPTestProtocol parseRequestBodyFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"200.json$"); + BTJSON *json = [[BTJSON alloc] initWithData:[httpRequestBody dataUsingEncoding:NSUTF8StringEncoding]]; + expect([json[@"param"] asString]).to.equal(@"value"); + expect(httpRequest.HTTPMethod).to.equal(@"PUT"); + expect(httpRequest.URL.query).to.beNil(); + done(); + }]; + }); +} + + +- (void)testSendsADELETERequest { + waitUntil(^(DoneCallback done){ + [http DELETE:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.path).to.match(@"200.json$"); + expect(httpRequest.HTTPBody).to.beNil(); + expect(httpRequest.HTTPMethod).to.equal(@"DELETE"); + done(); + }]; + }); +} + +- (void)testSendsDELETERequestWithParameters { + waitUntil(^(DoneCallback done) { + [http DELETE:@"200.json" parameters:@{@"param": @"value"} completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + expect(httpRequest.URL.path).to.match(@"/200.json$"); + expect(httpRequest.URL.query).to.contain(@"param=value"); + expect(httpRequest.HTTPMethod).to.equal(@"DELETE"); + expect(httpRequest.HTTPBody).to.beNil(); + done(); + }]; + }); +} + +#pragma mark Authentication + +- (void)testGETRequests_whenBTHTTPInitializedWithAuthorizationFingerprint_sendAuthorizationInQueryParams { + waitUntil(^(DoneCallback done){ + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.query).to.equal(@"authorization_fingerprint=test-authorization-fingerprint"); + + done(); + }]; + }); +} + +- (void)testGETRequests_whenBTHTTPInitializedWithTokenizationKey_sendTokenizationKeyInHeader { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] tokenizationKey:@"development_tokenization_key"]; + http.session = testURLSession(); + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + + XCTAssertEqualObjects(httpRequest.allHTTPHeaderFields[@"Client-Key"], @"development_tokenization_key"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testPOSTRequests_whenBTHTTPInitializedWithAuthorizationFingerprint_sendAuthorizationInBody { + waitUntil(^(DoneCallback done){ + [http POST:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSString *httpRequestBody = [BTHTTPTestProtocol parseRequestBodyFromTestResponseBody:body]; + expect(httpRequestBody).to.equal(@"{\"authorization_fingerprint\":\"test-authorization-fingerprint\"}"); + + done(); + }]; + }); +} + +- (void)testPOSTRequests_whenBTHTTPInitializedWithTokenizationKey_sendAuthorization { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] tokenizationKey:@"development_tokenization_key"]; + http.session = testURLSession(); + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + [http POST:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + XCTAssertEqualObjects(httpRequest.allHTTPHeaderFields[@"Client-Key"], @"development_tokenization_key"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testPUTRequests_whenBTHTTPInitializedWithAuthorizationFingerprint_sendAuthorizationInBody { + waitUntil(^(DoneCallback done){ + [http PUT:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSString *httpRequestBody = [BTHTTPTestProtocol parseRequestBodyFromTestResponseBody:body]; + expect(httpRequestBody).to.equal(@"{\"authorization_fingerprint\":\"test-authorization-fingerprint\"}"); + + done(); + }]; + }); +} + +- (void)testPUTRequests_whenBTHTTPInitializedWithTokenizationKey_sendAuthorization { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] tokenizationKey:@"development_tokenization_key"]; + http.session = testURLSession(); + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + [http PUT:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + XCTAssertEqualObjects(httpRequest.allHTTPHeaderFields[@"Client-Key"], @"development_tokenization_key"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testDELETERequests_whenBTHTTPInitializedWithAuthorizationFingerprint_sendAuthorizationInQueryParams { + waitUntil(^(DoneCallback done) { + [http DELETE:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + expect(httpRequest.URL.query).to.equal(@"authorization_fingerprint=test-authorization-fingerprint"); + + done(); + }]; + }); +} + +- (void)testDELETERequests_whenBTHTTPInitializedWithTokenizationKey_sendAuthorization { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] tokenizationKey:@"development_tokenization_key"]; + http.session = testURLSession(); + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + [http DELETE:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + XCTAssertEqualObjects(httpRequest.allHTTPHeaderFields[@"Client-Key"], @"development_tokenization_key"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +#pragma mark - default headers + +- (void)testIncludeAccept { + waitUntil(^(DoneCallback done){ + withStub(^(void (^removeStub)(void)){ + [http GET:@"stub://200/resource" parameters:nil completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; + expect(requestHeaders[@"Accept"]).to.equal(@"application/json"); + removeStub(); + done(); + }]; + }); + }); +} + +- (void)testIncludeUserAgent { + waitUntil(^(DoneCallback done){ + withStub(^(void (^removeStub)(void)){ + [http GET:@"stub://200/resource" parameters:nil completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; + expect(requestHeaders[@"User-Agent"]).to.match(@"^Braintree/iOS/\\d+\\.\\d+\\.\\d+(-[0-9a-zA-Z-]+)?$"); + removeStub(); + done(); + }]; + }); + }); +} + +- (void)testIncludeAcceptLanguage { + waitUntil(^(DoneCallback done) { + withStub(^(void (^removeStub)(void)) { + [http GET:@"stub://200/resource" parameters:nil completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSDictionary *requestHeaders = httpRequest.allHTTPHeaderFields; + NSLocale *locale = [NSLocale currentLocale]; + NSString *expectedLanguageString = [NSString stringWithFormat:@"%@-%@", [locale objectForKey:NSLocaleLanguageCode], [locale objectForKey:NSLocaleCountryCode]]; + expect(requestHeaders[@"Accept-Language"]).to.equal(expectedLanguageString); + removeStub(); + done(); + }]; + }); + }); +} + + +#pragma mark parameters + +#pragma mark in GET requests +- (void)testTransmitsTheParametersAsURLEncodedQueryParameters { + waitUntil(^(DoneCallback done){ + NSArray *expectedQueryParameters = @[ @"numericParameter=42", + @"falseBooleanParameter=0", + @"dictionaryParameter%5BdictionaryKey%5D=dictionaryValue", + @"trueBooleanParameter=1", + @"stringParameter=value", + @"crazyStringParameter%5B%5D=crazy%2520and%26value", + @"arrayParameter%5B%5D=arrayItem1", + @"arrayParameter%5B%5D=arrayItem2" ]; + + [http GET:@"200.json" parameters:parameterDictionary() completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSArray *actualQueryComponents = [httpRequest.URL.query componentsSeparatedByString:@"&"]; + + for(NSString *expectedComponent in expectedQueryParameters){ + expect(actualQueryComponents).to.contain(expectedComponent); + } + + done(); + }]; + }); +} + +#pragma mark in non-GET requests + +- (void)testTransmitsTheParametersAsJSON { + waitUntil(^(DoneCallback done){ + NSDictionary *expectedParameters = @{ @"numericParameter": @42, + @"falseBooleanParameter": @NO, + @"dictionaryParameter": @{ + @"dictionaryKey": @"dictionaryValue" + }, + @"trueBooleanParameter": @YES, + @"stringParameter": @"value", + @"crazyStringParameter[]": @"crazy%20and&value", + @"arrayParameter": @[ @"arrayItem1", @"arrayItem2" ], + @"authorization_fingerprint": @"test-authorization-fingerprint" }; + + [http POST:@"200.json" parameters:parameterDictionary() completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + NSURLRequest *httpRequest = [BTHTTPTestProtocol parseRequestFromTestResponseBody:body]; + NSString *httpRequestBody = [BTHTTPTestProtocol parseRequestBodyFromTestResponseBody:body]; + + expect([httpRequest valueForHTTPHeaderField:@"Content-type"]).to.equal(@"application/json; charset=utf-8"); + NSDictionary *actualParameters = [NSJSONSerialization JSONObjectWithData:[httpRequestBody dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:NULL]; + expect(actualParameters).to.equal(expectedParameters); + done(); + }]; + }); +} + +#pragma mark interpreting responses + +- (void)testCallsBackOnMainQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"receive callback"]; + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + expect(dispatch_get_current_queue()).to.equal(dispatch_get_main_queue()); +#pragma clang diagnostic pop + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testCallsBackOnSpecifiedQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"receive callback"]; + http.dispatchQueue = dispatch_queue_create("com.braintreepayments.BTHTTPSpec.callbackQueueTest", DISPATCH_QUEUE_SERIAL); + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + expect(dispatch_get_current_queue()).to.equal(http.dispatchQueue); +#pragma clang diagnostic pop + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +#pragma mark response code parser + +- (void)testInterprets2xxAsACompletionWithSuccess { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:NULL] statusCode:200 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + expect(response.statusCode).to.equal(200); + + expect(error).to.beNil(); + + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testResponseCodeParsing_whenStatusCodeIs4xx_returnsError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + NSDictionary *errorBody = @{ + @"error": @{ + @"message": @"This is an error message from the gateway" + } + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[NSJSONSerialization dataWithJSONObject:errorBody options:NSJSONWritingPrettyPrinted error:NULL] statusCode:403 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"403.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertEqualObjects(body.asDictionary, errorBody); + XCTAssertNotNil(response); + XCTAssertEqualObjects(error.domain, BTHTTPErrorDomain); + XCTAssertEqual(error.code, BTHTTPErrorCodeClientError); + XCTAssertEqualObjects(((BTJSON *)error.userInfo[BTHTTPJSONResponseBodyKey]).asDictionary, errorBody); + XCTAssertTrue([error.userInfo[BTHTTPURLResponseKey] isKindOfClass:[NSHTTPURLResponse class]]); + XCTAssertEqualObjects(error.localizedDescription, @"This is an error message from the gateway"); + XCTAssertNotNil(error.userInfo[NSLocalizedFailureReasonErrorKey]); + + [OHHTTPStubs removeStub:stub]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testResponseCodeParsing_whenStatusCodeIs429_returnsRateLimitError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:NULL] statusCode:429 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"429.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertEqualObjects(body.asDictionary, @{}); + XCTAssertNotNil(response); + XCTAssertEqualObjects(error.domain, BTHTTPErrorDomain); + XCTAssertEqual(error.code, BTHTTPErrorCodeRateLimitError); + XCTAssertEqualObjects(((BTJSON *)error.userInfo[BTHTTPJSONResponseBodyKey]).asDictionary, @{}); + XCTAssertTrue([error.userInfo[BTHTTPURLResponseKey] isKindOfClass:[NSHTTPURLResponse class]]); + XCTAssertNotNil(error.userInfo[NSLocalizedFailureReasonErrorKey]); + XCTAssertEqualObjects(error.userInfo[NSLocalizedDescriptionKey], @"You are being rate-limited."); + XCTAssertEqualObjects(error.userInfo[NSLocalizedRecoverySuggestionErrorKey], @"Please try again in a few minutes."); + + [OHHTTPStubs removeStub:stub]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testResponseCodeParsing_whenStatusCodeIs5xx_returnsError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + NSDictionary *errorBody = @{ + @"error": @{ + @"message": @"This is an error message from the gateway" + } + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"GET callback"]; + + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[NSJSONSerialization dataWithJSONObject:errorBody options:NSJSONWritingPrettyPrinted error:NULL] statusCode:503 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"403.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertEqualObjects(body.asDictionary, errorBody); + XCTAssertNotNil(response); + XCTAssertEqualObjects(error.domain, BTHTTPErrorDomain); + XCTAssertEqual(error.code, BTHTTPErrorCodeServerError); + XCTAssertEqualObjects(((BTJSON *)error.userInfo[BTHTTPJSONResponseBodyKey]).asDictionary, errorBody); + XCTAssertTrue([error.userInfo[BTHTTPURLResponseKey] isKindOfClass:[NSHTTPURLResponse class]]); + XCTAssertEqualObjects(error.localizedDescription, @"This is an error message from the gateway"); + XCTAssertEqualObjects(error.localizedRecoverySuggestion, @"Please try again later."); + XCTAssertNotNil(error.userInfo[NSLocalizedFailureReasonErrorKey]); + + [OHHTTPStubs removeStub:stub]; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + + +- (void)testInterpretsTheNetworkBeingDownAsAnError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorNotConnectedToInternet userInfo:nil]]; + }]; + + [http GET:@"network-down" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + expect(body).to.beNil(); + expect(response).to.beNil(); + expect(error.domain).to.equal(NSURLErrorDomain); + expect(error.code).to.equal(NSURLErrorNotConnectedToInternet); + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testInterpretsTheServerBeingUnavailableAsAnError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]]; + }]; + + + [http GET:@"gateway-down" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + expect(body).to.beNil(); + expect(response).to.beNil(); + expect(error.domain).to.equal(NSURLErrorDomain); + expect(error.code).to.equal(NSURLErrorCannotConnectToHost); + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +#pragma mark response body parser + +- (void)testParsesAJSONResponseBody { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[@"{\"status\": \"OK\"}" dataUsingEncoding:NSUTF8StringEncoding] statusCode:200 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"200.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error){ + XCTAssertNotNil(body); + XCTAssertNotNil(response); + XCTAssertNil(error); + + expect([body[@"status"] asString]).to.equal(@"OK"); + + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testAcceptsEmptyResponses { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[[NSData alloc] init] statusCode:200 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"empty.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error){ + expect(response.statusCode).to.equal(200); + expect(body).to.beKindOf([BTJSON class]); + expect(body.isObject).to.beTruthy(); + expect(body.asDictionary.count).to.equal(0); + expect(error).to.beNil(); + + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testInterpretsInvalidJSONResponsesAsAJSONError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[@"{ really invalid json ]" dataUsingEncoding:NSUTF8StringEncoding] statusCode:200 headers:@{@"Content-Type": @"application/json"}]; + }]; + + [http GET:@"invalid.json" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + expect(response).to.beNil(); + expect(body).to.beNil(); + expect(error.domain).to.equal(NSCocoaErrorDomain); + + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testInterpretsNonJSONResponsesAsAContentTypeNotAcceptableError { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + idstub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } withStubResponse:^OHHTTPStubsResponse *(__unused NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[@"response" dataUsingEncoding:NSUTF8StringEncoding] statusCode:200 headers:@{@"Content-Type": @"text/html"}]; + }]; + + [http GET:@"200.html" completion:^(BTJSON *body, NSHTTPURLResponse *response, NSError *error) { + XCTAssertNil(body); + XCTAssertNil(response); + XCTAssertNotNil(error); + + expect(response).to.beNil(); + + expect(error.domain).to.equal(BTHTTPErrorDomain); + expect(error.code).to.equal(BTHTTPErrorCodeResponseContentTypeNotAcceptable); + + [OHHTTPStubs removeStub:stub]; + done(); + }]; + }); +} + +- (void)testNoopsForANilCompletionBlock { + http = [[BTHTTP alloc] initWithBaseURL:[NSURL URLWithString:@"stub://stub"] authorizationFingerprint:@"test-authorization-fingerprint"]; + + waitUntil(^(DoneCallback done){ + setAsyncSpecTimeout(2); + + [http GET:@"200.json" parameters:nil completion:nil]; + + wait_for_potential_async_exceptions(done); + }); +} + +#pragma mark isEqual: + +- (void)testReturnsYESIfBTHTTPsHaveTheSameBaseURLAndAuthorizationFingerprint { + NSURL *baseURL = [NSURL URLWithString:@"an-url://hi"]; + BTHTTP *http1 = [[BTHTTP alloc] initWithBaseURL:baseURL authorizationFingerprint:@"test-authorization-fingerprint"]; + BTHTTP *http2 = [[BTHTTP alloc] initWithBaseURL:baseURL authorizationFingerprint:@"test-authorization-fingerprint"]; + + expect(http1).to.equal(http2); +} + +- (void)testReturnsNOIfBTHTTPsDoNotHaveTheSameBaseURL { + NSURL *baseURL1 = [NSURL URLWithString:@"an-url://hi"]; + NSURL *baseURL2 = [NSURL URLWithString:@"an-url://hi-again"]; + BTHTTP *http1 = [[BTHTTP alloc] initWithBaseURL:baseURL1 authorizationFingerprint:@"test-authorization-fingerprint"]; + BTHTTP *http2 = [[BTHTTP alloc] initWithBaseURL:baseURL2 authorizationFingerprint:@"test-authorization-fingerprint"]; + + expect(http1).notTo.equal(http2); +} + +- (void)testReturnsNOIfBTHTTPsDoNotHaveTheSameAuthorizationFingerprint { + NSURL *baseURL1 = [NSURL URLWithString:@"an-url://hi"]; + BTHTTP *http1 = [[BTHTTP alloc] initWithBaseURL:baseURL1 authorizationFingerprint:@"test-authorization-fingerprint"]; + BTHTTP *http2 = [[BTHTTP alloc] initWithBaseURL:baseURL1 authorizationFingerprint:@"OTHER"]; + + expect(http1).notTo.equal(http2); +} + +#pragma mark copy + +- (void)testReturnsADifferentInstance { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] authorizationFingerprint:@"test-authorization-fingerprint"]; + + expect(http).toNot.beIdenticalTo([http copy]); +} + +- (void)testReturnsAnEqualInstance { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] authorizationFingerprint:@"test-authorization-fingerprint"]; + + expect([http copy]).to.equal(http); +} + +- (void)testReturnedInstanceHasTheSameCertificates { + http = [[BTHTTP alloc] initWithBaseURL:[BTHTTPTestProtocol testBaseURL] authorizationFingerprint:@"test-authorization-fingerprint"]; + + BTHTTP *copiedHTTP = [http copy]; + expect(copiedHTTP.pinnedCertificates).to.equal(http.pinnedCertificates); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTJSON_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTJSON_Tests.swift new file mode 100755 index 00000000..c16d74c8 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTJSON_Tests.swift @@ -0,0 +1,295 @@ +import XCTest + +class BTJSON_Tests: XCTestCase { + func testEmptyJSON() { + let empty = BTJSON() + + XCTAssertNotNil(empty) + + XCTAssertTrue(empty.isObject) + + XCTAssertNil(empty.asString()) + XCTAssertNil(empty.asArray()) + XCTAssertNil(empty.asNumber()) + XCTAssertNil(empty.asURL()) + XCTAssertNil(empty.asStringArray()) + XCTAssertNil(empty.asError()) + + XCTAssertFalse(empty.isString) + XCTAssertFalse(empty.isNumber) + XCTAssertFalse(empty.isArray) + XCTAssertFalse(empty.isTrue) + XCTAssertFalse(empty.isFalse) + XCTAssertFalse(empty.isNull) + } + + func testInitializationFromValue() { + let string = BTJSON(value: "") + XCTAssertTrue(string.isString) + + let truth = BTJSON(value: true) + XCTAssertTrue(truth.isTrue) + + let falsehood = BTJSON(value: false) + XCTAssertTrue(falsehood.isFalse) + + let number = BTJSON(value: 42) + XCTAssertTrue(number.isNumber) + + let ary = BTJSON(value: [1,2,3]) + XCTAssertTrue(ary.isArray) + + let obj = BTJSON(value: ["one": 1, "two": 2]) + XCTAssertTrue(obj.isObject) + + let null = BTJSON(value: NSNull()) + XCTAssertTrue(null.isNull) + } + + func testInitializationFromEmptyData() { + let emptyDataJSON = BTJSON(data: Data()) + XCTAssertTrue(emptyDataJSON.isError) + } + + func testStringJSON() { + let JSON = "\"Hello, JSON!\"".data(using: String.Encoding.utf8)! + let string = BTJSON(data: JSON) + + XCTAssertTrue(string.isString) + XCTAssertEqual(string.asString()!, "Hello, JSON!") + } + + func testArrayJSON() { + let JSON = "[\"One\", \"Two\", \"Three\"]".data(using: String.Encoding.utf8)! + let array = BTJSON(data: JSON) + + XCTAssertTrue(array.isArray) + XCTAssertEqual((array as BTJSON).asArray()! as NSArray, ["One", "Two", "Three"]) + } + + func testArrayAccess() { + let JSON = "[\"One\", \"Two\", \"Three\"]".data(using: String.Encoding.utf8)! + let array = BTJSON(data: JSON) + + XCTAssertTrue(array[0].isString) + XCTAssertEqual(array[0].asString()!, "One") + XCTAssertEqual(array[1].asString()!, "Two") + XCTAssertEqual(array[2].asString()!, "Three") + + XCTAssertNil(array[3].asString()) + XCTAssertFalse(array[3].isString) + + XCTAssertNil((array["hello"] as AnyObject).asString()) + } + + func testObjectAccess() { + let JSON = "{ \"key\": \"value\" }".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertEqual((obj["key"] as AnyObject).asString()!, "value") + + XCTAssertNil((obj["not present"] as AnyObject).asString()) + XCTAssertNil(obj[0].asString()) + + XCTAssertFalse((obj["not present"] as AnyObject).isError as Bool) + + XCTAssertTrue(obj[0].isError) + } + + func testParsingError() { + let JSON = "INVALID JSON".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertTrue(obj.isError) + guard let error = obj as? NSError else {return} + XCTAssertEqual(error.domain, NSCocoaErrorDomain) + } + + func testMultipleErrorsTakesFirst() { + let JSON = "INVALID JSON".data(using: String.Encoding.utf8)! + let string = BTJSON(data: JSON) + + let error = (((string[0])["key"] as! BTJSON)[0]) + + XCTAssertTrue(error.isError as Bool) + guard let err = error as? NSError else {return} + XCTAssertEqual(err.domain, NSCocoaErrorDomain) + } + + func testNestedObjects() { + let JSON = "{ \"numbers\": [\"one\", \"two\", { \"tens\": 0, \"ones\": 1 } ], \"truthy\": true }".data(using: String.Encoding.utf8)! + let nested = BTJSON(data: JSON) + + XCTAssertEqual((nested["numbers"] as! BTJSON)[0].asString()!, "one") + XCTAssertEqual((nested["numbers"] as! BTJSON)[1].asString()!, "two") + XCTAssertEqual(((nested["numbers"] as! BTJSON)[2]["tens"] as! BTJSON).asNumber()!, NSDecimalNumber.zero) + XCTAssertEqual(((nested["numbers"] as! BTJSON)[2]["ones"] as! BTJSON).asNumber()!, NSDecimalNumber.one) + XCTAssertTrue((nested["truthy"] as! BTJSON).isTrue as Bool) + } + + func testTrueBoolInterpretation() { + let JSON = "true".data(using: String.Encoding.utf8)! + let truthy = BTJSON(data: JSON) + XCTAssertTrue(truthy.isTrue) + XCTAssertFalse(truthy.isFalse) + } + + func testFalseBoolInterpretation() { + let JSON = "false".data(using: String.Encoding.utf8)! + let truthy = BTJSON(data: JSON) + XCTAssertFalse(truthy.isTrue) + XCTAssertTrue(truthy.isFalse) + } + + func testAsURL() { + let JSON = "{ \"url\": \"http://example.com\" }".data(using: String.Encoding.utf8)! + let url = BTJSON(data: JSON) + XCTAssertEqual((url["url"] as AnyObject).asURL()!, URL(string: "http://example.com")!) + } + + func testAsURLForInvalidValue() { + let JSON = "{ \"url\": 42 }".data(using: String.Encoding.utf8)! + let url = BTJSON(data: JSON) + XCTAssertNil((url["url"] as AnyObject).asURL()) + } + + func testAsStringArray() { + let JSON = "[\"one\", \"two\", \"three\"]".data(using: String.Encoding.utf8)! + let stringArray = BTJSON(data: JSON) + XCTAssertEqual(stringArray.asStringArray()!, ["one", "two", "three"]) + } + + func testAsStringArrayForInvalidValue() { + let JSON = "[1, 2, false]".data(using: String.Encoding.utf8)! + let stringArray = BTJSON(data: JSON) + XCTAssertNil(stringArray.asStringArray()) + } + + func testAsStringArrayForHeterogeneousValue() { + let JSON = "[\"string\", false]".data(using: String.Encoding.utf8)! + let stringArray = BTJSON(data: JSON) + XCTAssertNil(stringArray.asStringArray()) + } + + func testAsStringArrayForEmptyArray() { + let JSON = "[]".data(using: String.Encoding.utf8)! + let stringArray = BTJSON(data: JSON) + XCTAssertEqual(stringArray.asStringArray()!, []) + } + + func testAsDictionary() { + let JSON = "{ \"key\": \"value\" }".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertEqual((obj.asDictionary()! as AnyObject) as! NSDictionary, ["key":"value"] as NSDictionary) + } + + func testAsDictionaryInvalidValue() { + let JSON = "[]".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertNil(obj.asDictionary()) + } + + func testAsIntegerOrZero() { + let cases = [ + "1": 1, + "1.2": 1, + "1.5": 1, + "1.9": 1, + "-4": -4, + "0": 0, + "\"Hello\"": 0, + ] + for (k,v) in cases { + let JSON = BTJSON(data: k.data(using: String.Encoding.utf8)!) + XCTAssertEqual(JSON.asIntegerOrZero(), v) + } + } + + func testAsEnumOrDefault() { + let JSON = "\"enum one\"".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertEqual(obj.asEnum(["enum one" : 1], orDefault: 0), 1) + } + + func testAsEnumOrDefaultWhenMappingNotPresentReturnsDefault() { + let JSON = "\"enum one\"".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertEqual(obj.asEnum(["enum two" : 2], orDefault: 1000), 1000) + } + + func testAsEnumOrDefaultWhenMapValueIsNotNumberReturnsDefault() { + let JSON = "\"enum one\"".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertEqual(obj.asEnum(["enum one" : "one"], orDefault: 1000), 1000) + } + + func testIsNull() { + let JSON = "null".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertTrue(obj.isNull); + } + + func testIsObject() { + let JSON = "{}".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertTrue(obj.isObject); + } + + func testIsObjectForNonObject() { + let JSON = "[]".data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + + XCTAssertFalse(obj.isObject); + } + + func testLargerMixedJSONWithEmoji() { + let JSON = ("{" + + "\"aString\": \"Hello, JSON 😍!\"," + + "\"anArray\": [1, 2, 3 ]," + + "\"aSetOfValues\": [\"a\", \"b\", \"c\"]," + + "\"aSetWithDuplicates\": [\"a\", \"a\", \"b\", \"b\" ]," + + "\"aLookupDictionary\": {" + + "\"foo\": { \"definition\": \"A meaningless word\"," + + "\"letterCount\": 3," + + "\"meaningful\": false }" + + "}," + + "\"aURL\": \"https://test.example.com:1234/path\"," + + "\"anInvalidURL\": \":™£¢://://://???!!!\"," + + "\"aTrue\": true," + + "\"aFalse\": false" + + "}").data(using: String.Encoding.utf8)! + let obj = BTJSON(data: JSON) + XCTAssertEqual((obj["aString"] as! BTJSON).asString(), "Hello, JSON 😍!") + XCTAssertNil((obj["notAString"] as! BTJSON).asString()) // nil for absent keys + XCTAssertNil((obj["anArray"] as! BTJSON).asString()) // nil for invalid values + XCTAssertEqual((obj["anArray"] as! BTJSON).asArray()! as NSArray, [1, 2, 3]) + XCTAssertNil((obj["notAnArray"] as! BTJSON).asArray()) // nil for absent keys + XCTAssertNil((obj["aString"] as! BTJSON).asArray()) // nil for invalid values + // sets can be parsed as arrays: + XCTAssertEqual((obj["aSetOfValues"] as! BTJSON).asArray()! as NSArray, ["a", "b", "c"]) + XCTAssertEqual((obj["aSetWithDuplicates"] as! BTJSON).asArray()! as NSArray, ["a", "a", "b", "b"]) + let dictionary = (obj["aLookupDictionary"] as! BTJSON).asDictionary()! + let foo = dictionary["foo"]! as! Dictionary + XCTAssertEqual((foo["definition"] as! String), "A meaningless word") + let letterCount = foo["letterCount"] as! NSNumber + XCTAssertEqual(letterCount, 3) + XCTAssertFalse(foo["meaningful"] as! Bool) + XCTAssertNil((obj["notADictionary"] as AnyObject).asDictionary()) + XCTAssertNil((obj["aString"] as AnyObject).asDictionary()) + XCTAssertEqual((obj["aURL"] as AnyObject).asURL(), URL(string: "https://test.example.com:1234/path")) + XCTAssertNil((obj["notAURL"] as AnyObject).asURL()) + XCTAssertNil((obj["aString"] as AnyObject).asURL()) + XCTAssertNil((obj["anInvalidURL"] as AnyObject).asURL()) // nil for invalid URLs + // nested resources: + let btJson = (obj["aLookupDictionary"] as! BTJSON).asDictionary() as! [String: AnyObject] + XCTAssertEqual((btJson["foo"] as! NSDictionary)["definition"] as! String, "A meaningless word") + XCTAssert((((obj["aLookupDictionary"] as! BTJSON)["aString"] as! BTJSON)["anyting"] as! BTJSON).isError as Bool) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTLogger_Internal_Tests.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTLogger_Internal_Tests.m new file mode 100755 index 00000000..9ac2a8ae --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTLogger_Internal_Tests.m @@ -0,0 +1,57 @@ +#import +#import "BTLogger_Internal.h" + +@interface BTLogger_Internal_Tests : XCTestCase +@end + +@implementation BTLogger_Internal_Tests + +- (void)testSharedLogger_returnsSingletonLogger { + BTLogger *logger1 = [BTLogger sharedLogger]; + BTLogger *logger2 = [BTLogger sharedLogger]; + XCTAssertTrue(logger1 == logger2); + XCTAssertTrue([logger1 isKindOfClass:[BTLogger class]]); +} + +- (void)testLevel_byDefault_isInfo { + XCTAssertEqual([[BTLogger alloc] init].level, BTLogLevelInfo); +} + +- (void)testLog_whenLogBlockIsDefined_invokesBlockWithLogMessageAndLogLevel { + BTLogger *logger = [[BTLogger alloc] init]; + NSString *messageLogged = @"BTLogger logBlock works!"; + XCTestExpectation *expectation = [self expectationWithDescription:@"logBlock invoked"]; + logger.logBlock = ^(BTLogLevel level, NSString *messageReceived) { + XCTAssertEqualObjects(messageReceived, messageLogged); + XCTAssertEqual(level, BTLogLevelInfo); + [expectation fulfill]; + }; + + [logger log:messageLogged]; + + [self waitForExpectationsWithTimeout:2 handler:nil]; +} + +- (void)testLog_whenLoggingAtOrBelowLevel_logsMessage { + BTLogger *logger = [[BTLogger alloc] init]; + for (BTLogLevel level = BTLogLevelNone; level <= BTLogLevelDebug; level++) { + NSString *message = [NSString stringWithFormat:@"test %lu", (unsigned long)level]; + NSMutableArray *messagesLogged = [NSMutableArray array]; + __block BTLogLevel maxLevel = level; + logger.logBlock = ^(BTLogLevel actualLevel, NSString *messageReceived) { + XCTAssertTrue(actualLevel <= maxLevel); + [messagesLogged addObject:messageReceived]; + }; + + logger.level = level; + [logger critical:message]; + [logger error:message]; + [logger warning:message]; + [logger info:message]; + [logger debug:message]; + + XCTAssertEqual(messagesLogged.count, level); + } +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTMacroTests.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTMacroTests.m new file mode 100755 index 00000000..ccb067a8 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTMacroTests.m @@ -0,0 +1,18 @@ +#import +#import "BraintreeCore.h" + +@interface BTMacroTests : XCTestCase + +@end + +@implementation BTMacroTests + +- (void)test__BT_AVAILABLE_returnsTrueForAvailableClass { + XCTAssertTrue(__BT_AVAILABLE(@"BTAPIClient")); +} + +- (void)test__BT_AVAILABLE_returnsFalseForUnavailableClass { + XCTAssertFalse(__BT_AVAILABLE(@"BTNotARealClass")); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPayPalDriver_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPayPalDriver_Tests.swift new file mode 100755 index 00000000..daa16e69 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPayPalDriver_Tests.swift @@ -0,0 +1,2088 @@ +import XCTest + +// MARK: Authorization + +class BTPayPalDriver_Authorization_Tests: XCTestCase { + + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + var observers : [NSObjectProtocol] = [] + let ValidClientToken = "eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiI3ODJhZmFlNDJlZTNiNTA4NWUxNmMzYjhkZTY3OGQxNTJhODFlYzk5MTBmZDNhY2YyYWU4MzA2OGI4NzE4YWZhfGNyZWF0ZWRfYXQ9MjAxNS0wOC0yMFQwMjoxMTo1Ni4yMTY1NDEwNjErMDAwMFx1MDAyNmN1c3RvbWVyX2lkPTM3OTU5QTE5LThCMjktNDVBNC1CNTA3LTRFQUNBM0VBOEM4Nlx1MDAyNm1lcmNoYW50X2lkPWRjcHNweTJicndkanIzcW5cdTAwMjZwdWJsaWNfa2V5PTl3d3J6cWszdnIzdDRuYzgiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJjaGFsbGVuZ2VzIjpbXSwiZW52aXJvbm1lbnQiOiJzYW5kYm94IiwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzL2RjcHNweTJicndkanIzcW4vY2xpZW50X2FwaSIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vY2xpZW50LWFuYWx5dGljcy5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIn0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsInRocmVlRFNlY3VyZSI6eyJsb29rdXBVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi90aHJlZV9kX3NlY3VyZS9sb29rdXAifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImRpc3BsYXlOYW1lIjoiQWNtZSBXaWRnZXRzLCBMdGQuIChTYW5kYm94KSIsImNsaWVudElkIjpudWxsLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJhbGxvd0h0dHAiOnRydWUsImVudmlyb25tZW50Tm9OZXR3b3JrIjp0cnVlLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjpmYWxzZSwibWVyY2hhbnRBY2NvdW50SWQiOiJzdGNoMm5mZGZ3c3p5dHc1IiwiY3VycmVuY3lJc29Db2RlIjoiVVNEIn0sImNvaW5iYXNlRW5hYmxlZCI6dHJ1ZSwiY29pbmJhc2UiOnsiY2xpZW50SWQiOiIxMWQyNzIyOWJhNThiNTZkN2UzYzAxYTA1MjdmNGQ1YjQ0NmQ0ZjY4NDgxN2NiNjIzZDI1NWI1NzNhZGRjNTliIiwibWVyY2hhbnRBY2NvdW50IjoiY29pbmJhc2UtZGV2ZWxvcG1lbnQtbWVyY2hhbnRAZ2V0YnJhaW50cmVlLmNvbSIsInNjb3BlcyI6ImF1dGhvcml6YXRpb25zOmJyYWludHJlZSB1c2VyIiwicmVkaXJlY3RVcmwiOiJodHRwczovL2Fzc2V0cy5icmFpbnRyZWVnYXRld2F5LmNvbS9jb2luYmFzZS9vYXV0aC9yZWRpcmVjdC1sYW5kaW5nLmh0bWwiLCJlbnZpcm9ubWVudCI6Im1vY2sifSwibWVyY2hhbnRJZCI6ImRjcHNweTJicndkanIzcW4iLCJ2ZW5tbyI6Im9mZmxpbmUiLCJhcHBsZVBheSI6eyJzdGF0dXMiOiJtb2NrIiwiY291bnRyeUNvZGUiOiJVUyIsImN1cnJlbmN5Q29kZSI6IlVTRCIsIm1lcmNoYW50SWRlbnRpZmllciI6Im1lcmNoYW50LmNvbS5icmFpbnRyZWVwYXltZW50cy5zYW5kYm94LkJyYWludHJlZS1EZW1vIiwic3VwcG9ydGVkTmV0d29ya3MiOlsidmlzYSIsIm1hc3RlcmNhcmQiLCJhbWV4Il19fQ==" + + + override func setUp() { + super.setUp() + + mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + FakePayPalOneTouchCore.setCannedIsWalletAppAvailable(true) + } + + override func tearDown() { + for observer in observers { NotificationCenter.default.removeObserver(observer) } + super.tearDown() + } + + func testAuthorization_whenAPIClientIsNil_callsBackWithError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.apiClient = nil + + let expectation = self.expectation(description: "Authorization fails with error") + payPalDriver.authorizeAccount { (tokenizedPayPalAccount, error) -> Void in + XCTAssertNil(tokenizedPayPalAccount) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + XCTAssertEqual(error.code, BTPayPalDriverErrorType.integration.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { + mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + + let expectation = self.expectation(description: "Authorization fails with error") + payPalDriver.authorizeAccount { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedConfigurationResponseError!) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenPayPalConfigurationDisabled_callsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ "paypalEnabled": false ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + + let expectation = self.expectation(description: "authorization callback") + payPalDriver.authorizeAccount { (tokenizedPayPalAccount, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + XCTAssertEqual(error.code, BTPayPalDriverErrorType.disabled.rawValue) + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenReturnURLSchemeIsNil_logsCriticalMessageAndCallsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ "paypalEnabled": true ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTAppSwitch.setReturnURLScheme("") + payPalDriver.returnURLScheme = "" + + var criticalMessageLogged = false + BTLogger.shared().logBlock = { + (level: BTLogLevel, message: String?) in + if (level == BTLogLevel.critical && message == "PayPal requires a return URL scheme to be configured via [BTAppSwitch setReturnURLScheme:]. This custom URL scheme must also be registered with your app.") { + criticalMessageLogged = true + } + BTLogger.shared().logBlock = nil + return + } + + let expectation = self.expectation(description: "authorization callback") + payPalDriver.authorizeAccount { (tokenizedPayPalAccount, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + XCTAssertEqual(error.code, BTPayPalDriverErrorType.integrationReturnURLScheme.rawValue) + expectation.fulfill() + } + + XCTAssertTrue(criticalMessageLogged) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenRemoteConfigurationIsAvailable_performsPayPalRequestAppSwitch() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.authorizeAccount { _ -> Void in } + + waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(mockRequestFactory.authorizationRequest.appSwitchPerformed) + } + + func testAuthorization_whenBillingAgreementsEnabledInConfiguration_performsBillingAgreements() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline", + "billingAgreementsEnabled": true, + "currencyIsoCode": "GBP", + ] ]) + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.authorizeAccount { _ -> Void in + } + + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + // We want to make sure that currency is not used for Billing Agreements + XCTAssertTrue(lastPostParameters["currency_iso_code"] == nil) + // We want to make sure that intent is not used for Billing Agreements + XCTAssertTrue(lastPostParameters["intent"] == nil) + XCTAssertEqual(lastPostParameters["return_url"] as? String, "scheme://return") + XCTAssertEqual(lastPostParameters["cancel_url"] as? String, "scheme://cancel") + } + + func testAuthorizationRequest_byDefault_containsEmailAndFuturePaymentsScopes() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.authorizeAccount { _ -> Void in } + + waitForExpectations(timeout: 5, handler: nil) + for expectedScope in ["email", "https://uri.paypal.com/services/payments/futurepayments"] { + XCTAssertTrue(mockRequestFactory.lastScopeValues!.contains(expectedScope as NSObject)) + } + } + + func testAuthorizationRequest_whenAdditionalScopesAreSpecified_includesThoseAdditionalScopes() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.authorizeAccount(withAdditionalScopes: Set(["foo", "bar"])) { _ -> Void in } + + waitForExpectations(timeout: 5, handler: nil) + for expectedScope in ["email", "https://uri.paypal.com/services/payments/futurepayments", "foo", "bar"] { + XCTAssertTrue(mockRequestFactory.lastScopeValues!.contains(expectedScope as NSObject)) + } + } + + func testAuthorizationRequest_whenUsingTokenizationKey_includesTokenizationKeyInAdditionalPayloadAttributes() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + let mockRequest = mockRequestFactory.authorizationRequest + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.authorizeAccount { _ -> Void in } + + waitForExpectations(timeout: 5, handler: nil) + XCTAssertEqual(mockRequest.additionalPayloadAttributes["client_key"] as? String, "development_tokenization_key") + } + + func testAuthorizationRequest_whenUsingClientToken_includesClientTokenInAdditionalPayloadAttributes() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + mockAPIClient.tokenizationKey = nil + mockAPIClient.clientToken = try! BTClientToken(clientToken: ValidClientToken) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + let mockRequest = mockRequestFactory.authorizationRequest + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.authorizeAccount { _ -> Void in } + + waitForExpectations(timeout: 5, handler: nil) + XCTAssertEqual(mockRequest.additionalPayloadAttributes["client_token"] as? String, ValidClientToken) + } + + + func testAuthorization_whenAppSwitchCancels_callsBackWithNoResultOrError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.cancel + + let expectation = self.expectation(description: "App switch return block invoked") + payPalDriver.setAuthorizationAppSwitchReturn { (tokenizedAccount, error) -> Void in + XCTAssertNil(tokenizedAccount) + XCTAssertNil(error) + expectation.fulfill() + } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenAppSwitchSucceeds_tokenizesPayPalAccount() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.clientMetadataId = "a-correlation-id" + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + payPalDriver.payPalRequest = BTPayPalRequest(); + mockAPIClient.cannedResponseBody = BTJSON(value: ["paypalAccounts": [ + ["nonce": "fake-nonce"] + ] ] ) + + payPalDriver.setAuthorizationAppSwitchReturn { _ -> Void in } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail("Expected POST to contain parameters") + return + } + + let paypalAccount = lastPostParameters["paypal_account"] as! NSDictionary + XCTAssertEqual(paypalAccount["correlation_id"] as? String, "a-correlation-id") + XCTAssertTrue(paypalAccount["intent"] == nil) + XCTAssertEqual(paypalAccount, FakePayPalOneTouchCoreResult().response as AnyObject as! NSDictionary) + } + + func testAuthorization_whenAppSwitchingToApp_makesAppSwitchDelegateCallbacks() { + if #available(iOS 9.0, *) { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + let delegate = MockAppSwitchDelegate(willPerform: expectation(description: "willPerformAppSwitch called"), didPerform: expectation(description: "didPerformAppSwitch called")) + delegate.willProcessAppSwitchExpectation = expectation(description: "willProcessPaymentInfo called") + payPalDriver.appSwitchDelegate = delegate + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + + payPalDriver.authorizeAccount { _ -> Void in } + payPalDriver.setAuthorizationAppSwitchReturn { _ -> Void in } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenAppSwitchingToApp_postsNotifications() { + if #available(iOS 9.0, *) { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + let delegate = MockAppSwitchDelegate() + delegate.willPerformAppSwitchExpectation = expectation(description: "willPerformAppSwitch called") + payPalDriver.appSwitchDelegate = delegate + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + + let willAppSwitchNotificationExpectation = expectation(description: "willAppSwitch notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchWillSwitch, object: nil, queue: nil) { (notification) -> Void in + willAppSwitchNotificationExpectation.fulfill() + }) + + let didAppSwitchNotificationExpectation = expectation(description: "didAppSwitch notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchDidSwitch, object: nil, queue: nil) { (notification) -> Void in + didAppSwitchNotificationExpectation.fulfill() + }) + + payPalDriver.authorizeAccount { _ -> Void in } + + let willProcessNotificationExpectation = expectation(description: "willProcess notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchWillProcessPaymentInfo, object: nil, queue: nil) { (notification) -> Void in + willProcessNotificationExpectation.fulfill() + }) + + payPalDriver.setAuthorizationAppSwitchReturn { _ -> Void in } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenSwitchingToSFSafariViewController_makesViewControllerPresentingDelegateCallbacks() { + guard #available(iOS 9.0, *) else { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + payPalDriver.requestFactory = FakePayPalRequestFactory() + let mockViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + mockViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = mockViewControllerPresentingDelegate + + payPalDriver.authorizeAccount { _ -> Void in } + waitForExpectations(timeout: 2, handler: nil) + + // Test dismissal of view controller + XCTAssertTrue(mockViewControllerPresentingDelegate.lastViewController is SFSafariViewController) + + let safariViewController = mockViewControllerPresentingDelegate.lastViewController + mockViewControllerPresentingDelegate.lastViewController = nil + mockViewControllerPresentingDelegate.lastPaymentDriver = nil + mockViewControllerPresentingDelegate.requestsDismissalOfViewControllerExpectation = expectation(description: "Delegate received requestsDismissalOfViewController") + + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertEqual(mockViewControllerPresentingDelegate.lastViewController, safariViewController) + XCTAssertEqual(mockViewControllerPresentingDelegate.lastPaymentDriver as? BTPayPalDriver, payPalDriver) + } + + func testAuthorization_whenSwitchingToSFSafariViewController_doesNotMakeAppSwitchDelegateCallbacks() { + guard #available(iOS 9.0, *) else { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + payPalDriver.requestFactory = FakePayPalRequestFactory() + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + let mockAppSwitchDelegate = MockAppSwitchDelegate() + payPalDriver.appSwitchDelegate = mockAppSwitchDelegate + + payPalDriver.authorizeAccount { _ -> Void in } + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertFalse(mockAppSwitchDelegate.willPerformAppSwitchCalled) + XCTAssertFalse(mockAppSwitchDelegate.didPerformAppSwitchCalled) + + stubViewControllerPresentingDelegate.requestsDismissalOfViewControllerExpectation = expectation(description: "Delegate received requestsDismissalOfViewController") + + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertFalse(mockAppSwitchDelegate.willProcessAppSwitchCalled) + } + + func testAuthorization_whenSwitchingToSFSafariViewControllerAndURLIsNotHTTP_callsBackWithError() { + guard #available(iOS 9.0, *) else { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + let stubPayPalRequestFactory = FakePayPalRequestFactory() + stubPayPalRequestFactory.authorizationRequest.cannedURL = URL(string: "garbage://garbage") + payPalDriver.requestFactory = stubPayPalRequestFactory + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + let mockAppSwitchDelegate = MockAppSwitchDelegate() + payPalDriver.appSwitchDelegate = mockAppSwitchDelegate + + let expectation = self.expectation(description: "Callback invoked") + payPalDriver.authorizeAccount { (tokenizedPayPalAccount, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenAppSwitchResultIsError_returnsUnderlyingError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.error + let fakeError = NSError(domain: "FakeError", code: 1, userInfo: nil) + BTPayPalDriver.payPalClass().cannedResult()?.cannedError = fakeError + + let expectation = self.expectation(description: "App switch completion callback") + payPalDriver.setAuthorizationAppSwitchReturn { (tokenizedAccount, error) -> Void in + guard let error = error else { + XCTFail() + return + } + XCTAssertEqual(error as NSError, fakeError) + expectation.fulfill() + } + + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + waitForExpectations(timeout: 5, handler: nil) + } + + func testTokenizedPayPalAccount_containsPayerInfo() { + let authResponse = [ + "paypalAccounts": [ + [ + "nonce": "a-nonce", + "description": "A description", + "details": [ + "email": "hello@world.com", + "payerInfo": [ + "accountAddress": [ + "recipientName": "Foo Bar", + "street1": "1 Foo Ct", + "street2": "Apt Bar", + "city": "Fubar", + "state": "FU", + "postalCode": "42", + "country": "USA" + ] + ] + ] + ] ] ] + assertSuccessfulAuthorizationResponse(authResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.nonce, "a-nonce") + XCTAssertEqual(tokenizedPayPalAccount!.localizedDescription, "A description") + XCTAssertEqual(tokenizedPayPalAccount!.email, "hello@world.com") + let shippingAddress = tokenizedPayPalAccount!.shippingAddress! + XCTAssertEqual(shippingAddress.recipientName, "Foo Bar") + XCTAssertEqual(shippingAddress.streetAddress, "1 Foo Ct") + XCTAssertEqual(shippingAddress.extendedAddress, "Apt Bar") + XCTAssertEqual(shippingAddress.locality, "Fubar") + XCTAssertEqual(shippingAddress.region, "FU") + XCTAssertEqual(shippingAddress.postalCode, "42") + XCTAssertEqual(shippingAddress.countryCodeAlpha2, "USA") + }) + } + + func testTokenizedPayPalAccount_whenEmailAddressIsNestedInsidePayerInfoJSON_usesNestedEmailAddress() { + let authResponse = [ + "paypalAccounts": [ + [ + "nonce": "fake-nonce", + "details": [ + "email": "not-hello@world.com", + "payerInfo": [ + "email": "hello@world.com", + ] + ], + ] + ] ] + assertSuccessfulAuthorizationResponse(authResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.email, "hello@world.com") + }) + } + + func testTokenizedPayPalAccount_whenDescriptionJSONIsPayPal_usesEmailAsLocalizedDescription() { + let authResponse = [ + "paypalAccounts": [ + [ + "nonce": "fake-nonce", + "description": "PayPal", + "details": [ + "email": "hello@world.com", + ], + ] + ] ] + assertSuccessfulAuthorizationResponse(authResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.localizedDescription, "hello@world.com") + }) + } + + // MARK: _meta parameter + + func testMetaParameter_whenAuthorizationAppSwitchIsSuccessful_isPOSTedToServer() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.cannedResult()?.cannedType = .success + stubPayPalClass.setCannedIsWalletAppAvailable(true) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + payPalDriver.setAuthorizationAppSwitchReturn { _ -> Void in } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "paypal-app") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } + + func testMetaParameter_whenAuthorizationBrowserSwitchIsSuccessful_isPOSTedToServer() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.cannedResult()?.cannedType = .success + stubPayPalClass.setCannedIsWalletAppAvailable(false) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + payPalDriver.setAuthorizationAppSwitchReturn { _ -> Void in } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "paypal-browser") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } + + // MARK: Helpers + + func assertSuccessfulAuthorizationResponse(_ response: [String:AnyObject], assertionBlock: @escaping (BTPayPalAccountNonce?, NSError?) -> Void) { + mockAPIClient.cannedResponseBody = BTJSON(value: response) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + + payPalDriver.setAuthorizationAppSwitchReturn { (tokenizedPayPalAccount, error) -> Void in + assertionBlock(tokenizedPayPalAccount, error as NSError?) + } + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + } +} + +// MARK: - Checkout + +class BTPayPalDriver_Checkout_Tests: XCTestCase { + + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + + override func setUp() { + super.setUp() + + mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "fakeURL://" + ] ]) + + } + + func testCheckout_whenAPIClientIsNil_callsBackWithError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.apiClient = nil + + let request = BTPayPalRequest(amount: "1") + let expectation = self.expectation(description: "Checkout fails with error") + + payPalDriver.requestOneTimePayment(request) { (tokenizedPayPalAccount, error) -> Void in + XCTAssertNil(tokenizedPayPalAccount) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + XCTAssertEqual(error.code, BTPayPalDriverErrorType.integration.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { + mockAPIClient.cannedConfigurationResponseBody = nil + mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let request = BTPayPalRequest(amount: "1") + let expectation = self.expectation(description: "Checkout fails with error") + payPalDriver.requestOneTimePayment(request) { (_, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedConfigurationResponseError!) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenRemoteConfigurationFetchSucceeds_postsPaymentResource() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["amount"] as? String, "1") + XCTAssertEqual(lastPostParameters["currency_iso_code"] as? String, "GBP") + XCTAssertEqual(lastPostParameters["return_url"] as? String, "scheme://return") + XCTAssertEqual(lastPostParameters["cancel_url"] as? String, "scheme://cancel") + } + + func testCheckout_byDefault_postsPaymentResourceWithNoShipping() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + // no_shipping = true should be the default. + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["no_shipping"] as? Bool, true) + } + + func testCheckout_whenShippingAddressIsRequired_postsPaymentResourceWithNoShippingAsFalse() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.isShippingAddressRequired = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["no_shipping"] as? Bool, false) + } + + func testCheckout_whenIntentIsNotSpecified_postsPaymentResourceWithAuthorizeIntent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.isShippingAddressRequired = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["intent"] as? String, "authorize") + XCTAssertEqual(request.intent, BTPayPalRequestIntent.authorize) + } + + func testCheckout_whenIntentIsSetToAuthorize_postsPaymentResourceWithIntent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.intent = .authorize; + request.isShippingAddressRequired = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["intent"] as? String, "authorize") + } + + func testCheckout_whenIntentIsSetToSale_postsPaymentResourceWithIntent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.intent = .sale; + request.isShippingAddressRequired = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["intent"] as? String, "sale") + } + + func testCheckout_whenIntentIsSetToOrder_postsPaymentResourceWithIntent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.intent = .order; + request.isShippingAddressRequired = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["intent"] as? String, "order") + } + + func testCheckout_whenLandingPageTypeIsNotSpecified_doesNotPostPaymentResourceWithLandingPageType() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + XCTAssertEqual(BTPayPalRequestLandingPageType.default, request.landingPageType) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertNil(experienceProfile["landing_page_type"]) + } + + func testCheckout_whenLandingPageTypeIsBilling_postsPaymentResourceWithBillingLandingPageType() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.landingPageType = .billing + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["landing_page_type"] as? String, "billing") + } + + func testCheckout_whenLandingPageTypeIsLogin_postsPaymentResourceWithLoginLandingPageType() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.landingPageType = .login + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["landing_page_type"] as? String, "login") + } + + func testCheckout_whenUserActionIsNotSet_approvalUrlIsNotModified() { + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "https://www.paypal.com/checkout/?EC-Token=EC-Random-Value" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + guard let lastApprovalURL = mockRequestFactory.lastApprovalURL, + let approvalURLComponents = URLComponents(url: lastApprovalURL, resolvingAgainstBaseURL: false) else { + XCTFail("Did not find the last approval URL") + return + } + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "EC-Token" && $0.value == "EC-Random-Value" }).count, 1, + "Did not find existing query parameter") + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "useraction" }).count, 0, + "Found useraction query item when not expected") + } + + func testCheckout_whenUserActionIsSetToDefault_approvalUrlIsNotModified() { + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "https://www.paypal.com/checkout/?EC-Token=EC-Random-Value" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.userAction = BTPayPalRequestUserAction.default + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + guard let lastApprovalURL = mockRequestFactory.lastApprovalURL, + let approvalURLComponents = URLComponents(url: lastApprovalURL, resolvingAgainstBaseURL: false) else { + XCTFail("Did not find the last approval URL") + return + } + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "EC-Token" && $0.value == "EC-Random-Value" }).count, 1, + "Did not find existing query parameter") + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "useraction" }).count, 0, + "Found useraction query item when not expected") + } + + func testCheckout_whenUserActionIsSetToCommit_approvalUrlIsModified() { + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "https://www.paypal.com/checkout/?EC-Token=EC-Random-Value" + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.userAction = BTPayPalRequestUserAction.commit + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + guard let lastApprovalURL = mockRequestFactory.lastApprovalURL, + let approvalURLComponents = URLComponents(url: lastApprovalURL, resolvingAgainstBaseURL: false) else { + XCTFail("Did not find the last approval URL") + return + } + + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "EC-Token" && $0.value == "EC-Random-Value" }).count, 1, + "Did not find existing query parameter") + XCTAssertEqual(approvalURLComponents.queryItems?.filter({ $0.name == "useraction" && $0.value == "commit" }).count, 1, + "Did not find useraction query item") + } + + func testCheckout_whenDisplayNameIsNotSet_doesNotPostPaymentResourceWithBrandName() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + XCTAssertNil(request.displayName) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertFalse(experienceProfile.keys.contains("brand_name")) + } + + func testCheckout_whenDisplayNameIsSet_postsPaymentResourceWithDisplayName() { + let merchantName = "My Random Merchant Name" + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.displayName = merchantName + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["brand_name"] as? String, merchantName) + } + + func testCheckout_whenDisplayNameIsSetInConfiguration_postsPaymentResourceWithConfigurationBrandName() { + let merchantName = "My Random Merchant Name" + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "displayName": merchantName + ] + ]) + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.displayName = merchantName + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(experienceProfile["brand_name"] as? String, merchantName) + } + + func testCheckout_whenRemoteConfigurationFetchSucceeds_postsPaymentResourceWithShippingAddress() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + let address : BTPostalAddress = BTPostalAddress() + address.streetAddress = "1234 Fake St." + address.extendedAddress = "Apt. 0" + address.region = "CA" + address.locality = "Oakland" + address.countryCodeAlpha2 = "US" + address.postalCode = "12345" + request.shippingAddressOverride = address + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + guard let experienceProfile = lastPostParameters["experience_profile"] as? Dictionary else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["offer_paypal_credit"] as? Bool, false) + XCTAssertEqual(experienceProfile["address_override"] as? Bool, true) + XCTAssertEqual(lastPostParameters["line1"] as? String, "1234 Fake St.") + XCTAssertEqual(lastPostParameters["line2"] as? String, "Apt. 0") + XCTAssertEqual(lastPostParameters["city"] as? String, "Oakland") + XCTAssertEqual(lastPostParameters["state"] as? String, "CA") + XCTAssertEqual(lastPostParameters["postal_code"] as? String, "12345") + XCTAssertEqual(lastPostParameters["country_code"] as? String, "US") + } + + func testCheckout_whenPayPalCreditOffered_performsSwitchCorrectly() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest(amount: "1") + request.currencyCode = "GBP" + request.offerCredit = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.requestOneTimePayment(request) { _ in } + + self.waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(mockRequestFactory.checkoutRequest.appSwitchPerformed) + + // Ensure the payment resource had the correct parameters + XCTAssertEqual("v1/paypal_hermes/create_payment_resource", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["offer_paypal_credit"] as? Bool, true) + + // Make sure analytics event was sent when switch occurred + let postedAnalyticsEvents = mockAPIClient.postedAnalyticsEvents + + if #available(iOS 9.0, *) { + XCTAssertTrue(postedAnalyticsEvents.contains("ios.paypal-single-payment.webswitch.credit.offered.started")) + } else { + XCTAssertTrue(postedAnalyticsEvents.contains("ios.paypal-single-payment.appswitch.credit.offered.started")) + } + } + + func testCheckout_whenPayPalPaymentCreationSuccessful_performsAppSwitch() { + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + let request = BTPayPalRequest(amount: "1") + payPalDriver.requestOneTimePayment(request) { _ -> Void in } + + self.waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(mockRequestFactory.checkoutRequest.appSwitchPerformed) + XCTAssertEqual(payPalDriver.clientMetadataId, "fake-canned-metadata-id") + } + + func testCheckout_whenPaymentResourceCreationFails_callsBackWithError() { + mockAPIClient.cannedResponseError = NSError(domain: "", code: 0, userInfo: nil) + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let dummyRequest = BTPayPalRequest(amount: "1") + let expectation = self.expectation(description: "Checkout fails with error") + payPalDriver.requestOneTimePayment(dummyRequest) { (_, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedResponseError!) + expectation.fulfill() + } + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenAppSwitchCancels_callsBackWithNoResultOrError() { + let payPalDriver = BTPayPalDriver(apiClient:mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let returnURL = URL(string: "bar://hello/world")! + + let continuationExpectation = self.expectation(description: "Continuation called") + + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .cancel + payPalDriver.setOneTimePaymentAppSwitchReturn ({ (tokenizedCheckout, error) -> Void in + XCTAssertNil(tokenizedCheckout) + XCTAssertNil(error) + continuationExpectation.fulfill() + }) + + BTPayPalDriver.handleAppSwitchReturn(returnURL) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenAppSwitchErrors_callsBackWithError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let returnURL = URL(string: "bar://hello/world")! + + let continuationExpectation = self.expectation(description: "Continuation called") + + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .error + BTPayPalDriver.payPalClass().cannedResult()?.cannedError = NSError(domain: "", code: 0, userInfo: nil) + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ (tokenizedCheckout, error) -> Void in + XCTAssertNil(tokenizedCheckout) + XCTAssertEqual(error! as NSError, BTPayPalDriver.payPalClass().cannedResult()?.error! as! NSError) + continuationExpectation.fulfill() + }) + + BTPayPalDriver.handleAppSwitchReturn(returnURL) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenAppSwitchSucceeds_tokenizesPayPalCheckout() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let paypalAccount = lastPostParameters["paypal_account"] as! NSDictionary + let options = paypalAccount["options"] as! NSDictionary + let validate = (options["validate"] as! NSNumber).boolValue + XCTAssertFalse(validate) + } + + func testCheckout_whenAppSwitchSucceeds_intentShouldExistAsPayPalAccountParameter() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + payPalDriver.payPalRequest = BTPayPalRequest(amount: "1.34") + payPalDriver.payPalRequest.intent = .sale + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let paypalAccount = lastPostParameters["paypal_account"] as! NSDictionary + XCTAssertEqual(paypalAccount["intent"] as? String, "sale") + let options = paypalAccount["options"] as! NSDictionary + let validate = (options["validate"] as! NSNumber).boolValue + XCTAssertFalse(validate) + } + + func testCheckout_whenCreditFinancingNotReturned_shouldNotSendCreditAcceptedAnalyticsEvent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ "paypalAccounts": + [ + [ + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + ], + "nonce": "a-nonce", + "type": "PayPalAccount", + ] + ] + ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + payPalDriver.payPalRequest = BTPayPalRequest(amount: "1.34") + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertFalse(mockAPIClient.postedAnalyticsEvents.contains("ios.paypal-single-payment.credit.accepted")) + } + + func testCheckout_whenCreditFinancingReturned_shouldSendCreditAcceptedAnalyticsEvent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ "paypalAccounts": + [ + [ + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + "creditFinancingOffered": [ + "cardAmountImmutable": true, + "monthlyPayment": [ + "currency": "USD", + "value": "13.88", + ], + "payerAcceptance": true, + "term": 18, + "totalCost": [ + "currency": "USD", + "value": "250.00", + ], + "totalInterest": [ + "currency": "USD", + "value": "0.00", + ], + ], + ], + "nonce": "a-nonce", + "type": "PayPalAccount", + ] + ] + ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + payPalDriver.payPalRequest = BTPayPalRequest(amount: "1.34") + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertTrue(mockAPIClient.postedAnalyticsEvents.contains("ios.paypal-single-payment.credit.accepted")) + } + + func testCheckout_whenAppSwitchSucceeds_makesDelegateCallback() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + let delegate = MockAppSwitchDelegate() + delegate.willProcessAppSwitchExpectation = expectation(description: "willProcessPaymentInfo called") + payPalDriver.appSwitchDelegate = delegate + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testCheckout_whenAppSwitchResultIsError_returnsUnderlyingError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .error + let fakeError = NSError(domain: "FakeError", code: 1, userInfo: nil) + BTPayPalDriver.payPalClass().cannedResult()?.cannedError = fakeError + + let expectation = self.expectation(description: "App switch completion callback") + payPalDriver.setOneTimePaymentAppSwitchReturn ({ (tokenizedCheckout, error) -> Void in + guard let error = error else { + XCTFail() + return + } + XCTAssertEqual(error as NSError, fakeError) + expectation.fulfill() + }) + + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + self.waitForExpectations(timeout: 5, handler: nil) + } + + func testCheckout_whenUsingCustomHandler_callsHandleApprovalDelegateMethod() { + guard #available(iOS 9.0, *) else { + return + } + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let handler = MockPayPalApprovalHandlerDelegate() + handler.url = NSURL(string: "some://url") + + handler.handleApprovalExpectation = self.expectation(description: "Delegate received handleApproval") + let blockExpectation = self.expectation(description: "Completion block reached") + payPalDriver.requestOneTimePayment(BTPayPalRequest(amount: "1"), handler: handler) { (_, _) in + XCTAssertNotNil(handler); + blockExpectation.fulfill() + } + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testtokenizedPayPalAccount_containsPayerInfo() { + let checkoutResponse = [ + "paypalAccounts": [ + [ + "nonce": "a-nonce", + "description": "A description", + "details": [ + "email": "hello@world.com", + "payerInfo": [ + "firstName": "Some", + "lastName": "Dude", + "phone": "867-5309", + "payerId": "FAKE-PAYER-ID", + "accountAddress": [ + "street1": "1 Foo Ct", + "street2": "Apt Bar", + "city": "Fubar", + "state": "FU", + "postalCode": "42", + "country": "USA" + ], + "billingAddress": [ + "recipientName": "Bar Foo", + "line1": "2 Foo Ct", + "line2": "Apt Foo", + "city": "Barfoo", + "state": "BF", + "postalCode": "24", + "countryCode": "ASU" + ], + "shippingAddress": [ + "recipientName": "Some Dude", + "line1": "3 Foo Ct", + "line2": "Apt 5", + "city": "Dudeville", + "state": "CA", + "postalCode": "24", + "countryCode": "US" + ] + ] + ] + ] ] ] + assertSuccessfulCheckoutResponse(checkoutResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.nonce, "a-nonce") + XCTAssertEqual(tokenizedPayPalAccount!.localizedDescription, "A description") + XCTAssertEqual(tokenizedPayPalAccount!.firstName, "Some") + XCTAssertEqual(tokenizedPayPalAccount!.lastName, "Dude") + XCTAssertEqual(tokenizedPayPalAccount!.phone, "867-5309") + XCTAssertEqual(tokenizedPayPalAccount!.email, "hello@world.com") + XCTAssertEqual(tokenizedPayPalAccount!.payerId, "FAKE-PAYER-ID") + let billingAddress = tokenizedPayPalAccount!.billingAddress! + let shippingAddress = tokenizedPayPalAccount!.shippingAddress! + XCTAssertEqual(billingAddress.recipientName, "Bar Foo") + XCTAssertEqual(billingAddress.streetAddress, "2 Foo Ct") + XCTAssertEqual(billingAddress.extendedAddress, "Apt Foo") + XCTAssertEqual(billingAddress.locality, "Barfoo") + XCTAssertEqual(billingAddress.region, "BF") + XCTAssertEqual(billingAddress.postalCode, "24") + XCTAssertEqual(billingAddress.countryCodeAlpha2, "ASU") + XCTAssertEqual(shippingAddress.recipientName, "Some Dude") + XCTAssertEqual(shippingAddress.streetAddress, "3 Foo Ct") + XCTAssertEqual(shippingAddress.extendedAddress, "Apt 5") + XCTAssertEqual(shippingAddress.locality, "Dudeville") + XCTAssertEqual(shippingAddress.region, "CA") + XCTAssertEqual(shippingAddress.postalCode, "24") + XCTAssertEqual(shippingAddress.countryCodeAlpha2, "US") + }) + } + + func testtokenizedPayPalAccount_whenEmailAddressIsNestedInsidePayerInfoJSON_usesNestedEmailAddress() { + let checkoutResponse = [ + "paypalAccounts": [ + [ + "nonce": "fake-nonce", + "details": [ + "email": "not-hello@world.com", + "payerInfo": [ + "email": "hello@world.com", + ] + ], + ] + ] ] + assertSuccessfulCheckoutResponse(checkoutResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.email, "hello@world.com") + }) + } + + func testtokenizedPayPalAccount_whenDescriptionJSONIsPayPal_usesEmailAsLocalizedDescription() { + let checkoutResponse = [ + "paypalAccounts": [ + [ + "nonce": "fake-nonce", + "description": "PayPal", + "details": [ + "email": "hello@world.com", + ], + ] + ] ] + assertSuccessfulCheckoutResponse(checkoutResponse as [String : AnyObject], + assertionBlock: { (tokenizedPayPalAccount, error) -> Void in + XCTAssertEqual(tokenizedPayPalAccount!.localizedDescription, "hello@world.com") + }) + } + + // MARK: _meta parameter + + func testMetadata_whenCheckoutAppSwitchIsSuccessful_isPOSTedToServer() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.cannedResult()?.cannedType = .success + stubPayPalClass.setCannedIsWalletAppAvailable(true) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "paypal-app") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } + + func testMetadata_whenCheckoutBrowserSwitchIsSuccessful_isPOSTedToServer() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.cannedResult()?.cannedType = .success + stubPayPalClass.setCannedIsWalletAppAvailable(false) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + payPalDriver.setOneTimePaymentAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let metaParameters = lastPostParameters["_meta"] as! NSDictionary + XCTAssertEqual(metaParameters["source"] as? String, "paypal-browser") + XCTAssertEqual(metaParameters["integration"] as? String, "custom") + XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionId) + } + + // MARK: Helpers + + func assertSuccessfulCheckoutResponse(_ response: [String:AnyObject], assertionBlock: @escaping (BTPayPalAccountNonce?, NSError?) -> Void) { + mockAPIClient.cannedResponseBody = BTJSON(value: response) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + + payPalDriver.setOneTimePaymentAppSwitchReturn ({ (tokenizedPayPalAccount, error) -> Void in + assertionBlock(tokenizedPayPalAccount, error as NSError?) + }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + } + + // MARK: - Analytics + + func testAPIClientMetadata_whenWalletAppIsInstalled_hasSourceSetToPayPalApp() { + // API client by default uses source = .Unknown and integration = .Custom + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + // It is critical to stub PayPalClass before instantiating the driver, since that is when source is set + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.setCannedIsWalletAppAvailable(true) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + let payPalDriver = BTPayPalDriver(apiClient: apiClient) + + XCTAssertEqual(payPalDriver.apiClient?.metadata.integration, BTClientMetadataIntegrationType.custom) + XCTAssertEqual(payPalDriver.apiClient?.metadata.source, BTClientMetadataSourceType.payPalApp) + } + + func testAPIClientMetadata_whenWalletAppIsNotAvailable_hasSourceSetToPayPalBrowser() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let stubPayPalClass = FakePayPalOneTouchCore.self + stubPayPalClass.setCannedIsWalletAppAvailable(false) + BTPayPalDriver.setPayPalClass(stubPayPalClass) + let payPalDriver = BTPayPalDriver(apiClient: apiClient) + + XCTAssertEqual(payPalDriver.apiClient?.metadata.integration, BTClientMetadataIntegrationType.custom) + XCTAssertEqual(payPalDriver.apiClient?.metadata.source, BTClientMetadataSourceType.payPalBrowser) + } +} + +// MARK: - Billing Agreements + +class BTPayPalDriver_BillingAgreements_Tests: XCTestCase { + + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + + override func setUp() { + super.setUp() + + mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "fakeURL://" + ] ]) + + } + + func testBillingAgreement_whenAPIClientIsNil_callsBackWithError() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.apiClient = nil + + let request = BTPayPalRequest(amount: "1") + let expectation = self.expectation(description: "Billing Agreement fails with error") + payPalDriver.requestBillingAgreement(request) { (tokenizedPayPalAccount, error) -> Void in + XCTAssertNil(tokenizedPayPalAccount) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTPayPalDriverErrorDomain) + XCTAssertEqual(error.code, BTPayPalDriverErrorType.integration.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testBillingAgreement_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { + mockAPIClient.cannedConfigurationResponseBody = nil + mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let request = BTPayPalRequest() + let expectation = self.expectation(description: "Checkout fails with error") + payPalDriver.requestBillingAgreement(request) { (_, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedConfigurationResponseError!) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testBillingAgreement_whenRemoteConfigurationFetchSucceeds_postsSetupBillingAgreement() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestBillingAgreement(BTPayPalRequest()) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["return_url"] as? String, "scheme://return") + XCTAssertEqual(lastPostParameters["cancel_url"] as? String, "scheme://cancel") + XCTAssertEqual(lastPostParameters["offer_paypal_credit"] as? Bool, false) + } + + func testBillingAgreement_whenPayPalCreditOffered_performsSwitchCorrectly() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let request = BTPayPalRequest() + request.offerCredit = true + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + payPalDriver.requestBillingAgreement(request) { _ in } + + self.waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(mockRequestFactory.billingAgreementRequest.appSwitchPerformed) + + // Ensure the payment resource had the correct parameters + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["offer_paypal_credit"] as? Bool, true) + + // Make sure analytics event was sent when switch occurred + let postedAnalyticsEvents = mockAPIClient.postedAnalyticsEvents + + if #available(iOS 9.0, *) { + XCTAssertTrue(postedAnalyticsEvents.contains("ios.paypal-ba.webswitch.credit.offered.started")) + } else { + XCTAssertTrue(postedAnalyticsEvents.contains("ios.paypal-ba.appswitch.credit.offered.started")) + } + } + + func testBillingAgreement_whenAppSwitchSucceeds_tokenizesPayPalAccount() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + + payPalDriver.setBillingAgreementAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v1/payment_methods/paypal_accounts") + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + let paypalAccount = lastPostParameters["paypal_account"] as! NSDictionary + XCTAssertEqual(paypalAccount, FakePayPalOneTouchCoreResult().response as AnyObject as! NSDictionary) + } + + func testBillingAgreement_whenConfigurationHasCurrency_doesNotSendCurrencyOrIntentViaPOSTParameters() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline", + "currencyIsoCode": "GBP", + ] ]) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + payPalDriver.requestBillingAgreement(BTPayPalRequest()) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertTrue(lastPostParameters["currency_iso_code"] == nil) + XCTAssertTrue(lastPostParameters["intent"] == nil) + } + + func testBillingAgreement_whenCheckoutRequestHasCurrency_doesNotSendCurrencyViaPOSTParameters() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let request = BTPayPalRequest() + request.currencyCode = "GBP" + + payPalDriver.requestBillingAgreement(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertTrue(lastPostParameters["currency_iso_code"] == nil) + } + + func testBillingAgreement_whenRequestHasBillingAgreementDescription_sendsDescriptionInParameters() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let request = BTPayPalRequest() + request.billingAgreementDescription = "My Billing Agreement description" + + payPalDriver.requestBillingAgreement(request) { _ -> Void in } + + XCTAssertEqual("v1/paypal_hermes/setup_billing_agreement", mockAPIClient.lastPOSTPath) + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + XCTAssertEqual(lastPostParameters["description"] as? String, "My Billing Agreement description") + } + + func testBillingAgreement_whenSetupBillingAgreementCreationSuccessful_performsPayPalRequestAppSwitch() { + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + let mockRequestFactory = FakePayPalRequestFactory() + payPalDriver.requestFactory = mockRequestFactory + // Depending on whether it's iOS 9 or not, we use different stub delegates to wait for the app switch to occur + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + let stubAppSwitchDelegate = MockAppSwitchDelegate() + if #available(iOS 9.0, *) { + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + } else { + stubAppSwitchDelegate.willPerformAppSwitchExpectation = expectation(description: "Delegate received willPerformAppSwitch") + stubAppSwitchDelegate.didPerformAppSwitchExpectation = expectation(description: "Delegate received didPerformAppSwitch") + payPalDriver.appSwitchDelegate = stubAppSwitchDelegate + } + + let request = BTPayPalRequest() + payPalDriver.requestBillingAgreement(request) { _ -> Void in } + + self.waitForExpectations(timeout: 2, handler: nil) + XCTAssertTrue(mockRequestFactory.billingAgreementRequest.appSwitchPerformed) + } + + func testBillingAgreement_whenSetupBillingAgreementCreationFails_callsBackWithError() { + mockAPIClient.cannedResponseError = NSError(domain: "", code: 0, userInfo: nil) + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + let dummyRequest = BTPayPalRequest() + let expectation = self.expectation(description: "Checkout fails with error") + payPalDriver.requestBillingAgreement(dummyRequest) { (_, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedResponseError!) + expectation.fulfill() + } + self.waitForExpectations(timeout: 2, handler: nil) + } + + + func testBillingAgreement_whenSFSafariViewControllerIsAvailable_callsViewControllerPresentationDelegateMethods() { + guard #available(iOS 9.0, *) else { + return + } + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + let viewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + + // Setup for requestsPersentationOfViewController + viewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = self.expectation(description: "Delegate received requestsPresentationOfViewController") + + payPalDriver.viewControllerPresentingDelegate = viewControllerPresentingDelegate + payPalDriver.informDelegatePresentingViewControllerRequestPresent(URL(string: "http://example.com")!) + + self.waitForExpectations(timeout: 2, handler: nil) + + XCTAssertTrue(viewControllerPresentingDelegate.lastViewController is SFSafariViewController) + XCTAssertEqual(viewControllerPresentingDelegate.lastViewController, payPalDriver.safariViewController) + let payPalDriverViewControllerPresented = payPalDriver.safariViewController + XCTAssertEqual(viewControllerPresentingDelegate.lastPaymentDriver as? BTPayPalDriver, payPalDriver) + + viewControllerPresentingDelegate.lastViewController = nil + viewControllerPresentingDelegate.lastPaymentDriver = nil + + // Setup for requestsDismissalOfViewController + viewControllerPresentingDelegate.requestsDismissalOfViewControllerExpectation = self.expectation(description: "Delegate received requestsDismissalOfViewController") + payPalDriver.informDelegatePresentingViewControllerNeedsDismissal() + + self.waitForExpectations(timeout: 2, handler: nil) + + XCTAssertTrue(viewControllerPresentingDelegate.lastViewController is SFSafariViewController) + XCTAssertEqual(viewControllerPresentingDelegate.lastViewController as? SFSafariViewController, payPalDriverViewControllerPresented) + XCTAssertNil(payPalDriver.safariViewController) + + XCTAssertEqual(viewControllerPresentingDelegate.lastPaymentDriver as? BTPayPalDriver, payPalDriver) + } + + func testBillingAgreement_whenSFSafariViewControllerIsAvailableButNoViewControllerPresentingDelegateSet_logsError() { + guard #available(iOS 9.0, *) else { + return + } + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + + var criticalMessageLogged = false + BTLogger.shared().logBlock = { + (level: BTLogLevel, message: String?) in + if (level == BTLogLevel.critical && message == "Unable to display View Controller to continue PayPal flow. BTPayPalDriver needs a viewControllerPresentingDelegate to be set.") { + criticalMessageLogged = true + } + return + } + + payPalDriver.informDelegatePresentingViewControllerRequestPresent(URL(string: "http://example.com")!) + XCTAssertTrue(criticalMessageLogged) + } + + func testBillingAgreement_whenSFSafariViewControllerIsAvailable_doesNotCallAppSwitchDelegateMethods() { + guard #available(iOS 9.0, *) else { + return + } + + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = PPOTResultType.success + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + payPalDriver.returnURLScheme = "foo://" + payPalDriver.requestFactory = FakePayPalRequestFactory() + let stubViewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + stubViewControllerPresentingDelegate.requestsPresentationOfViewControllerExpectation = expectation(description: "Delegate received requestsPresentationOfViewController") + payPalDriver.viewControllerPresentingDelegate = stubViewControllerPresentingDelegate + let mockAppSwitchDelegate = MockAppSwitchDelegate() + payPalDriver.appSwitchDelegate = mockAppSwitchDelegate + + payPalDriver.requestBillingAgreement(BTPayPalRequest(amount: "1")) { _ -> Void in } + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertFalse(mockAppSwitchDelegate.willPerformAppSwitchCalled) + XCTAssertFalse(mockAppSwitchDelegate.didPerformAppSwitchCalled) + + stubViewControllerPresentingDelegate.requestsDismissalOfViewControllerExpectation = expectation(description: "Delegate received requestsDismissalOfViewController") + + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + waitForExpectations(timeout: 2, handler: nil) + + XCTAssertFalse(mockAppSwitchDelegate.willProcessAppSwitchCalled) + } + + func testBillingAgreement_whenUsingCustomHandler_callsHandleApprovalDelegateMethod() { + guard #available(iOS 9.0, *) else { + return + } + + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + payPalDriver.returnURLScheme = "foo://" + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + + let handler = MockPayPalApprovalHandlerDelegate() + handler.url = NSURL(string: "some://url") + + handler.handleApprovalExpectation = self.expectation(description: "Delegate received handleApproval") + let blockExpectation = self.expectation(description: "Completion block reached") + payPalDriver.requestBillingAgreement(BTPayPalRequest(), handler: handler) { (_, _) in + XCTAssertNotNil(handler); + blockExpectation.fulfill() + } + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testBillingAgreement_whenCreditFinancingNotReturned_shouldNotSendCreditAcceptedAnalyticsEvent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ "paypalAccounts": + [ + [ + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + ], + "nonce": "a-nonce", + "type": "PayPalAccount", + ] + ] + ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + payPalDriver.payPalRequest = BTPayPalRequest() + + payPalDriver.setBillingAgreementAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertFalse(mockAPIClient.postedAnalyticsEvents.contains("ios.paypal-ba.credit.accepted")) + } + + func testBillingAgreement_whenCreditFinancingReturned_shouldSendCreditAcceptedAnalyticsEvent() { + let payPalDriver = BTPayPalDriver(apiClient: mockAPIClient) + mockAPIClient = payPalDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ "paypalAccounts": + [ + [ + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + "creditFinancingOffered": [ + "cardAmountImmutable": true, + "monthlyPayment": [ + "currency": "USD", + "value": "13.88", + ], + "payerAcceptance": true, + "term": 18, + "totalCost": [ + "currency": "USD", + "value": "250.00", + ], + "totalInterest": [ + "currency": "USD", + "value": "0.00", + ], + ], + ], + "nonce": "a-nonce", + "type": "PayPalAccount", + ] + ] + ]) + BTPayPalDriver.setPayPalClass(FakePayPalOneTouchCore.self) + BTPayPalDriver.payPalClass().cannedResult()?.cannedType = .success + payPalDriver.payPalRequest = BTPayPalRequest() + + payPalDriver.setBillingAgreementAppSwitchReturn ({ _ -> Void in }) + BTPayPalDriver.handleAppSwitchReturn(URL(string: "bar://hello/world")!) + + XCTAssertTrue(mockAPIClient.postedAnalyticsEvents.contains("ios.paypal-ba.credit.accepted")) + } +} + +class BTPayPalDriver_DropIn_Tests: XCTestCase { + + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + + override func setUp() { + super.setUp() + + mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline" + ] ]) + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "fakeURL://" + ] ]) + } + + func testDropInViewDelegateSet() { + let dropInViewController = BTDropInViewController(apiClient: mockAPIClient) + + var paymentButton : BTPaymentButton? = nil + for subView in dropInViewController.view.subviews.first!.subviews.first!.subviews { + if let view = subView as? BTPaymentButton { + paymentButton = view + } + } + + XCTAssertNotNil(paymentButton) + XCTAssertNotNil(paymentButton?.viewControllerPresentingDelegate) + XCTAssertEqual(paymentButton?.viewControllerPresentingDelegate as? BTDropInViewController, dropInViewController) + } + +} + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentButton_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentButton_Tests.swift new file mode 100755 index 00000000..d1df1f93 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentButton_Tests.swift @@ -0,0 +1,70 @@ +import XCTest + +class BTPaymentButton_Tests: XCTestCase { + + var window : UIWindow! + var viewController : UIViewController! + + override func setUp() { + super.setUp() + + viewController = UIApplication.shared.windows[0].rootViewController + } + + override func tearDown() { + if viewController.presentedViewController != nil { + viewController.dismiss(animated: false, completion: nil) + } + + super.tearDown() + } + + func testPaymentButton_whenUsingTokenizationKey_doesNotCrash() { + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let paymentButton = BTPaymentButton(apiClient: apiClient) { _ in } + let paymentButtonViewController = UIViewController() + paymentButtonViewController.view.addSubview(paymentButton) + + viewController.present(paymentButtonViewController, animated: true, completion: nil) + } + + func testPaymentButton_byDefault_hasAllPaymentOptions() { + let stubAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + let paymentButton = BTPaymentButton(apiClient: stubAPIClient) { _ in } + + XCTAssertEqual(paymentButton.enabledPaymentOptions, NSOrderedSet(array: ["PayPal", "Venmo"])) + } + + func testPaymentButton_whenPayPalIsEnabledInConfiguration_checksConfigurationForPaymentOptionAvailability() { + let stubAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + let paymentButton = BTPaymentButton(apiClient: stubAPIClient) { _ in } + paymentButton.configuration = BTConfiguration(json: BTJSON(value: [ "paypalEnabled": true ])) + + XCTAssertEqual(paymentButton.enabledPaymentOptions, NSOrderedSet(array: ["PayPal"])) + } + + func testPaymentButton_whenVenmoIsEnabledInConfiguration_checksConfigurationForPaymentOptionAvailability() { + let stubAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + let paymentButton = BTPaymentButton(apiClient: stubAPIClient) { _ in } + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = true + paymentButton.application = fakeApplication + paymentButton.configuration = BTConfiguration(json: BTJSON(value: [ "payWithVenmo": ["accessToken": "ACCESS_TOKEN"] ])) + BTConfiguration.setBetaPaymentOption("venmo", isEnabled: true) + + XCTAssertEqual(paymentButton.enabledPaymentOptions, NSOrderedSet(array: ["Venmo"])) + } + + func testPaymentButton_whenEnabledPaymentOptionsIsSetManually_skipsConfigurationValidation() { + let stubAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + let paymentButton = BTPaymentButton(apiClient: stubAPIClient) { _ in } + paymentButton.configuration = BTConfiguration(json: BTJSON(value: [ "paypalEnabled": false ])) + + paymentButton.enabledPaymentOptions = NSOrderedSet(array: ["PayPal"]) + XCTAssertEqual(paymentButton.enabledPaymentOptions, NSOrderedSet(array: ["PayPal"])) + + paymentButton.enabledPaymentOptions = NSOrderedSet(array: ["Venmo"]) + XCTAssertEqual(paymentButton.enabledPaymentOptions, NSOrderedSet(array: ["Venmo"])) + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentMethodNonceParser_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentMethodNonceParser_Tests.swift new file mode 100755 index 00000000..c8809709 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTPaymentMethodNonceParser_Tests.swift @@ -0,0 +1,302 @@ +import XCTest + +class BTPaymentMethodNonceParser_Tests: XCTestCase { + + var parser : BTPaymentMethodNonceParser = BTPaymentMethodNonceParser() + + func testRegisterType_addsTypeToTypes() { + parser.registerType("MyType") { _ -> BTPaymentMethodNonce? in return nil} + + XCTAssertTrue(parser.allTypes.contains("MyType")) + } + + func testAllTypes_whenTypeIsNotRegistered_doesntContainType() { + XCTAssertEqual(parser.allTypes.count, 0) + } + + func testIsTypeAvailable_whenTypeIsRegistered_isTrue() { + parser.registerType("MyType") { _ -> BTPaymentMethodNonce? in return nil} + XCTAssertTrue(parser.isTypeAvailable("MyType")) + } + + func testIsTypeAvailable_whenTypeIsNotRegistered_isFalse() { + XCTAssertFalse(parser.isTypeAvailable("MyType")) + } + + func testParseJSON_whenTypeIsRegistered_callsParsingBlock() { + let expectation = self.expectation(description: "Parsing block called") + parser.registerType("MyType") { _ -> BTPaymentMethodNonce? in + expectation.fulfill() + return nil + } + parser.parseJSON(BTJSON(), withParsingBlockForType: "MyType") + + waitForExpectations(timeout: 3, handler: nil) + } + + func testParseJSON_whenTypeIsNotRegisteredAndJSONContainsNonce_returnsBasicTokenizationObject() { + let json = BTJSON(value: ["nonce": "valid-nonce", + "description": "My Description"]) + + let paymentMethodNonce = parser.parseJSON(json, withParsingBlockForType: "MyType") + + XCTAssertEqual(paymentMethodNonce?.nonce, "valid-nonce") + XCTAssertEqual(paymentMethodNonce?.localizedDescription, "My Description") + } + + func testParseJSON_whenTypeIsNotRegisteredAndJSONDoesNotContainNonce_returnsNil() { + let paymentMethodNonce = parser.parseJSON(BTJSON(value: ["description": "blah"]), withParsingBlockForType: "MyType") + + XCTAssertNil(paymentMethodNonce) + } + + // MARK: - Payment-specific tests + + func testSharedParser_whenTypeIsCreditCard_returnsCorrectCardNonce() { + let sharedParser = BTPaymentMethodNonceParser.shared() + + let creditCardJSON = BTJSON(value: [ + "consumed": false, + "description": "ending in 31", + "details": [ + "cardType": "American Express", + "lastTwo": "31", + ], + "isLocked": false, + "nonce": "0099b1d0-7a1c-44c3-b1e4-297082290bb9", + "securityQuestions": ["cvv"], + "threeDSecureInfo": NSNull(), + "type": "CreditCard", + "default": true + ]) + + let cardNonce = sharedParser.parseJSON(creditCardJSON, withParsingBlockForType:"CreditCard")! + + XCTAssertEqual(cardNonce.nonce, "0099b1d0-7a1c-44c3-b1e4-297082290bb9") + XCTAssertEqual(cardNonce.type, "AMEX") + XCTAssertTrue(cardNonce.isDefault) + } + + func testSharedParser_whenTypeIsPayPal_returnsPayPalAccountNonce() { + let sharedParser = BTPaymentMethodNonceParser.shared() + let payPalAccountJSON = BTJSON(value: [ + "consumed": false, + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + ], + "isLocked": false, + "nonce": "a-nonce", + "securityQuestions": [], + "type": "PayPalAccount", + "default": true + ]) + + let payPalAccountNonce = sharedParser.parseJSON(payPalAccountJSON, withParsingBlockForType: "PayPalAccount") as! BTPayPalAccountNonce + + XCTAssertEqual(payPalAccountNonce.nonce, "a-nonce") + XCTAssertEqual(payPalAccountNonce.type, "PayPal") + XCTAssertEqual(payPalAccountNonce.email, "jane.doe@example.com") + XCTAssertTrue(payPalAccountNonce.isDefault) + XCTAssertNil(payPalAccountNonce.creditFinancing) + } + + func testParsePayPalCreditFinancingAmount() { + let payPalCreditFinancingAmount = BTJSON(value: [ + "currency": "USD", + "value": "123.45", + ]) + + guard let amount = BTPayPalDriver.creditFinancingAmount(from: payPalCreditFinancingAmount) else { + XCTFail("Expected amount") + return + } + XCTAssertEqual(amount.currency, "USD") + XCTAssertEqual(amount.value, "123.45") + } + + func testParsePayPalCreditFinancing() { + let payPalCreditFinancing = BTJSON(value: [ + "cardAmountImmutable": false, + "monthlyPayment": [ + "currency": "USD", + "value": "123.45", + ], + "payerAcceptance": false, + "term": 3, + "totalCost": [ + "currency": "ABC", + "value": "789.01", + ], + "totalInterest": [ + "currency": "XYZ", + "value": "456.78", + ], + ]) + + guard let creditFinancing = BTPayPalDriver.creditFinancing(from: payPalCreditFinancing) else { + XCTFail("Expected credit financing") + return + } + + XCTAssertFalse(creditFinancing.cardAmountImmutable) + guard let monthlyPayment = creditFinancing.monthlyPayment else { + XCTFail("Expected monthly payment details") + return + } + XCTAssertEqual(monthlyPayment.currency, "USD") + XCTAssertEqual(monthlyPayment.value, "123.45") + + XCTAssertFalse(creditFinancing.payerAcceptance) + XCTAssertEqual(creditFinancing.term, 3) + + XCTAssertNotNil(creditFinancing.totalCost) + + guard let totalCost = creditFinancing.totalCost else { + XCTFail("Expected total cost details") + return + } + XCTAssertEqual(totalCost.currency, "ABC") + XCTAssertEqual(totalCost.value, "789.01") + + guard let totalInterest = creditFinancing.totalInterest else { + XCTFail("Expected total interest details") + return + } + XCTAssertEqual(totalInterest.currency, "XYZ") + XCTAssertEqual(totalInterest.value, "456.78") + } + + func testSharedParser_whenTypeIsPayPal_returnsPayPalAccountNonceWithCreditFinancingOffered() { + let sharedParser = BTPaymentMethodNonceParser.shared() + let payPalAccountJSON = BTJSON(value: [ + "consumed": false, + "description": "jane.doe@example.com", + "details": [ + "email": "jane.doe@example.com", + "creditFinancingOffered": [ + "cardAmountImmutable": true, + "monthlyPayment": [ + "currency": "USD", + "value": "13.88", + ], + "payerAcceptance": true, + "term": 18, + "totalCost": [ + "currency": "USD", + "value": "250.00", + ], + "totalInterest": [ + "currency": "USD", + "value": "0.00", + ], + ], + ], + "isLocked": false, + "nonce": "a-nonce", + "securityQuestions": [], + "type": "PayPalAccount", + "default": true, + ]) + + let payPalAccountNonce = sharedParser.parseJSON(payPalAccountJSON, withParsingBlockForType: "PayPalAccount") as! BTPayPalAccountNonce + + XCTAssertEqual(payPalAccountNonce.nonce, "a-nonce") + XCTAssertEqual(payPalAccountNonce.type, "PayPal") + XCTAssertEqual(payPalAccountNonce.email, "jane.doe@example.com") + XCTAssertTrue(payPalAccountNonce.isDefault) + + guard let creditFinancing = payPalAccountNonce.creditFinancing else { + XCTFail("Expected credit financing terms") + return + } + + XCTAssertTrue(creditFinancing.cardAmountImmutable) + guard let monthlyPayment = creditFinancing.monthlyPayment else { + XCTFail("Expected monthly payment details") + return + } + XCTAssertEqual(monthlyPayment.currency, "USD") + XCTAssertEqual(monthlyPayment.value, "13.88") + + XCTAssertTrue(creditFinancing.payerAcceptance) + XCTAssertEqual(creditFinancing.term, 18) + + XCTAssertNotNil(creditFinancing.totalCost) + + guard let totalCost = creditFinancing.totalCost else { + XCTFail("Expected total cost details") + return + } + XCTAssertEqual(totalCost.currency, "USD") + XCTAssertEqual(totalCost.value, "250.00") + + guard let totalInterest = creditFinancing.totalInterest else { + XCTFail("Expected total interest details") + return + } + XCTAssertEqual(totalInterest.currency, "USD") + XCTAssertEqual(totalInterest.value, "0.00") + } + + func testSharedParser_whenTypeIsVenmo_returnsVenmoAccountNonce() { + let sharedParser = BTPaymentMethodNonceParser.shared() + let venmoAccountJSON = BTJSON(value: [ + "consumed": false, + "description": "VenmoAccount", + "details": ["username": "jane.doe.username@example.com", "cardType": "Discover"], + "isLocked": false, + "nonce": "a-nonce", + "securityQuestions": [], + "type": "VenmoAccount", + "default": true + ]) + + let venmoAccountNonce = sharedParser.parseJSON(venmoAccountJSON, withParsingBlockForType: "VenmoAccount") as! BTVenmoAccountNonce + + XCTAssertEqual(venmoAccountNonce.nonce, "a-nonce") + XCTAssertEqual(venmoAccountNonce.type, "Venmo") + XCTAssertEqual(venmoAccountNonce.username, "jane.doe.username@example.com") + XCTAssertTrue(venmoAccountNonce.isDefault) + } + + func testSharedParser_whenTypeIsApplePayCard_returnsApplePayCardNonce() { + let sharedParser = BTPaymentMethodNonceParser.shared() + let applePayCard = BTJSON(value: [ + "consumed": false, + "description": "Apple Pay Card ending in 11", + "details": [ + "cardType": "American Express" + ], + "isLocked": false, + "nonce": "a-nonce", + "securityQuestions": [], + "type": "ApplePayCard", + ]) + + let applePayCardNonce = sharedParser.parseJSON(applePayCard, withParsingBlockForType: "ApplePayCard") as? BTApplePayCardNonce + + XCTAssertEqual(applePayCardNonce?.nonce, "a-nonce") + XCTAssertEqual(applePayCardNonce?.type, "American Express") + XCTAssertEqual(applePayCardNonce?.localizedDescription, "Apple Pay Card ending in 11") + } + + func testSharedParser_whenTypeIsUnknown_returnsBasePaymentMethodNonce() { + let sharedParser = BTPaymentMethodNonceParser.shared() + let JSON = BTJSON(value: [ + "consumed": false, + "description": "Some thing", + "details": [], + "isLocked": false, + "nonce": "a-nonce", + "type": "asdfasdfasdf", + "default": true + ]) + + let unknownNonce = sharedParser.parseJSON(JSON, withParsingBlockForType: "asdfasdfasdf")! + + XCTAssertEqual(unknownNonce.nonce, "a-nonce") + XCTAssertEqual(unknownNonce.type, "Unknown") + XCTAssertTrue(unknownNonce.isDefault) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureDriver_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureDriver_Tests.swift new file mode 100755 index 00000000..811fc745 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureDriver_Tests.swift @@ -0,0 +1,236 @@ +import XCTest + +class BTThreeDSecureDriver_Tests: XCTestCase { + + let originalNonce_lookupEnrolledAuthenticationNotRequired = "some-credit-card-nonce-where-3ds-succeeds-without-user-authentication" + let originalNonce_lookupEnrolledAuthenticationRequired = "some-credit-card-nonce-where-3ds-succeeds-after-user-authentication" + let originalNonce_lookupCardNotEnrolled = "some-credit-card-nonce-where-card-is-not-enrolled-for-3ds" + let viewControllerPresentingDelegate = MockViewControllerPresentationDelegate() + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_client_key")! + var observers : [NSObjectProtocol] = [] + + override func setUp() { + super.setUp() + + mockAPIClient = MockAPIClient(authorization: "development_client_key")! + } + + override func tearDown() { + for observer in observers { NotificationCenter.default.removeObserver(observer) } + super.tearDown() + } + + func testInitialization_initializesWithClientAndDelegate() { + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate ) + XCTAssertNotNil(threeDSecureDriver) + } + + func testVerification_whenAPIClientIsNil_callsBackWithError() { + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate ) + threeDSecureDriver.apiClient = nil + + let expectation = self.expectation(description: "verification fails with errors") + + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupEnrolledAuthenticationNotRequired, amount: NSDecimalNumber.one, completion: { (tokenizedCard, error) -> Void in + XCTAssertNil(tokenizedCard) + XCTAssertNotNil(error) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTThreeDSecureErrorDomain) + XCTAssertEqual(error.code, BTThreeDSecureErrorType.integration.rawValue) + expectation.fulfill() + }) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testVerification_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { + mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate ) + mockAPIClient = threeDSecureDriver.apiClient as! MockAPIClient + + let expectation = self.expectation(description: "verification fails with errors") + + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupEnrolledAuthenticationNotRequired, amount: NSDecimalNumber.one, completion: { (tokenizedCard, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedConfigurationResponseError!) + expectation.fulfill() + }) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testVerification_withCardThatDoesntRequireAuthentication_callsCompletionWithACard() { + let responseBody = [ + "paymentMethod": [ + "consumed": false, + "description": "ending in 02", + "details": [ + "cardType": "Visa", + "lastTwo": "02", + ], + "nonce": "f689056d-aee1-421e-9d10-f2c9b34d4d6f", + "threeDSecureInfo": [ + "enrolled": "Y", + "liabilityShiftPossible": true, + "liabilityShifted": true, + "status": "authenticate_successful", + ], + "type": "CreditCard", + ], + "success": true, + "threeDSecureInfo": [ + "liabilityShiftPossible": true, + "liabilityShifted": true, + ] + ] as [String : Any] + mockAPIClient.cannedResponseBody = BTJSON(value: responseBody) + + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate ) + + let expectation = self.expectation(description: "willCallCompletion") + + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupEnrolledAuthenticationNotRequired, amount: NSDecimalNumber.one, completion: { (tokenizedCard, error) -> Void in + XCTAssert(isANonce(tokenizedCard!.nonce)) + XCTAssertNil(error) + XCTAssert(tokenizedCard!.liabilityShifted) + XCTAssert(tokenizedCard!.liabilityShiftPossible) + expectation.fulfill() + }) + + waitForExpectations(timeout: 3, handler: nil) + } + + func testVerification_withCardThatRequiresAuthentication_requestsPresentationOfViewController() { + let responseBody = [ + "paymentMethod": [ + "consumed": false, + "description": "ending in 02", + "details": [ + "cardType": "Visa", + "lastTwo": "02", + ], + "nonce": "f689056d-aee1-421e-9d10-f2c9b34d4d6f", + "threeDSecureInfo": [ + "enrolled": "Y", + "liabilityShiftPossible": true, + "liabilityShifted": true, + "status": "authenticate_successful", + ], + "type": "CreditCard", + ], + "success": true, + "threeDSecureInfo": [ + "liabilityShiftPossible": true, + "liabilityShifted": true, + ], + "lookup": [ + "acsUrl": "http://example.com", + "pareq": "", + "md": "", + "termUrl": "http://example.com" + ] + ] as [String : Any] + mockAPIClient.cannedResponseBody = BTJSON(value: responseBody) + + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate ) + let mockDelegate = MockViewControllerPresentationDelegate() + threeDSecureDriver.delegate = mockDelegate + + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupEnrolledAuthenticationRequired, amount: NSDecimalNumber.one) { (tokenizedCard, error) -> Void in } + + XCTAssertNotNil(mockDelegate.lastViewController) + } + + func testVerification_whenCardIsNotEnrolled_returnsCardWithNewNonceAndCorrectLiabilityShiftInformation() { + let responseBody = [ + "paymentMethod": [ + "consumed": false, + "description": "ending in 02", + "details": [ + "cardType": "Visa", + "lastTwo": "02", + ], + "nonce": "f689056d-aee1-421e-9d10-f2c9b34d4d6f", + "threeDSecureInfo": [ + "enrolled": "N", + "liabilityShiftPossible": false, + "liabilityShifted": false, + "status": "authenticate_successful_issuer_not_participating", + ], + "type": "CreditCard", + ], + "success": true, + "threeDSecureInfo": [ + "liabilityShiftPossible": false, + "liabilityShifted": false, + ] + ] as [String : Any] + mockAPIClient.cannedResponseBody = BTJSON(value: responseBody) + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate) + + let expectation = self.expectation(description: "Card is tokenized") + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupCardNotEnrolled, amount: NSDecimalNumber.one) { (tokenizedCard, error) -> Void in + guard let tokenizedCard = tokenizedCard else { + XCTFail() + return + } + XCTAssertTrue(isANonce(tokenizedCard.nonce)) + XCTAssertNotEqual(tokenizedCard.nonce, self.originalNonce_lookupCardNotEnrolled); + XCTAssertNil(error) + XCTAssertFalse(tokenizedCard.liabilityShifted) + XCTAssertFalse(tokenizedCard.liabilityShiftPossible) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testError_whenFinishIsCalledMoreThanOnce_sendsErrorAnalyticsEvent() { + let responseBody = [ + "paymentMethod": [ + "consumed": false, + "description": "ending in 02", + "details": [ + "cardType": "Visa", + "lastTwo": "02", + ], + "nonce": "f689056d-aee1-421e-9d10-f2c9b34d4d6f", + "threeDSecureInfo": [ + "enrolled": "N", + "liabilityShiftPossible": false, + "liabilityShifted": false, + "status": "authenticate_successful_issuer_not_participating", + ], + "type": "CreditCard", + ], + "success": true, + "threeDSecureInfo": [ + "liabilityShiftPossible": false, + "liabilityShifted": false, + ] + ] as [String : Any] + mockAPIClient.cannedResponseBody = BTJSON(value: responseBody) + let threeDSecureDriver = BTThreeDSecureDriver.init(apiClient: mockAPIClient, delegate:viewControllerPresentingDelegate) + + let expectation = self.expectation(description: "Card is tokenized") + threeDSecureDriver.verifyCard(withNonce: originalNonce_lookupCardNotEnrolled, amount: NSDecimalNumber.one) { (tokenizedCard, error) -> Void in + guard let tokenizedCard = tokenizedCard else { + XCTFail() + return + } + XCTAssertTrue(isANonce(tokenizedCard.nonce)) + XCTAssertNotEqual(tokenizedCard.nonce, self.originalNonce_lookupCardNotEnrolled); + XCTAssertNil(error) + XCTAssertFalse(tokenizedCard.liabilityShifted) + XCTAssertFalse(tokenizedCard.liabilityShiftPossible) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + + let threeDSecureAuthenticationViewController = BTThreeDSecureAuthenticationViewController.init(lookupResult: BTThreeDSecureLookupResult.init()) + threeDSecureDriver.perform(#selector(threeDSecureDriver.threeDSecureViewControllerDidFinish(_:)), with: threeDSecureAuthenticationViewController) + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.threedsecure.error.finished-without-handler") + } +} + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureLookupResult_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureLookupResult_Tests.swift new file mode 100755 index 00000000..9b306676 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTThreeDSecureLookupResult_Tests.swift @@ -0,0 +1,24 @@ +import XCTest + +class BTThreeDSecureLookupResult_Tests: XCTestCase { + + func testRequiresUserAuthentication_whenAcsUrlIsPresent_returnsTrue() { + let lookup = BTThreeDSecureLookupResult() + lookup.acsURL = URL(string: "http://example.com") + lookup.termURL = URL(string: "http://example.com") + lookup.md = "an-md" + lookup.paReq = "a-PAReq" + + XCTAssertTrue(lookup.requiresUserAuthentication()) + } + + func testRequiresUserAuthentication_whenAcsUrlIsNotPresent_returnsFalse() { + let lookup = BTThreeDSecureLookupResult() + lookup.acsURL = nil + lookup.termURL = URL(string: "http://example.com") + lookup.md = "an-md" + lookup.paReq = "a-PAReq" + + XCTAssertFalse(lookup.requiresUserAuthentication()) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTTokenizationService_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTTokenizationService_Tests.swift new file mode 100755 index 00000000..2e2f5240 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTTokenizationService_Tests.swift @@ -0,0 +1,146 @@ +import XCTest + +class BTTokenizationService_Tests: XCTestCase { + + var tokenizationService : BTTokenizationService! + + override func setUp() { + super.setUp() + tokenizationService = BTTokenizationService() + } + + override func tearDown() { + super.tearDown() + } + + func testRegisterType_addsTypeToTypes() { + tokenizationService.registerType("MyType") { _ -> Void in } + XCTAssertTrue(tokenizationService.allTypes.contains("MyType")) + } + + func testAllTypes_whenTypeIsNotRegistered_doesntContainType() { + XCTAssertFalse(tokenizationService.allTypes.contains("MyType")) + } + + func testIsTypeAvailable_whenTypeIsRegistered_isTrue() { + tokenizationService.registerType("MyType") { _ -> Void in } + XCTAssertTrue(tokenizationService.isTypeAvailable("MyType")) + } + + func testIsTypeAvailable_whenTypeIsNotRegistered_returnsFalse() { + XCTAssertFalse(tokenizationService.isTypeAvailable("MyType")) + } + + func testTokenizeType_whenTypeIsRegistered_callsTokenizationBlock() { + let expectation = self.expectation(description: "tokenization block called") + tokenizationService.registerType("MyType") { _ -> Void in + expectation.fulfill() + } + + tokenizationService.tokenizeType("MyType", options: nil, with: BTAPIClient(authorization: "development_testing_integration_merchant_id")!) { _ -> Void in + //nada + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenizeType_whenCalledWithOptions_callsTokenizationBlockAndPassesInOptions() { + let expectation = self.expectation(description: "tokenization block called") + let expectedOptions = ["Some Custom Option Key": "The Option Value"] + tokenizationService.registerType("MyType") { (_, options, _) -> Void in + XCTAssertEqual(options as! [String : String], expectedOptions) + expectation.fulfill() + } + + tokenizationService.tokenizeType("MyType", options: expectedOptions, with:BTAPIClient(authorization: "development_testing_integration_merchant_id")!) { _ -> Void in } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testTokenizeType_whenTypeIsNotRegistered_returnsError() { + let expectation = self.expectation(description: "Callback invoked") + tokenizationService.tokenizeType("UnknownType", options: nil, with:BTAPIClient(authorization: "development_testing_integration_merchant_id")!) { nonce, error -> Void in + XCTAssertNil(nonce) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTTokenizationServiceErrorDomain) + XCTAssertEqual(error.code, BTTokenizationServiceError.typeNotRegistered.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler:nil) + } + + // MARK: - Payment-specific tests + + func testSingleton_hasExpectedTypesAvailable() { + let sharedService = BTTokenizationService.shared() + + XCTAssertTrue(sharedService.isTypeAvailable("PayPal")) + XCTAssertTrue(sharedService.isTypeAvailable("Venmo")) + XCTAssertTrue(sharedService.isTypeAvailable("Card")) + } + + func testSingleton_canTokenizeCards() { + let sharedService = BTTokenizationService.shared() + let card = BTCard(number: "4111111111111111", expirationMonth: "12", expirationYear: "2020", cvv: "123") + let stubAPIClient = MockAPIClient(authorization: "development_fake_key")! + stubAPIClient.cannedResponseBody = BTJSON(value: [ + "creditCards": [ + [ + "nonce": "a-nonce", + "description": "A card" + ] + ] + ]) + + let expectation = self.expectation(description: "Card is tokenized") + sharedService.tokenizeType("Card", options: card.parameters() as? [String : AnyObject], with: stubAPIClient) { (cardNonce, error) -> Void in + XCTAssertEqual(cardNonce?.nonce, "a-nonce") + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + // This test only verifies that SFSafariViewController is presented + func testSingleton_canAuthorizePayPalThroughSFSafariViewController() { + if #available(iOS 9.0, *) { + let sharedService = BTTokenizationService.shared() + let stubAPIClient = MockAPIClient(authorization: "development_fake_key")! + stubAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "paypalEnabled": true, + "paypal": [ + "environment": "offline", + "privacyUrl": "", + "userAgreementUrl": "", + ] ]) + let mockDelegate = MockViewControllerPresentationDelegate() + BTAppSwitch.setReturnURLScheme("com.braintreepayments.Demo.payments") + + sharedService.tokenizeType("PayPal", options: [BTTokenizationServiceViewPresentingDelegateOption: mockDelegate], with: stubAPIClient) { _ -> Void in } + + XCTAssertTrue(mockDelegate.lastViewController is SFSafariViewController) + } + } + + func testSingleton_canAuthorizeVenmo() { + let sharedService = BTTokenizationService.shared() + BTConfiguration.setBetaPaymentOption("venmo", isEnabled: true) + BTOCMockHelper().stubApplicationCanOpenURL() + BTAppSwitch.setReturnURLScheme("com.braintreepayments.Demo.payments") + let stubAPIClient = MockAPIClient(authorization: "development_fake_key")! + stubAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo": [ + "accessToken": "fake-access-token", + "environment": "sandbox", + "merchantId": "stubmerchantid", + ], + ]) + let mockDelegate = MockAppSwitchDelegate(willPerform: expectation(description: "Will authorize Venmo Account"), didPerform: nil) + + sharedService.tokenizeType("Venmo", options: [BTTokenizationServiceAppSwitchDelegateOption: mockDelegate], with: stubAPIClient) { _ -> Void in } + + waitForExpectations(timeout: 2, handler: nil) + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTURLUtils_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTURLUtils_Tests.swift new file mode 100755 index 00000000..de2db491 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTURLUtils_Tests.swift @@ -0,0 +1,93 @@ +import XCTest + +class BTURLUtils_Tests: XCTestCase { + + // MARK: - dictionaryForQueryString: + + func testDictionaryForQueryString_whenQueryStringIsNil_returnsEmptyDictionary() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: nil) as NSDictionary, [:]) + } + + func testDictionaryForQueryString_whenQueryStringIsEmpty_returnsEmptyDictionary() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "") as NSDictionary, [:]) + } + + func testDictionaryForQueryString_whenQueryStringIsHasItems_returnsDictionaryContainingItems() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "foo=bar&baz=quux") as NSDictionary, [ + "foo": "bar", + "baz": "quux"]) + } + + func testDictionaryForQueryString_hasNSNullValueWhenKeyOnly() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "foo") as NSDictionary, [ + "foo": NSNull(), + ]) + } + + func testDictionaryForQueryString_whenKeyIsEmpty_hasEmptyStringForKey() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "&=asdf&") as NSDictionary, [ + "": "asdf" + ]) + } + + func testDictionaryForQueryString_withDuplicateKeys_usesRightMostValue() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "key=value1&key=value2") as NSDictionary, [ + "key": "value2" + ]) + } + + func testDictionaryForQueryString_replacesPlusWithSpace() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "foo+bar=baz+yaz") as NSDictionary, [ + "foo bar": "baz yaz" + ]) + } + + func testDictionaryForQueryString_decodesPercentEncodedCharacters() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "%20%2C=%26") as NSDictionary, [ + " ,": "&" + ]) + } + + func testDictionaryForQueryString_skipsKeysWithUndecodableCharacters() { + XCTAssertEqual(BTURLUtils.dictionary(forQueryString: "%84") as NSDictionary, [:]) + } + + // MARK: - URLfromURL:withAppendedQueryDictionary: + + func testURLWithAppendedQueryDictionary_appendsDictionaryAsQueryStringToURL() { + let url = URL(string: "http://example.com:80/path/to/file")! + + let appendedURL = BTURLUtils.urLfromURL(url, withAppendedQueryDictionary: ["key": "value"]) + + XCTAssertEqual(appendedURL, URL(string: "http://example.com:80/path/to/file?key=value")) + } + + func testURLWithAppendedQueryDictionary_acceptsNilDictionaries() { + let url = URL(string: "http://example.com")! + + let appendedURL = BTURLUtils.urLfromURL(url, withAppendedQueryDictionary: nil) + + XCTAssertEqual(appendedURL, URL(string: "http://example.com?")) + } + + func testURLWithAppendedQueryDictionary_whenDictionaryHasKeyValuePairsWithSpecialCharacters_percentEscapesThem() { + let url = URL(string: "http://example.com")! + + let appendedURL = BTURLUtils.urLfromURL(url, withAppendedQueryDictionary: ["space ": "sym&bol="]) + + XCTAssertEqual(appendedURL, URL(string: "http://example.com?space%20=sym%26bol%3D")) + } + + func testURLWithAppendedQueryDictionary_whenURLIsNil_returnsNil() { + XCTAssertNil(BTURLUtils.urLfromURL(nil, withAppendedQueryDictionary: [:])) + } + + func testURLWithAppendedQueryDictionary_whenURLIsRelative_returnsExpectedURL() { + let url = URL(string: "/relative/path")! + + let appendedURL = BTURLUtils.urLfromURL(url, withAppendedQueryDictionary: ["key": "value"]) + + XCTAssertEqual(appendedURL, URL(string: "/relative/path?key=value")) + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchReturnURLSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchReturnURLSpec.m new file mode 100755 index 00000000..cbc29340 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchReturnURLSpec.m @@ -0,0 +1,87 @@ +#import "BTVenmoAppSwitchReturnURL.h" +#import "BTSpecHelper.h" +#import +#import +#import + +SpecBegin(BTVenmoAppSwitchReturnURL) + +describe(@"URL parsing", ^{ + describe(@"valid success return URL", ^{ + it(@"creates the payment method", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=a-nonce"]]; + expect(returnURL.nonce).to.equal(@"a-nonce"); + expect(returnURL.error).to.beNil(); + }); + + it(@"sets the state to succeeded", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=a-nonce"]]; + expect(returnURL.state).to.equal(BTVenmoAppSwitchReturnURLStateSucceeded); + }); + + }); + + describe(@"valid cancel return URL", ^{ + it(@"sets the state canceled", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/cancel"]]; + + expect(returnURL.state).to.equal(BTVenmoAppSwitchReturnURLStateCanceled); + }); + + it(@"does not parse an error or payment method", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/cancel"]]; + + expect(returnURL.error).to.beNil(); + expect(returnURL.nonce).to.beNil(); + }); + }); + + describe(@"valid error return URL", ^{ + it(@"sets the state to failed", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/error?errorMessage=Venmo%20Fail&errorCode=-7"]]; + expect(returnURL.state).to.equal(BTVenmoAppSwitchReturnURLStateFailed); + }); + + it(@"parses the error message and code from a failed Venmo App Switch return", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/error?errorMessage=Venmo%20Fail&errorCode=-7"]]; + + expect(returnURL.nonce).to.beNil(); + expect(returnURL.error).to.equal([NSError errorWithDomain:BTVenmoAppSwitchReturnURLErrorDomain + code:-7 + userInfo:@{ + NSLocalizedDescriptionKey: @"Venmo Fail" + }]); + }); + }); + + describe(@"invalid return URL", ^{ + it(@"sets the state to unknown", ^{ + BTVenmoAppSwitchReturnURL *returnURL = [[BTVenmoAppSwitchReturnURL alloc] initWithURL:[NSURL URLWithString:@"com.example.app://x-callback-url/vzero/auth/venmo/something"]]; + expect(returnURL.state).to.equal(BTVenmoAppSwitchReturnURLStateUnknown); + }); + }); +}); + +describe(@"isValidURL:sourceApplication:", ^{ + NSURL *url = [NSURL URLWithString:@"scheme://x-callback-url/vzero/auth/venmo/foo"]; + + it(@"accepts app switches received from Venmo", ^{ + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"net.kortina.labs.Venmo"]).to.beTruthy(); + }); + + it(@"accepts app switches received from other Venmo builds", ^{ + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"net.kortina.labs.Venmo.debug"]).to.beTruthy(); + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"net.kortina.labs.Venmo.internal"]).to.beTruthy(); + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"net.kortina.labs.Venmo.some-new-feature"]).to.beTruthy(); + }); + + it(@"accepts app switches received from PayPal Debug (for developer-facing test wallet)", ^{ + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"com.paypal.PPClient.Debug"]).to.beTruthy(); + }); + + it(@"rejects app switches received from all others", ^{ + expect([BTVenmoAppSwitchReturnURL isValidURL:url sourceApplication:@"com.YourCompany.Some-App"]).to.beFalsy(); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchURLSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchURLSpec.m new file mode 100755 index 00000000..e0f1a843 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoAppSwitchURLSpec.m @@ -0,0 +1,69 @@ +#import +#import +#import +#import + +#import "BTVenmoAppSwitchRequestURL.h" +#import "BTVenmoDriver.h" +#import "Braintree-Version.h" +#import "BTSpecHelper.h" + +SpecBegin(BTVenmoAppSwitchRequestURL) + +describe(@"appSwitchURLForMerchantID:accessToken:sdkVersion:returnURLScheme:bundleDisplayName:environment:", ^{ + context(@"with valid params", ^{ + it(@"returns a URL containing params in query string", ^{ + + BTMutableClientMetadata *meta = [BTMutableClientMetadata new]; + [meta setSessionId:@"session-id"]; + [meta setIntegration:BTClientMetadataIntegrationCustom]; + + NSURL *url = [BTVenmoAppSwitchRequestURL appSwitchURLForMerchantID:@"merchant-id" + accessToken:@"access-token" + returnURLScheme:@"a.scheme" + bundleDisplayName:@"An App" + environment:@"sandbox" + metadata:meta]; + + NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem *queryItem in urlComponents.queryItems) { + if ([queryItem.name isEqualToString:@"braintree_environment"]) { + expect(queryItem.value).to.equal(@"sandbox"); + }else if ([queryItem.name isEqualToString:@"braintree_access_token"]) { + expect(queryItem.value).to.equal(@"access-token"); + }else if ([queryItem.name isEqualToString:@"braintree_merchant_id"]) { + expect(queryItem.value).to.equal(@"merchant-id"); + }else if ([queryItem.name isEqualToString:@"x-source"]) { + expect(queryItem.value).to.equal(@"An App"); + }else if ([queryItem.name isEqualToString:@"braintree_auth_fingerprint"]) { + expect(queryItem.value).to.equal(@"a.fingerprint"); + }else if ([queryItem.name isEqualToString:@"braintree_validate"]) { + expect(queryItem.value).to.beTruthy(); + }else if ([queryItem.name isEqualToString:@"braintree_sdk_data"]) { + expect(queryItem.value).toNot.beNil(); + + NSData *data = [[NSData alloc] initWithBase64EncodedString:queryItem.value options:0]; + BTJSON *json = [[BTJSON alloc] initWithData:data]; + + BTJSON *meta = json[@"_meta"]; + expect([meta[@"sessionId"] asString]).to.equal(@"session-id"); + expect([meta[@"platform"] asString]).to.equal(@"ios"); + expect([meta[@"integration"] asString]).to.equal(@"custom"); + expect([meta[@"version"] asString]).to.equal(BRAINTREE_VERSION); + } + } + }); + }); + +}); + +describe(@"baseAppSwitchURL", ^{ + it(@"returns expected base URL for Pay with Venmo", ^{ + expect([BTVenmoAppSwitchRequestURL baseAppSwitchURL].scheme).to.equal(@"com.venmo.touch.v2"); + expect([BTVenmoAppSwitchRequestURL baseAppSwitchURL].host).to.equal(@"x-callback-url"); + expect([BTVenmoAppSwitchRequestURL baseAppSwitchURL].path).to.equal(@"/vzero/auth"); + expect([BTVenmoAppSwitchRequestURL baseAppSwitchURL].query).to.beNil(); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoDriver_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoDriver_Tests.swift new file mode 100755 index 00000000..f7367bfa --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVenmoDriver_Tests.swift @@ -0,0 +1,743 @@ +import UIKit +import XCTest + +class FakeApplication { + var lastOpenURL : URL? = nil + var openURLWasCalled : Bool = false + var cannedOpenURLSuccess : Bool = true + var cannedCanOpenURL : Bool = true + var canOpenURLWhitelist : [URL] = [] + + @objc func openURL(_ url: URL) -> Bool { + lastOpenURL = url + openURLWasCalled = true + return cannedOpenURLSuccess + } + + @objc func canOpenURL(_ url: URL) -> Bool { + for whitelistURL in canOpenURLWhitelist { + if whitelistURL.scheme == url.scheme { + return true + } + } + return cannedCanOpenURL + } +} + +class FakeBundle : Bundle { + override func object(forInfoDictionaryKey key: String) -> Any? { + return "An App"; + } +} + +class FakeDevice : UIDevice { + var fakeSystemVersion:String = "8.9" + override var systemVersion: String { + get { + return fakeSystemVersion + } + set(newSystemVersion) { + fakeSystemVersion = newSystemVersion + } + } +} + +class BTVenmoDriver_Tests: XCTestCase { + var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + var observers : [NSObjectProtocol] = [] + var viewController : UIViewController! + + override func setUp() { + super.setUp() + viewController = UIApplication.shared.windows[0].rootViewController + mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! + } + + override func tearDown() { + if viewController.presentedViewController != nil { + viewController.dismiss(animated: false, completion: nil) + } + + for observer in observers { NotificationCenter.default.removeObserver(observer) } + super.tearDown() + } + + func testAuthorizeAccount_whenAPIClientIsNil_callsBackWithError() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + venmoDriver.apiClient = nil + + let expectation = self.expectation(description: "Callback invoked with error") + venmoDriver.authorizeAccountAndVault(false) { (tokenizedCard, error) -> Void in + XCTAssertNil(tokenizedCard) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTVenmoDriverErrorDomain) + XCTAssertEqual(error.code, BTVenmoDriverErrorType.integration.rawValue) + expectation.fulfill() + } + + self.waitForExpectations(timeout: 10, handler: nil) + } + + func testAuthorizeAccount_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { + mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + + let expectation = self.expectation(description: "Tokenize fails with error") + venmoDriver.authorizeAccountAndVault(false) { (tokenizedCard, error) -> Void in + XCTAssertEqual(error! as NSError, self.mockAPIClient.cannedConfigurationResponseError!) + expectation.fulfill() + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenVenmoConfigurationDisabled_callsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ "venmo": "off" ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + + let expectation = self.expectation(description: "tokenization callback") + venmoDriver.authorizeAccountAndVault(false) { (tokenizedCard, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTVenmoDriverErrorDomain) + XCTAssertEqual(error.code, BTVenmoDriverErrorType.disabled.rawValue) + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenVenmoConfigurationMissing_callsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [:]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + + let expectation = self.expectation(description: "tokenization callback") + venmoDriver.authorizeAccountAndVault(false) { (tokenizedCard, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTVenmoDriverErrorDomain) + XCTAssertEqual(error.code, BTVenmoDriverErrorType.disabled.rawValue) + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenReturnURLSchemeIsNil_logsCriticalMessageAndCallsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + BTConfiguration.enableVenmo(true); + + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "" + + + var criticalMessageLogged = false + BTLogger.shared().logBlock = { + (level: BTLogLevel, message: String?) in + if (level == BTLogLevel.critical && message == "Venmo requires a return URL scheme to be configured via [BTAppSwitch setReturnURLScheme:]") { + criticalMessageLogged = true + } + BTLogger.shared().logBlock = nil + return + } + + let expectation = self.expectation(description: "authorization callback") + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, BTVenmoDriverErrorDomain) + XCTAssertEqual(error.code, BTVenmoDriverErrorType.appNotAvailable.rawValue) + expectation.fulfill() + } + + XCTAssertTrue(criticalMessageLogged) + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorization_whenVenmoIsEnabledInControlPanelAndConfiguredCorrectly_opensVenmoURL() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "environment": "rockbox", + "merchantId": "top_level_merchant_id", + "payWithVenmo" : [ + "environment":"venmobox", + "accessToken": "access-token", + "merchantId": "venmo_merchant_id" ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + venmoDriver.application = fakeApplication + venmoDriver.bundle = FakeBundle() + + venmoDriver.authorizeAccountAndVault(false) { _ -> Void in } + + XCTAssertTrue(fakeApplication.openURLWasCalled) + XCTAssertEqual(fakeApplication.lastOpenURL!.scheme, "com.venmo.touch.v2") + XCTAssertNotNil(fakeApplication.lastOpenURL!.absoluteString.range(of: "venmo_merchant_id")); + XCTAssertNotNil(fakeApplication.lastOpenURL!.absoluteString.range(of: "venmobox")); + XCTAssertNotNil(fakeApplication.lastOpenURL!.absoluteString.range(of: "access-token")); + } + + func testAuthorizeAccount_beforeAppSwitch_informsDelegate() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + let delegate = MockAppSwitchDelegate(willPerform: expectation(description: "willPerform called"), didPerform: expectation(description: "didPerform called")) + venmoDriver.appSwitchDelegate = delegate + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + venmoDriver.application = fakeApplication + venmoDriver.bundle = FakeBundle() + + venmoDriver.authorizeAccountAndVault(false) { _ -> Void in + XCTAssertEqual(delegate.lastAppSwitcher as? BTVenmoDriver, venmoDriver) + } + + waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenUsingTokenizationKeyAndAppSwitchSucceeds_tokenizesVenmoAccount() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback") + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + guard let venmoAccount = venmoAccount else { + XCTFail("Received an error: \(error)") + return + } + + XCTAssertNil(error) + XCTAssertEqual(venmoAccount.nonce, "fake-nonce") + XCTAssertEqual(venmoAccount.localizedDescription, "fake-username") + XCTAssertEqual(venmoAccount.username, "fake-username") + expectation.fulfill() + } + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenUsingClientTokenAndAppSwitchSucceeds_tokenizesVenmoAccount() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + // Test setup sets up mockAPIClient with a tokenization key, we want a client token + mockAPIClient.tokenizationKey = nil + mockAPIClient.clientToken = try! BTClientToken(clientToken: BTTestClientTokenFactory.token(withVersion: 2)) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback") + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + guard let venmoAccount = venmoAccount else { + XCTFail("Received an error: \(error)") + return + } + + XCTAssertNil(error) + XCTAssertEqual(venmoAccount.nonce, "fake-nonce") + XCTAssertEqual(venmoAccount.localizedDescription, "fake-username") + XCTAssertEqual(venmoAccount.username, "fake-username") + expectation.fulfill() + } + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenAppSwitchSucceeds_makesDelegateCallbacks() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + let delegate = MockAppSwitchDelegate(willPerform: self.expectation(description: "willPerform called"), didPerform: self.expectation(description: "didPerform called")) + venmoDriver.appSwitchDelegate = delegate + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback") + venmoDriver.authorizeAccountAndVault(false) { _ -> Void in + XCTAssertEqual(delegate.lastAppSwitcher as? BTVenmoDriver, venmoDriver) + expectation.fulfill() + } + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenAppSwitchSucceeds_postsNotifications() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + let delegate = MockAppSwitchDelegate(willPerform: expectation(description: "willPerform called"), didPerform: expectation(description: "didPerform called")) + venmoDriver.appSwitchDelegate = delegate + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let willAppSwitchNotificationExpectation = expectation(description: "willAppSwitch notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchWillSwitch, object: nil, queue: nil) { (notification) -> Void in + willAppSwitchNotificationExpectation.fulfill() + }) + + let didAppSwitchNotificationExpectation = expectation(description: "didAppSwitch notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchDidSwitch, object: nil, queue: nil) { (notification) -> Void in + didAppSwitchNotificationExpectation.fulfill() + }) + + venmoDriver.authorizeAccountAndVault(false) { _ -> Void in } + + let willProcessNotificationExpectation = expectation(description: "willProcess notification received") + observers.append(NotificationCenter.default.addObserver(forName: NSNotification.Name.BTAppSwitchWillProcessPaymentInfo, object: nil, queue: nil) { (notification) -> Void in + willProcessNotificationExpectation.fulfill() + }) + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_whenAppSwitchFails_callsBackWithError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + XCTAssertNil(venmoAccount) + guard let error = error as? NSError else {return} + XCTAssertEqual(error.domain, "com.braintreepayments.BTVenmoAppSwitchReturnURLErrorDomain") + expectation.fulfill() + } + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/error")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_vaultTrue_setsShouldVaultProperty() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + + venmoDriver.authorizeAccountAndVault(true) { (venmoAccount, error) -> Void in + XCTAssertTrue(venmoDriver.shouldVault) + expectation.fulfill() + } + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_vaultFalse_setsVaultToFalse() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + XCTAssertFalse(venmoDriver.shouldVault) + expectation.fulfill() + } + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_vaultTrue_callsBackWithNonce() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "venmoAccounts": [[ + "type": "VenmoAccount", + "nonce": "abcd-venmo-nonce", + "description": "VenmoAccount", + "consumed": false, + "default": true, + "details": [ + "cardType": "Discover", + "username": "venmojoe" + ]] + ] + ]) + + + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + + venmoDriver.authorizeAccountAndVault(true) { (venmoAccount, error) -> Void in + XCTAssertNil(error) + + XCTAssertEqual(venmoAccount?.username, "venmojoe") + XCTAssertEqual(venmoAccount?.nonce, "abcd-venmo-nonce") + XCTAssertTrue(venmoAccount!.isDefault) + + expectation.fulfill() + } + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + self.waitForExpectations(timeout: 2, handler: nil) + } + + func testAuthorizeAccount_vaultTrue_sendsSucessAnalyticsEvent() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "venmoAccounts": [[ + "type": "VenmoAccount", + "nonce": "abcd-venmo-nonce", + "description": "VenmoAccount", + "consumed": false, + "default": true, + "details": [ + "cardType": "Discover", + "username": "venmojoe" + ] + ]] + ]) + + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + + venmoDriver.authorizeAccountAndVault(true) { (venmoAccount, error) -> Void in + XCTAssertNil(error) + + XCTAssertEqual(venmoAccount?.username, "venmojoe") + XCTAssertEqual(venmoAccount?.nonce, "abcd-venmo-nonce") + XCTAssertTrue(venmoAccount!.isDefault) + + expectation.fulfill() + } + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + self.waitForExpectations(timeout: 2, handler: nil) + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.pay-with-venmo.vault.success") + } + + func testAuthorizeAccount_vaultTrue_sendsFailureAnalyticsEvent() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" + ] + ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + mockAPIClient.cannedResponseError = NSError(domain: "Fake Error", code: 400, userInfo: nil) + + + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + + venmoDriver.authorizeAccountAndVault(true) { (venmoAccount, error) -> Void in + XCTAssertNotNil(error) + expectation.fulfill() + } + + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/success?paymentMethodNonce=fake-nonce&username=fake-username")!) + self.waitForExpectations(timeout: 2, handler: nil) + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "ios.pay-with-venmo.vault.failure") + } + + func testAuthorizeAccount_whenAppSwitchCancelled_callsBackWithNoError() { + mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [ + "payWithVenmo" : [ + "environment":"sandbox", + "accessToken": "access-token", + "merchantId": "merchant_id" ] ]) + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + venmoDriver.application = FakeApplication() + venmoDriver.bundle = FakeBundle() + + let expectation = self.expectation(description: "Callback invoked") + venmoDriver.authorizeAccountAndVault(false) { (venmoAccount, error) -> Void in + XCTAssertNil(venmoAccount) + XCTAssertNil(error) + expectation.fulfill() + } + BTVenmoDriver.handleAppSwitchReturn(URL(string: "scheme://x-callback-url/vzero/auth/venmo/cancel")!) + + self.waitForExpectations(timeout: 2, handler: nil) + } + + // MARK: - Analytics + + func testAPIClientMetadata_hasSourceSetToVenmoApp() { + // API client by default uses source = .Unknown and integration = .Custom + let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! + let venmoDriver = BTVenmoDriver(apiClient: apiClient) + + XCTAssertEqual(venmoDriver.apiClient.metadata.integration, BTClientMetadataIntegrationType.custom) + XCTAssertEqual(venmoDriver.apiClient.metadata.source, BTClientMetadataSourceType.venmoApp) + } + + // MARK: - BTAppSwitchHandler + + func testIsiOSAppSwitchAvailable_whenApplicationCanOpenVenmoURL_returnsTrue() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + fakeApplication.canOpenURLWhitelist.append(URL(string: "com.venmo.touch.v2://x-callback-url/path")!) + venmoDriver.application = fakeApplication + + XCTAssertTrue(venmoDriver.isiOSAppAvailableForAppSwitch()) + } + + func testIsiOSAppSwitchAvailable_whenApplicationCantOpenVenmoURL_returnsFalse() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + venmoDriver.application = fakeApplication + + XCTAssertFalse(venmoDriver.isiOSAppAvailableForAppSwitch()) + } + + func testIsiOSAppSwitchAvailable_whenApplicationCanOpenVenmoURL_andIosLessThan9_returnsFalse() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + fakeApplication.canOpenURLWhitelist.append(URL(string: "com.venmo.touch.v2://x-callback-url/path")!) + venmoDriver.application = fakeApplication + let fakeDevice = FakeDevice() + venmoDriver.device = fakeDevice + + XCTAssertFalse(venmoDriver.isiOSAppAvailableForAppSwitch()) + } + + func testIsiOSAppSwitchAvailable_whenApplicationCantOpenVenmoURL_andIosEqualTo9_3_returnsFalse() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + venmoDriver.application = fakeApplication + let fakeDevice = FakeDevice() + fakeDevice.systemVersion = "9.3" + venmoDriver.device = fakeDevice + + XCTAssertFalse(venmoDriver.isiOSAppAvailableForAppSwitch()) + } + + func testIsiOSAppSwitchAvailable_whenApplicationCanOpenVenmoURL_andIosEqualTo11_1_returnsTrue() { + let venmoDriver = BTVenmoDriver(apiClient: mockAPIClient) + mockAPIClient = venmoDriver.apiClient as! MockAPIClient + BTAppSwitch.sharedInstance().returnURLScheme = "scheme" + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + fakeApplication.canOpenURLWhitelist.append(URL(string: "com.venmo.touch.v2://x-callback-url/path")!) + venmoDriver.application = fakeApplication + let fakeDevice = FakeDevice() + fakeDevice.systemVersion = "11.1" + venmoDriver.device = fakeDevice + + XCTAssertTrue(venmoDriver.isiOSAppAvailableForAppSwitch()) + } + + let venmoProductionSourceApplication = "net.kortina.labs.Venmo" + let venmoDebugSourceApplication = "net.kortina.labs.Venmo.debug" + let fakeWalletSourceApplication = "com.paypal.PPClient.Debug" + + func testCanHandleAppSwitchReturnURL_whenSourceApplicationIsVenmoDebugApp_returnsTrue() { + XCTAssertTrue(BTVenmoDriver.canHandleAppSwitchReturn(URL(string: "fake://fake")!, sourceApplication: venmoProductionSourceApplication)) + } + + func testCanHandleAppSwitchReturnURL_whenSourceApplicationIsVenmoProductionApp_returnsTrue() { + XCTAssertTrue(BTVenmoDriver.canHandleAppSwitchReturn(URL(string: "fake://fake")!, sourceApplication: venmoDebugSourceApplication)) + } + + func testCanHandleAppSwitchReturnURL_whenSourceApplicationIsFakeWalletAppAndURLIsValid_returnsTrue() { + XCTAssertTrue(BTVenmoDriver.canHandleAppSwitchReturn(URL(string: "doesntmatter://x-callback-url/vzero/auth/venmo/stuffffff")!, sourceApplication: fakeWalletSourceApplication)) + } + + func testCanHandleAppSwitchReturnURL_whenSourceApplicationIsNotVenmo_returnsFalse() { + XCTAssertFalse(BTVenmoDriver.canHandleAppSwitchReturn(URL(string: "fake://fake")!, sourceApplication: "invalid.source.application")) + } + + // Note: testing of handleAppSwitchReturnURL is done implicitly while testing authorizeAccountWithCompletion + + // MARK: - Drop-in + + /// Helper + func client(_ configurationDictionary: Dictionary) -> BTAPIClient { + let apiClient = BTAPIClient(authorization: "development_tokenization_key")! + let fakeHttp = BTFakeHTTP()! + fakeHttp.cannedResponse = BTJSON(value: configurationDictionary) + fakeHttp.cannedStatusCode = 200 + apiClient.configurationHTTP = fakeHttp + return apiClient + } + + func clientWithJson(_ configurationJson: BTJSON) -> BTAPIClient { + let apiClient = BTAPIClient(authorization: "development_tokenization_key")! + let fakeHttp = BTFakeHTTP()! + fakeHttp.cannedResponse = configurationJson + fakeHttp.cannedStatusCode = 200 + apiClient.configurationHTTP = fakeHttp + return apiClient + } + + class BTDropInViewControllerTestDelegate : NSObject, BTDropInViewControllerDelegate { + var didLoadExpectation: XCTestExpectation + + init(didLoadExpectation: XCTestExpectation) { + self.didLoadExpectation = didLoadExpectation + } + + @objc func drop(_ viewController: BTDropInViewController, didSucceedWithTokenization paymentMethodNonce: BTPaymentMethodNonce) {} + + @objc func drop(inViewControllerDidCancel viewController: BTDropInViewController) {} + + @objc func drop(inViewControllerDidLoad viewController: BTDropInViewController) { + didLoadExpectation.fulfill() + } + } + + func testFetchConfiguration_whenVenmoIsOff_isVenmoEnabledIsFalse() { + let apiClient = self.client(["venmo": "off"]) + + let expectation = self.expectation(description: "Fetch configuration") + apiClient.fetchOrReturnRemoteConfiguration { (configuration, error) -> Void in + XCTAssertNotNil(configuration) + XCTAssertNil(error) + XCTAssertFalse(configuration!.isVenmoEnabled) + expectation.fulfill() + } + self.waitForExpectations(timeout: 5, handler: nil) + } + + // Flaky + func pendDropIn_whenVenmoIsNotEnabled_doesNotDisplayVenmoButton() { + let apiClient = self.client(["venmo": "off"]) + + let dropInViewController = BTDropInViewController(apiClient: apiClient) + let didLoadExpectation = self.expectation(description: "Drop-in did finish loading") + + // Must be assigned here for a strong reference. The delegate property of the BTDropInViewController is a weak reference. + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) + dropInViewController.delegate = testDelegate + + viewController.present(dropInViewController, animated: false, completion: nil) + + self.waitForExpectations(timeout: 5, handler: nil) + + let enabledPaymentOptions = dropInViewController.dropInContentView.paymentButton.enabledPaymentOptions + XCTAssertFalse(enabledPaymentOptions.contains("Venmo")) + } + + // Flaky + func pendDropIn_whenVenmoIsEnabled_displaysVenmoButton() { + let json = BTJSON(value: [ + "payWithVenmo" : ["accessToken" : "access-token"], + "merchantId": "merchant_id" ]) + let apiClient = self.clientWithJson(json) + BTConfiguration.enableVenmo(true) + + let dropInViewController = BTDropInViewController(apiClient: apiClient) + let didLoadExpectation = self.expectation(description: "Drop-in did finish loading") + + // Must be assigned here for a strong reference. The delegate property of the BTDropInViewController is a weak reference. + let testDelegate = BTDropInViewControllerTestDelegate(didLoadExpectation: didLoadExpectation) + + dropInViewController.delegate = testDelegate + + dropInViewController.dropInContentView.paymentButton.application = FakeApplication() + + viewController.present(dropInViewController, animated: false, completion: nil) + + self.waitForExpectations(timeout: 5, handler: nil) + + let enabledPaymentOptions = dropInViewController.dropInContentView.paymentButton.enabledPaymentOptions + XCTAssertTrue(enabledPaymentOptions.contains("Venmo")) + } +} + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVersion_Tests.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVersion_Tests.swift new file mode 100755 index 00000000..fd5e17db --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/BTVersion_Tests.swift @@ -0,0 +1,11 @@ +import XCTest + +class BTVersion_Tests: XCTestCase { + + func testVersion_returnsAVersion() { + let regex = try! NSRegularExpression(pattern: "\\d+\\.\\d+\\.\\d+", options: []) + let matches = regex.matches(in: BRAINTREE_VERSION, options: [], range: NSMakeRange(0, BRAINTREE_VERSION.characters.count)) + XCTAssertTrue(matches.count == 1) + } + +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpirationValidatorSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpirationValidatorSpec.m new file mode 100755 index 00000000..67bb6b2b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpirationValidatorSpec.m @@ -0,0 +1,112 @@ +#import +#import "BTSpecDependencies.h" +#import "BTUICardExpirationValidator.h" + +SpecBegin(BTUICardExpirationValidator) + +describe(@"month:year:validForDate:", ^{ + context(@"validating month and year relative to given validation date", ^{ + __block NSDate *today; + + beforeEach(^{ + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + components.day = 2; + components.month = 5; + components.year = 2014; + today = components.date; + }); + + it(@"returns false when the month year are before the provided date ", ^{ + BOOL monthYearBeforeTodayValid = [BTUICardExpirationValidator month:4 year:14 validForDate:today]; + expect(monthYearBeforeTodayValid).to.beFalsy(); + }); + + it(@"returns true when the month year are the same as the provided date ", ^{ + BOOL monthYearSameAsTodayValid = [BTUICardExpirationValidator month:5 year:14 validForDate:today]; + expect(monthYearSameAsTodayValid).to.beTruthy(); + }); + + it(@"returns true when the month year are after the provided date ", ^{ + BOOL monthYearAfterTodayValid = [BTUICardExpirationValidator month:8 year:14 validForDate:today]; + expect(monthYearAfterTodayValid).to.beTruthy(); + }); + + describe(@"Year in YYYY", ^{ + it(@"returns true when the month year are after the provided date", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:8 year:2014 validForDate:today]; + expect(monthYearValid).to.beTruthy(); + }); + + it(@"returns false when the month year are before the provided date", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:4 year:2014 validForDate:today]; + expect(monthYearValid).to.beFalsy(); + }); + }); + }); + + context(@"validating dates at the end of the year", ^{ + __block NSDate *endOfYearToday; + beforeEach(^{ + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + components.day = 1; + components.month = 12; + components.year = 2014; + endOfYearToday = components.date; + }); + + it(@"returns true when the month/year are the same as the provided date", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:12 year:2014 validForDate:endOfYearToday]; + expect(monthYearValid).to.beTruthy(); + }); + }); + + context(@"validating dates far in the future", ^{ + __block NSDate *today; + + beforeEach(^{ + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + components.day = 2; + components.month = 5; + components.year = 2014; + today = components.date; + }); + + it(@"returns true when the month year are before but near the far future date", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:4 year:14 + kBTUICardExpirationValidatorFarFutureYears validForDate:today]; + expect(monthYearValid).to.beTruthy(); + }); + + it(@"returns false when the month year are not before the far future date", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:5 year:14 + kBTUICardExpirationValidatorFarFutureYears validForDate:today]; + expect(monthYearValid).to.beFalsy(); + }); + }); + + context(@"month and year formats", ^{ + __block NSDate *today; + + beforeEach(^{ + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + components.day = 2; + components.month = 2; + components.year = 2014; + today = components.date; + }); + + it(@"accepts 2 digit years", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:4 year:14 validForDate:today]; + expect(monthYearValid).to.beTruthy(); + }); + + it(@"accepts 4 digit years", ^{ + BOOL monthYearValid = [BTUICardExpirationValidator month:4 year:2014 validForDate:today]; + expect(monthYearValid).to.beTruthy(); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpiryFormatterSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpiryFormatterSpec.m new file mode 100755 index 00000000..f3e31e3a --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardExpiryFormatterSpec.m @@ -0,0 +1,133 @@ +#import +#import "BTSpecDependencies.h" +#import "BTUICardExpiryFormat.h" + +SpecBegin(BTUICardExpiryFormatter) + +describe(@"formattedValue", ^{ + + __block BTUICardExpiryFormat *format; + beforeEach(^{ + format = [[BTUICardExpiryFormat alloc] init]; + }); + + describe(@"backspace", ^{ + beforeEach(^{ + format.backspace = YES; + }); + + it(@"is a no-op when the value is empty", ^{ + format.value = @""; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@""); + expect(formattedCursorLocation).to.equal(0); + }); + + it(@"maintains the slash when deleting the first year digit", ^{ + format.value = @"12/"; + format.cursorLocation = 3; + format.backspace = YES; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"12/"); + expect(formattedCursorLocation).to.equal(3); + }); + + it(@"deletes the second month digit when backspacing the slash", ^{ + format.value = @"12"; + format.cursorLocation = 2; + format.backspace = YES; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"1"); + expect(formattedCursorLocation).to.equal(1); + }); + }); + + describe(@"insertion", ^{ + it(@"is a no-op when the value is empty", ^{ + format.value = @""; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@""); + expect(formattedCursorLocation).to.equal(0); + }); + + it(@"prepends 0 and appends / if one digit >1 is entered", ^{ + format.value = @"2"; + format.cursorLocation = 1; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"02/"); + expect(formattedCursorLocation).to.equal(3); + }); + + it(@"does not insert a slash when appending the first month digit", ^{ + format.value = @"1"; + format.cursorLocation = 1; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"1"); + expect(formattedCursorLocation).to.equal(1); + }); + + it(@"inserts a slash when appending the second digit of the month", ^{ + format.value = @"12"; + format.cursorLocation = 2; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"12/"); + expect(formattedCursorLocation).to.equal(3); + }); + + it(@"maintains the slash when inserting a digit before", ^{ + format.value = @"012/"; + format.cursorLocation = 3; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"01/2"); + expect(formattedCursorLocation).to.equal(4); + }); + + it(@"maintains the slash when inserting two digits before", ^{ + format.value = @"0123/"; + format.cursorLocation = 4; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"01/23"); + expect(formattedCursorLocation).to.equal(5); + }); + + it(@"inserts the slash when pasting in a non-slash date", ^{ + format.value = @"0123"; + format.cursorLocation = 4; + NSString *formattedValue; + NSUInteger formattedCursorLocation; + [format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation]; + + expect(formattedValue).to.equal(@"01/23"); + expect(formattedCursorLocation).to.equal(5); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardTypeSpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardTypeSpec.m new file mode 100755 index 00000000..60a9d7ad --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUICardTypeSpec.m @@ -0,0 +1,253 @@ +#import +#import "BTSpecDependencies.h" +#import "BTUICardType.h" +#import "EXPMatchers+haveKerning.h" +#import "BTUIViewUtil.h" + +SpecBegin(BTUICardType) + +describe(@"BTUICardType", ^{ + + it(@"should only have one instance of each brand", ^{ + BTUICardType *t1 = [BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)]; + BTUICardType *t2 = [BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)]; + expect(t1).to.beIdenticalTo(t2); + }); + + describe(@"possible card types for number", ^{ + + it(@"should recognize all cards with empty string", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@""]; + expect(possibleCardTypes.count).to.equal(9); + }); + + it(@"should recognize no cards starting with 1", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"1"]; + expect(possibleCardTypes.count).to.equal(0); + }); + + it(@"should recognize AmEx and Diners Club and JCB cards with 3", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"3"]; + expect(possibleCardTypes.count).to.equal(3); + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_DINERS_CLUB)]); + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)]); + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_JCB)]); + }); + + it(@"should recognize MasterCard and Maestro with a 5", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"5"]; + expect(possibleCardTypes.count).to.equal(2); + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MASTER_CARD)]); + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MAESTRO)]); + }); + + it(@"should recognize Maestro cards starting with 63", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"63"]; + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MAESTRO)]); + }); + + it(@"should recognize Maestro cards starting with 67", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"67"]; + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MAESTRO)]); + }); + + it(@"should recognize the start of a Visa", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"4"]; + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + expect(possibleCardTypes.count).to.equal(1); + }); + + it(@"should recognize a whole Visa", ^{ + NSArray *possibleCardTypes = [BTUICardType possibleCardTypesForNumber:@"4111111111111111"]; + expect(possibleCardTypes).to.contain([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + expect(possibleCardTypes.count).to.equal(1); + }); + }); + + describe(@"payment method type for card type", ^{ + it(@"recognizes Visa", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"Visa"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeVisa); + }); + + it(@"recognizes MasterCard", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"MasterCard"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeMasterCard); + }); + + it(@"recognizes Amex", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"American Express"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeAMEX); + }); + + it(@"recognizes Discover", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"Discover"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeDiscover); + }); + + it(@"recognizes JCB", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"JCB"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeJCB); + }); + + it(@"recognizes Maestro", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"Maestro"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeMaestro); + }); + + it(@"recognizes Diners Club", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"Diners Club"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeDinersClub); + }); + + it(@"ignores unknown card brands", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:@"Unknown Card Brand"]; + expect([BTUIViewUtil paymentMethodTypeForCardType:cardType]).to.equal(BTUIPaymentOptionTypeUnknown); + }); + }); + + describe(@"card number recognition", ^{ + + it(@"should recognize a valid, formatted Visa", ^{ + expect([BTUICardType cardTypeForNumber:@"4111 1111 1111 1111"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + }); + + it(@"should recognize an invalid Visa", ^{ + expect([BTUICardType cardTypeForNumber:@"4111 1111 1111 1112"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + }); + + it(@"should recognize a non-formatted Visa", ^{ + expect([BTUICardType cardTypeForNumber:@"4111111111111111"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + }); + + it(@"should recognize an incomplete Visa", ^{ + expect([BTUICardType cardTypeForNumber:@"4"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)]); + }); + + it(@"should recognize a valid MasterCard", ^{ + expect([BTUICardType cardTypeForNumber:@"5555555555554444"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MASTER_CARD)]); + }); + + it(@"should recognize a valid American Express", ^{ + expect([BTUICardType cardTypeForNumber:@"378282246310005"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)]); + }); + + it(@"should recognize a valid Discover", ^{ + expect([BTUICardType cardTypeForNumber:@"6011 1111 1111 1117"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_DISCOVER)]); + }); + + it(@"should recognize a valid JCB", ^{ + expect([BTUICardType cardTypeForNumber:@"3530 1113 3330 0000"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_JCB)]); + }); + + it(@"should recognize a valid Union Pay", ^{ + expect([BTUICardType cardTypeForNumber:@"6221 2345 6789 0123 450"]).to.equal([BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_UNION_PAY)]); + }); + + it(@"should not recognize a non-number", ^{ + expect([BTUICardType cardTypeForNumber:@"notanumber"]).to.beNil(); + }); + + it(@"should not recognize an unrecognizable number", ^{ + expect([BTUICardType cardTypeForNumber:@"notanumber"]).to.beNil(); + }); + + }); + + describe(@"validNumber", ^{ + NSArray *braintreeTestCardNumbers = + @[ + @[@"378282246310005", BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)], + @[@"371449635398431", BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)], + @[@"6011111111111117", BTUILocalizedString(CARD_TYPE_DISCOVER)], + @[@"3530111333300000", BTUILocalizedString(CARD_TYPE_JCB)], + @[@"6304000000000000", BTUILocalizedString(CARD_TYPE_MAESTRO)], + @[@"5555555555554444", BTUILocalizedString(CARD_TYPE_MASTER_CARD)], + @[@"4111111111111111", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4005519200000004", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4009348888881881", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4012000033330026", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4012000077777777", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4012888888881881", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4217651111111119", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"4500600000000061", BTUILocalizedString(CARD_TYPE_VISA)], + @[@"6221234567890123450", BTUILocalizedString(CARD_TYPE_UNION_PAY)], + ]; + + for (NSArray *testCase in braintreeTestCardNumbers) { + NSString *testNumber = testCase[0]; + NSString *cardBrand = testCase[1]; + BTUICardType *cardType = [BTUICardType cardTypeForBrand:cardBrand]; + it([NSString stringWithFormat:@"should recognize %@ as a valid %@", testNumber, cardBrand], ^{ + expect([cardType validNumber:testNumber]).to.beTruthy(); + }); + } + + context(@"when card type is Union Pay", ^{ + it(@"returns true when number is not Luhn valid", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_UNION_PAY)]; + expect([cardType validNumber:@"6221234567890123451"]).to.beTruthy(); + }); + }); + }); + + describe(@"validAndNecessarilyCompleteNumber", ^{ + + it(@"should return NO for short Maestro", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MAESTRO)]; + expect([cardType validAndNecessarilyCompleteNumber:@"630400000000"]).to.beFalsy(); + expect([cardType validAndNecessarilyCompleteNumber:@"6304000000000000"]).to.beFalsy(); + }); + + it(@"should return YES for full-length Maestro", ^{ + BTUICardType *cardType = [BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MAESTRO)]; + expect([cardType validAndNecessarilyCompleteNumber:@"6304000000000000000"]).to.beTruthy(); + }); + + }); + + describe(@"card number formatting", ^{ + + it(@"should format a non-number as an empty string", ^{ + expect([[[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)] formatNumber:@"notanumber"] string]).to.equal(@""); + }); + + it(@"should return a too-long number without formatting", ^{ + expect([[[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)] formatNumber:@"00000000000000000"] string]).to.equal(@"00000000000000000"); + }); + + it(@"should format a valid, formatted number as a Visa", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)] formatNumber:@"0000 0000 0000 0000"]).to.haveKerning(@[@3, @7, @11]); + }); + + it(@"should format a non-formatted number as a Visa", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)] formatNumber:@"0000000000000000"]).to.haveKerning(@[@3, @7, @11]); + }); + + it(@"should format an incomplete number as a Visa", ^{ + expect([[[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_VISA)] formatNumber:@"0"] string]).to.equal(@"0"); + }); + + it(@"should format as a MasterCard", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_MASTER_CARD)] formatNumber:@"0000000000000000"]).to.haveKerning(@[@3, @7, @11]); + }); + + it(@"should format as an American Express", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)] formatNumber:@"000000000000000"]).to.haveKerning(@[@3, @9]); + }); + + it(@"should format as an incomplete American Express", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_AMERICAN_EXPRESS)] formatNumber:@"00000"]).to.haveKerning(@[@3]); + }); + + it(@"should format as a Discover", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_DISCOVER)] formatNumber:@"1234123412341234"]).to.haveKerning(@[@3, @7, @11]); + }); + + it(@"should format as a JCB", ^{ + expect([[BTUICardType cardTypeForBrand:BTUILocalizedString(CARD_TYPE_JCB)] formatNumber:@"1234123412341234"]).to.haveKerning(@[@3, @7, @11]); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUISpec.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUISpec.m new file mode 100755 index 00000000..fbed8921 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUISpec.m @@ -0,0 +1,24 @@ +#import +#import "BTSpecDependencies.h" +#import "BTUI.h" + +SpecBegin(BTUI) + +describe(@"BTUI", ^{ + it(@"has a braintree theme", ^{ + BTUI *theme = [BTUI braintreeTheme]; + expect(theme.callToActionColor).notTo.beNil(); + }); +}); + +describe(@"activity indicator style", ^{ + it(@"returns white for a dark background", ^{ + expect([BTUI activityIndicatorViewStyleForBarTintColor:[UIColor blackColor]]).to.equal(UIActivityIndicatorViewStyleWhite); + }); + + it(@"returns gray for a light background", ^{ + expect([BTUI activityIndicatorViewStyleForBarTintColor:[UIColor whiteColor]]).to.equal(UIActivityIndicatorViewStyleGray); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUI_UIColor.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUI_UIColor.m new file mode 100755 index 00000000..25c777f3 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/BTUI_UIColor.m @@ -0,0 +1,53 @@ +#import +#import "BTSpecDependencies.h" +#import "UIColor+BTUI.h" + +SpecBegin(BTUI_UIColor) + +describe(@"UIColor+BTUI", ^{ + describe(@"bt_colorFromHex", ^{ + it(@"converts simple valid strings", ^{ + UIColor *red = [UIColor bt_colorFromHex:@"#ff0000" alpha:1.0f]; + expect(red).to.equal([UIColor redColor]); + UIColor *green = [UIColor bt_colorFromHex:@"#00ff00" alpha:1.0f]; + expect(green).to.equal([UIColor greenColor]); + UIColor *blue = [UIColor bt_colorFromHex:@"#0000ff" alpha:1.0f]; + expect(blue).to.equal([UIColor blueColor]); + }); + + it(@"converts mixed color strings", ^{ + UIColor *c = [UIColor bt_colorFromHex:@"#ffffff" alpha:1.0f]; + expect(CGColorGetNumberOfComponents(c.CGColor)).to.equal(4); + CGFloat r, g, b, a; + [c getRed:&r green:&g blue:&b alpha:&a]; + expect(r).to.equal(1.0f); + expect(g).to.equal(1.0f); + expect(b).to.equal(1.0f); + expect(a).to.equal(1.0f); + + }); + + it(@"can take an alpha value", ^{ + UIColor *blueClear = [UIColor bt_colorFromHex:@"#0000ff" alpha:0.0f]; + expect(blueClear).notTo.equal([UIColor blueColor]); + expect(CGColorGetNumberOfComponents(blueClear.CGColor)).to.equal(4); + CGFloat r, g, b, a; + [blueClear getRed:&r green:&g blue:&b alpha:&a]; + expect(r).to.equal(0.0f); + expect(g).to.equal(0.0f); + expect(b).to.equal(1.0f); + expect(a).to.equal(0.0f); + }); + + it(@"doesn't choke on invalid strings", ^{ + UIColor *c; + c = [UIColor bt_colorFromHex:@"#nnn" alpha:1.0f]; + expect(c).to.equal([UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:1.0f]); + + c = [UIColor bt_colorFromHex:@"#im un ur hex and i am not real" alpha:1.0f]; + expect(c).to.equal([UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:1.0f]); + }); + }); +}); + +SpecEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.h new file mode 100755 index 00000000..9ebb2ac3 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.h @@ -0,0 +1,5 @@ +#import "Expecta.h" + +EXPMatcherInterface(haveKerning, (NSArray *expectedIndices)); + +#define haveKerning haveKerning diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.m new file mode 100755 index 00000000..b1d1ffba --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Braintree-UI-Specs/EXPMatchers+haveKerning.m @@ -0,0 +1,51 @@ +#import "EXPMatchers+haveKerning.h" +#import + +EXPMatcherImplementationBegin(haveKerning, (NSArray *expectedIndices)) { + BOOL actualIsNil = (actual == nil); + BOOL expectedIsNil = (expectedIndices == nil); + + prerequisite(^BOOL { + return !(actualIsNil || expectedIsNil); + // Return `NO` if matcher should fail whether or not the result is inverted + // using `.Not`. + }); + + match(^BOOL { + for (NSNumber *n in expectedIndices) { + NSUInteger i = [n unsignedIntegerValue]; + NSDictionary *attributes = [actual attributesAtIndex:i effectiveRange:nil]; + NSNumber *v = [attributes objectForKey:NSKernAttributeName]; + if ([v floatValue] <= 0) { + return NO; + } + } + return YES; + }); + + failureMessageForTo(^NSString * { + if (actualIsNil) + return @"the actual value is nil/null"; + if (expectedIsNil) + return @"the expected value is nil/null"; + return [NSString + stringWithFormat:@"expected: %@" + "got: an instance of %@ with non-matching kerning", + expectedIndices, [actual class]]; + // Return the message to be displayed when the match function returns `YES`. + }); + + failureMessageForNotTo(^NSString * { + return @"fail"; +// if (actualIsNil) +// return @"the actual value is nil/null"; +// if (expectedIsNil) +// return @"the expected value is nil/null"; +// return [NSString +// stringWithFormat:@"expected: not a kind of %@, " +// "got: an instance of %@, which is a kind of %@", +// [expected class], [actual class], [expected class]]; +// // Return the message to be displayed when the match function returns `NO`. + }); +} +EXPMatcherImplementationEnd diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.h new file mode 100755 index 00000000..e8c1c107 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.h @@ -0,0 +1,26 @@ +#import +#import "BTHTTP.h" + +@interface BTFakeHTTP : BTHTTP + +@property (nonatomic, assign) NSUInteger GETRequestCount; +@property (nonatomic, assign) NSUInteger POSTRequestCount; +@property (nonatomic, copy, nullable) NSString *lastRequestEndpoint; +@property (nonatomic, copy, nullable) NSString *lastRequestMethod; +@property (nonatomic, strong, nullable) NSDictionary *lastRequestParameters; +@property (nonatomic, copy, nullable) NSString *stubMethod; +@property (nonatomic, copy, nullable) NSString *stubEndpoint; +@property (nonatomic, strong, nullable) BTJSON *cannedResponse; +@property (nonatomic, strong, nullable) BTJSON *cannedConfiguration; +@property (nonatomic, assign) NSUInteger cannedStatusCode; +@property (nonatomic, strong, nullable) NSError *cannedError; + +- (nullable instancetype)init; + ++ (nullable instancetype)fakeHTTP; + +- (void)stubRequest:(nonnull NSString *)httpMethod toEndpoint:(nonnull NSString *)endpoint respondWith:(nonnull id)value statusCode:(NSUInteger)statusCode; + +- (void)stubRequest:(nonnull NSString *)httpMethod toEndpoint:(nonnull NSString *)endpoint respondWithError:(nonnull NSError *)error; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.m new file mode 100755 index 00000000..4f5d9091 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTFakeHTTP.m @@ -0,0 +1,96 @@ +#import "BTFakeHTTP.h" + +@implementation BTFakeHTTP + +- (nullable instancetype)init { + return [self initWithBaseURL:[[NSURL alloc] init] authorizationFingerprint:@""]; +} + ++ (instancetype)fakeHTTP { + return [[BTFakeHTTP alloc] initWithBaseURL:[[NSURL alloc] init] authorizationFingerprint:@""]; +} + +- (id)copyWithZone:(NSZone *)zone { + BTFakeHTTP *copiedHTTP = [super copyWithZone:zone]; + + copiedHTTP.GETRequestCount = self.GETRequestCount; + copiedHTTP.POSTRequestCount = self.POSTRequestCount; + copiedHTTP.lastRequestEndpoint = self.lastRequestEndpoint; + copiedHTTP.lastRequestParameters = [self.lastRequestParameters copy]; + copiedHTTP.stubMethod = self.stubMethod; + copiedHTTP.stubEndpoint = self.stubEndpoint; + copiedHTTP.cannedResponse = self.cannedResponse; + copiedHTTP.cannedStatusCode = self.cannedStatusCode; + copiedHTTP.cannedError = self.cannedError; + + return copiedHTTP; +} + +- (void)stubRequest:(NSString *)httpMethod toEndpoint:(NSString *)endpoint respondWith:(id)value statusCode:(NSUInteger)statusCode { + self.stubMethod = httpMethod; + self.stubEndpoint = endpoint; + self.cannedResponse = [[BTJSON alloc] initWithValue:value]; + self.cannedStatusCode = statusCode; +} + +- (void)stubRequest:(NSString *)httpMethod toEndpoint:(NSString *)endpoint respondWithError:(NSError *)error { + self.stubMethod = httpMethod; + self.stubEndpoint = endpoint; + self.cannedError = error; +} + +- (void)GET:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void(^)(BTJSON *, NSHTTPURLResponse *, NSError *))completionBlock { + self.GETRequestCount++; + self.lastRequestEndpoint = endpoint; + self.lastRequestParameters = parameters; + self.lastRequestMethod = @"GET"; + + if (self.cannedError) { + [self dispatchBlock:^{ + completionBlock(nil, nil, self.cannedError); + }]; + } else { + NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:endpoint] + statusCode:self.cannedStatusCode + HTTPVersion:nil + headerFields:nil]; + [self dispatchBlock:^{ + BTJSON *jsonResponse = [endpoint rangeOfString:@"v1/configuration"].location != NSNotFound ? self.cannedConfiguration : self.cannedResponse; + completionBlock(jsonResponse, httpResponse, nil); + }]; + } +} + +- (void)POST:(NSString *)endpoint parameters:(NSDictionary *)parameters completion:(void (^)(BTJSON *, NSHTTPURLResponse *, NSError *))completionBlock { + self.POSTRequestCount++; + self.lastRequestEndpoint = endpoint; + self.lastRequestParameters = parameters; + self.lastRequestMethod = @"POST"; + + if (self.cannedError) { + [self dispatchBlock:^{ + completionBlock(nil, nil, self.cannedError); + }]; + } else { + NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:endpoint] + statusCode:self.cannedStatusCode + HTTPVersion:nil + headerFields:nil]; + [self dispatchBlock:^{ + completionBlock(self.cannedResponse, httpResponse, nil); + }]; + } +} + +/// Helper method to dispatch callbacks to dispatchQueue +- (void)dispatchBlock:(void(^)())block { + if (self.dispatchQueue) { + dispatch_async(self.dispatchQueue, ^{ + block(); + }); + } else { + block(); + } +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.h new file mode 100755 index 00000000..81603edf --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.h @@ -0,0 +1,15 @@ +#import +@class BTJSON; + +#define kBTHTTPTestProtocolScheme @"bt-http-test" +#define kBTHTTPTestProtocolHost @"base.example.com" +#define kBTHTTPTestProtocolBasePath @"/base/path" +#define kBTHTTPTestProtocolPort @1234 + +@interface BTHTTPTestProtocol : NSURLProtocol + ++ (NSURLRequest *)parseRequestFromTestResponseBody:(BTJSON *)responseBody; ++ (NSString *)parseRequestBodyFromTestResponseBody:(BTJSON *)responseBody; ++ (NSURL *)testBaseURL; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.m new file mode 100755 index 00000000..f1431418 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTHTTPTestProtocol.m @@ -0,0 +1,73 @@ +#import "BTHTTPTestProtocol.h" +#import "BTHTTP.h" + +@implementation BTHTTPTestProtocol + ++ (BOOL)canInitWithRequest:(__unused NSURLRequest *)request { + return YES; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { + return request; +} + +- (void)startLoading { + id client = self.client; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:@{@"Content-Type": @"application/json"}]; + + NSData *archivedRequest = [NSKeyedArchiver archivedDataWithRootObject:self.request]; + NSString *base64ArchivedRequest = [archivedRequest base64EncodedStringWithOptions:0]; + + NSData *requestBodyData; + if (self.request.HTTPBodyStream) { + NSInputStream *inputStream = self.request.HTTPBodyStream; + [inputStream open]; + NSMutableData *mutableBodyData = [NSMutableData data]; + + while ([inputStream hasBytesAvailable]) { + uint8_t buffer[128]; + NSUInteger bytesRead = [inputStream read:buffer maxLength:128]; + [mutableBodyData appendBytes:buffer length:bytesRead]; + } + [inputStream close]; + requestBodyData = [mutableBodyData copy]; + } else { + requestBodyData = self.request.HTTPBody; + } + + NSDictionary *responseBody = @{ @"request": base64ArchivedRequest, + @"requestBody": [[NSString alloc] initWithData:requestBodyData encoding:NSUTF8StringEncoding] }; + + [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + + [client URLProtocol:self didLoadData:[NSJSONSerialization dataWithJSONObject:responseBody options:NSJSONWritingPrettyPrinted error:NULL]]; + + [client URLProtocolDidFinishLoading:self]; +} + +- (void)stopLoading { +} + ++ (NSURL *)testBaseURL { + NSURLComponents *components = [[NSURLComponents alloc] init]; + components.scheme = kBTHTTPTestProtocolScheme; + components.host = kBTHTTPTestProtocolHost; + components.path = kBTHTTPTestProtocolBasePath; + components.port = kBTHTTPTestProtocolPort; + return components.URL; +} + ++ (NSURLRequest *)parseRequestFromTestResponseBody:(BTJSON *)responseBody { + return [NSKeyedUnarchiver unarchiveObjectWithData:[[NSData alloc] initWithBase64EncodedString:[responseBody[@"request"] asString] options:0]]; +} + ++ (NSString *)parseRequestBodyFromTestResponseBody:(BTJSON *)responseBody { + return [responseBody[@"requestBody"] asString]; +} + +@end + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecDependencies.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecDependencies.h new file mode 100755 index 00000000..393bbc34 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecDependencies.h @@ -0,0 +1,3 @@ +#import +#import +#import diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.h new file mode 100755 index 00000000..a7c06a94 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.h @@ -0,0 +1,22 @@ +#import +#import +#import + +typedef NS_ENUM(NSInteger, BTTestMode_t) { + BTTestModeDebug = 1, + BTTestModeRelease = 2 +}; + +extern BTTestMode_t BTTestMode; + +extern NSString * const BTValidTestClientToken; + +void wait_for_potential_async_exceptions(void (^done)(void)); + +BOOL isANonce(NSString *nonce); + +@interface BTOCMockHelper : NSObject + +- (void)stubApplicationCanOpenURL; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.m new file mode 100755 index 00000000..6b7f4353 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTSpecHelper.m @@ -0,0 +1,40 @@ +#import "BTSpecHelper.h" +#import + +#ifdef DEBUG +BTTestMode_t BTTestMode = BTTestModeDebug; +#else +BTTestMode_t BTTestMode = BTTestModeRelease; +#endif + +NSString * const BTValidTestClientToken = @"eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0aW9uRmluZ2VycHJpbnQiOiI3ODJhZmFlNDJlZTNiNTA4NWUxNmMzYjhkZTY3OGQxNTJhODFlYzk5MTBmZDNhY2YyYWU4MzA2OGI4NzE4YWZhfGNyZWF0ZWRfYXQ9MjAxNS0wOC0yMFQwMjoxMTo1Ni4yMTY1NDEwNjErMDAwMFx1MDAyNmN1c3RvbWVyX2lkPTM3OTU5QTE5LThCMjktNDVBNC1CNTA3LTRFQUNBM0VBOEM4Nlx1MDAyNm1lcmNoYW50X2lkPWRjcHNweTJicndkanIzcW5cdTAwMjZwdWJsaWNfa2V5PTl3d3J6cWszdnIzdDRuYzgiLCJjb25maWdVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi9jbGllbnRfYXBpL3YxL2NvbmZpZ3VyYXRpb24iLCJjaGFsbGVuZ2VzIjpbXSwiZW52aXJvbm1lbnQiOiJzYW5kYm94IiwiY2xpZW50QXBpVXJsIjoiaHR0cHM6Ly9hcGkuc2FuZGJveC5icmFpbnRyZWVnYXRld2F5LmNvbTo0NDMvbWVyY2hhbnRzL2RjcHNweTJicndkanIzcW4vY2xpZW50X2FwaSIsImFzc2V0c1VybCI6Imh0dHBzOi8vYXNzZXRzLmJyYWludHJlZWdhdGV3YXkuY29tIiwiYXV0aFVybCI6Imh0dHBzOi8vYXV0aC52ZW5tby5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIiwiYW5hbHl0aWNzIjp7InVybCI6Imh0dHBzOi8vY2xpZW50LWFuYWx5dGljcy5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tIn0sInRocmVlRFNlY3VyZUVuYWJsZWQiOnRydWUsInRocmVlRFNlY3VyZSI6eyJsb29rdXBVcmwiOiJodHRwczovL2FwaS5zYW5kYm94LmJyYWludHJlZWdhdGV3YXkuY29tOjQ0My9tZXJjaGFudHMvZGNwc3B5MmJyd2RqcjNxbi90aHJlZV9kX3NlY3VyZS9sb29rdXAifSwicGF5cGFsRW5hYmxlZCI6dHJ1ZSwicGF5cGFsIjp7ImRpc3BsYXlOYW1lIjoiQWNtZSBXaWRnZXRzLCBMdGQuIChTYW5kYm94KSIsImNsaWVudElkIjpudWxsLCJwcml2YWN5VXJsIjoiaHR0cDovL2V4YW1wbGUuY29tL3BwIiwidXNlckFncmVlbWVudFVybCI6Imh0dHA6Ly9leGFtcGxlLmNvbS90b3MiLCJiYXNlVXJsIjoiaHR0cHM6Ly9hc3NldHMuYnJhaW50cmVlZ2F0ZXdheS5jb20iLCJhc3NldHNVcmwiOiJodHRwczovL2NoZWNrb3V0LnBheXBhbC5jb20iLCJkaXJlY3RCYXNlVXJsIjpudWxsLCJhbGxvd0h0dHAiOnRydWUsImVudmlyb25tZW50Tm9OZXR3b3JrIjp0cnVlLCJlbnZpcm9ubWVudCI6Im9mZmxpbmUiLCJ1bnZldHRlZE1lcmNoYW50IjpmYWxzZSwiYnJhaW50cmVlQ2xpZW50SWQiOiJtYXN0ZXJjbGllbnQzIiwiYmlsbGluZ0FncmVlbWVudHNFbmFibGVkIjpmYWxzZSwibWVyY2hhbnRBY2NvdW50SWQiOiJzdGNoMm5mZGZ3c3p5dHc1IiwiY3VycmVuY3lJc29Db2RlIjoiVVNEIn0sImNvaW5iYXNlRW5hYmxlZCI6dHJ1ZSwiY29pbmJhc2UiOnsiY2xpZW50SWQiOiIxMWQyNzIyOWJhNThiNTZkN2UzYzAxYTA1MjdmNGQ1YjQ0NmQ0ZjY4NDgxN2NiNjIzZDI1NWI1NzNhZGRjNTliIiwibWVyY2hhbnRBY2NvdW50IjoiY29pbmJhc2UtZGV2ZWxvcG1lbnQtbWVyY2hhbnRAZ2V0YnJhaW50cmVlLmNvbSIsInNjb3BlcyI6ImF1dGhvcml6YXRpb25zOmJyYWludHJlZSB1c2VyIiwicmVkaXJlY3RVcmwiOiJodHRwczovL2Fzc2V0cy5icmFpbnRyZWVnYXRld2F5LmNvbS9jb2luYmFzZS9vYXV0aC9yZWRpcmVjdC1sYW5kaW5nLmh0bWwiLCJlbnZpcm9ubWVudCI6Im1vY2sifSwibWVyY2hhbnRJZCI6ImRjcHNweTJicndkanIzcW4iLCJ2ZW5tbyI6Im9mZmxpbmUiLCJhcHBsZVBheSI6eyJzdGF0dXMiOiJtb2NrIiwiY291bnRyeUNvZGUiOiJVUyIsImN1cnJlbmN5Q29kZSI6IlVTRCIsIm1lcmNoYW50SWRlbnRpZmllciI6Im1lcmNoYW50LmNvbS5icmFpbnRyZWVwYXltZW50cy5zYW5kYm94LkJyYWludHJlZS1EZW1vIiwic3VwcG9ydGVkTmV0d29ya3MiOlsidmlzYSIsIm1hc3RlcmNhcmQiLCJhbWV4Il19fQ=="; + +void wait_for_potential_async_exceptions(void (^done)(void)) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ + done(); + }); +} + +BOOL isANonce(NSString *nonce) { + NSString *nonceRegularExpressionString = @"\\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\Z"; + + NSError *error; + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:nonceRegularExpressionString + options:0 + error:&error]; + if (error) { + NSLog(@"Error parsing regex: %@", error); + return NO; + } + + return [regex numberOfMatchesInString:nonce options:0 range:NSMakeRange(0, [nonce length])] > 0; +} + +@implementation BTOCMockHelper + +- (void)stubApplicationCanOpenURL { + id stubApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([stubApplication canOpenURL:[OCMArg any]]).andReturn(YES); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.h new file mode 100755 index 00000000..15b5e71e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.h @@ -0,0 +1,9 @@ +#import + +@interface BTTestClientTokenFactory : NSObject + ++ (NSString *)tokenWithVersion:(NSInteger)version; ++ (NSString *)tokenWithVersion:(NSInteger)version + overrides:(NSDictionary *)dictionary; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.m new file mode 100755 index 00000000..06e23359 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/BTTestClientTokenFactory.m @@ -0,0 +1,174 @@ +#import "BTTestClientTokenFactory.h" + +@implementation BTTestClientTokenFactory + ++ (NSDictionary *)clientTokenWithVersion:(NSInteger)version configurationURL:(NSURL *)url { + if (version < 3) { + return @{ + @"version": @(version), + @"authorizationFingerprint": @"an_authorization_fingerprint", + @"configUrl": url.absoluteString, + @"challenges": @[ + @"cvv" + ], + @"clientApiUrl": @"https://api.example.com:443/merchants/a_merchant_id/client_api", + @"assetsUrl": @"https://assets.example.com", + @"authUrl": @"https://auth.venmo.example.com", + @"analytics": @{ + @"url": @"https://client-analytics.example.com" + }, + @"threeDSecureEnabled": @NO, + @"paypalEnabled": @YES, + @"paypal": @{ + @"displayName": @"Acme Widgets, Ltd. (Sandbox)", + @"clientId": @"a_paypal_client_id", + @"privacyUrl": @"http://example.com/pp", + @"userAgreementUrl": @"http://example.com/tos", + @"baseUrl": @"https://assets.example.com", + @"assetsUrl": @"https://checkout.paypal.example.com", + @"directBaseUrl": [NSNull null], + @"allowHttp": @YES, + @"environmentNoNetwork": @YES, + @"environment": @"offline", + @"merchantAccountId": @"a_merchant_account_id", + @"currencyIsoCode": @"USD" + }, + @"merchantId": @"a_merchant_id", + @"venmo": @"offline", + @"applePay": @{ + @"status": @"mock", + @"countryCode": @"US", + @"currencyCode": @"USD", + @"merchantIdentifier": @"apple-pay-merchant-id", + @"supportedNetworks": @[ @"visa", + @"mastercard", + @"amex" ] + }, + @"coinbaseEnabled": @YES, + @"coinbase": @{ + @"clientId": @"a_coinbase_client_id", + @"merchantAccount": @"coinbase-account@example.com", + @"scopes": @"authorizations:braintree user", + @"redirectUrl": @"https://assets.example.com/coinbase/oauth/redirect" + }, + @"merchantAccountId": @"some-merchant-account-id", + }; + } else { + return @{ + @"version": @(version), + @"authorizationFingerprint": @"an_authorization_fingerprint", + @"configUrl": url.absoluteString, + }; + } +} + ++ (NSDictionary *)configuration { + return @{ + @"challenges": @[ + @"cvv" + ], + @"clientApiUrl": @"https://api.example.com:443/merchants/a_merchant_id/client_api", + @"assetsUrl": @"https://assets.example.com", + @"authUrl": @"https://auth.venmo.example.com", + @"analytics": @{ + @"url": @"https://client-analytics.example.com" + }, + @"threeDSecureEnabled": @NO, + @"paypalEnabled": @YES, + @"paypal": @{ + @"displayName": @"Acme Widgets, Ltd. (Sandbox)", + @"clientId": @"a_paypal_client_id", + @"privacyUrl": @"http://example.com/pp", + @"userAgreementUrl": @"http://example.com/tos", + @"baseUrl": @"https://assets.example.com", + @"assetsUrl": @"https://checkout.paypal.example.com", + @"directBaseUrl": [NSNull null], + @"allowHttp": @YES, + @"environmentNoNetwork": @YES, + @"environment": @"offline", + @"merchantAccountId": @"a_merchant_account_id", + @"currencyIsoCode": @"USD" + }, + @"merchantId": @"a_merchant_id", + @"venmo": @"offline", + @"applePay": @{ + @"status": @"mock", + @"countryCode": @"US", + @"currencyCode": @"USD", + @"merchantIdentifier": @"apple-pay-merchant-id", + @"supportedNetworks": @[ @"visa", + @"mastercard", + @"amex" ] + + }, + @"coinbaseEnabled": @YES, + @"coinbase": @{ + @"clientId": @"a_coinbase_client_id", + @"merchantAccount": @"coinbase-account@example.com", + @"scopes": @"authorizations:braintree user", + @"redirectUrl": @"https://assets.example.com/coinbase/oauth/redirect" + }, + @"merchantAccountId": @"some-merchant-account-id", + }; +} + ++ (NSDictionary *)configurationWithOverrides:(NSDictionary *)overrides { + return [self extendDictionary:self.configuration withOverrides:overrides]; +} + ++ (NSString *)tokenWithVersion:(NSInteger)version { + return [self tokenWithVersion:version overrides:nil]; +} + ++ (NSString *)tokenWithVersion:(NSInteger)version + overrides:(NSDictionary *)overrides { + BOOL base64Encoded = version == 1 ? NO : YES; + + NSURL *configurationURL = [self dataURLWithJSONObject:[self configurationWithOverrides:overrides]]; + + NSDictionary *clientToken = [self extendDictionary:[self clientTokenWithVersion:version configurationURL:configurationURL] + withOverrides:overrides]; + + NSError *jsonSerializationError; + NSData *clientTokenData = [NSJSONSerialization dataWithJSONObject:clientToken + options:0 + error:&jsonSerializationError]; + NSAssert(jsonSerializationError == nil, @"Failed to generated test client token JSON: %@", jsonSerializationError); + + if (base64Encoded) { + return [clientTokenData base64EncodedStringWithOptions:0]; + } else { + return [[NSString alloc] initWithData:clientTokenData + encoding:NSUTF8StringEncoding]; + } +} + ++ (NSURL *)dataURLWithJSONObject:(id)object { + NSError *jsonSerializationError; + NSData *configurationData = [NSJSONSerialization dataWithJSONObject:object + options:0 + error:&jsonSerializationError]; + NSAssert(jsonSerializationError == nil, @"Failed to generated test client token JSON: %@", jsonSerializationError); + NSString *base64EncodedConfigurationData = [configurationData base64EncodedStringWithOptions:0]; + NSString *dataURLString = [NSString stringWithFormat:@"data:application/json;base64,%@", base64EncodedConfigurationData]; + return [NSURL URLWithString:dataURLString]; +} + ++ (NSDictionary *)extendDictionary:(NSDictionary *)dictionary withOverrides:(NSDictionary *)overrides { + NSMutableDictionary *extendedDictionary = [dictionary mutableCopy]; + + [overrides enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop){ + if ([obj isKindOfClass:[NSNull class]]) { + [extendedDictionary removeObjectForKey:key]; + } else if ([obj isKindOfClass:[NSDictionary class]] && [overrides[key] isKindOfClass:[NSDictionary class]]) { + // Overriding values nested inside a dictionary + extendedDictionary[key] = [self extendDictionary:obj withOverrides:overrides[key]]; + } else { + extendedDictionary[key] = obj; + } + }]; + + return extendedDictionary; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.h new file mode 100755 index 00000000..3e4a60a0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.h @@ -0,0 +1,62 @@ +#import "BTPayPalRequestFactory.h" +#import "PPOTCore.h" +#import "PPOTRequest.h" + +#pragma mark - FakePayPalOneTouchCoreResult + +@interface FakePayPalOneTouchCoreResult : PPOTResult +@property (nonatomic, strong, nullable) NSError *cannedError; +@property (nonatomic, assign) PPOTResultType cannedType; +@property (nonatomic, assign) PPOTRequestTarget cannedTarget; +@end + +#pragma mark - FakePayPalOneTouchCore + +@interface FakePayPalOneTouchCore : PPOTCore ++ (nullable FakePayPalOneTouchCoreResult *)cannedResult; ++ (void)setCannedResult:(nullable FakePayPalOneTouchCoreResult *)result; ++ (BOOL)cannedIsWalletAppAvailable; ++ (void)setCannedIsWalletAppAvailable:(BOOL)isWalletAppAvailable; +@end + +#pragma mark - FakePayPalCheckoutRequest + +@interface FakePayPalCheckoutRequest : PPOTCheckoutRequest +@property (nonatomic, strong, nullable) NSError *cannedError; +@property (nonatomic, assign) BOOL cannedSuccess; +@property (nonatomic, assign) PPOTRequestTarget cannedTarget; +@property (nonatomic, strong, nullable) NSString *cannedMetadataId; +@property (nonatomic, assign) BOOL appSwitchPerformed; +@end + +#pragma mark - FakePayPalAuthorizationRequest + +@interface FakePayPalAuthorizationRequest : PPOTAuthorizationRequest +@property (nonatomic, strong, nullable) NSError *cannedError; +@property (nonatomic, assign) BOOL cannedSuccess; +@property (nonatomic, assign) PPOTRequestTarget cannedTarget; +@property (nonatomic, strong, nullable) NSString *cannedMetadataId; +@property (nonatomic, assign) BOOL appSwitchPerformed; +@property (nonatomic, strong, nullable) NSURL *cannedURL; +@end + +#pragma mark - FakePayPalBillingAgreementRequest + +@interface FakePayPalBillingAgreementRequest : PPOTBillingAgreementRequest +@property (nonatomic, strong, nullable) NSError *cannedError; +@property (nonatomic, assign) BOOL cannedSuccess; +@property (nonatomic, assign) PPOTRequestTarget cannedTarget; +@property (nonatomic, strong, nullable) NSString *cannedMetadataId; +@property (nonatomic, assign) BOOL appSwitchPerformed; +@end + +#pragma mark - FakePayPalRequestFactory + +@interface FakePayPalRequestFactory : BTPayPalRequestFactory +@property (nonatomic, strong, nonnull) FakePayPalCheckoutRequest *checkoutRequest; +@property (nonatomic, strong, nonnull) FakePayPalAuthorizationRequest *authorizationRequest; +@property (nonatomic, strong, nonnull) FakePayPalBillingAgreementRequest *billingAgreementRequest; +@property (nonatomic, strong, nullable) NSSet *lastScopeValues; +@property (nonatomic, strong, nullable) NSURL *lastApprovalURL; + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.m new file mode 100755 index 00000000..351690a0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/FakePayPalClasses.m @@ -0,0 +1,193 @@ +#import "FakePayPalClasses.h" + +#pragma mark - FakePayPalOneTouchCoreResult + +@implementation FakePayPalOneTouchCoreResult + +- (instancetype)init { + if (self = [super init]) { + _cannedType = PPOTResultTypeSuccess; + _cannedTarget = PPOTRequestTargetUnknown; + } + return self; +} + +- (NSError *)error { + return self.cannedError; +} + +- (PPOTResultType)type { + return self.cannedType; +} + +- (PPOTRequestTarget)target { + return self.cannedTarget; +} + +- (NSDictionary *)response { + return @{ @"foo" : @"bar", @"correlation_id" : @"a-correlation-id" }; +} + +@end + +#pragma mark - FakePayPalOneTouchCore + +@implementation FakePayPalOneTouchCore + +static FakePayPalOneTouchCoreResult *cannedResult; +static BOOL cannedIsWalletAppAvailable = YES; + ++ (void)initialize { + cannedResult = [[FakePayPalOneTouchCoreResult alloc] init]; +} + ++ (BOOL)cannedIsWalletAppAvailable { + return cannedIsWalletAppAvailable; +} + ++ (void)setCannedIsWalletAppAvailable:(BOOL)isWalletAppAvailable { + cannedIsWalletAppAvailable = isWalletAppAvailable; +} + ++ (FakePayPalOneTouchCoreResult *)cannedResult { + return cannedResult; +} + ++ (void)setCannedResult:(FakePayPalOneTouchCoreResult *)result { + cannedResult = result; +} + + ++ (void)parseResponseURL:(__unused NSURL *)url completionBlock:(PPOTCompletionBlock)completionBlock { + completionBlock([self cannedResult]); +} + ++ (void)redirectURLsForCallbackURLScheme:(__unused NSString *)callbackURLScheme + withReturnURL:(NSString *__autoreleasing *)returnURL + withCancelURL:(NSString *__autoreleasing *)cancelURL { + *cancelURL = @"scheme://cancel"; + *returnURL = @"scheme://return"; +} + ++ (NSString *)clientMetadataID { + return @"fake-client-metadata-id"; +} + ++ (BOOL)doesApplicationSupportOneTouchCallbackURLScheme:(__unused NSString *)callbackURLScheme { + return YES; +} + ++ (BOOL)isWalletAppInstalled { + return [self cannedIsWalletAppAvailable]; +} + +@end + +#pragma mark - FakePayPalCheckoutRequest + +@implementation FakePayPalCheckoutRequest + +- (instancetype)init { + if (self = [super init]) { + _cannedError = nil; + _cannedTarget = PPOTRequestTargetBrowser; + _cannedSuccess = YES; + _cannedMetadataId = @"fake-canned-metadata-id"; + } + return self; +} + +- (void)performWithAdapterBlock:(PPOTRequestAdapterBlock)adapterBlock { + self.appSwitchPerformed = YES; + adapterBlock(self.cannedSuccess, [NSURL URLWithString:@"http://example.com"], self.cannedTarget, self.cannedMetadataId, self.cannedError); +} + +@end + +#pragma mark - FakePayPalAuthorizationRequest + +@implementation FakePayPalAuthorizationRequest + +- (instancetype)init { + if (self = [super init]) { + _cannedError = nil; + _cannedTarget = PPOTRequestTargetBrowser; + _cannedSuccess = YES; + _cannedMetadataId = @"fake-canned-metadata-id"; + } + return self; +} + +- (void)performWithAdapterBlock:(PPOTRequestAdapterBlock)adapterBlock { + self.appSwitchPerformed = YES; + adapterBlock(self.cannedSuccess, self.cannedURL ? self.cannedURL : [NSURL URLWithString:@"http://example.com"], self.cannedTarget, self.cannedMetadataId, self.cannedError); +} + +@end + +#pragma mark - FakePayPalBillingAgreementRequest + +@implementation FakePayPalBillingAgreementRequest + +- (instancetype)init { + if (self = [super init]) { + _cannedError = nil; + _cannedTarget = PPOTRequestTargetBrowser; + _cannedSuccess = YES; + _cannedMetadataId = @"fake-canned-metadata-id"; + } + return self; +} + +- (void)performWithAdapterBlock:(PPOTRequestAdapterBlock)adapterBlock { + self.appSwitchPerformed = YES; + adapterBlock(self.cannedSuccess, [NSURL URLWithString:@"http://example.com"], self.cannedTarget, self.cannedMetadataId, self.cannedError); +} + +@end + +#pragma mark - FakePayPalRequestFactory + +@implementation FakePayPalRequestFactory + +- (instancetype)init { + if (self = [super init]) { + _authorizationRequest = [[FakePayPalAuthorizationRequest alloc] init]; + _checkoutRequest = [[FakePayPalCheckoutRequest alloc] init]; + _billingAgreementRequest = [[FakePayPalBillingAgreementRequest alloc] init]; + } + return self; +} + +- (PPOTCheckoutRequest *)checkoutRequestWithApprovalURL:(__unused NSURL *)approvalURL + clientID:(__unused NSString *)clientID + environment:(__unused NSString *)environment + callbackURLScheme:(__unused NSString *)callbackURLScheme +{ + self.lastApprovalURL = [approvalURL copy]; + return self.checkoutRequest; +} + +- (PPOTBillingAgreementRequest *)billingAgreementRequestWithApprovalURL:(__unused NSURL *)approvalURL + clientID:(__unused NSString *)clientID + environment:(__unused NSString *)environment + callbackURLScheme:(__unused NSString *)callbackURLScheme +{ + self.lastApprovalURL = [approvalURL copy]; + return self.billingAgreementRequest; +} + +- (PPOTAuthorizationRequest *)requestWithScopeValues:(NSSet *)scopeValues + privacyURL:(__unused NSURL *)privacyURL + agreementURL:(__unused NSURL *)agreementURL + clientID:(__unused NSString *)clientID + environment:(__unused NSString *)environment + callbackURLScheme:(__unused NSString *)callbackURLScheme +{ + self.lastScopeValues = scopeValues; + return self.authorizationRequest; +} + +@end + + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockAPIClient.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockAPIClient.swift new file mode 100755 index 00000000..4ac9e4b2 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockAPIClient.swift @@ -0,0 +1,74 @@ +import BraintreeCore + +class MockAPIClient : BTAPIClient { + var lastPOSTPath = "" + var lastPOSTParameters = [:] as [AnyHashable: Any]? + var lastGETPath = "" + var lastGETParameters = [:] as [String : String]? + var postedAnalyticsEvents : [String] = [] + + var cannedConfigurationResponseBody : BTJSON? = nil + var cannedConfigurationResponseError : NSError? = nil + + var cannedResponseError : NSError? = nil + var cannedHTTPURLResponse : HTTPURLResponse? = nil + var cannedResponseBody : BTJSON? = nil + + var fetchedPaymentMethods = false + var fetchPaymentMethodsSorting = false + + override func get(_ path: String, parameters: [String : String]?, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + lastGETPath = path + lastGETParameters = parameters + + guard let completionBlock = completionBlock else { + return + } + completionBlock(cannedResponseBody, cannedHTTPURLResponse, cannedResponseError) + } + + override func post(_ path: String, parameters: [AnyHashable : Any]?, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + lastPOSTPath = path + lastPOSTParameters = parameters + + guard let completionBlock = completionBlock else { + return + } + completionBlock(cannedResponseBody, cannedHTTPURLResponse, cannedResponseError) + } + + override func fetchOrReturnRemoteConfiguration(_ completionBlock: @escaping (BTConfiguration?, Error?) -> Void) { + guard let responseBody = cannedConfigurationResponseBody else { + completionBlock(nil, cannedConfigurationResponseError) + return + } + completionBlock(BTConfiguration(json: responseBody), cannedConfigurationResponseError) + } + + override func fetchPaymentMethodNonces(_ completion: @escaping ([BTPaymentMethodNonce]?, Error?) -> Void) { + fetchedPaymentMethods = true + fetchPaymentMethodsSorting = false + completion([], nil) + } + + override func fetchPaymentMethodNonces(_ defaultFirst: Bool, completion: @escaping ([BTPaymentMethodNonce]?, Error?) -> Void) { + fetchedPaymentMethods = true + fetchPaymentMethodsSorting = false + completion([], nil) + } + + /// BTAPIClient gets copied by other classes like BTPayPalDriver, BTVenmoDriver, etc. + /// This copy causes MockAPIClient to lose its stubbed data (canned responses), so the + /// workaround for tests is to stub copyWithSource:integration: to *not* copy itself + override func copy(with source: BTClientMetadataSourceType, integration: BTClientMetadataIntegrationType) -> Self { + return self + } + + override func sendAnalyticsEvent(_ name: String) { + postedAnalyticsEvents.append(name) + } + + func didFetchPaymentMethods(sorted: Bool) -> Bool { + return fetchedPaymentMethods && fetchPaymentMethodsSorting == sorted + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockDelegates.swift b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockDelegates.swift new file mode 100755 index 00000000..f9937cbd --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Helpers/MockDelegates.swift @@ -0,0 +1,71 @@ +import XCTest + +@objc class MockAppSwitchDelegate : NSObject, BTAppSwitchDelegate { + var willPerformAppSwitchExpectation : XCTestExpectation? = nil + var didPerformAppSwitchExpectation : XCTestExpectation? = nil + var willProcessAppSwitchExpectation : XCTestExpectation? = nil + // XCTestExpectations verify that delegates callbacks are made; the below bools verify that they are NOT made + var willPerformAppSwitchCalled = false + var didPerformAppSwitchCalled = false + var willProcessAppSwitchCalled = false + var lastAppSwitcher : AnyObject? = nil + + override init() { } + + init(willPerform: XCTestExpectation?, didPerform: XCTestExpectation?) { + willPerformAppSwitchExpectation = willPerform + didPerformAppSwitchExpectation = didPerform + } + + @objc func appSwitcherWillPerformAppSwitch(_ appSwitcher: Any) { + lastAppSwitcher = appSwitcher as AnyObject? + willPerformAppSwitchExpectation?.fulfill() + willPerformAppSwitchCalled = true + } + + @objc func appSwitcher(_ appSwitcher: Any, didPerformSwitchTo target: BTAppSwitchTarget) { + lastAppSwitcher = appSwitcher as AnyObject? + didPerformAppSwitchExpectation?.fulfill() + didPerformAppSwitchCalled = true + } + + @objc func appSwitcherWillProcessPaymentInfo(_ appSwitcher: Any) { + lastAppSwitcher = appSwitcher as AnyObject? + willProcessAppSwitchExpectation?.fulfill() + willProcessAppSwitchCalled = true + } +} + +@objc class MockViewControllerPresentationDelegate : NSObject, BTViewControllerPresentingDelegate { + var requestsPresentationOfViewControllerExpectation : XCTestExpectation? = nil + var requestsDismissalOfViewControllerExpectation : XCTestExpectation? = nil + var lastViewController : UIViewController? = nil + var lastPaymentDriver : AnyObject? = nil + + func paymentDriver(_ driver: Any, requestsDismissalOf viewController: UIViewController) { + lastPaymentDriver = driver as AnyObject? + lastViewController = viewController + requestsDismissalOfViewControllerExpectation?.fulfill() + } + + func paymentDriver(_ driver: Any, requestsPresentationOf viewController: UIViewController) { + lastPaymentDriver = driver as AnyObject? + lastViewController = viewController + requestsPresentationOfViewControllerExpectation?.fulfill() + } +} + +@objc class MockPayPalApprovalHandlerDelegate : NSObject, BTPayPalApprovalHandler { + var handleApprovalExpectation : XCTestExpectation? = nil + var url : NSURL? = nil + var cancel : Bool = false + + func handleApproval(_ request: PPOTRequest, paypalApprovalDelegate delegate: BTPayPalApprovalDelegate) { + if (cancel) { + delegate.onApprovalCancel() + } else { + delegate.onApprovalComplete(url as! URL) + } + handleApprovalExpectation?.fulfill() + } +} diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/Info.plist b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Info.plist new file mode 100755 index 00000000..ba72822e --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPDataCollectorTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPDataCollectorTest.m new file mode 100755 index 00000000..fe515471 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPDataCollectorTest.m @@ -0,0 +1,59 @@ +// +// PPDataCollectorTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import "PPDataCollector_Internal.h" + +@interface PPDataCollectorTest : XCTestCase + +@end + +@implementation PPDataCollectorTest + +- (void)testDeviceData_containsCorrelationId { + // Collect client metadata ID with a canned pairing ID to guarantee that the pairing ID + // hasn't already been configured by another test. Also, we can then assert the value of + // the correlation_id in the JSON object because we know the client metadata ID will be + // equal to the pairing ID. + [PPDataCollector generateClientMetadataID:@"expected_correlation_id"]; + NSString *deviceData = [PPDataCollector collectPayPalDeviceData]; + NSData *data = [deviceData dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:NULL]; + NSString *cmid = [dictionary objectForKey:@"correlation_id"]; + + XCTAssertEqualObjects(cmid, @"expected_correlation_id"); +} + +- (void)testClientMetadata_isNotJSON { + NSString *cmid = [PPDataCollector generateClientMetadataID]; + NSData *cmidJSONData = [cmid dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + id json = [NSJSONSerialization JSONObjectWithData:cmidJSONData options:0 error:&error]; + + XCTAssertNil(json); + XCTAssertNotNil(error); +} + +- (void)testClientMetadata_isConsistentOnRepeatedTries { + NSString *cmid = [PPDataCollector generateClientMetadataID]; + XCTAssertEqualObjects(cmid, [PPDataCollector generateClientMetadataID]); +} + +- (void)testClientMetadataValue_whenUsingPairingID_isSameWhenSubsequentCallsDoNotSpecifyPairingID { + NSString *pairingID = @"random pairing id"; + XCTAssertEqualObjects(pairingID, [PPDataCollector generateClientMetadataID:pairingID]); + XCTAssertEqualObjects(pairingID, [PPDataCollector generateClientMetadataID]); + XCTAssertEqualObjects(pairingID, [PPDataCollector generateClientMetadataID:nil]); +} + +- (void)testClientMetadataValue_isRegeneratedOnNonNullPairingID { + NSString *cmid = [PPDataCollector generateClientMetadataID]; + NSString *cmid2 = [PPDataCollector generateClientMetadataID:@"some pairing id"]; + XCTAssertNotEqualObjects(cmid, cmid2); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTIDataTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTIDataTest.m new file mode 100755 index 00000000..4784fdab --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTIDataTest.m @@ -0,0 +1,77 @@ +// +// PPFPTIDataTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import + +#import "PPFPTIData.h" + +@interface PPFPTIDataTest : XCTestCase + +@property (nonatomic, copy, readwrite) NSString *deviceID; +@property (nonatomic, copy, readwrite) NSString *sessionID; +@property (nonatomic, copy, readwrite) NSDictionary *eventParams; +@property (nonatomic, copy, readwrite) NSString *userAgent; +@property (nonatomic, strong, readwrite) NSURL *trackerURL; + +@end + +@implementation PPFPTIDataTest + +- (void)setUp { + [super setUp]; + self.eventParams = @{ + @"abc" : @"xyz", + @"onetwothree": @"789" + }; + self.deviceID = @"myDeviceID"; + self.sessionID = @"mySessionID"; + self.userAgent = @"myUserAgent"; + self.trackerURL = [NSURL URLWithString:@"http://example.com/v1/analytics"]; +} + +- (void)testDataAsDictionary { + PPFPTIData *data = [[PPFPTIData alloc] initWithParams:self.eventParams + deviceID:self.deviceID + sessionID:self.sessionID + userAgent:self.userAgent + trackerURL:self.trackerURL]; + XCTAssertEqualObjects(data.userAgent, self.userAgent); + XCTAssertEqualObjects(data.trackerURL, self.trackerURL); + + NSDictionary *dataDictionary = [data dataAsDictionary]; + NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; + + NSDictionary *eventsDictionary = dataDictionary[@"events"]; + XCTAssertEqualObjects(eventsDictionary[@"actor"][@"tracking_visitor_id"], self.deviceID); + XCTAssertEqualObjects(eventsDictionary[@"actor"][@"tracking_visit_id"], self.sessionID); + XCTAssertEqualObjects(eventsDictionary[@"channel"], @"mobile"); + + NSString *trackingEventsString = eventsDictionary[@"tracking_event"]; + XCTAssertNotNil(trackingEventsString); + long long convertedTimeInterval = [trackingEventsString longLongValue]; + // Tests the generated interval is within 1 second of the expected value + XCTAssertEqualWithAccuracy(convertedTimeInterval, currentTime * 1000, 1000); + + NSDictionary *eventParamsDictionary = eventsDictionary[@"event_params"]; + XCTAssertEqualObjects(eventParamsDictionary[@"abc"], self.eventParams[@"abc"]); + XCTAssertEqualObjects(eventParamsDictionary[@"onetwothree"], self.eventParams[@"onetwothree"]); + + // These were other added values passed in the past + XCTAssertNotNil(eventParamsDictionary[@"g"]); + XCTAssertNotNil(eventParamsDictionary[@"t"]); + + long long gmtOffsetInMilliseconds = [eventParamsDictionary[@"g"] integerValue] * 60 * 1000; + XCTAssertEqualWithAccuracy( + convertedTimeInterval, + gmtOffsetInMilliseconds + [eventParamsDictionary[@"t"] longLongValue], + 1000 + ); + + XCTAssertEqualObjects(eventParamsDictionary[@"sv"], @"mobile"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTITrackerTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTITrackerTest.m new file mode 100755 index 00000000..2a4e5ee0 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPFPTITrackerTest.m @@ -0,0 +1,71 @@ +// +// PPFPTITrackerTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import + +#import "PPFPTITracker.h" +#import "PPFPTIData.h" +#import "PPOTVersion.h" + +@interface PPFPTITrackerTest : XCTestCase + +@property (nonatomic, copy, readwrite) NSString *deviceID; +@property (nonatomic, copy, readwrite) NSString *sessionID; +@property (nonatomic, strong, readwrite) XCTestExpectation *expectation; + +@end + + +@implementation PPFPTITrackerTest + +- (void)setUp { + [super setUp]; + self.deviceID = @"myDeviceID"; + self.sessionID = @"mySessionID"; + self.expectation = nil; +} + +- (void)tearDown { + self.expectation = nil; + [super tearDown]; +} + +- (void)testDelegateNotSetDoesNotCrash { + PPFPTITracker *tracker = [[PPFPTITracker alloc] initWithDeviceUDID:self.deviceID + sessionID:self.sessionID + networkAdapterDelegate:nil]; + [tracker submitEventWithParams:[NSDictionary dictionary]]; +} + +- (void)testDelegatePassedInformation { + PPFPTITracker *tracker = [[PPFPTITracker alloc] initWithDeviceUDID:self.deviceID + sessionID:self.sessionID + networkAdapterDelegate:self]; + self.expectation = [self expectationWithDescription:@"Expect sendRequestWithData to be called"]; + + [tracker submitEventWithParams:[NSDictionary dictionary]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +#pragma mark PPFPTINetworkAdapterDelegate methods + +- (void)sendRequestWithData:(nonnull PPFPTIData*)fptiData { + NSString *userAgentPrefix = [NSString stringWithFormat:@"PayPalSDK/OneTouchCore-iOS %@", PayPalOTVersion()]; + XCTAssertTrue([fptiData.userAgent hasPrefix:userAgentPrefix], @"Expect PayPal to be in the prefix to help FPTI"); + XCTAssertEqualObjects(fptiData.trackerURL, [NSURL URLWithString:@"https://api-m.paypal.com/v1/tracking/events"]); + + NSDictionary *dataDictionary = [fptiData dataAsDictionary]; + + NSDictionary *eventsDictionary = dataDictionary[@"events"]; + XCTAssertEqualObjects(eventsDictionary[@"actor"][@"tracking_visitor_id"], self.deviceID); + XCTAssertEqualObjects(eventsDictionary[@"actor"][@"tracking_visit_id"], self.sessionID); + + [self.expectation fulfill]; +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchResponseTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchResponseTest.m new file mode 100755 index 00000000..47492c8f --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchResponseTest.m @@ -0,0 +1,85 @@ +// +// PPAppSwitchResponse.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import +#import "PPOTAppSwitchResponse.h" +#import "PPOTEncryptionHelper.h" +#import "PPOTString.h" + +@interface PPOTAppSwitchResponseTest : XCTestCase + +@end + +@implementation PPOTAppSwitchResponseTest + +- (void)test1InvalidHermesResponse { + PPOTAppSwitchResponse *response = [[PPOTAppSwitchResponse alloc] initWithHermesURL:nil environment:nil]; + XCTAssertNil(response); + + + response = [[PPOTAppSwitchResponse alloc] initWithHermesURL:[NSURL URLWithString:@"http"] environment:nil]; + XCTAssertFalse(response.validResponse); + + response = [[PPOTAppSwitchResponse alloc] initWithHermesURL:[NSURL URLWithString:@"http://success"] environment:nil]; + XCTAssertTrue(response.validResponse); + response = [[PPOTAppSwitchResponse alloc] initWithHermesURL:[NSURL URLWithString:@"http://cancel"] environment:nil]; + XCTAssertTrue(response.validResponse); +} + +- (void)testInvalidEncodedURLResponse { + PPOTAppSwitchResponse *response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:nil encryptionKey:nil]; + XCTAssertNil(response); + NSURL *encodedURL = [NSURL URLWithString:@""]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + XCTAssertFalse(response.validResponse); + encodedURL = [NSURL URLWithString:@"http://success"]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + XCTAssertFalse(response.validResponse); + encodedURL = [NSURL URLWithString:@"http://cancel"]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + // cancel is a cancel + XCTAssertTrue(response.validResponse && response.action == PPAppSwitchResponseActionCancel); + + encodedURL = [NSURL URLWithString:@"http://success?payload=84032840274927rowueoruwohrwlrhwourowr&payloadEnc=8043729742964uoeruwohrkwjr20r82048"]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + XCTAssertFalse(response.validResponse); + // any cancel we don't care about result + encodedURL = [NSURL URLWithString:@"http://cancel?payload=84032840274927rowueoruwohrwlrhwourowr&payloadEnc=8043729742964uoeruwohrkwjr20r82048"]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + XCTAssertTrue(response.validResponse); +} + +- (void)testInvalidEncryptedURLResponse { + NSURL *encodedURL = [NSURL URLWithString:@"http://success?payload=eyJ0ZXN0IjoidGVzdCJ9&payloadEnc=eyJ0ZXN0IjoidGVzdCJ9"]; + PPOTAppSwitchResponse *response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:@"test"]; + XCTAssertFalse(response.validResponse); + NSData *key = [PPOTEncryptionHelper generate256BitKey]; + NSString *hexKey = [PPOTString hexStringFromData:key]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:hexKey]; + XCTAssertFalse(response.validResponse); + + encodedURL = [NSURL URLWithString:@"http://success?payload=eyJ0ZXN0IjoidGVzdCJ9==&payloadEnc=eyJ0ZXN0IjoidGVzdCJ9+/80\n"]; + response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:hexKey]; + XCTAssertFalse(response.validResponse); +} + +- (void)testErrorWithDictionaryAlreadyInResponse { + NSURL *encodedURL = [NSURL URLWithString:@"http://shouldBeError?payload=eyJ2ZXJzaW9uIjozLCJtc2dfR1VJRCI6bnVsbCwicmVzcG9uc2VfdHlwZSI6bnVsbCwiZW52aXJvbm1lbnQiOiJtb2NrIiwiZXJyb3IiOnsiZGVidWdfaWQiOm51bGwsIm1lc3NhZ2UiOiJFbmNyeXB0ZWQgcGF5bG9hZCBoYXMgZXhwaXJlZCJ9LCJsYW5ndWFnZSI6bnVsbH0"]; + PPOTAppSwitchResponse *response = [[PPOTAppSwitchResponse alloc] initWithEncodedURL:encodedURL encryptionKey:nil]; + XCTAssertFalse(response.validResponse); + XCTAssertEqual(response.responseType, PPAppSwitchResponseActionUnknown); + XCTAssertEqual(response.version, 3); + XCTAssertEqual(response.action, PPAppSwitchResponseActionUnknown); + NSLog(@"%@", response.error); + NSDictionary* expectedError = @{ @"message": @"Encrypted payload has expired", @"debug_id": [NSNull null] }; + XCTAssertEqualObjects(response.error, expectedError); + XCTAssertNil(response.msgID); + XCTAssertEqualObjects(response.environment, @"mock"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchUtilTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchUtilTest.m new file mode 100755 index 00000000..33873b40 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTAppSwitchUtilTest.m @@ -0,0 +1,96 @@ +// +// PayPalTouchCoreTests.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import +#import "PPOTAppSwitchUtil.h" +#import "PPOTJSONHelper.h" + +@interface PPOTAppSwitchUtilTest : XCTestCase + +@end + +@implementation PPOTAppSwitchUtilTest + +- (void)testAppSwitchNotPossible { + BOOL possible = [PPOTAppSwitchUtil isCallbackURLSchemeValid:@"com.scheme.bla"]; + XCTAssertFalse(possible, @"app switch should not be possible when unit tests running"); +} + +- (void)testParseQueryTest { + NSString *quearyToTest = nil; + NSDictionary *parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @""; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"&"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"?"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"hello&"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"hello&hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"&hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"&=hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertFalse(parseQueryStringDict.count, @"count should be 0"); + + quearyToTest = @"hello=hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertTrue(parseQueryStringDict.count == 1, @"count should be 0"); + + quearyToTest = @"&hello=hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertTrue(parseQueryStringDict.count == 1, @"count should be 0"); + + quearyToTest = @"&hello=hello"; + parseQueryStringDict = [PPOTAppSwitchUtil parseQueryString:quearyToTest]; + XCTAssertTrue(parseQueryStringDict.count == 1, @"count should be 0"); +} + +- (void)testJsonEncodingDecoding { + NSDictionary *dict1 = @{@"key1": @1, @"key2": @"some.strings", @"key2": @{@"dict1": @"value"}, @"key3": @[@"el1", @"el2"]}; + + NSString *encoded = [PPOTJSONHelper base64EncodedJSONStringWithDictionary:dict1]; + + NSDictionary *dict2 = [PPOTJSONHelper dictionaryWithBase64EncodedJSONString:encoded]; + + XCTAssertEqualObjects(dict1, dict2, @"dictionaries must be the same"); +} + +// TODO: Test fails with PayPal OneTouchCoreSDK: callback URL scheme must start with com.braintreepayments.demo +- (void)pendURLAction { + NSString *urlTest = @"com.test.mytest://test?payload=e30%3D&x-source=(null)&x-success=com.test.callback://success&x-cancel=com.test.callback://cancel"; + NSURL *urlAction = [PPOTAppSwitchUtil URLAction:@"test" targetAppURLScheme:@"com.test.mytest" callbackURLScheme:@"com.test.callback" payload:@{}]; + XCTAssertNotNil(urlAction, @"action should not be nil"); + XCTAssertEqualObjects([urlAction absoluteString], urlTest, @"links should be the same"); +} + +- (void)testInvalidURL { + XCTAssertFalse([PPOTAppSwitchUtil isValidURLAction:nil], @"should fail"); + + NSString *urlTest = @"com.test.mytest://test?payload=e30%3D&x-source=(null)&x-success=com.test.callback://success&x-cancel=com.test.callback://cancel"; + NSURL *url = [NSURL URLWithString:urlTest]; + XCTAssertFalse([PPOTAppSwitchUtil isValidURLAction:url], @"should fail"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTConfigurationTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTConfigurationTest.m new file mode 100755 index 00000000..ef4c7840 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTConfigurationTest.m @@ -0,0 +1,296 @@ +// +// PPOTConfigurationTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import +#import "PPOTConfiguration.h" + +@interface PPOTConfigurationTest : XCTestCase + +@end + +@implementation PPOTConfigurationTest + +- (void)testPPOTConfiguration_whenBadOS_returnsNilConfig { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"Android", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + } + }]; + XCTAssertNil(configuration); +} + +- (void)testPPOTConfiguration_whenMissingExpectedVersion_returnsNilConfig { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"2.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + ], + } + }]; + XCTAssertNil(configuration); +} + +- (void)testPPOTConfiguration_whenMultipleVersions_processesExpectedVersionCorrectly { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + ], + }, + @"2.0": @{ + @"foo": @[ + ], + } + }]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)0); +} + + +- (void)testPPOTConfiguration_whenNoOAuthConfigRecipe_loadsConfigWithZeroRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + ], + } + }]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)0); +} + +- (void)testPPOTConfiguration_whenGoodOAuthConfigRecipes_loadsMultipleRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"protocol": @"2", + @"target": @"wallet", + @"scope": @[@"*"], + @"scheme": @"com.paypal.ppclient.touch.v2", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"protocol": @"0", + @"target": @"browser", + @"scope": @[@"*"], + }, + ], + } + }]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)2); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + XCTAssert([configuration.prioritizedOAuthRecipes[1] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); +} + +- (void)testPPOTConfiguration_whenConfigRecipeWithUnknownTargetInList_onlyLoadsRecognizedRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"target": @"UNKNOWN TARGET", + @"protocol": @"0", + @"scope": @[@"*"], + @"scheme": @"com.paypal.ppclient.touch.v2", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"protocol": @"0", + @"target": @"browser", + @"scope": @[@"*"], + }, + ], + } + }]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + + PPOTConfigurationOAuthRecipe *oauthRecipe = (PPOTConfigurationOAuthRecipe *)configuration.prioritizedOAuthRecipes[0]; + XCTAssertEqual(oauthRecipe.target, PPOTRequestTargetBrowser); + XCTAssertEqualObjects(oauthRecipe.scope, [NSSet setWithObject:@"*"]); + XCTAssertEqualObjects(oauthRecipe.protocolVersion, @0); +} + +- (void)testPPOTConfiguration_whenConfigRecipeWithUnknownProtocolInList_onlyLoadsRecognizedRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"target": @"UNKNOWN TARGET", + @"protocol": @"9999", + @"scope": @[@"*"], + @"scheme": @"com.paypal.ppclient.touch.v2", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"protocol": @"0", + @"target": @"browser", + @"scope": @[@"*"], + }, + ], + } + }]; + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + + PPOTConfigurationOAuthRecipe *oauthRecipe = (PPOTConfigurationOAuthRecipe *)configuration.prioritizedOAuthRecipes[0]; + XCTAssertEqual(oauthRecipe.target, PPOTRequestTargetBrowser); + XCTAssertEqualObjects(oauthRecipe.scope, [NSSet setWithObject:@"*"]); + XCTAssertEqualObjects(oauthRecipe.protocolVersion, @0); +} + +- (void)testPPOTConfiguration_whenOAuthConfigRecipeWithMissingScopeInList_onlyLoadsRecognizedRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"target": @"wallet", + @"scheme": @"com.paypal.ppclient.touch.v2", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"target": @"browser", + @"protocol": @"0", + @"scope": @[@"*"], + } + ], + } + }]; + + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + + PPOTConfigurationOAuthRecipe *oauthRecipe = (PPOTConfigurationOAuthRecipe *)configuration.prioritizedOAuthRecipes[0]; + XCTAssertEqual(oauthRecipe.target, PPOTRequestTargetBrowser); + XCTAssertEqualObjects(oauthRecipe.scope, [NSSet setWithObject:@"*"]); + XCTAssertEqualObjects(oauthRecipe.protocolVersion, @0); +} + +- (void)testPPOTConfiguration_whenBrowserRecipeWithMissingURLInProtocol3List_onlyLoadsRecognizedRecipes { + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"target": @"wallet", + @"scope": @[@"*"], + @"protocol": @"1", + @"scheme": @"com.paypal.ppclient.touch.v9999", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"target": @"browser", + @"protocol": @"3", + @"scope": @[@"*"], + }, + ], + } + }]; + + XCTAssertNotNil(configuration); + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + + PPOTConfigurationOAuthRecipe *oauthRecipe = (PPOTConfigurationOAuthRecipe *)configuration.prioritizedOAuthRecipes[0]; + XCTAssertEqual(oauthRecipe.target, PPOTRequestTargetOnDeviceApplication); + XCTAssertEqualObjects(oauthRecipe.scope, [NSSet setWithObject:@"*"]); + XCTAssertEqualObjects(oauthRecipe.protocolVersion, @1); + XCTAssertEqualObjects(oauthRecipe.targetAppURLScheme, @"com.paypal.ppclient.touch.v9999"); + NSArray *expectedTargetAppBundleIDs = @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"]; + XCTAssertEqualObjects(oauthRecipe.targetAppBundleIDs, expectedTargetAppBundleIDs); +} + +- (void)testPPOTConfiguration_whenDifferentBadConfigRecipes_stillLoadsAllGoodRecipes { + // Expected 1 OAuth, 1 Checkout Recipe + PPOTConfiguration *configuration = [PPOTConfiguration configurationWithDictionary: + @{@"os": @"iOS", + @"file_timestamp": @"2014-12-19T16:39:57-08:00", + @"1.0": @{ + @"checkout_recipes_in_decreasing_priority_order": @[ + @{ + @"target": @"browser", + @"protocol": @"9999", + // Unrecognized Protocol Version + }, + @{ + @"target": @"wallet", + @"protocol": @"3", + @"scheme": @"com.paypal.ppclient.touch.v3", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + ], + @"oauth2_recipes_in_decreasing_priority_order": @[ + @{ + @"protocol": @"1", + @"target": @"wallet", + @"scope": @[@"*"], + @"scheme": @"com.paypal.ppclient.touch.v2", + @"applications": @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"], + }, + @{ + @"protocol": @"3", + @"target": @"browser", + @"scope": @[@"*"], + // Missing URL in scope for protocol version 3 + }, + ], + }, + }]; + + XCTAssertNotNil(configuration); + + XCTAssertNotNil(configuration.prioritizedOAuthRecipes); + XCTAssertEqual([configuration.prioritizedOAuthRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedOAuthRecipes[0] isKindOfClass:[PPOTConfigurationOAuthRecipe class]]); + + PPOTConfigurationOAuthRecipe *oauthRecipe = (PPOTConfigurationOAuthRecipe *)configuration.prioritizedOAuthRecipes[0]; + XCTAssertEqual(oauthRecipe.target, PPOTRequestTargetOnDeviceApplication); + XCTAssertEqualObjects(oauthRecipe.scope, [NSSet setWithObject:@"*"]); + XCTAssertEqualObjects(oauthRecipe.protocolVersion, @1); + XCTAssertEqualObjects(oauthRecipe.targetAppURLScheme, @"com.paypal.ppclient.touch.v2"); + NSArray *expectedTargetAppBundleIDs = @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"]; + XCTAssertEqualObjects(oauthRecipe.targetAppBundleIDs, expectedTargetAppBundleIDs); + + XCTAssertNotNil(configuration.prioritizedCheckoutRecipes); + XCTAssertEqual([configuration.prioritizedCheckoutRecipes count], (NSUInteger)1); + XCTAssert([configuration.prioritizedCheckoutRecipes[0] isKindOfClass:[PPOTConfigurationCheckoutRecipe class]]); + + PPOTConfigurationCheckoutRecipe *checkoutRecipe = (PPOTConfigurationCheckoutRecipe *)configuration.prioritizedCheckoutRecipes[0]; + XCTAssertEqual(checkoutRecipe.target, PPOTRequestTargetOnDeviceApplication); + XCTAssertEqualObjects(checkoutRecipe.protocolVersion, @3); + XCTAssertEqualObjects(checkoutRecipe.targetAppURLScheme, @"com.paypal.ppclient.touch.v3"); + expectedTargetAppBundleIDs = @[@"com.paypal.ppclient", @"com.yourcompany.ppclient"]; + XCTAssertEqualObjects(checkoutRecipe.targetAppBundleIDs, expectedTargetAppBundleIDs); +} + + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTEncryptionTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTEncryptionTest.m new file mode 100755 index 00000000..3a390fa9 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTEncryptionTest.m @@ -0,0 +1,149 @@ +// +// PPOTEncryptionTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import +#import "PPOTEncryptionHelper.h" +#import "PPOTJSONHelper.h" +#import "PPOTString.h" + +@interface PPOTEncryptionTest : XCTestCase + +@end + +@implementation PPOTEncryptionTest + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + ++ (NSData *)randomData:(NSUInteger)length { + NSMutableData *randomKey = [NSMutableData dataWithLength:length]; + int error = SecRandomCopyBytes(kSecRandomDefault, length, [randomKey mutableBytes]); + return (error == 0) ? randomKey : nil; +} + +- (void)testEncryptionMultiple { + for (uint i=1; i<8*1024; i++) { + NSData *plainData = [[self class] randomData:i]; + NSData *key = [PPOTEncryptionHelper generate256BitKey]; + NSData *cipherData = [PPOTEncryptionHelper encryptAESCTRData:plainData encryptionKey:key]; + XCTAssertNotEqualObjects(plainData, cipherData); + + NSData *outData = [PPOTEncryptionHelper decryptAESCTRData:cipherData encryptionKey:key]; + XCTAssertEqualObjects(outData, plainData); + } +} + +- (void)testRandomKeyGenerator { + NSInteger numberOfKeys = 10; + NSMutableArray *array = [NSMutableArray arrayWithCapacity:numberOfKeys]; + for (NSInteger i = 0; i < numberOfKeys; i++) { + NSData *key = [PPOTEncryptionHelper generate256BitKey]; + XCTAssertTrue(key.length == 32); + XCTAssertNotNil(key); + XCTAssertFalse([array containsObject:key]); + [array addObject:key]; + } +} + +- (void)testHexFunction { + NSString *hexStringExpected = @"68656C6C6F"; + NSString *string = @"hello"; + NSString *hexString = [PPOTString hexStringFromData:[string dataUsingEncoding:NSUTF8StringEncoding]]; + XCTAssertEqualObjects(hexString, [hexStringExpected uppercaseString]); + + NSData *data = [PPOTString dataWithHexString:hexString]; + XCTAssertEqualObjects(data, [string dataUsingEncoding:NSUTF8StringEncoding]); +} + +- (void)testEncryption { + NSString *plainData = @"Trust me i am an Engineer !"; + NSData *key = [PPOTEncryptionHelper generate256BitKey]; + NSData *cipherData = [PPOTEncryptionHelper encryptAESCTRData:[plainData dataUsingEncoding:NSUTF8StringEncoding] encryptionKey:key]; + XCTAssertNotEqualObjects([plainData dataUsingEncoding:NSUTF8StringEncoding], cipherData); + + NSData *outData = [PPOTEncryptionHelper decryptAESCTRData:cipherData encryptionKey:key]; + NSString *expectedData = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(expectedData, plainData); +} + +- (void)testRealPayloadDecryption { + NSString *cipherText = @"WTn6Ww9s7KCwu3QUPVe05stH01hy4easqZeR71E7tXKIuEOMWxuara26u3VHACpRGHHxO3SSn+1WbCLRCwI6UCxbUkYl5eWN1TVpQ7a4FvDDkbhH7fSZz/DjENLo9Ap5w/EQ2PyrQt00fkxjf2HW/W2r/SI3GRL/KKu7rCRIjcEgr3RAsqTrDILOhx2ASo99YCSpzETlILqOF7p4bDGzwy5L8AeQcSgDIFqtvhzL6gbud2A90KpRIb5b+ftbf+RCRkW1NSEC/Vb+0MHyFNGJCnSOgz9t3cn/kuF+uQgozsAkTE+PmFSrBvtPag5AKQAgM44E"; + NSData *key = [PPOTString dataWithHexString:@"9b30e222b129c989547f1a6ab6022e2bd191a0217f2efcbf891f3eb07990582c"]; + NSString *message = @"{\"payment_code_type\":\"authcode\",\"payment_code\":\"XXXXX\",\"timestamp\":\"2015-01-16T19:20:30.45+01:00\",\"expires_in\":900,\"scope\":\"\",\"display_name\":\"mockDisplayName\",\"email\":\"mockemailaddress@mock.com\"}"; + + NSData *cipherData = [[NSData alloc] initWithBase64EncodedString:cipherText options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSData *plainData = [PPOTEncryptionHelper decryptAESCTRData:cipherData encryptionKey:key]; + // decode base64 + NSString *plainString = [[NSString alloc] initWithData:plainData encoding:NSUTF8StringEncoding]; + XCTAssertTrue(plainString); + XCTAssertEqualObjects(plainData, [message dataUsingEncoding:NSUTF8StringEncoding]); +} + + +- (void)testPublicKeyImport { + + // openssl req -x509 -out public_key2.crt -outform DER -new -newkey rsa:2048 -keyout private_key2.pem -keyform DER + + NSString *cert = @"MIIDOzCCAiOgAwIBAgIJAMlvCS4UtR7PMA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczESMBAGA1UECgwJQnJhaW50cmVlMB4XDTE1MDMyMDAxMTcyMVoXDTE2MDMxOTAxMTcyMVowNDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMRIwEAYDVQQKDAlCcmFpbnRyZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfCRhOGeMj4ci5Bbbs/x0G+PkbeL7iGEsX5UWQeA8oCWU8jpipFTC271Q0f5BQzXCN8L4LnwGvtm2cgAEivSBODo7XHsmxrFjKdQx1S7FIuFRKO18Uf8rIGmZHiJfhCbUEGilpwMt7hUMjjv2XDufPCMrJ8Yn2y/yDi5nhs7UsFhROm9oI2PyiJX01yR2ag8cPBb5Ahlwmj1yMWmSuHVnUN8T0rjIXyrBhxTAk3omQkQdHKj2w8afdrAcNUGi4yU/a5/pmb8tZpAa73OZVdOEQepJAAIRWXeS2BdKTkhfRJc7WEIlbi+9a2OdtM3OkIs+rZE7+WVT8XQoiLxpUd/wNAgMBAAGjUDBOMB0GA1UdDgQWBBQhbJ8DtuKFhGTsrvZ41Vw5jYbmazAfBgNVHSMEGDAWgBQhbJ8DtuKFhGTsrvZ41Vw5jYbmazAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQARg2wjhJanhKu1bw63+Xfj25OUa02jK+i4vhkWeuCGd5/kxA1dZMjBfSMxh484xBpaqRIOHvZmRpKcxCgci8xRbbJiaXrb1vIePTTi4lfU6cpfsnjMFCHDk8E/0AxIfOpQ0BSJY35WqB45xaIWBAY8lQ2pNfiPyK4kzajSOg+kbEKLmA0udYy8tsydt+88+R88rYKt4qDBo+Z5zgJ2fZvbAp99cBASHqMCoUoPb96YWEhaWhjArVGzgevpopKA9aOAFdndPKLbe6y29bbfLfQqat0B1fVmutCIHGIXtsPHQDe/cXJtoJk7HmD08++C9YvjxlSi8jxLb5nIA0QGI0yj"; + + NSData *certData = [[NSData alloc] initWithBase64EncodedString:cert options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + NSString *plainText = @"hello"; + NSData *cipherText = [PPOTEncryptionHelper encryptRSAData:[plainText dataUsingEncoding:NSUTF8StringEncoding] certificate:certData]; + XCTAssertTrue(cipherText); + NSString *base64 = [cipherText base64EncodedStringWithOptions:0]; + XCTAssertTrue(base64); +} + + +- (void)testPublicKeyEncryption { + + // openssl req -x509 -out public_key2.crt -outform DER -new -newkey rsa:2048 -keyout private_key2.pem -keyform DER + + NSString *cert = @"MIIDOzCCAiOgAwIBAgIJAMlvCS4UtR7PMA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczESMBAGA1UECgwJQnJhaW50cmVlMB4XDTE1MDMyMDAxMTcyMVoXDTE2MDMxOTAxMTcyMVowNDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMRIwEAYDVQQKDAlCcmFpbnRyZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfCRhOGeMj4ci5Bbbs/x0G+PkbeL7iGEsX5UWQeA8oCWU8jpipFTC271Q0f5BQzXCN8L4LnwGvtm2cgAEivSBODo7XHsmxrFjKdQx1S7FIuFRKO18Uf8rIGmZHiJfhCbUEGilpwMt7hUMjjv2XDufPCMrJ8Yn2y/yDi5nhs7UsFhROm9oI2PyiJX01yR2ag8cPBb5Ahlwmj1yMWmSuHVnUN8T0rjIXyrBhxTAk3omQkQdHKj2w8afdrAcNUGi4yU/a5/pmb8tZpAa73OZVdOEQepJAAIRWXeS2BdKTkhfRJc7WEIlbi+9a2OdtM3OkIs+rZE7+WVT8XQoiLxpUd/wNAgMBAAGjUDBOMB0GA1UdDgQWBBQhbJ8DtuKFhGTsrvZ41Vw5jYbmazAfBgNVHSMEGDAWgBQhbJ8DtuKFhGTsrvZ41Vw5jYbmazAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQARg2wjhJanhKu1bw63+Xfj25OUa02jK+i4vhkWeuCGd5/kxA1dZMjBfSMxh484xBpaqRIOHvZmRpKcxCgci8xRbbJiaXrb1vIePTTi4lfU6cpfsnjMFCHDk8E/0AxIfOpQ0BSJY35WqB45xaIWBAY8lQ2pNfiPyK4kzajSOg+kbEKLmA0udYy8tsydt+88+R88rYKt4qDBo+Z5zgJ2fZvbAp99cBASHqMCoUoPb96YWEhaWhjArVGzgevpopKA9aOAFdndPKLbe6y29bbfLfQqat0B1fVmutCIHGIXtsPHQDe/cXJtoJk7HmD08++C9YvjxlSi8jxLb5nIA0QGI0yj"; + + NSData *certData = [[NSData alloc] initWithBase64EncodedString:cert options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + NSString *expectedCipher = @"ZgzjmDx5iD6ogzpiIB9bl2C08pscYQ+8gV8VbRbo2kZiNYj4WBh4rmMWnsADVpXj1JS0xq9HmuQhomm5KUewfEZRTNdk09hI1hWtU+cPucd4gwI7mmqzNUFbfPSMPtWTB2yFSDIiJ1K7XN0C1d5NnfbsI0rxUSxCpgP7S+ckEaWH4uqzV9NFIsplmj+4yBQokngk7j5fn9QcsSylKCBq7rp3Z1Wg/qA5tVPj9osZYwr29kot6onDtajJkl/7ZYxuRrQkqXi5QY2CPvN8A8WpEaMwy0EwZUAB7RZjBAHlKxDZXNRjgfzT+vpnwF+gzebP+k4H44/1wGecSuGYrU+l9Q=="; + + NSString *plainText = @"{\"sym_key\": \"9a51aed0da8fb363933efc0f739df5dfbcba34b2e1ea8337f6d5dc1cbd61ced4\", \"some\": \"other_stuff\"}"; + NSData *cipherText = [PPOTEncryptionHelper encryptRSAData:[plainText dataUsingEncoding:NSUTF8StringEncoding] certificate:certData]; + XCTAssertTrue(cipherText); + NSString *base64 = [cipherText base64EncodedStringWithOptions:0]; + XCTAssertTrue(base64); + XCTAssertNotEqualObjects(base64, expectedCipher); +} + +- (void)testAESCTREncryptionDecryption { + NSString *plainData = @"Trust me i am an Engineer !"; + NSData *key = [[self class] randomData:32]; + NSData *cipherData = [PPOTEncryptionHelper encryptAESCTRData:[plainData dataUsingEncoding:NSUTF8StringEncoding] encryptionKey:key]; + XCTAssertNotEqualObjects([plainData dataUsingEncoding:NSUTF8StringEncoding], cipherData, @"data shouldn't match"); + + NSData *outData = [PPOTEncryptionHelper decryptAESCTRData:cipherData encryptionKey:key]; + NSString *expectedData = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(expectedData, plainData, @"data should match"); +} + + +- (void)testPythonCTRDecryption { + NSData *cipherText = [[NSData alloc] initWithBase64EncodedString:@"O4ibtOnGreHg1UGcJen7OFOlT/qdBo/3h8Gvc1Jibgj31UGbH+G3ottYCuwHeyJXYX5ubtr8O1SXAoy3X4IEq6o1oPFMJRP8/D5PI0qhexBTXsEEJvuRDlEV/Rcl" options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSData *key = [PPOTString dataWithHexString:@"dc6d0e61c0a3cd187dd0e41f455effdac77c23c95c2cfcb81993916ab19c0e09"]; + NSData *outData = [PPOTEncryptionHelper decryptAESCTRData:cipherText encryptionKey:key]; + NSString *message = @"A really secret message. Not for prying eyes."; + XCTAssertEqualObjects([message dataUsingEncoding:NSUTF8StringEncoding], outData, @"message should be the same"); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTErrorTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTErrorTest.m new file mode 100755 index 00000000..1fb7ec8b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTErrorTest.m @@ -0,0 +1,34 @@ +// +// PPOTErrorTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import "PPOTError.h" + +@interface PPOTErrorTest : XCTestCase + +@end + +@implementation PPOTErrorTest + +- (void)testPPErrorWithErrorCode { + NSError *error = [PPOTError errorWithErrorCode:PPOTErrorCodeNoTargetAppFound]; + XCTAssertEqualObjects(error.domain, kPayPalOneTouchErrorDomain); + XCTAssertEqual(error.code, PPOTErrorCodeNoTargetAppFound); + XCTAssertEqualObjects(error.userInfo, [NSDictionary dictionary]); + +} + +- (void)testPPErrorWithErrorCodeAndUserInfo { + NSDictionary *dict = @{ @"k" : @"v"}; + NSError *error = [PPOTError errorWithErrorCode:PPOTErrorCodeNoTargetAppFound + userInfo:dict]; + XCTAssertEqualObjects(error.domain, kPayPalOneTouchErrorDomain); + XCTAssertEqual(error.code, PPOTErrorCodeNoTargetAppFound); + XCTAssertEqualObjects(error.userInfo, dict); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTTimeTest.m b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTTimeTest.m new file mode 100755 index 00000000..2dc5ff8b --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/PayPalOneTouch/PPOTTimeTest.m @@ -0,0 +1,43 @@ +// +// PPOTTimeTest.m +// PayPalOneTouch +// +// Copyright © 2015 PayPal, Inc. All rights reserved. +// + +#import +#import "PPOTTime.h" + +@interface PPOTTimeTest : XCTestCase + +@end + +@implementation PPOTTimeTest + +- (void)testRFC3339DateConversion { + NSDate *date = [NSDate date]; + NSString *dateAsString = [[PPOTTime rfc3339DateFormatter] stringFromDate:date]; + NSDate *dateFromRFC3339String = [PPOTTime dateFromRFC3339LikeString:dateAsString]; + // Within 1 millisecond + XCTAssertEqualWithAccuracy([date timeIntervalSince1970], [dateFromRFC3339String timeIntervalSince1970], 1); +} + +- (void)testRFC3339DateWithMillisecondConversion { + NSDate *date = [NSDate date]; + NSString *dateAsString = [[PPOTTime rfc3339DateFormatter] stringFromDate:date]; + NSString *dateAsStringWithMilliseconds = [NSString stringWithFormat:@"%@.123Z", + [dateAsString substringWithRange:NSMakeRange(0, [dateAsString length] - 1)]]; + NSDate *dateFromRFC3339LikeString = [PPOTTime dateFromRFC3339LikeString:dateAsStringWithMilliseconds]; + // Within 1 millisecond + XCTAssertEqualWithAccuracy([date timeIntervalSince1970], [dateFromRFC3339LikeString timeIntervalSince1970], 1); +} + +- (void)testRFC3339DateNil { + XCTAssertNil([PPOTTime dateFromRFC3339LikeString:nil]); +} + +- (void)testRFC3339DateIllegalString { + XCTAssertNil([PPOTTime dateFromRFC3339LikeString:@"random string"]); +} + +@end diff --git a/examples/braintree/ios/Frameworks/Braintree/UnitTests/UnitTests-Bridging-Header.h b/examples/braintree/ios/Frameworks/Braintree/UnitTests/UnitTests-Bridging-Header.h new file mode 100755 index 00000000..cf690919 --- /dev/null +++ b/examples/braintree/ios/Frameworks/Braintree/UnitTests/UnitTests-Bridging-Header.h @@ -0,0 +1,40 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "BraintreeCore.h" +#import "BraintreeCard.h" +#import "BraintreeApplePay.h" +#import "BraintreePayPal.h" +#import "BraintreeVenmo.h" +#import "Braintree3DSecure.h" +#import "BraintreeDataCollector.h" +#import "BraintreeUI.h" +#import "PayPalOneTouch.h" + +// Internal headers for testing +#import "BTAPIClient_Internal.h" +#import "BTApplePayClient_Internal.h" +#import "BTCard_Internal.h" +#import "BTCardClient_Internal.h" +#import "BTCardClient+UnionPay.h" +#import "BTConfiguration.h" +#import "BTDataCollector_Internal.h" +#import "BTPayPalDriver_Internal.h" +#import "BTVenmoDriver_Internal.h" +#import "BTThreeDSecureDriver_Internal.h" +#import "BTThreeDSecureAuthenticationViewController.h" +#import "BTURLUtils.h" +#import "FakePayPalClasses.h" +#import "BTLogger_Internal.h" +#import "BTFakeHTTP.h" +#import "BTDropInViewController_Internal.h" +#import "BTPaymentButton_Internal.h" +#import "BTThreeDSecureLookupResult.h" +#import "Braintree-Version.h" +#import "PPDataCollector_Internal.h" +#import "BTDropInUtil.h" + +#import "BTSpecHelper.h" +#import +#import "BTTestClientTokenFactory.h" diff --git a/examples/braintree/ios/Frameworks/Braintree/screenshot.png b/examples/braintree/ios/Frameworks/Braintree/screenshot.png new file mode 100755 index 00000000..4b8c014c Binary files /dev/null and b/examples/braintree/ios/Frameworks/Braintree/screenshot.png differ diff --git a/examples/braintree/package.json b/examples/braintree/package.json new file mode 100644 index 00000000..8439ab31 --- /dev/null +++ b/examples/braintree/package.json @@ -0,0 +1,26 @@ +{ + "name": "BraintreeExample", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node node_modules/react-native/local-cli/cli.js start", + "test": "jest", + "run:packager": "yarn run haul start -- --platform ios", + "run:ios": "react-native run-ios" + }, + "dependencies": { + "react": "~15.4.0-rc.4", + "react-native": "0.41.0", + "react-native-payments": "file:../.." + }, + "devDependencies": { + "babel-jest": "20.0.3", + "babel-preset-react-native": "2.1.0", + "haul": "^1.0.0-beta.1", + "jest": "20.0.4", + "react-test-renderer": "~15.4.0-rc.4" + }, + "jest": { + "preset": "react-native" + } +} \ No newline at end of file diff --git a/examples/braintree/webpack.haul.js b/examples/braintree/webpack.haul.js new file mode 100644 index 00000000..8910da5b --- /dev/null +++ b/examples/braintree/webpack.haul.js @@ -0,0 +1,12 @@ +const path = require('path'); + +module.exports = ({ platform }, defaults) => ({ + entry: `./index.${platform}.js`, + resolve: { + ...defaults.resolve, + modules: [ + path.resolve(__dirname, 'node_modules'), + path.resolve(__dirname, '../../node_modules') + ] + } +}); diff --git a/examples/braintree/yarn.lock b/examples/braintree/yarn.lock new file mode 100644 index 00000000..b16f40cb --- /dev/null +++ b/examples/braintree/yarn.lock @@ -0,0 +1,5294 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abab@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +absolute-path@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7" + +accepts@~1.2.12, accepts@~1.2.13: + version "1.2.13" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.2.13.tgz#e5f1f3928c6d95fd96558c36ec3d9d0de4a6ecea" + dependencies: + mime-types "~2.1.6" + negotiator "0.5.3" + +accepts@~1.3.0, accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn-globals@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" + dependencies: + acorn "^4.0.4" + +acorn@^4.0.3, acorn@^4.0.4: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" + +ajv-keywords@^1.1.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0, ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.1.0, ansi-escapes@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-escapes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" + +ansi-regex@^2.0.0, ansi-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.0.0, ansi-styles@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.1.0.tgz#09c202d5c917ec23188caa5c9cb9179cd9547750" + dependencies: + color-convert "^1.0.0" + +ansi@^0.3.0, ansi@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + +anymatch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" + dependencies: + arrify "^1.0.0" + micromatch "^2.1.5" + +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + dependencies: + default-require-extensions "^1.0.0" + +aproba@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1, array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +art@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/art/-/art-0.10.1.tgz#38541883e399225c5e193ff246e8f157cf7b2146" + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.0.tgz#2796642723573859565633fc6274444bee2f8ce3" + +async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.0.1, async@^2.1.2, async@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +babel-core@^6.0.0, babel-core@^6.21.0, babel-core@^6.24.0, babel-core@^6.24.1, babel-core@^6.7.2: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729" + dependencies: + babel-code-frame "^6.22.0" + babel-generator "^6.25.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.25.0" + babel-traverse "^6.25.0" + babel-types "^6.25.0" + babylon "^6.17.2" + convert-source-map "^1.1.0" + debug "^2.1.1" + json5 "^0.5.0" + lodash "^4.2.0" + minimatch "^3.0.2" + path-is-absolute "^1.0.0" + private "^0.1.6" + slash "^1.0.0" + source-map "^0.5.0" + +babel-generator@^6.18.0, babel-generator@^6.21.0, babel-generator@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-types "^6.25.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-builder-react-jsx@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.24.1.tgz#0ad7917e33c8d751e646daca4e77cc19377d2cbc" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + esutils "^2.0.0" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + 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@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + 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@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + 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@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + 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-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-jest@20.0.3, babel-jest@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.3.tgz#e4a03b13dc10389e140fc645d09ffc4ced301671" + dependencies: + babel-core "^6.0.0" + babel-plugin-istanbul "^4.0.0" + babel-preset-jest "^20.0.3" + +babel-loader@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.4.1.tgz#0b34112d5b0748a8dcdbf51acf6f9bd42d50b8ca" + dependencies: + find-cache-dir "^0.1.1" + loader-utils "^0.2.16" + mkdirp "^0.5.1" + object-assign "^4.0.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0, babel-plugin-check-es2015-constants@^6.5.0, babel-plugin-check-es2015-constants@^6.7.2, babel-plugin-check-es2015-constants@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-external-helpers@^6.18.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-istanbul@^4.0.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.4.tgz#18dde84bf3ce329fddf3f4103fae921456d8e587" + dependencies: + find-up "^2.1.0" + istanbul-lib-instrument "^1.7.2" + test-exclude "^4.1.1" + +babel-plugin-jest-hoist@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767" + +babel-plugin-react-transform@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-react-transform/-/babel-plugin-react-transform-2.0.2.tgz#515bbfa996893981142d90b1f9b1635de2995109" + dependencies: + lodash "^4.6.1" + +babel-plugin-syntax-async-functions@^6.5.0, babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-class-properties@^6.5.0, babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-flow@^6.18.0, babel-plugin-syntax-flow@^6.5.0, babel-plugin-syntax-flow@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + +babel-plugin-syntax-jsx@^6.5.0, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + +babel-plugin-syntax-object-rest-spread@^6.5.0, babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.20.0, babel-plugin-syntax-trailing-function-commas@^6.22.0, babel-plugin-syntax-trailing-function-commas@^6.5.0, babel-plugin-syntax-trailing-function-commas@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + 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-class-properties@^6.5.0, babel-plugin-transform-class-properties@^6.6.0, babel-plugin-transform-class-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0, babel-plugin-transform-es2015-arrow-functions@^6.5.0, babel-plugin-transform-es2015-arrow-functions@^6.5.2, babel-plugin-transform-es2015-arrow-functions@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0, babel-plugin-transform-es2015-block-scoped-functions@^6.6.5, babel-plugin-transform-es2015-block-scoped-functions@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.5.0, babel-plugin-transform-es2015-block-scoping@^6.7.1, babel-plugin-transform-es2015-block-scoping@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + lodash "^4.2.0" + +babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.5.0, babel-plugin-transform-es2015-classes@^6.6.5, babel-plugin-transform-es2015-classes@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + 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@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.5.0, babel-plugin-transform-es2015-computed-properties@^6.6.5, babel-plugin-transform-es2015-computed-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@6.x, babel-plugin-transform-es2015-destructuring@^6.23.0, babel-plugin-transform-es2015-destructuring@^6.5.0, babel-plugin-transform-es2015-destructuring@^6.6.5, babel-plugin-transform-es2015-destructuring@^6.8.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0, babel-plugin-transform-es2015-for-of@^6.5.0, babel-plugin-transform-es2015-for-of@^6.6.0, babel-plugin-transform-es2015-for-of@^6.8.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@6.x, babel-plugin-transform-es2015-function-name@^6.22.0, babel-plugin-transform-es2015-function-name@^6.5.0, babel-plugin-transform-es2015-function-name@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0, babel-plugin-transform-es2015-literals@^6.5.0, babel-plugin-transform-es2015-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + 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@6.x, babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.5.0, babel-plugin-transform-es2015-modules-commonjs@^6.7.0, babel-plugin-transform-es2015-modules-commonjs@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + 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@^6.22.0, babel-plugin-transform-es2015-object-super@^6.6.5, babel-plugin-transform-es2015-object-super@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@6.x, babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.5.0, babel-plugin-transform-es2015-parameters@^6.7.0, babel-plugin-transform-es2015-parameters@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + 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@6.x, babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.5.0, babel-plugin-transform-es2015-shorthand-properties@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@6.x, babel-plugin-transform-es2015-spread@^6.22.0, babel-plugin-transform-es2015-spread@^6.5.0, babel-plugin-transform-es2015-spread@^6.6.5, babel-plugin-transform-es2015-spread@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@6.x, babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0, babel-plugin-transform-es2015-template-literals@^6.5.0, babel-plugin-transform-es2015-template-literals@^6.6.5, babel-plugin-transform-es2015-template-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@6.x, babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-es3-member-expression-literals@^6.5.0, babel-plugin-transform-es3-member-expression-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz#733d3444f3ecc41bef8ed1a6a4e09657b8969ebb" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es3-property-literals@^6.5.0, babel-plugin-transform-es3-property-literals@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz#b2078d5842e22abf40f73e8cde9cd3711abd5758" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + 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-flow-strip-types@^6.21.0, babel-plugin-transform-flow-strip-types@^6.22.0, babel-plugin-transform-flow-strip-types@^6.5.0, babel-plugin-transform-flow-strip-types@^6.7.0, babel-plugin-transform-flow-strip-types@^6.8.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-assign@^6.5.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-assign/-/babel-plugin-transform-object-assign-6.22.0.tgz#f99d2f66f1a0b0d498e346c5359684740caa20ba" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.20.2, babel-plugin-transform-object-rest-spread@^6.23.0, babel-plugin-transform-object-rest-spread@^6.5.0, babel-plugin-transform-object-rest-spread@^6.6.5, babel-plugin-transform-object-rest-spread@^6.8.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-display-name@^6.5.0, babel-plugin-transform-react-display-name@^6.8.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@^6.5.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.5.0, babel-plugin-transform-react-jsx@^6.8.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3" + dependencies: + babel-helper-builder-react-jsx "^6.24.1" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0, babel-plugin-transform-regenerator@^6.5.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418" + dependencies: + regenerator-transform "0.9.11" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-polyfill@^6.20.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" + dependencies: + babel-runtime "^6.22.0" + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-preset-env@^1.2.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4" + dependencies: + 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 "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-preset-es2015-node@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-preset-es2015-node/-/babel-preset-es2015-node-6.1.1.tgz#60b23157024b0cfebf3a63554cb05ee035b4e55f" + dependencies: + babel-plugin-transform-es2015-destructuring "6.x" + babel-plugin-transform-es2015-function-name "6.x" + babel-plugin-transform-es2015-modules-commonjs "6.x" + babel-plugin-transform-es2015-parameters "6.x" + babel-plugin-transform-es2015-shorthand-properties "6.x" + babel-plugin-transform-es2015-spread "6.x" + babel-plugin-transform-es2015-sticky-regex "6.x" + babel-plugin-transform-es2015-unicode-regex "6.x" + semver "5.x" + +babel-preset-fbjs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-1.0.0.tgz#c972e5c9b301d4ec9e7971f4aec3e14ac017a8b0" + dependencies: + babel-plugin-check-es2015-constants "^6.7.2" + babel-plugin-syntax-flow "^6.5.0" + babel-plugin-syntax-object-rest-spread "^6.5.0" + babel-plugin-syntax-trailing-function-commas "^6.5.0" + babel-plugin-transform-class-properties "^6.6.0" + babel-plugin-transform-es2015-arrow-functions "^6.5.2" + babel-plugin-transform-es2015-block-scoped-functions "^6.6.5" + babel-plugin-transform-es2015-block-scoping "^6.7.1" + babel-plugin-transform-es2015-classes "^6.6.5" + babel-plugin-transform-es2015-computed-properties "^6.6.5" + babel-plugin-transform-es2015-destructuring "^6.6.5" + babel-plugin-transform-es2015-for-of "^6.6.0" + babel-plugin-transform-es2015-literals "^6.5.0" + babel-plugin-transform-es2015-modules-commonjs "^6.7.0" + babel-plugin-transform-es2015-object-super "^6.6.5" + babel-plugin-transform-es2015-parameters "^6.7.0" + babel-plugin-transform-es2015-shorthand-properties "^6.5.0" + babel-plugin-transform-es2015-spread "^6.6.5" + babel-plugin-transform-es2015-template-literals "^6.6.5" + babel-plugin-transform-es3-member-expression-literals "^6.5.0" + babel-plugin-transform-es3-property-literals "^6.5.0" + babel-plugin-transform-flow-strip-types "^6.7.0" + babel-plugin-transform-object-rest-spread "^6.6.5" + object-assign "^4.0.1" + +babel-preset-fbjs@^2.1.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz#22f358e6654073acf61e47a052a777d7bccf03af" + dependencies: + babel-plugin-check-es2015-constants "^6.8.0" + babel-plugin-syntax-class-properties "^6.8.0" + babel-plugin-syntax-flow "^6.8.0" + babel-plugin-syntax-jsx "^6.8.0" + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-plugin-syntax-trailing-function-commas "^6.8.0" + babel-plugin-transform-class-properties "^6.8.0" + babel-plugin-transform-es2015-arrow-functions "^6.8.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.8.0" + babel-plugin-transform-es2015-block-scoping "^6.8.0" + babel-plugin-transform-es2015-classes "^6.8.0" + babel-plugin-transform-es2015-computed-properties "^6.8.0" + babel-plugin-transform-es2015-destructuring "^6.8.0" + babel-plugin-transform-es2015-for-of "^6.8.0" + babel-plugin-transform-es2015-function-name "^6.8.0" + babel-plugin-transform-es2015-literals "^6.8.0" + babel-plugin-transform-es2015-modules-commonjs "^6.8.0" + babel-plugin-transform-es2015-object-super "^6.8.0" + babel-plugin-transform-es2015-parameters "^6.8.0" + babel-plugin-transform-es2015-shorthand-properties "^6.8.0" + babel-plugin-transform-es2015-spread "^6.8.0" + babel-plugin-transform-es2015-template-literals "^6.8.0" + babel-plugin-transform-es3-member-expression-literals "^6.8.0" + babel-plugin-transform-es3-property-literals "^6.8.0" + babel-plugin-transform-flow-strip-types "^6.8.0" + babel-plugin-transform-object-rest-spread "^6.8.0" + babel-plugin-transform-react-display-name "^6.8.0" + babel-plugin-transform-react-jsx "^6.8.0" + +babel-preset-jest@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz#cbacaadecb5d689ca1e1de1360ebfc66862c178a" + dependencies: + babel-plugin-jest-hoist "^20.0.3" + +babel-preset-react-native@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-react-native/-/babel-preset-react-native-2.1.0.tgz#9013ebd82da1c88102bf588810ff59e209ca2b8a" + dependencies: + babel-plugin-check-es2015-constants "^6.5.0" + babel-plugin-react-transform "2.0.2" + babel-plugin-syntax-async-functions "^6.5.0" + babel-plugin-syntax-class-properties "^6.5.0" + babel-plugin-syntax-flow "^6.5.0" + babel-plugin-syntax-jsx "^6.5.0" + babel-plugin-syntax-trailing-function-commas "^6.5.0" + babel-plugin-transform-class-properties "^6.5.0" + babel-plugin-transform-es2015-arrow-functions "^6.5.0" + babel-plugin-transform-es2015-block-scoping "^6.5.0" + babel-plugin-transform-es2015-classes "^6.5.0" + babel-plugin-transform-es2015-computed-properties "^6.5.0" + babel-plugin-transform-es2015-destructuring "^6.5.0" + babel-plugin-transform-es2015-for-of "^6.5.0" + babel-plugin-transform-es2015-function-name "^6.5.0" + babel-plugin-transform-es2015-literals "^6.5.0" + babel-plugin-transform-es2015-modules-commonjs "^6.5.0" + babel-plugin-transform-es2015-parameters "^6.5.0" + babel-plugin-transform-es2015-shorthand-properties "^6.5.0" + babel-plugin-transform-es2015-spread "^6.5.0" + babel-plugin-transform-es2015-template-literals "^6.5.0" + babel-plugin-transform-flow-strip-types "^6.5.0" + babel-plugin-transform-object-assign "^6.5.0" + babel-plugin-transform-object-rest-spread "^6.5.0" + babel-plugin-transform-react-display-name "^6.5.0" + babel-plugin-transform-react-jsx "^6.5.0" + babel-plugin-transform-react-jsx-source "^6.5.0" + babel-plugin-transform-regenerator "^6.5.0" + react-transform-hmr "^1.0.4" + +babel-preset-react-native@^1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/babel-preset-react-native/-/babel-preset-react-native-1.9.2.tgz#b22addd2e355ff3b39671b79be807e52dfa145f2" + dependencies: + babel-plugin-check-es2015-constants "^6.5.0" + babel-plugin-react-transform "2.0.2" + babel-plugin-syntax-async-functions "^6.5.0" + babel-plugin-syntax-class-properties "^6.5.0" + babel-plugin-syntax-flow "^6.5.0" + babel-plugin-syntax-jsx "^6.5.0" + babel-plugin-syntax-trailing-function-commas "^6.5.0" + babel-plugin-transform-class-properties "^6.5.0" + babel-plugin-transform-es2015-arrow-functions "^6.5.0" + babel-plugin-transform-es2015-block-scoping "^6.5.0" + babel-plugin-transform-es2015-classes "^6.5.0" + babel-plugin-transform-es2015-computed-properties "^6.5.0" + babel-plugin-transform-es2015-destructuring "^6.5.0" + babel-plugin-transform-es2015-for-of "^6.5.0" + babel-plugin-transform-es2015-function-name "^6.5.0" + babel-plugin-transform-es2015-literals "^6.5.0" + babel-plugin-transform-es2015-modules-commonjs "^6.5.0" + babel-plugin-transform-es2015-parameters "^6.5.0" + babel-plugin-transform-es2015-shorthand-properties "^6.5.0" + babel-plugin-transform-es2015-spread "^6.5.0" + babel-plugin-transform-es2015-template-literals "^6.5.0" + babel-plugin-transform-flow-strip-types "^6.5.0" + babel-plugin-transform-object-assign "^6.5.0" + babel-plugin-transform-object-rest-spread "^6.5.0" + babel-plugin-transform-react-display-name "^6.5.0" + babel-plugin-transform-react-jsx "^6.5.0" + babel-plugin-transform-react-jsx-source "^6.5.0" + babel-plugin-transform-regenerator "^6.5.0" + react-transform-hmr "^1.0.4" + +babel-register@^6.18.0, babel-register@^6.24.0, babel-register@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" + dependencies: + babel-core "^6.24.1" + babel-runtime "^6.22.0" + core-js "^2.4.0" + home-or-tmp "^2.0.0" + lodash "^4.2.0" + mkdirp "^0.5.1" + source-map-support "^0.4.2" + +babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.10.0" + +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.25.0" + babel-types "^6.25.0" + babylon "^6.17.2" + lodash "^4.2.0" + +babel-traverse@^6.18.0, babel-traverse@^6.21.0, babel-traverse@^6.24.1, babel-traverse@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1" + dependencies: + babel-code-frame "^6.22.0" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-types "^6.25.0" + babylon "^6.17.2" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.21.0, babel-types@^6.24.1, babel-types@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e" + dependencies: + babel-runtime "^6.22.0" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + +babylon@^6.14.1, babylon@^6.17.2, babylon@^6.17.4: + version "6.17.4" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + +base64-js@^1.0.2, base64-js@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +base64-url@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78" + +basic-auth-connect@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" + +basic-auth@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" + +basic-auth@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" + +batch@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +beeper@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" + +big.js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" + +binary-extensions@^1.0.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.7" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46" + +body-parser@~1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.13.3.tgz#c08cf330c3358e151016a05746f13f029c97fa97" + dependencies: + bytes "2.1.0" + content-type "~1.0.1" + debug "~2.2.0" + depd "~1.0.1" + http-errors "~1.3.1" + iconv-lite "0.4.11" + on-finished "~2.3.0" + qs "4.0.0" + raw-body "~2.1.2" + type-is "~1.6.6" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +bplist-creator@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.0.4.tgz#4ac0496782e127a85c1d2026a4f5eb22a7aff991" + dependencies: + stream-buffers "~0.2.3" + +bplist-parser@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.0.6.tgz#38da3471817df9d44ab3892e27707bbbd75a11b9" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-resolve@^1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserslist@^2.1.2: + version "2.1.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.1.5.tgz#e882550df3d1cd6d481c1a3e0038f2baf13a4711" + dependencies: + caniuse-lite "^1.0.30000684" + electron-to-chromium "^1.3.14" + +bser@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169" + dependencies: + node-int64 "^0.4.0" + +bser@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.3.tgz#d63da19ee17330a0e260d2a34422b21a89520317" + dependencies: + node-int64 "^0.4.0" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + dependencies: + node-int64 "^0.4.0" + +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.1.0.tgz#ac93c410e2ffc9cc7cf4b464b38289067f5e47b4" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + +camelcase-keys@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.1.0.tgz#214d348cc5457f39316a2c31cc3e37246325e73f" + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-lite@^1.0.30000684: + version "1.0.30000701" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000701.tgz#9d673cf6b74dcb3d5c21d213176b011ac6a45baa" + +case-sensitive-paths-webpack-plugin@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.1.tgz#3d29ced8c1f124bf6f53846fb3f5894731fdc909" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + 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" + +chalk@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +ci-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.0.tgz#59c90fe3e137104746ff19469a27a634ff68c857" + dependencies: + chalk "^1.1.3" + +clear@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clear/-/clear-0.0.1.tgz#e5186e229d99448179c130311b6f9d30bff6b0ba" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.0.0.tgz#ef987ed3d48391ac3dab9180b406a742180d6e6a" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + +clone@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.0.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +compressible@~2.0.5: + version "2.0.10" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" + dependencies: + mime-db ">= 1.27.0 < 2" + +compression@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.5.2.tgz#b03b8d86e6f8ad29683cba8df91ddc6ffc77b395" + dependencies: + accepts "~1.2.12" + bytes "2.1.0" + compressible "~2.0.5" + debug "~2.2.0" + on-headers "~1.0.0" + vary "~1.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +connect-timeout@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/connect-timeout/-/connect-timeout-1.6.2.tgz#de9a5ec61e33a12b6edaab7b5f062e98c599b88e" + dependencies: + debug "~2.2.0" + http-errors "~1.3.1" + ms "0.7.1" + on-headers "~1.0.0" + +connect@^2.8.3: + version "2.30.2" + resolved "https://registry.yarnpkg.com/connect/-/connect-2.30.2.tgz#8da9bcbe8a054d3d318d74dfec903b5c39a1b609" + dependencies: + basic-auth-connect "1.0.0" + body-parser "~1.13.3" + bytes "2.1.0" + compression "~1.5.2" + connect-timeout "~1.6.2" + content-type "~1.0.1" + cookie "0.1.3" + cookie-parser "~1.3.5" + cookie-signature "1.0.6" + csurf "~1.8.3" + debug "~2.2.0" + depd "~1.0.1" + errorhandler "~1.4.2" + express-session "~1.11.3" + finalhandler "0.4.0" + fresh "0.3.0" + http-errors "~1.3.1" + method-override "~2.3.5" + morgan "~1.6.1" + multiparty "3.3.2" + on-headers "~1.0.0" + parseurl "~1.3.0" + pause "0.1.0" + qs "4.0.0" + response-time "~2.3.1" + serve-favicon "~2.3.0" + serve-index "~1.7.2" + serve-static "~1.10.0" + type-is "~1.6.6" + utils-merge "1.0.0" + vhost "~3.0.1" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94" + +content-type@~1.0.1, content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +convert-source-map@^1.1.0, convert-source-map@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + +cookie-parser@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.3.5.tgz#9d755570fb5d17890771227a02314d9be7cf8356" + dependencies: + cookie "0.1.3" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.3.tgz#e734a5c1417fce472d5aef82c381cabb64d1a435" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + +core-js@^2.2.2, core-js@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +crc@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.3.0.tgz#fa622e1bc388bf257309082d6b65200ce67090ba" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-browserify@^3.11.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + +csrf@~3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a" + dependencies: + rndm "1.2.0" + tsscmp "1.0.5" + uid-safe "2.1.4" + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" + +"cssstyle@>= 0.2.37 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + dependencies: + cssom "0.3.x" + +csurf@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.8.3.tgz#23f2a13bf1d8fce1d0c996588394442cba86a56a" + dependencies: + cookie "0.1.3" + cookie-signature "1.0.6" + csrf "~3.0.0" + http-errors "~1.3.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +dateformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.0.0.tgz#2743e3abb5c3fc2462e527dca445e04e9f4dee17" + +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + +debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + dependencies: + strip-bom "^2.0.0" + +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +denodeify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" + +depd@1.1.0, depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +depd@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.0.1.tgz#80aec64c9d6d97e65cc2a9caa93c0aa6abf73aaa" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +diff@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dlv@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.0.tgz#fee1a7c43f63be75f3f679e85262da5f102764a7" + +dom-walk@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +duplexer2@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + dependencies: + readable-stream "~1.1.9" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.3.14: + version "1.3.15" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +enhanced-resolve@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz#950964ecc7f0332a42321b673b38dc8ff15535b3" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.5" + +errno@^0.1.3, errno@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +errorhandler@~1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.4.3.tgz#b7b70ed8f359e9db88092f2d20c0f831420ad83f" + dependencies: + accepts "~1.3.0" + escape-html "~1.0.3" + +es6-error@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.0.2.tgz#eec5c726eacef51b7f6b73c20db6e1b13b069c98" + +escape-html@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.2.tgz#d77d32fa98e38c2f41ae85e9278e0e0e6ba1022c" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@^1.6.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +eslint-plugin-prettier@^2.0.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.1.2.tgz#4b90f4ee7f92bfbe2e926017e1ca40eb628965ea" + dependencies: + fast-diff "^1.1.1" + jest-docblock "^20.0.1" + +esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" + +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +event-target-shim@^1.0.5: + version "1.1.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-1.1.1.tgz#a86e5ee6bdaa16054475da797ccddf0c55698491" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + dependencies: + create-hash "^1.1.1" + +exec-sh@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" + dependencies: + merge "^1.1.3" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express-session@~1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.11.3.tgz#5cc98f3f5ff84ed835f91cbf0aabd0c7107400af" + dependencies: + cookie "0.1.3" + cookie-signature "1.0.6" + crc "3.3.0" + debug "~2.2.0" + depd "~1.0.1" + on-headers "~1.0.0" + parseurl "~1.3.0" + uid-safe "~2.0.0" + utils-merge "1.0.0" + +express@^4.15.2: + version "4.15.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.7" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.3" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.4" + qs "6.4.0" + range-parser "~1.2.0" + send "0.15.3" + serve-static "1.12.3" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + +extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +external-editor@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" + dependencies: + iconv-lite "^0.4.17" + jschardet "^1.4.2" + tmp "^0.0.31" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fancy-log@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" + dependencies: + chalk "^1.1.1" + time-stamp "^1.0.0" + +fast-diff@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.1.tgz#0aea0e4e605b6a2189f0e936d4b7fbaf1b7cfd9b" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fb-watchman@^1.8.0, fb-watchman@^1.9.0: + version "1.9.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-1.9.2.tgz#a24cf47827f82d38fb59a69ad70b76e3b6ae7383" + dependencies: + bser "1.0.2" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + dependencies: + bser "^2.0.0" + +fbjs-scripts@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/fbjs-scripts/-/fbjs-scripts-0.7.1.tgz#4f115e218e243e3addbf0eddaac1e3c62f703fac" + dependencies: + babel-core "^6.7.2" + babel-preset-fbjs "^1.0.0" + core-js "^1.0.0" + cross-spawn "^3.0.1" + gulp-util "^3.0.4" + object-assign "^4.0.1" + semver "^5.1.0" + through2 "^2.0.0" + +fbjs@^0.8.4, fbjs@^0.8.5: + version "0.8.12" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fileset@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +finalhandler@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.4.0.tgz#965a52d9e8d05d2b857548541fb89b53a2497d9b" + dependencies: + debug "~2.2.0" + escape-html "1.0.2" + on-finished "~2.3.0" + unpipe "~1.0.0" + +finalhandler@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" + dependencies: + debug "2.6.7" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" + +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +fs-extra@^0.26.2: + version "0.26.7" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.36" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" + +gauge@~1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" + dependencies: + ansi "^0.3.0" + has-unicode "^2.0.0" + lodash.pad "^4.1.0" + lodash.padend "^4.1.0" + lodash.padstart "^4.1.0" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" + dependencies: + min-document "^2.19.0" + process "~0.5.1" + +globals@^9.0.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +glogg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" + dependencies: + sparkles "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + +gulp-util@^3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" + dependencies: + array-differ "^1.0.0" + array-uniq "^1.0.2" + beeper "^1.0.0" + chalk "^1.0.0" + dateformat "^2.0.0" + fancy-log "^1.1.0" + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash._reescape "^3.0.0" + lodash._reevaluate "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.template "^3.0.0" + minimist "^1.1.0" + multipipe "^0.1.2" + object-assign "^3.0.0" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl "^0.5.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + dependencies: + glogg "^1.0.0" + +handlebars@^4.0.3: + version "4.0.10" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +"happypack@github:amireh/happypack#3256a3380dde2e06e3ad2ca3b41e9a81fd4f9673": + version "3.0.3" + resolved "https://codeload.github.com/amireh/happypack/tar.gz/3256a3380dde2e06e3ad2ca3b41e9a81fd4f9673" + dependencies: + async "1.5.0" + json-stringify-safe "5.0.1" + loader-utils "0.2.16" + mkdirp "0.5.1" + serialize-error "^2.1.0" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-gulplog@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" + dependencies: + sparkles "^1.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hasha@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" + dependencies: + is-stream "^1.0.1" + pinkie-promise "^2.0.0" + +haul@^1.0.0-beta.1: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/haul/-/haul-1.0.0-beta.2.tgz#d8fe6d66d5f6e2419e145e425003489fef54931b" + dependencies: + babel-core "^6.24.0" + babel-loader "^6.4.1" + babel-plugin-transform-flow-strip-types "^6.22.0" + babel-plugin-transform-object-rest-spread "^6.23.0" + babel-preset-env "^1.2.2" + babel-preset-react-native "^1.9.1" + babel-register "^6.24.0" + camelcase-keys "^4.0.0" + case-sensitive-paths-webpack-plugin "^2.0.0" + chalk "1.1.3" + clear "^0.0.1" + cliui "^3.2.0" + decamelize "^1.2.0" + dedent "^0.7.0" + dlv "^1.1.0" + escape-string-regexp "^1.0.5" + eslint-plugin-prettier "^2.0.1" + express "^4.15.2" + happypack "github:amireh/happypack#3256a3380dde2e06e3ad2ca3b41e9a81fd4f9673" + hasha "^2.2.0" + image-size "^0.5.1" + inquirer "^3.0.6" + loader-utils "^1.1.0" + minimist "^1.2.0" + morgan "^1.8.1" + open-in-editor "^2.2.0" + opn "^4.0.2" + ora "^1.2.0" + progress-bar-webpack-plugin "^1.9.3" + resolve "^1.3.3" + source-map "^0.5.6" + strip-ansi "^3.0.1" + webpack "^2.3.1" + webpack-dev-middleware "^1.10.1" + ws "^2.2.2" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +html-encoding-sniffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da" + dependencies: + whatwg-encoding "^1.0.1" + +http-errors@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" + dependencies: + inherits "~2.0.1" + statuses "1" + +http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" + dependencies: + depd "1.1.0" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +iconv-lite@0.4.11: + version "0.4.11" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.11.tgz#2ecb42fd294744922209a2e7c404dac8793d8ade" + +iconv-lite@0.4.13: + version "0.4.13" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" + +iconv-lite@^0.4.17, iconv-lite@~0.4.13: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +image-size@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.3.5.tgz#83240eab2fb5b00b04aab8c74b0471e9cba7ad8c" + +image-size@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + +immutable@~3.7.6: + version "3.7.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +inquirer@^3.0.6: + version "3.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.0.tgz#45b44c2160c729d7578c54060b3eed94487bb42b" + dependencies: + ansi-escapes "^2.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" + +invariant@^2.2.0, invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ipaddr.js@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-ci@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isemail@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul-api@^1.1.1: + version "1.1.11" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.11.tgz#fcc0b461e2b3bda71e305155138238768257d9de" + dependencies: + async "^2.1.4" + fileset "^2.0.2" + istanbul-lib-coverage "^1.1.1" + istanbul-lib-hook "^1.0.7" + istanbul-lib-instrument "^1.7.4" + istanbul-lib-report "^1.1.1" + istanbul-lib-source-maps "^1.2.1" + istanbul-reports "^1.1.1" + js-yaml "^3.7.0" + mkdirp "^0.5.1" + once "^1.4.0" + +istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" + +istanbul-lib-hook@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc" + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.2, istanbul-lib-instrument@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz#e9fd920e4767f3d19edc765e2d6b3f5ccbd0eea8" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.17.4" + istanbul-lib-coverage "^1.1.1" + semver "^5.3.0" + +istanbul-lib-report@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9" + dependencies: + istanbul-lib-coverage "^1.1.1" + mkdirp "^0.5.1" + path-parse "^1.0.5" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c" + dependencies: + debug "^2.6.3" + istanbul-lib-coverage "^1.1.1" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.1.tgz#042be5c89e175bc3f86523caab29c014e77fee4e" + dependencies: + handlebars "^4.0.3" + +jest-changed-files@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8" + +jest-cli@^20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-20.0.4.tgz#e532b19d88ae5bc6c417e8b0593a6fe954b1dc93" + dependencies: + ansi-escapes "^1.4.0" + callsites "^2.0.0" + chalk "^1.1.3" + graceful-fs "^4.1.11" + is-ci "^1.0.10" + istanbul-api "^1.1.1" + istanbul-lib-coverage "^1.0.1" + istanbul-lib-instrument "^1.4.2" + istanbul-lib-source-maps "^1.1.0" + jest-changed-files "^20.0.3" + jest-config "^20.0.4" + jest-docblock "^20.0.3" + jest-environment-jsdom "^20.0.3" + jest-haste-map "^20.0.4" + jest-jasmine2 "^20.0.4" + jest-message-util "^20.0.3" + jest-regex-util "^20.0.3" + jest-resolve-dependencies "^20.0.3" + jest-runtime "^20.0.4" + jest-snapshot "^20.0.3" + jest-util "^20.0.3" + micromatch "^2.3.11" + node-notifier "^5.0.2" + pify "^2.3.0" + slash "^1.0.0" + string-length "^1.0.1" + throat "^3.0.0" + which "^1.2.12" + worker-farm "^1.3.1" + yargs "^7.0.2" + +jest-config@^20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-20.0.4.tgz#e37930ab2217c913605eff13e7bd763ec48faeea" + dependencies: + chalk "^1.1.3" + glob "^7.1.1" + jest-environment-jsdom "^20.0.3" + jest-environment-node "^20.0.3" + jest-jasmine2 "^20.0.4" + jest-matcher-utils "^20.0.3" + jest-regex-util "^20.0.3" + jest-resolve "^20.0.4" + jest-validate "^20.0.3" + pretty-format "^20.0.3" + +jest-diff@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-20.0.3.tgz#81f288fd9e675f0fb23c75f1c2b19445fe586617" + dependencies: + chalk "^1.1.3" + diff "^3.2.0" + jest-matcher-utils "^20.0.3" + pretty-format "^20.0.3" + +jest-docblock@^20.0.1, jest-docblock@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712" + +jest-environment-jsdom@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz#048a8ac12ee225f7190417713834bb999787de99" + dependencies: + jest-mock "^20.0.3" + jest-util "^20.0.3" + jsdom "^9.12.0" + +jest-environment-node@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-20.0.3.tgz#d488bc4612af2c246e986e8ae7671a099163d403" + dependencies: + jest-mock "^20.0.3" + jest-util "^20.0.3" + +jest-haste-map@18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-18.0.0.tgz#707d3b5ae3bcbda971c39e8b911d20ad8502c748" + dependencies: + fb-watchman "^1.9.0" + graceful-fs "^4.1.6" + multimatch "^2.1.0" + sane "~1.4.1" + worker-farm "^1.3.1" + +jest-haste-map@^20.0.4: + version "20.0.5" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112" + dependencies: + fb-watchman "^2.0.0" + graceful-fs "^4.1.11" + jest-docblock "^20.0.3" + micromatch "^2.3.11" + sane "~1.6.0" + worker-farm "^1.3.1" + +jest-jasmine2@^20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz#fcc5b1411780d911d042902ef1859e852e60d5e1" + dependencies: + chalk "^1.1.3" + graceful-fs "^4.1.11" + jest-diff "^20.0.3" + jest-matcher-utils "^20.0.3" + jest-matchers "^20.0.3" + jest-message-util "^20.0.3" + jest-snapshot "^20.0.3" + once "^1.4.0" + p-map "^1.1.1" + +jest-matcher-utils@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz#b3a6b8e37ca577803b0832a98b164f44b7815612" + dependencies: + chalk "^1.1.3" + pretty-format "^20.0.3" + +jest-matchers@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-20.0.3.tgz#ca69db1c32db5a6f707fa5e0401abb55700dfd60" + dependencies: + jest-diff "^20.0.3" + jest-matcher-utils "^20.0.3" + jest-message-util "^20.0.3" + jest-regex-util "^20.0.3" + +jest-message-util@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-20.0.3.tgz#6aec2844306fcb0e6e74d5796c1006d96fdd831c" + dependencies: + chalk "^1.1.3" + micromatch "^2.3.11" + slash "^1.0.0" + +jest-mock@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-20.0.3.tgz#8bc070e90414aa155c11a8d64c869a0d5c71da59" + +jest-regex-util@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-20.0.3.tgz#85bbab5d133e44625b19faf8c6aa5122d085d762" + +jest-resolve-dependencies@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz#6e14a7b717af0f2cb3667c549de40af017b1723a" + dependencies: + jest-regex-util "^20.0.3" + +jest-resolve@^20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-20.0.4.tgz#9448b3e8b6bafc15479444c6499045b7ffe597a5" + dependencies: + browser-resolve "^1.11.2" + is-builtin-module "^1.0.0" + resolve "^1.3.2" + +jest-runtime@^20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.4.tgz#a2c802219c4203f754df1404e490186169d124d8" + dependencies: + babel-core "^6.0.0" + babel-jest "^20.0.3" + babel-plugin-istanbul "^4.0.0" + chalk "^1.1.3" + convert-source-map "^1.4.0" + graceful-fs "^4.1.11" + jest-config "^20.0.4" + jest-haste-map "^20.0.4" + jest-regex-util "^20.0.3" + jest-resolve "^20.0.4" + jest-util "^20.0.3" + json-stable-stringify "^1.0.1" + micromatch "^2.3.11" + strip-bom "3.0.0" + yargs "^7.0.2" + +jest-snapshot@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-20.0.3.tgz#5b847e1adb1a4d90852a7f9f125086e187c76566" + dependencies: + chalk "^1.1.3" + jest-diff "^20.0.3" + jest-matcher-utils "^20.0.3" + jest-util "^20.0.3" + natural-compare "^1.4.0" + pretty-format "^20.0.3" + +jest-util@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-20.0.3.tgz#0c07f7d80d82f4e5a67c6f8b9c3fe7f65cfd32ad" + dependencies: + chalk "^1.1.3" + graceful-fs "^4.1.11" + jest-message-util "^20.0.3" + jest-mock "^20.0.3" + jest-validate "^20.0.3" + leven "^2.1.0" + mkdirp "^0.5.1" + +jest-validate@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-20.0.3.tgz#d0cfd1de4f579f298484925c280f8f1d94ec3cab" + dependencies: + chalk "^1.1.3" + jest-matcher-utils "^20.0.3" + leven "^2.1.0" + pretty-format "^20.0.3" + +jest@20.0.4: + version "20.0.4" + resolved "https://registry.yarnpkg.com/jest/-/jest-20.0.4.tgz#3dd260c2989d6dad678b1e9cc4d91944f6d602ac" + dependencies: + jest-cli "^20.0.4" + +joi@^6.6.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" + dependencies: + hoek "2.x.x" + isemail "1.x.x" + moment "2.x.x" + topo "1.x.x" + +js-tokens@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jschardet@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e" + +jsdom@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" + dependencies: + abab "^1.0.3" + acorn "^4.0.4" + acorn-globals "^3.1.0" + array-equal "^1.0.0" + content-type-parser "^1.0.1" + cssom ">= 0.3.2 < 0.4.0" + cssstyle ">= 0.2.37 < 0.3.0" + escodegen "^1.6.1" + html-encoding-sniffer "^1.0.1" + nwmatcher ">= 1.3.9 < 2.0.0" + parse5 "^1.5.1" + request "^2.79.0" + sax "^1.2.1" + symbol-tree "^3.2.1" + tough-cookie "^2.3.2" + webidl-conversions "^4.0.0" + whatwg-encoding "^1.0.1" + whatwg-url "^4.3.0" + xml-name-validator "^2.0.1" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@5.0.1, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.4.0.tgz#054352e4c4c80c86c0923877d449de176a732c8d" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" + dependencies: + assert-plus "1.0.0" + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +left-pad@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.1.3.tgz#612f61c033f3a9e08e939f1caebeea41b6f3199a" + +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@0.2.16: + version "0.2.16" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._reescape@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" + +lodash._reevaluate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + dependencies: + lodash._root "^3.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.pad@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" + +lodash.padend@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + +lodash.padstart@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + +lodash.template@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash@^3.5.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@^4.14.0, lodash@^4.16.6, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.6.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0, loose-envify@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + dependencies: + tmpl "1.0.x" + +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +method-override@~2.3.5: + version "2.3.9" + resolved "https://registry.yarnpkg.com/method-override/-/method-override-2.3.9.tgz#bd151f2ce34cf01a76ca400ab95c012b102d8f71" + dependencies: + debug "2.6.8" + methods "~1.1.2" + parseurl "~1.3.1" + vary "~1.1.1" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-db@~1.23.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.23.0.tgz#a31b4070adaea27d732ea333740a64d0ec9a6659" + +mime-types@2.1.11, mime-types@~2.1.7, mime-types@~2.1.9: + version "2.1.11" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.11.tgz#c259c471bda808a85d6cd193b430a5fae4473b3c" + dependencies: + mime-db "~1.23.0" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.6: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +mime@^1.3.4: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + dependencies: + dom-walk "^0.1.0" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8, minimist@~0.0.1: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +moment@2.x.x: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +morgan@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" + dependencies: + basic-auth "~1.1.0" + debug "2.6.8" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +morgan@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.6.1.tgz#5fd818398c6819cba28a7cd6664f292fe1c0bbf2" + dependencies: + basic-auth "~1.0.3" + debug "~2.2.0" + depd "~1.0.1" + on-finished "~2.3.0" + on-headers "~1.0.0" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +multimatch@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + +multiparty@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-3.3.2.tgz#35de6804dc19643e5249f3d3e3bdc6c8ce301d3f" + dependencies: + readable-stream "~1.1.9" + stream-counter "~0.2.0" + +multipipe@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" + dependencies: + duplexer2 "0.0.2" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +nan@^2.3.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.5.3.tgz#269d5c476810ec92edbe7b6c2f28316384f9a7e8" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-fetch@^1.0.1, node-fetch@^1.3.3: + version "1.7.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-notifier@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" + dependencies: + growly "^1.3.0" + semver "^5.3.0" + shellwords "^0.1.0" + which "^1.2.12" + +node-pre-gyp@^0.6.36: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-uuid@1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npmlog@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" + dependencies: + ansi "~0.3.1" + are-we-there-yet "~1.1.2" + gauge "~1.2.5" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +"nwmatcher@>= 1.3.9 < 2.0.0": + version "1.4.1" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.1.tgz#7ae9b07b0ea804db7e25f05cb5fe4097d4e4949f" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-keys@^1.0.10, object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +object.assign@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.0" + object-keys "^1.0.10" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.0, on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +open-in-editor@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/open-in-editor/-/open-in-editor-2.2.0.tgz#c5b21aa76f6acd4cbbd3c3b2e77dccb4b75a2020" + dependencies: + clap "^1.1.3" + os-homedir "~1.0.2" + +opn@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a" + dependencies: + object-assign "^4.0.1" + +opn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +ora@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-1.3.0.tgz#80078dd2b92a934af66a3ad72a5b910694ede51a" + dependencies: + chalk "^1.1.1" + cli-cursor "^2.1.0" + cli-spinners "^1.0.0" + log-symbols "^1.0.2" + +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + +os-homedir@^1.0.0, os-homedir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + +parseurl@~1.3.0, parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pause@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.1.0.tgz#ebc8a4a8619ff0b8a81ac1513c3434ff469fdb74" + +pbkdf2@^3.0.3: + version "3.0.12" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pegjs@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.9.0.tgz#f6aefa2e3ce56169208e52179dfe41f89141a369" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +plist@1.2.0, plist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593" + dependencies: + base64-js "0.0.8" + util-deprecate "1.0.2" + xmlbuilder "4.0.0" + xmldom "0.1.x" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +pretty-format@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14" + dependencies: + ansi-regex "^2.1.1" + ansi-styles "^3.0.0" + +private@^0.1.6: + version "0.1.7" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +process@~0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" + +progress-bar-webpack-plugin@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-1.10.0.tgz#e0b1063aa03c79e298a9340598590bb61efef9a4" + dependencies: + chalk "^1.1.1" + object.assign "^4.0.1" + progress "^1.1.8" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +proxy-addr@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.3.0" + +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607" + +qs@6.4.0, qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +quick-lru@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.0.0.tgz#7fa80304ab72c1f81cef738739cd47d7cc0c8bff" + +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +range-parser@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.0.3.tgz#6872823535c692e2c2a0103826afd82c2e0ff175" + +raw-body@~2.1.2: + version "2.1.7" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.13" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-clone-referenced-element@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-clone-referenced-element/-/react-clone-referenced-element-1.0.1.tgz#2bba8c69404c5e4a944398600bcc4c941f860682" + +react-deep-force-update@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.0.1.tgz#f911b5be1d2a6fe387507dd6e9a767aa2924b4c7" + +"react-native-payments@file:../..": + version "0.1.1" + dependencies: + es6-error "^4.0.2" + uuid "^3.1.0" + validator "^7.0.0" + +react-native@0.41.0: + version "0.41.0" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.41.0.tgz#09ea967b182885f302724f14587145da3b2d2e22" + dependencies: + absolute-path "^0.0.0" + art "^0.10.0" + async "^2.0.1" + babel-core "^6.21.0" + babel-generator "^6.21.0" + babel-plugin-external-helpers "^6.18.0" + babel-plugin-syntax-trailing-function-commas "^6.20.0" + babel-plugin-transform-flow-strip-types "^6.21.0" + babel-plugin-transform-object-rest-spread "^6.20.2" + babel-polyfill "^6.20.0" + babel-preset-es2015-node "^6.1.1" + babel-preset-fbjs "^2.1.0" + babel-preset-react-native "^1.9.1" + babel-register "^6.18.0" + babel-runtime "^6.20.0" + babel-traverse "^6.21.0" + babel-types "^6.21.0" + babylon "^6.14.1" + base64-js "^1.1.2" + bser "^1.0.2" + chalk "^1.1.1" + commander "^2.9.0" + connect "^2.8.3" + core-js "^2.2.2" + debug "^2.2.0" + denodeify "^1.2.1" + event-target-shim "^1.0.5" + fbjs "^0.8.5" + fbjs-scripts "^0.7.0" + fs-extra "^0.26.2" + glob "^5.0.15" + graceful-fs "^4.1.3" + image-size "^0.3.5" + immutable "~3.7.6" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + jest-haste-map "18.0.0" + joi "^6.6.1" + json-stable-stringify "^1.0.1" + json5 "^0.4.0" + left-pad "^1.1.3" + lodash "^4.16.6" + mime "^1.3.4" + mime-types "2.1.11" + minimist "^1.2.0" + mkdirp "^0.5.1" + node-fetch "^1.3.3" + npmlog "^2.0.4" + opn "^3.0.2" + optimist "^0.6.1" + plist "^1.2.0" + promise "^7.1.1" + react-clone-referenced-element "^1.0.1" + react-timer-mixin "^0.13.2" + react-transform-hmr "^1.0.4" + rebound "^0.0.13" + regenerator-runtime "^0.9.5" + request "^2.79.0" + rimraf "^2.5.4" + sane "~1.4.1" + semver "^5.0.3" + shell-quote "1.6.1" + source-map "^0.5.6" + stacktrace-parser "^0.1.3" + temp "0.8.3" + throat "^3.0.0" + uglify-js "^2.6.2" + whatwg-fetch "^1.0.0" + wordwrap "^1.0.0" + worker-farm "^1.3.1" + write-file-atomic "^1.2.0" + ws "^1.1.0" + xcode "^0.8.9" + xmldoc "^0.4.0" + yargs "^6.4.0" + +react-proxy@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a" + dependencies: + lodash "^4.6.1" + react-deep-force-update "^1.0.0" + +react-test-renderer@~15.4.0-rc.4: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.4.2.tgz#27e1dff5d26d0e830f99614c487622bc831416f3" + dependencies: + fbjs "^0.8.4" + object-assign "^4.1.0" + +react-timer-mixin@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz#0da8b9f807ec07dc3e854d082c737c65605b3d22" + +react-transform-hmr@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz#e1a40bd0aaefc72e8dfd7a7cda09af85066397bb" + dependencies: + global "^4.3.0" + react-proxy "^1.1.7" + +react@~15.4.0-rc.4: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" + dependencies: + fbjs "^0.8.4" + loose-envify "^1.1.0" + object-assign "^4.1.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readable-stream@~1.1.8, readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rebound@^0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.0.13.tgz#4a225254caf7da756797b19c5817bf7a7941fac1" + +regenerate@^1.2.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" + +regenerator-runtime@^0.10.0: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-runtime@^0.9.5: + version "0.9.6" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" + +regenerator-transform@0.9.11: + version "0.9.11" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + dependencies: + is-equal-shallow "^0.1.3" + is-primitive "^2.0.0" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + +request@^2.79.0, request@^2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +resolve@^1.3.2, resolve@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" + dependencies: + path-parse "^1.0.5" + +response-time@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/response-time/-/response-time-2.3.2.tgz#ffa71bab952d62f7c1d49b7434355fbc68dffc5a" + dependencies: + depd "~1.1.0" + on-headers "~1.0.1" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +rndm@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +safe-buffer@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + +sane@~1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715" + dependencies: + exec-sh "^0.2.0" + fb-watchman "^1.8.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sane@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-1.6.0.tgz#9610c452307a135d29c1fdfe2547034180c46775" + dependencies: + anymatch "^1.3.0" + exec-sh "^0.2.0" + fb-watchman "^1.8.0" + minimatch "^3.0.2" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.10.0" + +sax@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +sax@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240" + +"semver@2 || 3 || 4 || 5", semver@5.x, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de" + dependencies: + debug "~2.2.0" + depd "~1.1.0" + destroy "~1.0.4" + escape-html "~1.0.3" + etag "~1.7.0" + fresh "0.3.0" + http-errors "~1.3.1" + mime "1.3.4" + ms "0.7.1" + on-finished "~2.3.0" + range-parser "~1.0.3" + statuses "~1.2.1" + +send@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" + dependencies: + debug "2.6.7" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.1" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + +serve-favicon@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.3.2.tgz#dd419e268de012ab72b319d337f2105013f9381f" + dependencies: + etag "~1.7.0" + fresh "0.3.0" + ms "0.7.2" + parseurl "~1.3.1" + +serve-index@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.7.3.tgz#7a057fc6ee28dc63f64566e5fa57b111a86aecd2" + dependencies: + accepts "~1.2.13" + batch "0.5.3" + debug "~2.2.0" + escape-html "~1.0.3" + http-errors "~1.3.1" + mime-types "~2.1.9" + parseurl "~1.3.1" + +serve-static@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.3" + +serve-static@~1.10.0: + version "1.10.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.10.3.tgz#ce5a6ecd3101fed5ec09827dac22a9c29bfb0535" + dependencies: + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.13.2" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +setimmediate@^1.0.4, setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + +shell-quote@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +shellwords@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.0.tgz#66afd47b6a12932d9071cbfd98a52e785cd0ba14" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-plist@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-0.1.4.tgz#10eb51b47e33c556eb8ec46d5ee64d64e717db5d" + dependencies: + bplist-creator "0.0.4" + bplist-parser "0.0.6" + plist "1.2.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-support@^0.4.2: + version "0.4.15" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" + dependencies: + source-map "^0.5.6" + +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +sparkles@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stacktrace-parser@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz#01397922e5f62ecf30845522c95c4fe1d25e7d4e" + +statuses@1, "statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +statuses@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-buffers@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-0.2.6.tgz#181c08d5bb3690045f69401b9ae6a7a0cf3313fc" + +stream-counter@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/stream-counter/-/stream-counter-0.2.0.tgz#ded266556319c8b0e222812b9cf3b26fa7d947de" + dependencies: + readable-stream "~1.1.8" + +stream-http@^2.3.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +string-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" + dependencies: + strip-ansi "^3.0.0" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.0.tgz#030664561fc146c9423ec7d978fe2457437fe6d0" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^0.10.25, string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.0, supports-color@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.0.tgz#ad986dc7eb2315d009b4d77c8169c2231a684037" + dependencies: + has-flag "^2.0.0" + +symbol-tree@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" + +tapable@^0.2.5, tapable@~0.2.5: + version "0.2.6" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +temp@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + +test-exclude@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +throat@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-3.2.0.tgz#50cb0670edbc40237b9e347d7e1f88e4620af836" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + +timers-browserify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86" + dependencies: + setimmediate "^1.0.4" + +tmp@^0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +topo@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" + dependencies: + hoek "2.x.x" + +tough-cookie@^2.3.2, tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +tsscmp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.15, type-is@~1.6.6: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +ua-parser-js@^0.7.9: + version "0.7.13" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.13.tgz#cd9dd2f86493b3f44dbeeef3780fda74c5ee14be" + +uglify-js@^2.6, uglify-js@^2.6.2, uglify-js@^2.8.27: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +uid-safe@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81" + dependencies: + random-bytes "~1.0.0" + +uid-safe@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137" + dependencies: + base64-url "1.2.1" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +ultron@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^3.0.0, uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +validator@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-7.2.0.tgz#a63dcbaba51d4350bf8df20988e0d5a54d711791" + +vary@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.0.1.tgz#99e4981566a286118dfb2b817357df7993376d10" + +vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +vhost@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5" + +vinyl@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + dependencies: + makeerror "1.0.x" + +watch@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" + +watchpack@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + +webidl-conversions@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.1.tgz#8015a17ab83e7e1b311638486ace81da6ce206a0" + +webpack-dev-middleware@^1.10.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" + dependencies: + memory-fs "~0.4.1" + mime "^1.3.4" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +webpack@^2.3.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.7.0.tgz#b2a1226804373ffd3d03ea9c6bd525067034f6b1" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^4.7.0" + ajv-keywords "^1.1.1" + async "^2.1.2" + enhanced-resolve "^3.3.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^0.2.16" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^3.1.0" + tapable "~0.2.5" + uglify-js "^2.8.27" + watchpack "^1.3.1" + webpack-sources "^1.0.1" + yargs "^6.0.0" + +whatwg-encoding@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4" + dependencies: + iconv-lite "0.4.13" + +whatwg-fetch@>=0.10.0, whatwg-fetch@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319" + +whatwg-url@^4.3.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which@^1.2.12, which@^1.2.9: + version "1.2.14" + resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@^1.0.0, wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +worker-farm@^1.3.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.4.1.tgz#a438bc993a7a7d133bcb6547c95eca7cff4897d8" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^1.2.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + +ws@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.4.tgz#57f40d036832e5f5055662a397c4de76ed66bf61" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +ws@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80" + dependencies: + safe-buffer "~5.0.1" + ultron "~1.1.0" + +xcode@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/xcode/-/xcode-0.8.9.tgz#ec6765f70e9dccccc9f6e9a5b9b4e7e814b4cf35" + dependencies: + node-uuid "1.4.7" + pegjs "0.9.0" + simple-plist "0.1.4" + +xml-name-validator@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + +xmlbuilder@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.0.0.tgz#98b8f651ca30aa624036f127d11cc66dc7b907a3" + dependencies: + lodash "^3.5.0" + +xmldoc@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-0.4.0.tgz#d257224be8393eaacbf837ef227fd8ec25b36888" + dependencies: + sax "~1.1.1" + +xmldom@0.1.x: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs@^6.0.0, yargs@^6.4.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/lib/ios/GatewayManager.h b/lib/ios/GatewayManager.h index 34d6f2fd..81fd2a65 100644 --- a/lib/ios/GatewayManager.h +++ b/lib/ios/GatewayManager.h @@ -2,17 +2,29 @@ #import +#if __has_include("BraintreeApplePay.h") +#import "BraintreeApplePay.h" +#endif + @interface GatewayManager : NSObject +#if __has_include("BraintreeApplePay.h") +@property (nonatomic, strong) BTAPIClient * _Nullable braintreeClient; +#endif + + (NSArray *_Nonnull)getSupportedGateways; -+ (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters +- (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters merchantIdentifier:(NSString *_Nonnull)merchantId; -+ (void)createTokenWithPayment:(PKPayment *_Nonnull)payment +- (void)createTokenWithPayment:(PKPayment *_Nonnull)payment completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion; // Stripe -+ (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters +- (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters merchantIdentifier:(NSString *_Nonnull)merchantId; -+ (void)createStripeTokenWithPayment:(PKPayment *_Nonnull)payment +- (void)createStripeTokenWithPayment:(PKPayment *_Nonnull)payment completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion; +// Braintree +- (void)configureBraintreeGateway:(NSDictionary *_Nonnull)gatewayParameters; +- (void)createBraintreeTokenWithPayment:(PKPayment *_Nonnull)payment + completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion; @end diff --git a/lib/ios/GatewayManager.m b/lib/ios/GatewayManager.m index 462efb97..511fcd37 100644 --- a/lib/ios/GatewayManager.m +++ b/lib/ios/GatewayManager.m @@ -4,56 +4,111 @@ #import #endif +#if __has_include("BraintreeApplePay.h") +#import "BraintreeApplePay.h" +#endif + @implementation GatewayManager + (NSArray *)getSupportedGateways { NSMutableArray *supportedGateways = [NSMutableArray array]; - + #if __has_include() [supportedGateways addObject:@"stripe"]; #endif - + +#if __has_include("BraintreeApplePay.h") + [supportedGateways addObject:@"braintree"]; +#endif + return [supportedGateways copy]; } -+ (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters +- (void)configureGateway:(NSDictionary *_Nonnull)gatewayParameters merchantIdentifier:(NSString *_Nonnull)merchantId { #if __has_include() if ([gatewayParameters[@"gateway"] isEqualToString:@"stripe"]) { - [GatewayManager configureStripeGateway:gatewayParameters merchantIdentifier:merchantId]; + [self configureStripeGateway:gatewayParameters merchantIdentifier:merchantId]; + } +#endif + +#if __has_include("BraintreeApplePay.h") + if ([gatewayParameters[@"gateway"] isEqualToString:@"braintree"]) { + [self configureBraintreeGateway:gatewayParameters]; } #endif } -+ (void)createTokenWithPayment:(PKPayment *_Nonnull)payment +- (void)createTokenWithPayment:(PKPayment *_Nonnull)payment completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion { #if __has_include() - [GatewayManager createStripeTokenWithPayment:payment completion:completion]; + [self createStripeTokenWithPayment:payment completion:completion]; +#endif + +#if __has_include("BraintreeApplePay.h") + [self createBraintreeTokenWithPayment:payment completion:completion]; #endif } // Stripe -+ (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters +- (void)configureStripeGateway:(NSDictionary *_Nonnull)gatewayParameters merchantIdentifier:(NSString *_Nonnull)merchantId { +#if __has_include() NSString *stripePublishableKey = gatewayParameters[@"stripe:publishableKey"]; [[STPPaymentConfiguration sharedConfiguration] setPublishableKey:stripePublishableKey]; [[STPPaymentConfiguration sharedConfiguration] setAppleMerchantIdentifier:merchantId]; +#endif } -+ (void)createStripeTokenWithPayment:(PKPayment *)payment completion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion +- (void)createStripeTokenWithPayment:(PKPayment *)payment completion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion { - [[STPAPIClient sharedClient] createTokenWithPayment:payment completion:^(STPToken * _Nullable token, NSError * _Nullable error) { +#if __has_include() + [[STPAPIClient sharedClient] createTokenWithPayment:payment completion:^(STPToken * _Nullable token, NSError * _Nullable error) + { if (error) { completion(nil, error); } else { completion(token.tokenId, nil); } }]; +#endif } +// Braintree +- (void)configureBraintreeGateway:(NSDictionary *_Nonnull)gatewayParameters +{ +#if __has_include("BraintreeApplePay.h") + NSString *braintreeTokenizationKey = gatewayParameters[@"braintree:tokenizationKey"]; + self.braintreeClient = [[BTAPIClient alloc] initWithAuthorization:braintreeTokenizationKey]; +#endif +} + +- (void)createBraintreeTokenWithPayment:(PKPayment *_Nonnull)payment + completion:(void (^_Nullable)(NSString * _Nullable token, NSError * _Nullable error))completion +{ +#if __has_include("BraintreeApplePay.h") + BTApplePayClient *applePayClient = [[BTApplePayClient alloc] + initWithAPIClient:self.braintreeClient]; + + [applePayClient tokenizeApplePayPayment:payment + completion:^(BTApplePayCardNonce *tokenizedApplePayPayment, + NSError *error) + { + + + if (error) { + + completion(nil, error); + } else { + + completion(tokenizedApplePayPayment.nonce, nil); + } + }]; +#endif +} @end diff --git a/lib/ios/ReactNativePayments.h b/lib/ios/ReactNativePayments.h index 60bc09bb..3552b71e 100644 --- a/lib/ios/ReactNativePayments.h +++ b/lib/ios/ReactNativePayments.h @@ -8,11 +8,14 @@ #import #endif +#import "GatewayManager.h" + @interface ReactNativePayments : NSObject @property (nonatomic, strong) RCTResponseSenderBlock callback; @property (nonatomic, strong) PKPaymentRequest *paymentRequest; @property (nonatomic, strong) NSDictionary *initialOptions; +@property (nonatomic, strong) GatewayManager *gatewayManager; @property BOOL *hasGatewayParameters; @property (nonatomic, strong) PKPaymentAuthorizationViewController *viewController; @property (nonatomic, copy) void (^completion)(PKPaymentAuthorizationStatus); @@ -27,6 +30,6 @@ - (PKShippingMethod *_Nonnull)convertShippingOptionToShippingMethod:(NSDictionary *_Nonnull)shippingOption; - (void)handleUserAccept:(PKPayment *_Nonnull)payment paymentToken:(NSString *_Nullable)token; -- (void)handleGatewayERror:(NSError *_Nonnull)error; +- (void)handleGatewayError:(NSError *_Nonnull)error; @end diff --git a/lib/ios/ReactNativePayments.m b/lib/ios/ReactNativePayments.m index c8054073..567b8060 100644 --- a/lib/ios/ReactNativePayments.m +++ b/lib/ios/ReactNativePayments.m @@ -1,5 +1,4 @@ #import "ReactNativePayments.h" -#import "GatewayManager.h" #import @implementation ReactNativePayments @@ -27,12 +26,13 @@ - (NSDictionary *)constantsToExport { NSString *merchantId = methodData[@"merchantIdentifier"]; NSDictionary *gatewayParameters = methodData[@"paymentMethodTokenizationParameters"][@"parameters"]; - + if (gatewayParameters) { self.hasGatewayParameters = true; - [GatewayManager configureGateway:gatewayParameters merchantIdentifier:merchantId]; + self.gatewayManager = [GatewayManager new]; + [self.gatewayManager configureGateway:gatewayParameters merchantIdentifier:merchantId]; } - + self.paymentRequest = [[PKPaymentRequest alloc] init]; self.paymentRequest.merchantIdentifier = merchantId; self.paymentRequest.merchantCapabilities = PKMerchantCapability3DS; @@ -150,13 +150,12 @@ - (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewControlle self.completion = completion; if (self.hasGatewayParameters) { - [GatewayManager createTokenWithPayment:payment completion:^(NSString * _Nullable token, NSError * _Nullable error) { + [self.gatewayManager createTokenWithPayment:payment completion:^(NSString * _Nullable token, NSError * _Nullable error) { if (error) { - NSLog(@"WTF"); [self handleGatewayError:error]; return; } - + [self handleUserAccept:payment paymentToken:token]; }]; } else { @@ -319,11 +318,11 @@ - (void)handleUserAccept:(PKPayment *_Nonnull)payment NSMutableDictionary *paymentResponse = [[NSMutableDictionary alloc]initWithCapacity:3]; [paymentResponse setObject:transactionId forKey:@"transactionIdentifier"]; [paymentResponse setObject:paymentData forKey:@"paymentData"]; - + if (token) { [paymentResponse setObject:token forKey:@"paymentToken"]; } - + [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:onuseraccept" body:paymentResponse ]; @@ -331,7 +330,6 @@ - (void)handleUserAccept:(PKPayment *_Nonnull)payment - (void)handleGatewayError:(NSError *_Nonnull)error { - NSLog(@"ERRROOOORRR"); [self.bridge.eventDispatcher sendDeviceEventWithName:@"NativePayments:ongatewayerror" body: @{ @"error": [error localizedDescription]