From 75c7beeb599eaed9b9d6bb7b2747d89511dbbddf Mon Sep 17 00:00:00 2001 From: John Brophy Date: Wed, 13 Apr 2016 10:45:30 -0700 Subject: [PATCH] Update to v0.4.0 --- CHANGELOG.md | 56 ++- README.md | 441 +++++++++++++++--- UberRides.podspec | 7 +- .../Obj-C SDK.xcodeproj/project.pbxproj | 151 +++++- examples/Obj-C SDK/Obj-C SDK/AppDelegate.m | 11 +- .../Base.lproj/LaunchScreen.storyboard | 5 +- .../Obj-C SDK/Base.lproj/Main.storyboard | 56 ++- examples/Obj-C SDK/Obj-C SDK/Info.plist | 6 + .../en.lproj/UberRides.strings | 5 + .../zh-Hans.lproj/UberRides.strings | 5 + .../zh-Hant.lproj/UberRides.strings | 5 + .../UBSDKDeeplinkExampleViewController.h | 34 ++ .../UBSDKDeeplinkExampleViewController.m | 229 +++++++++ .../Obj-C SDK/UBSDKExampleTableViewCell.h | 51 ++ .../Obj-C SDK/UBSDKExampleTableViewCell.m | 61 +++ ...er.h => UBSDKExampleTableViewController.h} | 6 +- .../UBSDKExampleTableViewController.m | 167 +++++++ .../UBSDKImplicitGrantExampleViewController.h | 34 ++ .../UBSDKImplicitGrantExampleViewController.m | 117 +++++ .../Obj-C SDK/Obj-C SDK/UBSDKLocalization.h | 26 ++ ...DKRideRequestWidgetExampleViewController.h | 34 ++ ...DKRideRequestWidgetExampleViewController.m | 122 +++++ examples/Obj-C SDK/Obj-C SDK/ViewController.m | 98 ---- .../hi-IN.lproj/LaunchScreen.strings | 1 + .../Obj-C SDK/hi-IN.lproj/Main.strings | 3 + .../zh-Hans.lproj/LaunchScreen.strings | 1 + .../Obj-C SDK/zh-Hans.lproj/Main.strings | 3 + .../zh-Hant.lproj/LaunchScreen.strings | 1 + .../Obj-C SDK/zh-Hant.lproj/Main.strings | 3 + .../Swift SDK.xcodeproj/project.pbxproj | 109 ++++- .../Swift SDK/Swift SDK/AppDelegate.swift | 7 +- .../Base.lproj/LaunchScreen.storyboard | 5 +- .../Swift SDK/Base.lproj/Main.storyboard | 56 ++- ...ft => DeeplinkExampleViewController.swift} | 42 +- .../ExampleTableViewController.swift | 119 +++++ .../ImplicitGrantExampleViewController.swift | 90 ++++ examples/Swift SDK/Swift SDK/Info.plist | 6 + ...deRequestWidgetExampleViewController.swift | 132 ++++++ .../hi-IN.lproj/LaunchScreen.strings | 1 + .../Swift SDK/hi-IN.lproj/Main.strings | 3 + .../zh-Hans.lproj/LaunchScreen.strings | 1 + .../Swift SDK/zh-Hans.lproj/Main.strings | 3 + .../zh-Hant.lproj/LaunchScreen.strings | 1 + .../Swift SDK/zh-Hant.lproj/Main.strings | 3 + source/Podfile | 6 + source/Podfile.lock | 10 + source/UberRides.xcodeproj/project.pbxproj | 377 ++++++++++++++- .../xcshareddata/xcschemes/UberRides.xcscheme | 3 +- .../contents.xcworkspacedata | 10 + source/UberRides/Configuration.swift | 251 ++++++++++ ...swift => DeeplinkRequestingBehavior.swift} | 55 +-- source/UberRides/EndpointsManager.swift | 184 ++++++++ source/UberRides/Info.plist | 2 +- .../ModalRideRequestViewController.swift | 90 ++++ source/UberRides/ModalViewController.swift | 207 ++++++++ source/UberRides/Model/RideParameters.swift | 284 +++++++++++ source/UberRides/Model/RidesError.swift | 247 ++++++++++ source/UberRides/Model/RidesScope.swift | 210 +++++++++ source/UberRides/ModelMapper.swift | 45 ++ source/UberRides/OAuth/AccessToken.swift | 111 +++++ .../UberRides/OAuth/AccessTokenFactory.swift | 82 ++++ source/UberRides/OAuth/KeychainWrapper.swift | 135 ++++++ source/UberRides/OAuth/LoginManager.swift | 191 ++++++++ source/UberRides/OAuth/LoginView.swift | 219 +++++++++ .../UberRides/OAuth/OAuthViewController.swift | 109 +++++ source/UberRides/Request.swift | 81 ++++ source/UberRides/RequestButton.swift | 250 ---------- source/UberRides/RequestDeeplink.swift | 259 ++-------- source/UberRides/RequestURLBuilder.swift | 110 +++++ .../Badge.imageset/Contents.json | 6 +- .../Badge.imageset/ios_rides-api_badge.png | Bin 0 -> 608 bytes .../Badge.imageset/ios_rides-api_badge@2x.png | Bin 0 -> 1211 bytes .../Badge.imageset/ios_rides-api_badge@3x.png | Bin 0 -> 1631 bytes .../Badge.imageset/uber_badge_24.png | Bin 785 -> 0 bytes .../Badge.imageset/uber_badge_24@2x.png | Bin 1522 -> 0 bytes .../Badge.imageset/uber_badge_24@3x.png | Bin 2147 -> 0 bytes .../Surge-BlackOutline.imageset/Contents.json | 12 + .../surgeIconDark.pdf | Bin 0 -> 4146 bytes .../Surge-WhiteOutline.imageset/Contents.json | 12 + .../Surge-WhiteOutline.imageset/page1.pdf | Bin 0 -> 4159 bytes .../Contents.json | 23 + .../back_arrow@1x.png | Bin 0 -> 203 bytes .../back_arrow@2x.png | Bin 0 -> 260 bytes .../back_arrow@3x.png | Bin 0 -> 387 bytes .../ic_logo_white.imageset/Contents.json | 23 + .../uber_logotype_white@1x.png | Bin 0 -> 1045 bytes .../uber_logotype_white@2x.png | Bin 0 -> 1897 bytes .../uber_logotype_white@3x.png | Bin 0 -> 2813 bytes .../Resources/en.lproj/Localizable.strings | 20 +- .../Resources/hi-In.lproj/Localizable.strings | 20 + .../zh-Hans.lproj/Localizable.strings | 20 +- .../zh-Hant.lproj/Localizable.strings | 20 +- source/UberRides/RideRequestButton.swift | 177 +++++++ source/UberRides/RideRequestView.swift | 250 ++++++++++ .../UberRides/RideRequestViewController.swift | 261 +++++++++++ .../RideRequestViewErrorFactory.swift | 66 +++ .../UberRides/RideRequestViewErrorType.swift | 51 ++ .../RideRequestViewRequestingBehavior.swift | 95 ++++ source/UberRides/RideRequestingProtocol.swift | 36 ++ source/UberRides/RidesClient.swift | 122 ++++- source/UberRides/RidesUtil.swift | 260 +++++++++++ source/UberRides/TokenManager.swift | 186 ++++++++ source/UberRides/UberButton.swift | 112 +++++ .../AccessTokenFactoryTests.swift | 212 +++++++++ .../UberRidesTests/ConfigurationTests.swift | 196 ++++++++ source/UberRidesTests/Info.plist | 2 +- .../LocalizationUtilTests.swift | 73 +++ source/UberRidesTests/LoginManagerTests.swift | 78 ++++ .../ModalViewControllerTests.swift | 106 +++++ source/UberRidesTests/OAuthTests.swift | 317 +++++++++++++ .../UberRidesTests/OauthEndpointTests.swift | 150 ++++++ .../UberRidesTests/RequestButtonTests.swift | 155 ++++++ .../UberRidesTests/RequestDeeplinkTests.swift | 234 +++++----- .../UberRidesTests/RideParametersTest.swift | 158 +++++++ .../RideRequestViewControllerTests.swift | 406 ++++++++++++++++ .../RideRequestViewErrorFactoryTests.swift | 59 +++ ...deRequestViewRequestingBehaviorTests.swift | 105 +++++ .../UberRidesTests/RideRequestViewTests.swift | 193 ++++++++ ...RidesAuthenticationErrorFactoryTests.swift | 74 +++ source/UberRidesTests/RidesClientTests.swift | 225 +++++++++ source/UberRidesTests/RidesMocks.swift | 185 ++++++++ .../RidesScopeExtensionsTests.swift | 131 ++++++ .../RidesScopeFactoryTests.swift | 65 +++ source/UberRidesTests/TokenManagerTests.swift | 201 ++++++++ .../UberRidesTests/WidgetsEndpointTests.swift | 126 +++++ source/UberRidesTests/testInfo.plist | 10 + 126 files changed, 10174 insertions(+), 904 deletions(-) create mode 100644 examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/en.lproj/UberRides.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hans.lproj/UberRides.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hant.lproj/UberRides.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.h create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.m create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.h create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.m rename examples/Obj-C SDK/Obj-C SDK/{ViewController.h => UBSDKExampleTableViewController.h} (92%) create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.m create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.h create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.m create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKLocalization.h create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.h create mode 100644 examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.m delete mode 100644 examples/Obj-C SDK/Obj-C SDK/ViewController.m create mode 100644 examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/LaunchScreen.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/Main.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/LaunchScreen.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/Main.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/LaunchScreen.strings create mode 100644 examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/Main.strings rename examples/Swift SDK/Swift SDK/{ViewController.swift => DeeplinkExampleViewController.swift} (74%) create mode 100644 examples/Swift SDK/Swift SDK/ExampleTableViewController.swift create mode 100644 examples/Swift SDK/Swift SDK/ImplicitGrantExampleViewController.swift create mode 100644 examples/Swift SDK/Swift SDK/RideRequestWidgetExampleViewController.swift create mode 100644 examples/Swift SDK/Swift SDK/hi-IN.lproj/LaunchScreen.strings create mode 100644 examples/Swift SDK/Swift SDK/hi-IN.lproj/Main.strings create mode 100644 examples/Swift SDK/Swift SDK/zh-Hans.lproj/LaunchScreen.strings create mode 100644 examples/Swift SDK/Swift SDK/zh-Hans.lproj/Main.strings create mode 100644 examples/Swift SDK/Swift SDK/zh-Hant.lproj/LaunchScreen.strings create mode 100644 examples/Swift SDK/Swift SDK/zh-Hant.lproj/Main.strings create mode 100644 source/Podfile create mode 100644 source/Podfile.lock create mode 100644 source/UberRides.xcworkspace/contents.xcworkspacedata create mode 100644 source/UberRides/Configuration.swift rename source/UberRides/{ColorUtil.swift => DeeplinkRequestingBehavior.swift} (54%) create mode 100644 source/UberRides/EndpointsManager.swift create mode 100644 source/UberRides/ModalRideRequestViewController.swift create mode 100644 source/UberRides/ModalViewController.swift create mode 100644 source/UberRides/Model/RideParameters.swift create mode 100644 source/UberRides/Model/RidesError.swift create mode 100644 source/UberRides/Model/RidesScope.swift create mode 100644 source/UberRides/ModelMapper.swift create mode 100644 source/UberRides/OAuth/AccessToken.swift create mode 100644 source/UberRides/OAuth/AccessTokenFactory.swift create mode 100644 source/UberRides/OAuth/KeychainWrapper.swift create mode 100644 source/UberRides/OAuth/LoginManager.swift create mode 100644 source/UberRides/OAuth/LoginView.swift create mode 100644 source/UberRides/OAuth/OAuthViewController.swift create mode 100644 source/UberRides/Request.swift delete mode 100644 source/UberRides/RequestButton.swift create mode 100644 source/UberRides/RequestURLBuilder.swift create mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/ios_rides-api_badge.png create mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/ios_rides-api_badge@2x.png create mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/ios_rides-api_badge@3x.png delete mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24.png delete mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@2x.png delete mode 100644 source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@3x.png create mode 100644 source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/Contents.json create mode 100644 source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/surgeIconDark.pdf create mode 100644 source/UberRides/Resources/Media.xcassets/Surge-WhiteOutline.imageset/Contents.json create mode 100644 source/UberRides/Resources/Media.xcassets/Surge-WhiteOutline.imageset/page1.pdf create mode 100644 source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/Contents.json create mode 100644 source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@1x.png create mode 100644 source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@2x.png create mode 100644 source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@3x.png create mode 100644 source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/Contents.json create mode 100644 source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@1x.png create mode 100644 source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@2x.png create mode 100644 source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@3x.png create mode 100644 source/UberRides/Resources/hi-In.lproj/Localizable.strings create mode 100644 source/UberRides/RideRequestButton.swift create mode 100644 source/UberRides/RideRequestView.swift create mode 100644 source/UberRides/RideRequestViewController.swift create mode 100644 source/UberRides/RideRequestViewErrorFactory.swift create mode 100644 source/UberRides/RideRequestViewErrorType.swift create mode 100644 source/UberRides/RideRequestViewRequestingBehavior.swift create mode 100644 source/UberRides/RideRequestingProtocol.swift create mode 100644 source/UberRides/RidesUtil.swift create mode 100644 source/UberRides/TokenManager.swift create mode 100644 source/UberRides/UberButton.swift create mode 100644 source/UberRidesTests/AccessTokenFactoryTests.swift create mode 100644 source/UberRidesTests/ConfigurationTests.swift create mode 100644 source/UberRidesTests/LocalizationUtilTests.swift create mode 100644 source/UberRidesTests/LoginManagerTests.swift create mode 100644 source/UberRidesTests/ModalViewControllerTests.swift create mode 100644 source/UberRidesTests/OAuthTests.swift create mode 100644 source/UberRidesTests/OauthEndpointTests.swift create mode 100644 source/UberRidesTests/RequestButtonTests.swift create mode 100644 source/UberRidesTests/RideParametersTest.swift create mode 100644 source/UberRidesTests/RideRequestViewControllerTests.swift create mode 100644 source/UberRidesTests/RideRequestViewErrorFactoryTests.swift create mode 100644 source/UberRidesTests/RideRequestViewRequestingBehaviorTests.swift create mode 100644 source/UberRidesTests/RideRequestViewTests.swift create mode 100644 source/UberRidesTests/RidesAuthenticationErrorFactoryTests.swift create mode 100644 source/UberRidesTests/RidesClientTests.swift create mode 100644 source/UberRidesTests/RidesMocks.swift create mode 100644 source/UberRidesTests/RidesScopeExtensionsTests.swift create mode 100644 source/UberRidesTests/RidesScopeFactoryTests.swift create mode 100644 source/UberRidesTests/TokenManagerTests.swift create mode 100644 source/UberRidesTests/WidgetsEndpointTests.swift create mode 100644 source/UberRidesTests/testInfo.plist diff --git a/CHANGELOG.md b/CHANGELOG.md index 214b1f50..f914a1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Change Log +## [0.4.0] 2016-04-11 +### Added + +#### Configuration + +Handles SDK Configuration, including `ClientID` and `RedirectURI`. Values are pulled from your app's `Info.plist` + +#### LoginManager / Implicit Grant flow +Added implicit grant (i.e. token) login authorization flow for non-privileged scopes (profile, history, places, ride_widgets) + +- Added `OAuthViewController` & `LoginView` +- Added `LoginManager` to handle login flow +- Added `TokenManager` to handle access token management + +#### Ride Request Widget +Introducing the **Ride Request Widget**. Allows for an end to end Uber experience without leaving your application. + +- Requires the `ride_widgets` scope +- Base view is `RideRequestView` +- `RideRequestViewController` & `ModalRideRequestViewController` for easy implementation that handles presenting login to the user + +#### RideParameters +All ride requests are now specified by a `RideParameters` object. It allows you to set pickup/dropoff location, nickname, formatted address & product ID. Use the `RideParametersBuilder` to easily create `RideParameters` objects + +#### RideRequestButton Updates +`RequestButton` has been renamed to `RideRequestButton` + +`RideRequestButton` now works by using a `RideParameters` and a `RequestingBehavior`. The `RideParameters` defines the parameters for the ride and the `requestingBehavior` defines how to execute it. +Currently available `requestingBehaviors` are: + +- `DeeplinkRequestingBehavior` + - Deeplinks into the Uber app to request a ride +- `RideRequestViewRequestingBehavior` + - Presents the **Ride Request Widget** modally in your app to provide and end to end Uber experience + +### Fixed + +- [Issue #13](https://github.com/uber/rides-ios-sdk/issues/13) Using CLLocation for ride parameters +- [Issue #16](https://github.com/uber/rides-ios-sdk/issues/16) Added new Uber logo for `RideRequestButton` + +### Breaking +- `ClientID` must now be set in your app's `Info.plist` under the `UberClientID` key +- `RequestButton` --> `RideRequestButton` + - Removed `init(colorStyle: RequestButtonColorStyle)` use `init(rideParameters: RideParameters, requestingBehavior: RideRequesting)` + - Removed all setting parameter methods (`setPickupLocation()`, `setDropoffLocation()`, ect) use a `RideParameters` object instead + - Removed `RequestButtonError`, only used to indicate no `ClientID` which is now handled by `Configuration` + - `uberButtonTapped()` no longer public +- `RequestDeeplink` + - Removed `init(withClientID: String, fromSource: SourceParameter)` use `init(rideParameters: RideParameters)` instead + - Removed all setting parameter methods (`setPickupLocation()`, `setDropoffLocation`, ect) use a `RideParameters` object instead + - `SourceParameter` removed +- Removed Carthage support + + ## [0.3.1] 2016-02-11 ### Fixed - [Issue #12](https://github.com/uber/rides-ios-sdk/issues/12) where there's a "&" missing in the user-agent query item. @@ -20,4 +74,4 @@ - [Issue #6](https://github.com/uber/rides-ios-sdk/issues/6) where custom pick-up location is ignored and reset to current location. ## [0.1.1] 2015-12-02 -- Initial version. +- Initial version. \ No newline at end of file diff --git a/README.md b/README.md index fe3712fd..6aadc11b 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Before using this SDK, register your application on the [Uber Developer Site](ht The Uber Rides iOS SDK is a CocoaPod written in Swift. [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: ```bash -$ sudo gem install cocoapods +$ gem install cocoapods ``` To integrate Uber Rides into your Xcode project, navigate to the directory that contains your project and create a new **Podfile** with `pod init` or open an existing one, then add `pod 'UberRides'` to the main loop. If you are using the Swift SDK, make sure to add the line `use_frameworks!`. @@ -35,38 +35,7 @@ Then, run the following command to install the dependency: $ pod install ``` -For Objective-C projects, set the **Embedded Content Contains Swift Code** flag in your project to **Yes** (found under **Build Options** in the **Build Phases** tab). - -### Carthage - -Uber Rides is also available through [Carthage](https://github.com/Carthage/Carthage), a decentralized dependency manager that builds dependencies and provides you with binary frameworks, giving you full control over your project structure and setup. - -Install Carthage with [Homebrew](http://brew.sh/): - -```bash -$ brew update -$ brew install carthage -``` - -To integrate Uber Rides into your Xcode project, navigate to the directory that contains your project and create a new **Cartfile** with `touch Cartfile` or open an existing one, then add the following line: - -``` -github "uber/rides-ios-sdk" -``` - -Build the framework: - -```bash -$ carthage update -``` - -Now add the `UberRides.framework` (in `Carthage/Build/iOS`) as a Linked Framework in Xcode (See the **Linked Frameworks and Libraries** section under the **General** tab of your project target). - -Then, on your application targets' **Build Phases** tab, click the '+' button and choose **New Run Script Phase**. Add the run script `/usr/local/bin/carthage copy-frameworks` and add the path to the UberRides framework under **Input Files**: `$(SRCROOT)/Carthage/Build/iOS/UberRides.framework`. - -![Screenshot](/img/carthage_script.png?raw=true "Carthage Run Script Screenshot") - -For Objective-C projects, set the **Embedded Content Contains Swift Code** flag to **Yes** (found under **Build Options** in the **Build Phases** tab). +For Objective-C projects, set the **Embedded Content Contains Swift Code** flag in your project to **Yes** (found under **Build Options** in the **Build Settings** tab). ### Manually Add Subprojects @@ -95,85 +64,427 @@ If you are compiling on iOS SDK 9.0, you will need to modify your application’ This will allow the Uber iOS integration to properly identify and switch to the installed Uber application. If you are not on iOS SDK 9.0, then you are allowed to have up to 50 unique app schemes and do not need to modify your app’s `plist`. -## Example Usage +## SDK Configuration +In order for the SDK to function correctly, you need to add some information about your app. Locate the **Info.plist** file for your application. Usually found in the **Supporting Files** folder. Right-click this file and select **Open As > Source Code** + +Add the following code snippet, replacing the placeholders with your app’s information from the developer dashboard. + +``` +UberClientID +[ClientID] +UberCallbackURI +[redirect URL] +``` -First, configure `RidesClient` with your registered Client ID. The end of `application:didFinishLaunchingWithOptions:` in your `AppDelegate` is a good place to do this: +Additionally, the SDK provides a static Configuration class to further customize your settings. Inside of `application:didFinishLaunchingWithOptions:` in your `AppDelegate` is a good place to do this: -```swift +``` +// Don’t forget to import UberRides // Swift func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - RidesClient.sharedInstance.configureClientID("YOUR_CLIENT_ID") - return true -} + // China based apps should specify the region + Configuration.setRegion(.China) + // If true, all requests will hit the sandbox, useful for testing + Configuration.setSandboxEnabled(true) + // Complete other setup + return true + } ``` -```objective-c +``` // Objective-C - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [[RidesClient sharedInstance] configureClientID:@"YOUR_CLIENT_ID"]; + // China based apps should specify the region + [UBSDKConfiguration setRegion:RegionChina]; + // If true, all requests will hit the sandbox, useful for testing + [UBSDKConfiguration setSandboxEnabled:YES]; + // Complete other setup return YES; } ``` +## Location Services +Getting the user to authorize location services can be done with Apple’s CoreLocation framework. The Uber Rides SDK checks the value of `locationServicesEnabled()` in `CLLocationManager`, which must be true to be able to retrieve the user’s current location. + +## Example Usage +### Quick Integration +#### Ride Request Widget + +The Uber Rides SDK provides a simple way to add the Ride Request Widget in only a few lines of code via the `RideRequestButton`. You simply need to provide a `RideRequesting` object and an optional `RideParameters` object. + +``` +// Swift +// Pass in a UIViewController to modally present the Ride Request Widget over +let behavior = RideRequestViewRequestingBehavior(presentingViewController: self) +// Optional, defaults to using the user’s current location for pickup +let location = CLLocation(latitude: 37.787654, longitude: -122.402760) +let parameters = RideParametersBuilder().setPickupLocation(location).build() +let button = RideRequestButton(rideParameters: parameters, requestingBehavior: behavior) +self.view.addSubview(button) +``` + +``` +// Objective-C +// Pass in a UIViewController to modally present the Ride Request Widget over +id behavior = [[UBSDKRideRequestViewRequestingBehavior alloc] initWithPresentingViewController: self]; +// Optional, defaults to using the user’s current location for pickup +CLLocation *location = [[CLLocation alloc] initWithLatitude: 37.787654 longitude: -122.402760]; +UBSDKRideParametersBuilder *builder = [[UBSDKRideParametersBuilder alloc] init]; +[builder setPickupLocation:location]; +UBSDKRideParameters *parameters = [builder build]; +UBSDKRideRequestButton *button = [[UBSDKRideRequestButton alloc] initWithRideParameters: parameters requestingBehavior: behavior]; +[self.view addSubview:button]; +``` + +That’s it! When a user taps the button, a **RideRequestViewController** will be modally presented, containing a **RideRequestView** prefilled with the information provided from the **RideParameters** object. If they aren’t signed in, the modal will display a login page and automatically continue to the Ride Request Widget once they sign in. + +Basic error handling is provided by default, but can be overwritten by specifying a **RideRequestViewControllerDelegate**. + +``` +// Swift +extension your_class : RideRequestViewControllerDelegate { + func rideRequestViewController(rideRequestViewController: RideRequestViewController, didReceiveError error: NSError) { + let errorType = RideRequestViewErrorType(rawValue: error.code) ?? .Unknown + // Handle error here + switch errorType { + case .AccessTokenMissing: + // No AccessToken saved + case .AccessTokenExpired: + // AccessToken expired / invalid + case .Unknown: + // Other error + } + } +} + +// Use your_class as the delegate +let behavior = RideRequestViewRequestingBehavior(presentingViewController: self) +let delegate = your_class() +behavior.modalRideRequestViewController.rideRequestViewController.delegate = delegate +// Create the button same as before +let button = RideRequestButton(rideParameters: parameters, requestingBehavior: behavior) +``` + +``` +// Objective-C +// Need to implement the UBSDKRideRequestViewControllerDelegate +@interface your_class () + +@end + +// Implement the delegate methods +- (void)rideRequestViewController:(RideRequestViewController *)rideRequestViewController didReceiveError:(NSError *)error { + // Handle error here + RideRequestViewErrorType errorType = (RideRequestViewErrorType)error.code; + + switch (errorType) { + case RideRequestViewErrorTypeAccessTokenExpired: + // No AccessToken saved + break; + case RideRequestViewErrorTypeAccessTokenMissing: + // AccessToken expired / invalid + break; + case RideRequestViewErrorTypeUnknown: + // Other error + break; + default: + break; + } +} + +// Assign the delegate when you initialize your UBSDKRideRequestViewRequestingBehavior +UBSDKRideRequestViewRequestingBehavior *requestBehavior = [[UBSDKRideRequestViewRequestingBehavior alloc] initWithPresentingViewController:self]; +// Subscribe as the delegete +requestBehavior.modalRideRequestViewController.delegate = self; +// Create the button same as before +UBSDKRideRequestButton *button = [[UBSDKRideRequestButton alloc] initWithRideParameters: parameters requestingBehavior: requestBehavior]; +[self.view addSubview:button]; +``` + +#### Deep linking + Import the library into your project, and add a Ride Request Button to your view like you would any other UIView: -```swift +``` // Swift import UberRides -let button = RequestButton() + +let button = RideRequestButton() view.addSubview(button) ``` -```objective-c +``` // Objective-C @import UberRides; -RequestButton *button = [[RequestButton alloc] init]; + +UBSDKRideRequestButton *button = [[UBSDKRideRequestButton alloc] init]; [view addSubview:button]; ``` This will create a request button with default behavior, with the pickup pin set to the user’s current location. The user will need to select a product and input additional information when they are switched over to the Uber application. -### Adding Parameters +### Adding Parameters with RideParameters + +The SDK provides an simple object for defining your ride requests. The `RideParameters` object lets you specify pickup location, dropoff location, product ID, and more. Creating `RideParameters` is easy using the `RideParametersBuilder` object. + +``` +// Swift +let builder = RideParametersBuilder() +let pickupLocation = CLLocation(latitude: 37.787654, longitude: -122.402760) +let dropoffLocation = CLLocation(latitude: 37.775200, longitude: -122.417587) +// You can chain builder function calls +builder.setPickupLocation(pickupLocation).setDropoffLocation(dropoffLocation) +let rideParameters = builder.build() +``` + +``` +// Objective-C +UBSDKRideParametersBuilder *builder = [[UBSDKRideParametersBuilder alloc] init]; +CLLocation *pickupLocation = [[CLLocation alloc] initWithLatitude:37.787654 longitude:-122.402760]; +CLLocation *dropoffLocation = [[CLLocation alloc] initWithLatitude:37.775200 longitude:-122.417587]; +// You can chain builder function calls +[[builder setPickupLocation:pickupLocation] setDropoffLocation:dropoffLocation]; +UBSDKRideParameters *rideParameters = [builder build]; +``` + +You can also have the SDK determine the user’s current location (you must handle getting location permission beforehand, however) + +``` +// Swift +// If no pickup location is specified, the default is to use current location +let parameters = RideParametersBuilder().build() +// You can also explicitly the parameters to use current location +let builder = RideParametersBuilder() +builder.setPickupToCurrentLocation() +let parameters = builder.build() // Both 'parameters' variables are equivalent +``` + +``` +// Objective-C +// If no pickup location is specified, the default is to use current location +UBSDKRideParameters *parameters = [[[UBSDKRideParametersBuilder alloc] init] build]; +// You can also explicitly the parameters to use current location +UBSDKRideParametersBuilder *builder = [[UBSDKRideParametersBuilder alloc] init]; +[builder setPickupToCurrentLocation]; +UBSDKRideParameters *parameters = [builder build]; // Both 'parameters' variables are equivalent +``` + +We suggest passing additional parameters to make the Uber experience even more seamless for your users. For example, dropoff location parameters can be used to automatically pass the user’s destination information over to the driver. With all the necessary parameters set, pressing the button will seamlessly prompt a ride request confirmation screen. + +### RideRequestButton Color Style + +The default color has a black background with white text. You can update the button to have a white background with black text by setting the color style + +``` +// Swift +let button = RideRequestButton() // Black Background, White Text +button.colorStyle = .White // White Background, Black Text +``` + +``` +// Objective-C +UBSDKRideRequestButton *button = [[UBSDKRideRequestButton alloc] init]; // Black Background, White Text +[button setColorStyle:RequestButtonColorStyleWhite]; // White Background, Black Text +``` + +## Custom Integration +If you want to provide a more custom experience in your app, there are a few classes to familiarize yourself with. Read the sections below and you’ll be requesting rides in no time! + +### Implicit Grant Authorization +Before you can request any rides, you need to get an `AccessToken`. The Uber Rides SDK provides the `LoginManager` class for this task. Simply instantiate an instance use its login method to present the login screen to the user. + +``` +// Swift +let loginManager = LoginManager() +loginManager.login(requestedScopes:[.RideWidgets], presentingViewController: self, completion: { accessToken, error in + // Completion block. If accessToken is non-nil, you’re good to go + // Otherwise, error.code corresponds to the RidesAuthenticationErrorType that occured +}) +``` + +``` +// Objective-C +UBSDKLoginManager *loginManager = [[UBSDKLoginManager alloc] init]; +[loginManager loginWithRequestedScopes:@[ UBSDKRidesScope.RideWidgets ] presentingViewController: self completion: ^(UBSDKAccessToken * _Nullable accessToken, NSError * _Nullable error) { + // Completion block. If accessToken is non-nil, you're good to go + // Otherwise, error.code corresponds to the RidesAuthenticationErrorType that occured + }]; +``` + +The only required scope for the Ride Request Widget is the `RideWidgets` scope, but you can pass in any other scopes that you’d like access to. + +The SDK presents a web view controller where the user logs into their Uber account, or creates an account, and authorizes the requested scopes, retrieving an access token which is automatically saved to the keychain. Once the SDK has the access token, the embedded ride request control is ready to be used! + +### Custom Authorization / TokenManager +If your app allows users to authorize via your own customized logic, you will need to create an `AccessToken` manually and save it in the keychain using the `TokenManager`. + +``` +// Swift +let accessTokenString = "access_token_string" +let token = AccessToken(tokenString: accessTokenString) +if TokenManager.saveToken(token) { + // Success +} else { + // Unable to save +} +``` + +``` +// Objective-C +NSString *accessTokenString = @"access_token_string"; +UBSDKAccessToken *token = [[UBSDKAccessToken alloc] initWithTokenString: accessTokenString]; +if ([UBSDKTokenManager saveToken: token]) { + // Success +} else { + // Unable to save +} +``` -We suggest passing additional parameters to make the Uber experience even more seamless for your users. For example, dropoff location parameters can be used to automatically pass the user’s destination information over to the driver: +The `TokenManager` can also be used to fetch and delete `AccessToken`s -```swift +``` // Swift -button.setProductID("abc123-productID") -button.setPickupLocation(latitude: 37.770, longitude: -122.466, nickname: "California Academy of Sciences") -button.setDropoffLocation(latitude: 37.791, longitude: -122.405, nickname: "Pier 39") +TokenManger.fetchToken() +TokenManager.deleteToken() ``` -```objective-c +``` // Objective-C -[button setProductID:@"abc123-productID"]; -[button setPickupLocationWithLatitude:37.770 longitude:-122.466 nickname:@"California Academy of Sciences" address:nil]; -[button setDropoffLocationWithLatitude:37.791 longitude:-122.405 nickname:@"Pier 39" address:nil]; +[UBSDKTokenManager fetchToken]; +[UBSDKTokenManager deleteToken]; +``` + +### RideRequestView +The `RideRequestView` is like any other view you’d add to your app. Create a new instance using a `RideParameters` object and add it to your app wherever you like. + +``` +// Swift +// Example of setting up the RideRequestView +let location = CLLocation(latitude: 37.787654, longitude: -122.402760) +let parameters = RideParametersBuilder().setPickupLocation(location).build() +let rideRequestView = RideRequestView(rideParameters: parameters, frame: self.view.bounds) +self.view.addSubview(rideRequestView) ``` -With all the necessary parameters set, pressing the button will seamlessly prompt a ride request confirmation screen. +``` +// Objective-C +// Example of setting up the UBSDKRideRequestView +CLLocation *location = [[CLLocation alloc] initWithLatitude: 37.787654 longitude: -122.402760]; +UBSDKRideParametersBuilder *builder = [[UBSDKRideParametersBuilder alloc] init]; +UBSDKRideParameters *parameters = [[builder setPickupLocation:location] build]; +UBSDKRideRequestView *rideRequestView = [[UBSDKRideRequestView alloc] initWithRideParameters:parameters frame:self.view.bounds]; +[self.view addSubview:rideRequestView]; +``` -### Color Style +That’s it! When you’re ready to show the control, call the load() function. This function will also poll for the user’s current location, if set in your `RideParameters`, before loading the widget. -The default color has a black background with white text. You can intialize the button to have a white background with black text by passing an additional parameter. +You can also optionally specify a `RideRequestViewDelegate` to handle errors loading the widget. -```swift +``` // Swift -let blackButton = RequestButton() -let whiteButton = RequestButton(.white) +extension your_class : RideRequestViewDelegate { + func rideRequestView(rideRequestView: RideRequestView, didReceiveError error: NSError) { + let errorType = RideRequestViewErrorType(rawValue: error.code) ?? .Unknown + // Handle error here + switch errorType { + case .AccessTokenMissing: + // No AccessToken saved + case .AccessTokenExpired: + // AccessToken expired / invalid + case .Unknown: + // Other error + } + } +} ``` +``` +// Objective-C +// Delegate methods +- (void)rideRequestView:(UBSDKRideRequestView *)rideRequestView didReceiveError:(NSError *)error { + // Handle error here + RideRequestViewErrorType errorType = (RideRequestViewErrorType)error.code; + + switch (errorType) { + case RideRequestViewErrorTypeAccessTokenExpired: + // No AccessToken saved + break; + case RideRequestViewErrorTypeAccessTokenMissing: + // AccessToken expired / invalid + break; + case RideRequestViewErrorTypeUnknown: + // Other error + break; + default: + break; + } +} +``` + +### RideRequestViewController +A `RideRequestViewController` is simply a `UIViewController` that contains a fullscreen `RideRequestView`. It also handles logging in non-authenticated users for you. Create a new instance with your desired `RideParameters` and `LoginManager` (used to log in, if necessary). -```objective-c +``` +// Swift +// Setting up a RideRequestViewController +let parameters = RideParametersBuilder().build() +let loginManager = LoginManager() +let rideRequestViewController = RideRequestViewController(rideParameters: parameters, loginManager: loginManager) +``` +``` // Objective-C -RequestButton *blackButton = [[RequestButton alloc] init]; -RequestButton *whiteButton = [[RequestButton alloc] initWithColorStyle:RequestButtonColorStyleWhite]; +// Setting up a RideRequestViewController +UBSDKRideParameters *parameters = [[[UBSDKRideParametersBuilder alloc] init] build]; +UBSDKLoginManager *loginManager = [[UBSDKLoginManager alloc] init]; +UBSDKRideRequestViewController *rideRequestViewController = [[UBSDKRideRequestViewController alloc] initWithRideParameters:parameters loginManager:loginManager]; +``` + +You can also optionally specify a RideRequestViewControllerDelegate to handle potential errors passed from the wrapped RideRequestView + +``` +// Swift +extension your_class : RideRequestViewControllerDelegate { + func rideRequestViewController(rideRequestViewController: RideRequestViewController, didReceiveError error: NSError) { + let errorType = RideRequestViewErrorType(rawValue: error.code) ?? .Unknown + // Handle error here + switch errorType { + case .AccessTokenMissing: + // No AccessToken saved + case .AccessTokenExpired: + // AccessToken expired / invalid + case .Unknown: + // Other error + } + } +} +``` +``` +// Objective-C +// Implement the delegate methods +- (void)rideRequestViewController:(UBSDKRideRequestViewController *)rideRequestViewController didReceiveError:(NSError *)error { + // Handle error here + RideRequestViewErrorType errorType = (RideRequestViewErrorType)error.code; + + switch (errorType) { + case RideRequestViewErrorTypeAccessTokenExpired: + // No AccessToken saved + break; + case RideRequestViewErrorTypeAccessTokenMissing: + // AccessToken expired / invalid + break; + case RideRequestViewErrorTypeUnknown: + // Other error + break; + default: + break; + } +} ``` ## Example Apps Example apps can be found in the `examples` folder. To run it, browse to the `examples` directory, run `pod install`, then open `SwiftSDK.xcworkspace` or `ObjcSDK.xcworkspace` in Xcode and run it. -Don’t forget to configure `RidesClient` with your Client ID in your `AppDelegate` file. +Don’t forget to set `UberClientID` with your Client ID in your `Info.plist` file.

Example App Screenshot diff --git a/UberRides.podspec b/UberRides.podspec index efd97243..0a605df5 100644 --- a/UberRides.podspec +++ b/UberRides.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "UberRides" - s.version = "0.3.1" + s.version = "0.4.0" s.summary = "The Official Uber Rides iOS SDK." s.description = <<-DESC This Swift library allows you to integrate Uber into your iOS app. It is designed to make it quick and easy to add a 'Request a Ride' button in your application, seamlessly connecting your users with Uber. @@ -9,11 +9,12 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/uber/rides-ios-sdk" s.screenshots = "https://raw.githubusercontent.com/uber/rides-ios-sdk/master/img/example_app.png" s.license = { :type => "MIT", :file => "LICENSE" } - s.authors = { "Christine Kim" => "christinek@uber.com", "Farwa Naqi" => "farwa@uber.com" } + s.authors = { "Christine Kim" => "christinek@uber.com", "Farwa Naqi" => "farwa@uber.com", "John Brophy" => "jbrophy@uber.com" } s.platform = :ios, "8.0" s.source = { :git => "https://github.com/uber/rides-ios-sdk.git", :tag => 'v' + s.version.to_s } - s.source_files = "source/UberRides/*.swift" + s.source_files = ["source/UberRides/*.swift", "source/UberRides/Model/*.swift", "source/UberRides/OAuth/*.swift"] s.resource = "source/UberRides/Resources/**" s.requires_arc = true + s.dependency 'ObjectMapper', '~> 1.1.0' end diff --git a/examples/Obj-C SDK/Obj-C SDK.xcodeproj/project.pbxproj b/examples/Obj-C SDK/Obj-C SDK.xcodeproj/project.pbxproj index c63448d0..cbbd9618 100644 --- a/examples/Obj-C SDK/Obj-C SDK.xcodeproj/project.pbxproj +++ b/examples/Obj-C SDK/Obj-C SDK.xcodeproj/project.pbxproj @@ -7,14 +7,20 @@ objects = { /* Begin PBXBuildFile section */ + 4B084CB5DB2D61553B75B29F /* Pods_Obj_C_SDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4731F65F1A889A3400FFF8CB /* Pods_Obj_C_SDK.framework */; }; AC0404F21BFB82CC00AC1501 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0404F11BFB82CC00AC1501 /* main.m */; }; AC0404F51BFB82CC00AC1501 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0404F41BFB82CC00AC1501 /* AppDelegate.m */; }; - AC0404F81BFB82CC00AC1501 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0404F71BFB82CC00AC1501 /* ViewController.m */; }; + AC0404F81BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0404F71BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.m */; }; AC0404FB1BFB82CC00AC1501 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC0404F91BFB82CC00AC1501 /* Main.storyboard */; }; AC0404FD1BFB82CC00AC1501 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC0404FC1BFB82CC00AC1501 /* Assets.xcassets */; }; AC0405001BFB82CC00AC1501 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC0404FE1BFB82CC00AC1501 /* LaunchScreen.storyboard */; }; AC04050B1BFB82CC00AC1501 /* Obj_C_SDKTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC04050A1BFB82CC00AC1501 /* Obj_C_SDKTests.m */; }; AC0405161BFB82CC00AC1501 /* Obj_C_SDKUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0405151BFB82CC00AC1501 /* Obj_C_SDKUITests.m */; }; + DC44CE531CB7256800E09AAC /* UBSDKExampleTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DC44CE521CB7256800E09AAC /* UBSDKExampleTableViewCell.m */; }; + DC44CE561CB72CB400E09AAC /* Localizations.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DC44CE551CB72CB400E09AAC /* Localizations.bundle */; }; + DCEC0CAA1CB5D7780086C6D7 /* UBSDKExampleTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DCEC0CA91CB5D7780086C6D7 /* UBSDKExampleTableViewController.m */; }; + DCEC0CAD1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DCEC0CAC1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.m */; }; + DCEC0CB01CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DCEC0CAF1CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -48,12 +54,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 37530499F1568B296FCB29A9 /* Pods-Obj-C SDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Obj-C SDK.release.xcconfig"; path = "Pods/Target Support Files/Pods-Obj-C SDK/Pods-Obj-C SDK.release.xcconfig"; sourceTree = ""; }; + 4731F65F1A889A3400FFF8CB /* Pods_Obj_C_SDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Obj_C_SDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 69772C0193161EB42F63FC6B /* Pods-Obj-C SDK.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Obj-C SDK.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Obj-C SDK/Pods-Obj-C SDK.debug.xcconfig"; sourceTree = ""; }; AC0404ED1BFB82CC00AC1501 /* Obj-C SDK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Obj-C SDK.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AC0404F11BFB82CC00AC1501 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; AC0404F31BFB82CC00AC1501 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; AC0404F41BFB82CC00AC1501 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - AC0404F61BFB82CC00AC1501 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; - AC0404F71BFB82CC00AC1501 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AC0404F61BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UBSDKDeeplinkExampleViewController.h; sourceTree = ""; }; + AC0404F71BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UBSDKDeeplinkExampleViewController.m; sourceTree = ""; }; AC0404FA1BFB82CC00AC1501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; AC0404FC1BFB82CC00AC1501 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AC0404FF1BFB82CC00AC1501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -64,6 +73,22 @@ AC0405111BFB82CC00AC1501 /* Obj-C SDKUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Obj-C SDKUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; AC0405151BFB82CC00AC1501 /* Obj_C_SDKUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Obj_C_SDKUITests.m; sourceTree = ""; }; AC0405171BFB82CC00AC1501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC44CE511CB7256800E09AAC /* UBSDKExampleTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UBSDKExampleTableViewCell.h; sourceTree = ""; }; + DC44CE521CB7256800E09AAC /* UBSDKExampleTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UBSDKExampleTableViewCell.m; sourceTree = ""; }; + DC44CE551CB72CB400E09AAC /* Localizations.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Localizations.bundle; path = Resources/Localizations.bundle; sourceTree = ""; }; + DC44CE571CB72D5000E09AAC /* UBSDKLocalization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UBSDKLocalization.h; sourceTree = ""; }; + DC8FC36D1CBDF7E700D58839 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + DC8FC36E1CBDF7E700D58839 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + DC8FC36F1CBDF7ED00D58839 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; + DC8FC3701CBDF7ED00D58839 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/LaunchScreen.strings"; sourceTree = ""; }; + DC8FC3711CBDF7FD00D58839 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "hi-IN.lproj/Main.strings"; sourceTree = ""; }; + DC8FC3721CBDF7FD00D58839 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "hi-IN.lproj/LaunchScreen.strings"; sourceTree = ""; }; + DCEC0CA81CB5D7780086C6D7 /* UBSDKExampleTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UBSDKExampleTableViewController.h; sourceTree = ""; }; + DCEC0CA91CB5D7780086C6D7 /* UBSDKExampleTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UBSDKExampleTableViewController.m; sourceTree = ""; }; + DCEC0CAB1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UBSDKRideRequestWidgetExampleViewController.h; sourceTree = ""; }; + DCEC0CAC1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UBSDKRideRequestWidgetExampleViewController.m; sourceTree = ""; }; + DCEC0CAE1CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UBSDKImplicitGrantExampleViewController.h; sourceTree = ""; }; + DCEC0CAF1CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UBSDKImplicitGrantExampleViewController.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -71,6 +96,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B084CB5DB2D61553B75B29F /* Pods_Obj_C_SDK.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,6 +117,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 28A6CFC4B4A38C240D740F18 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4731F65F1A889A3400FFF8CB /* Pods_Obj_C_SDK.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; AC0404E41BFB82CC00AC1501 = { isa = PBXGroup; children = ( @@ -98,6 +132,8 @@ AC0405091BFB82CC00AC1501 /* Obj-C SDKTests */, AC0405141BFB82CC00AC1501 /* Obj-C SDKUITests */, AC0404EE1BFB82CC00AC1501 /* Products */, + F00ED5F19A7501D03901A5C7 /* Pods */, + 28A6CFC4B4A38C240D740F18 /* Frameworks */, ); sourceTree = ""; }; @@ -114,14 +150,23 @@ AC0404EF1BFB82CC00AC1501 /* Obj-C SDK */ = { isa = PBXGroup; children = ( + DC44CE571CB72D5000E09AAC /* UBSDKLocalization.h */, + DC44CE501CB7254500E09AAC /* Views */, AC0404F31BFB82CC00AC1501 /* AppDelegate.h */, AC0404F41BFB82CC00AC1501 /* AppDelegate.m */, - AC0404F61BFB82CC00AC1501 /* ViewController.h */, - AC0404F71BFB82CC00AC1501 /* ViewController.m */, + DCEC0CA81CB5D7780086C6D7 /* UBSDKExampleTableViewController.h */, + DCEC0CA91CB5D7780086C6D7 /* UBSDKExampleTableViewController.m */, + AC0404F61BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.h */, + AC0404F71BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.m */, + DCEC0CAB1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.h */, + DCEC0CAC1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.m */, + DCEC0CAE1CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.h */, + DCEC0CAF1CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.m */, AC0404F91BFB82CC00AC1501 /* Main.storyboard */, AC0404FC1BFB82CC00AC1501 /* Assets.xcassets */, AC0404FE1BFB82CC00AC1501 /* LaunchScreen.storyboard */, AC0405011BFB82CC00AC1501 /* Info.plist */, + DC44CE541CB72C8D00E09AAC /* Resources */, AC0404F01BFB82CC00AC1501 /* Supporting Files */, ); path = "Obj-C SDK"; @@ -153,6 +198,32 @@ path = "Obj-C SDKUITests"; sourceTree = ""; }; + DC44CE501CB7254500E09AAC /* Views */ = { + isa = PBXGroup; + children = ( + DC44CE511CB7256800E09AAC /* UBSDKExampleTableViewCell.h */, + DC44CE521CB7256800E09AAC /* UBSDKExampleTableViewCell.m */, + ); + name = Views; + sourceTree = ""; + }; + DC44CE541CB72C8D00E09AAC /* Resources */ = { + isa = PBXGroup; + children = ( + DC44CE551CB72CB400E09AAC /* Localizations.bundle */, + ); + name = Resources; + sourceTree = ""; + }; + F00ED5F19A7501D03901A5C7 /* Pods */ = { + isa = PBXGroup; + children = ( + 69772C0193161EB42F63FC6B /* Pods-Obj-C SDK.debug.xcconfig */, + 37530499F1568B296FCB29A9 /* Pods-Obj-C SDK.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -160,10 +231,13 @@ isa = PBXNativeTarget; buildConfigurationList = AC04051A1BFB82CC00AC1501 /* Build configuration list for PBXNativeTarget "Obj-C SDK" */; buildPhases = ( + A9039065CE15663FDAF89019 /* Check Pods Manifest.lock */, AC0404E91BFB82CC00AC1501 /* Sources */, AC0404EA1BFB82CC00AC1501 /* Frameworks */, AC0404EB1BFB82CC00AC1501 /* Resources */, AC0405781BFBB32500AC1501 /* Embed Frameworks */, + 454384D96A25F017381B8A5A /* Embed Pods Frameworks */, + 7038EA1FAB7DC76D36E9E0F7 /* Copy Pods Resources */, ); buildRules = ( ); @@ -259,6 +333,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC44CE561CB72CB400E09AAC /* Localizations.bundle in Resources */, AC0405001BFB82CC00AC1501 /* LaunchScreen.storyboard in Resources */, AC0404FD1BFB82CC00AC1501 /* Assets.xcassets in Resources */, AC0404FB1BFB82CC00AC1501 /* Main.storyboard in Resources */, @@ -281,14 +356,66 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 454384D96A25F017381B8A5A /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Obj-C SDK/Pods-Obj-C SDK-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7038EA1FAB7DC76D36E9E0F7 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Obj-C SDK/Pods-Obj-C SDK-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + A9039065CE15663FDAF89019 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ AC0404E91BFB82CC00AC1501 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AC0404F81BFB82CC00AC1501 /* ViewController.m in Sources */, + DCEC0CAA1CB5D7780086C6D7 /* UBSDKExampleTableViewController.m in Sources */, + DCEC0CAD1CB5DCB10086C6D7 /* UBSDKRideRequestWidgetExampleViewController.m in Sources */, + AC0404F81BFB82CC00AC1501 /* UBSDKDeeplinkExampleViewController.m in Sources */, AC0404F51BFB82CC00AC1501 /* AppDelegate.m in Sources */, + DC44CE531CB7256800E09AAC /* UBSDKExampleTableViewCell.m in Sources */, AC0404F21BFB82CC00AC1501 /* main.m in Sources */, + DCEC0CB01CB5E3D30086C6D7 /* UBSDKImplicitGrantExampleViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -328,6 +455,9 @@ isa = PBXVariantGroup; children = ( AC0404FA1BFB82CC00AC1501 /* Base */, + DC8FC36D1CBDF7E700D58839 /* zh-Hans */, + DC8FC36F1CBDF7ED00D58839 /* zh-Hant */, + DC8FC3711CBDF7FD00D58839 /* hi-IN */, ); name = Main.storyboard; sourceTree = ""; @@ -336,6 +466,9 @@ isa = PBXVariantGroup; children = ( AC0404FF1BFB82CC00AC1501 /* Base */, + DC8FC36E1CBDF7E700D58839 /* zh-Hans */, + DC8FC3701CBDF7ED00D58839 /* zh-Hant */, + DC8FC3721CBDF7FD00D58839 /* hi-IN */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -425,10 +558,11 @@ }; AC04051B1BFB82CC00AC1501 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 69772C0193161EB42F63FC6B /* Pods-Obj-C SDK.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + EMBEDDED_CONTENT_CONTAINS_SWIFT = "$(inherited)"; INFOPLIST_FILE = "Obj-C SDK/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.uber.sdk.Obj-C-SDK"; @@ -438,10 +572,11 @@ }; AC04051C1BFB82CC00AC1501 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37530499F1568B296FCB29A9 /* Pods-Obj-C SDK.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + EMBEDDED_CONTENT_CONTAINS_SWIFT = "$(inherited)"; INFOPLIST_FILE = "Obj-C SDK/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.uber.sdk.Obj-C-SDK"; diff --git a/examples/Obj-C SDK/Obj-C SDK/AppDelegate.m b/examples/Obj-C SDK/Obj-C SDK/AppDelegate.m index a115acd2..fc4d78e8 100644 --- a/examples/Obj-C SDK/Obj-C SDK/AppDelegate.m +++ b/examples/Obj-C SDK/Obj-C SDK/AppDelegate.m @@ -23,7 +23,8 @@ // THE SOFTWARE. #import "AppDelegate.h" -@import UberRides; + +#import @interface AppDelegate () @@ -34,7 +35,13 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. - [[RidesClient sharedInstance] configureClientID:@"YOUR_CLIENT_ID"]; + + // Uncomment if your app is registered in China + //[UBSDKConfiguration setRegion:RegionChina]; + + //Make requests to sandbox for development + [UBSDKConfiguration setSandboxEnabled:YES]; + return YES; } diff --git a/examples/Obj-C SDK/Obj-C SDK/Base.lproj/LaunchScreen.storyboard b/examples/Obj-C SDK/Obj-C SDK/Base.lproj/LaunchScreen.storyboard index 497e9d79..2e721e18 100644 --- a/examples/Obj-C SDK/Obj-C SDK/Base.lproj/LaunchScreen.storyboard +++ b/examples/Obj-C SDK/Obj-C SDK/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,7 @@ - + - - + diff --git a/examples/Obj-C SDK/Obj-C SDK/Base.lproj/Main.storyboard b/examples/Obj-C SDK/Obj-C SDK/Base.lproj/Main.storyboard index c88e9e6f..b18e244f 100644 --- a/examples/Obj-C SDK/Obj-C SDK/Base.lproj/Main.storyboard +++ b/examples/Obj-C SDK/Obj-C SDK/Base.lproj/Main.storyboard @@ -1,26 +1,54 @@ - + - + - - + + - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/Obj-C SDK/Obj-C SDK/Info.plist b/examples/Obj-C SDK/Obj-C SDK/Info.plist index a0f6913a..bf20adf7 100644 --- a/examples/Obj-C SDK/Obj-C SDK/Info.plist +++ b/examples/Obj-C SDK/Obj-C SDK/Info.plist @@ -40,5 +40,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UberClientID + YOUR_CLIENT_ID + UberCallbackURI + callback://your_callback_uri + NSLocationWhenInUseUsageDescription + We use your location to make specifying a pickup spot easy as pie diff --git a/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/en.lproj/UberRides.strings b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/en.lproj/UberRides.strings new file mode 100644 index 00000000..0fc9c081 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/en.lproj/UberRides.strings @@ -0,0 +1,5 @@ +"Deeplink Request Buttons" = "Deeplink Request Buttons"; +"Ride Request Widget Button" = "Ride Request Widget Button"; +"Implicit Grant / Login Manager" = "Implicit Grant / Login Manager"; +"Logout" = "Logout"; +"Saved access token!" = "Saved access token!"; \ No newline at end of file diff --git a/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hans.lproj/UberRides.strings b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hans.lproj/UberRides.strings new file mode 100644 index 00000000..0fc9c081 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hans.lproj/UberRides.strings @@ -0,0 +1,5 @@ +"Deeplink Request Buttons" = "Deeplink Request Buttons"; +"Ride Request Widget Button" = "Ride Request Widget Button"; +"Implicit Grant / Login Manager" = "Implicit Grant / Login Manager"; +"Logout" = "Logout"; +"Saved access token!" = "Saved access token!"; \ No newline at end of file diff --git a/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hant.lproj/UberRides.strings b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hant.lproj/UberRides.strings new file mode 100644 index 00000000..0fc9c081 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/Resources/Localizations.bundle/zh-Hant.lproj/UberRides.strings @@ -0,0 +1,5 @@ +"Deeplink Request Buttons" = "Deeplink Request Buttons"; +"Ride Request Widget Button" = "Ride Request Widget Button"; +"Implicit Grant / Login Manager" = "Implicit Grant / Login Manager"; +"Logout" = "Logout"; +"Saved access token!" = "Saved access token!"; \ No newline at end of file diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.h new file mode 100644 index 00000000..1c4e5f0a --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.h @@ -0,0 +1,34 @@ +// +// ViewController.h +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +/** + This class provides an example for using the UBSDKRideRequestButton to initiate a deeplink into the Uber app + */ +@interface UBSDKDeeplinkExampleViewController : UIViewController + + +@end + diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.m b/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.m new file mode 100644 index 00000000..f185cc71 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKDeeplinkExampleViewController.m @@ -0,0 +1,229 @@ +// +// UBSDKDeeplinkExampleViewController.m +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UBSDKDeeplinkExampleViewController.h" +#import "UBSDKLocalization.h" + +#import + +#import + +@interface UBSDKDeeplinkExampleViewController () + +@property (nonatomic, readonly, nullable) UBSDKRideRequestButton *blackRideRequestButton; +@property (nonatomic, readonly, nullable) UBSDKRideRequestButton *whiteRideRequestButton; +@property (nonatomic, readonly, nullable) UIView *topView; +@property (nonatomic, readonly, nullable) UIView *bottomView; + +@end + +@implementation UBSDKDeeplinkExampleViewController + +#pragma mark - UIViewController + +- (instancetype)init { + self = [super init]; + if (self) { + [self _initialSetup]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self _initialSetup]; + } + return self; +} + +#pragma mark - View Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.navigationItem.title = UBSDKLOC(@"Deeplink Buttons"); + self.view.backgroundColor = [UIColor whiteColor]; + + [self.view addSubview:self.topView]; + [self.view addSubview:self.bottomView]; + [self.topView addSubview:self.blackRideRequestButton]; + [self.bottomView addSubview:self.whiteRideRequestButton]; + + [self _addTopViewConstraints]; + [self _addBottomViewConstraints]; + [self _addBlackButtonConstraints]; + [self _addWhiteButtonConstraints]; +} + +#pragma mark - Private + +- (void)_initialSetup { + _topView = ({ + UIView *view = [[UIView alloc] init]; + view.backgroundColor = [UIColor whiteColor]; + view; + }); + + _bottomView = ({ + UIView *view = [[UIView alloc] init]; + view.backgroundColor = [UIColor blackColor]; + view; + }); + + _blackRideRequestButton = [[UBSDKRideRequestButton alloc] init]; + + _whiteRideRequestButton = ({ + UBSDKRideParameters *rideParameters = [self _buildRideParameters]; + id deeplinkBehavior = [[UBSDKDeeplinkRequestingBehavior alloc] init]; + UBSDKRideRequestButton *rideRequestButton = [[UBSDKRideRequestButton alloc] initWithRideParameters:rideParameters requestingBehavior:deeplinkBehavior]; + rideRequestButton.colorStyle = RequestButtonColorStyleWhite; + rideRequestButton; + }); +} + +- (UBSDKRideParameters *)_buildRideParameters { + UBSDKRideParametersBuilder *builder = [[UBSDKRideParametersBuilder alloc] init]; + [builder setProductID:@"a1111c8c-c720-46c3-8534-2fcdd730040d"]; + + CLLocation *pickupLocation = [[CLLocation alloc] initWithLatitude:37.770 longitude:-122.466]; + [builder setPickupLocation:pickupLocation nickname:@"California Academy of Sciences"]; + + CLLocation *dropoffLocation = [[CLLocation alloc] initWithLatitude:37.791 longitude:-122.405]; + [builder setDropoffLocation:dropoffLocation nickname:@"Pier 39"]; + + return [builder build]; +} + +- (void)_addTopViewConstraints { + self.topView.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.topView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.topView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.topView + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.topView + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:0.0]; + [self.view addConstraints:@[topConstraint, bottomConstraint, leftConstraint, rightConstraint]]; +} + +- (void)_addBottomViewConstraints { + self.bottomView.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.bottomView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self.bottomLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.bottomView + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.bottomView + attribute:NSLayoutAttributeRight + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeRight + multiplier:1.0 + constant:0.0]; + [self.view addConstraints:@[topConstraint, bottomConstraint, leftConstraint, rightConstraint]]; +} + +- (void)_addBlackButtonConstraints { + self.blackRideRequestButton.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:self.blackRideRequestButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.topView + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.blackRideRequestButton + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self.topView + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + + [self.topView addConstraints:@[centerXConstraint, centerYConstraint]]; +} + +- (void)_addWhiteButtonConstraints { + self.whiteRideRequestButton.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:self.whiteRideRequestButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.bottomView + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.whiteRideRequestButton + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self.bottomView + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + + [self.bottomView addConstraints:@[centerXConstraint, centerYConstraint]]; +} + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.h new file mode 100644 index 00000000..17682d00 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.h @@ -0,0 +1,51 @@ +// +// UBSDKExampleTableViewCell.h +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UBSDKExampleTableViewCell : UITableViewCell + +/** + Initializes a UBSDKExampleTableViewCell with the provided behaviorBlock. + + @param behaviorBlock The block to be executed when executeBehaviorBlock is called + + @return Initialized UBSDKExcampleTableViewCell + */ +- (id)initWithBehaviorBlock:(void (^_Nullable)(void))behaviorBlock NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier NS_UNAVAILABLE; + +/** + Executes the behavior block that was set during initialization. If no block was provided, this method does nothing. + */ +- (void)executeBehaviorBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.m b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.m new file mode 100644 index 00000000..6d71f85d --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewCell.m @@ -0,0 +1,61 @@ +// +// UBSDKExampleTableViewCell.m +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UBSDKExampleTableViewCell.h" + +@interface UBSDKExampleTableViewCell () + +@property (nonatomic, readonly, nullable) void (^behaviorBlock)(); + +@end + +@implementation UBSDKExampleTableViewCell + +#pragma mark - Initializers + +- (id)initWithBehaviorBlock:(void (^_Nullable)(void))behaviorBlock { + self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + if (self) { + _behaviorBlock = behaviorBlock; + [self _setupCellStyle]; + } + return self; +} + +#pragma mark - Public + +- (void)executeBehaviorBlock { + if (self.behaviorBlock) { + self.behaviorBlock(); + } +} + +#pragma mark - Private + +- (void)_setupCellStyle { + self.textLabel.textColor = [UIColor blackColor]; + self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; +} + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/ViewController.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.h similarity index 92% rename from examples/Obj-C SDK/Obj-C SDK/ViewController.h rename to examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.h index 880abd23..38b7b975 100644 --- a/examples/Obj-C SDK/Obj-C SDK/ViewController.h +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.h @@ -1,5 +1,5 @@ // -// ViewController.h +// UBSDKExampleTableViewController.h // Obj-C SDK // // Copyright © 2015 Uber Technologies, Inc. All rights reserved. @@ -24,8 +24,6 @@ #import -@interface ViewController : UIViewController - +@interface UBSDKExampleTableViewController : UITableViewController @end - diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.m b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.m new file mode 100644 index 00000000..54e950ed --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKExampleTableViewController.m @@ -0,0 +1,167 @@ +// +// UBSDKExampleTableViewController.m +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UBSDKExampleTableViewController.h" + +#import "UBSDKDeeplinkExampleViewController.h" +#import "UBSDKExampleTableViewCell.h" +#import "UBSDKImplicitGrantExampleViewController.h" +#import "UBSDKLocalization.h" +#import "UBSDKRideRequestWidgetExampleViewController.h" + +#import + +@interface UBSDKExampleTableViewController () + +@property (nonatomic, readonly, nonnull) NSDictionary *> *tableViewCellMap; + +@end + +@implementation UBSDKExampleTableViewController + +#pragma mark - UIViewController + +- (instancetype)initWithStyle:(UITableViewStyle)style { + self = [super initWithStyle:style]; + if (self) { + [self _initialSetup]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self _initialSetup]; + } + return self; +} + +#pragma mark - Private + +- (void)_initialSetup { + _tableViewCellMap = [self _buildTableCellMap]; +} + +- (NSDictionary *> *)_buildTableCellMap { + NSMutableDictionary *> *tableCellMap = [NSMutableDictionary dictionary]; + + NSMutableArray *sectionOneExampleCells = [NSMutableArray array]; + + [sectionOneExampleCells addObject:[self _createDeeplinkExampleCell]]; + [sectionOneExampleCells addObject:[self _createRideRequestWidgetButtonExampleCell]]; + [sectionOneExampleCells addObject:[self _createImplicitGrantExampleCell]]; + + [tableCellMap setObject:sectionOneExampleCells forKey:@(0)]; + + NSMutableArray *sectionTwoExampleCells= [NSMutableArray array]; + + [sectionTwoExampleCells addObject:[self _createLogoutExampleCell]]; + + [tableCellMap setObject:sectionTwoExampleCells forKey:@(1)]; + + return tableCellMap; +} + +- (UBSDKExampleTableViewCell *)_createDeeplinkExampleCell { + UBSDKExampleTableViewController __weak *weakSelf = self; + void (^behaviorBlock)() = ^void() { + UBSDKDeeplinkExampleViewController *deeplinkExampleViewController = [[UBSDKDeeplinkExampleViewController alloc] init]; + [weakSelf.navigationController pushViewController:deeplinkExampleViewController animated:YES]; + }; + UBSDKExampleTableViewCell *deeplinkExampleCell = [[UBSDKExampleTableViewCell alloc] initWithBehaviorBlock:behaviorBlock]; + deeplinkExampleCell.textLabel.text = UBSDKLOC(@"Deeplink Request Buttons"); + return deeplinkExampleCell; +} + +- (UBSDKExampleTableViewCell *)_createRideRequestWidgetButtonExampleCell { + UBSDKExampleTableViewController __weak *weakSelf = self; + void (^behaviorBlock)() = ^void() { + UBSDKRideRequestWidgetExampleViewController *rideRequestWidgetExampleViewController = [[UBSDKRideRequestWidgetExampleViewController alloc] init]; + [weakSelf.navigationController pushViewController:rideRequestWidgetExampleViewController animated:YES]; + }; + UBSDKExampleTableViewCell *rideRequestWidgetExampleCell = [[UBSDKExampleTableViewCell alloc] initWithBehaviorBlock:behaviorBlock]; + rideRequestWidgetExampleCell.textLabel.text = UBSDKLOC(@"Ride Request Widget Button"); + return rideRequestWidgetExampleCell; +} + +- (UBSDKExampleTableViewCell *)_createImplicitGrantExampleCell { + UBSDKExampleTableViewController __weak *weakSelf = self; + void (^behaviorBlock)() = ^void() { + UBSDKImplicitGrantExampleViewController *implicitGrantExampleViewController = [[UBSDKImplicitGrantExampleViewController alloc] init]; + [weakSelf.navigationController pushViewController:implicitGrantExampleViewController animated:YES]; + }; + UBSDKExampleTableViewCell *implicitGrantExampleCell = [[UBSDKExampleTableViewCell alloc] initWithBehaviorBlock:behaviorBlock]; + implicitGrantExampleCell.textLabel.text = UBSDKLOC(@"Implicit Grant / Login Manager"); + return implicitGrantExampleCell; +} + +- (UBSDKExampleTableViewCell *)_createLogoutExampleCell { + void (^behaviorBlock)() = ^void() { + [UBSDKTokenManager deleteToken]; + }; + UBSDKExampleTableViewCell *logoutExampleCell = [[UBSDKExampleTableViewCell alloc] initWithBehaviorBlock:behaviorBlock]; + logoutExampleCell.textLabel.text = UBSDKLOC(@"Logout"); + logoutExampleCell.textLabel.textColor = [UIColor redColor]; + logoutExampleCell.accessoryType = UITableViewCellAccessoryNone; + return logoutExampleCell; +} + +#pragma mark - + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSInteger section = indexPath.section; + NSInteger index = indexPath.row; + + NSArray *sectionCells = self.tableViewCellMap[@(section)]; + if (sectionCells && index < sectionCells.count) { + return sectionCells[index]; + } + + return [[UBSDKExampleTableViewCell alloc] initWithBehaviorBlock:nil]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + + NSInteger section = indexPath.section; + NSInteger index = indexPath.row; + + NSArray *sectionCells = self.tableViewCellMap[@(section)]; + if (sectionCells && index < sectionCells.count) { + [sectionCells[index] executeBehaviorBlock]; + } +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return self.tableViewCellMap.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.tableViewCellMap[@(section)].count; +} + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.h new file mode 100644 index 00000000..64d71c25 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.h @@ -0,0 +1,34 @@ +// +// UBSDKImplicitGrantExampleViewController.h +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +/** + This class demonstrates how do use the UBSDKLoginManager to complete Implicit Grant Authorization. + By Default it is requesting the RideRequestControl, Profile, and Places scopes, so be sure + that your ClientID has them added. + */ +@interface UBSDKImplicitGrantExampleViewController : UIViewController + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.m b/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.m new file mode 100644 index 00000000..ed10771b --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKImplicitGrantExampleViewController.m @@ -0,0 +1,117 @@ +// +// UBSDKImplicitGrantExampleViewController.m +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UBSDKImplicitGrantExampleViewController.h" + +#import "UBSDKLocalization.h" + +#import + +@interface UBSDKImplicitGrantExampleViewController () + +@property (nonatomic, readonly, nonnull) UBSDKLoginManager *loginManager; +@property (nonatomic, readonly, nonnull) UIButton *loginButton; + +@end + +@implementation UBSDKImplicitGrantExampleViewController + +#pragma mark - UIViewController + +- (id)init { + self = [super init]; + if (self) { + [self _initialSetup]; + } + return self; +} + +#pragma mark - View Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; + self.navigationItem.title = UBSDKLOC(@"Implicit Grant / Login Manager"); + + [self.view addSubview:self.loginButton]; + [self _addLoginButtonConstraints]; +} + +#pragma mark - Private + +- (void)_initialSetup { + _loginManager = [[UBSDKLoginManager alloc] init]; + + _loginButton = ({ + UIButton *loginButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; + [loginButton setTitle:UBSDKLOC(@"Login") forState:UIControlStateNormal]; + [loginButton sizeToFit]; + [loginButton addTarget:self action:@selector(_loginButtonAction:) forControlEvents:UIControlEventTouchUpInside]; + loginButton; + }); +} + +- (void)_addLoginButtonConstraints { + self.loginButton.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:self.loginButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.loginButton + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + [self.view addConstraints:@[centerXConstraint, centerYConstraint]]; +} + +- (void)_showMessage:(NSString *)message { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okayAction = [UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleDefault handler:nil]; + [alert addAction:okayAction]; + [self presentViewController:alert animated:YES completion:nil]; +} + +#pragma mark - Actions + +- (void)_loginButtonAction:(UIButton *)button { + NSArray *requestedScopes = @[ UBSDKRidesScope.RideWidgets, UBSDKRidesScope.Profile, UBSDKRidesScope.Places ]; + + [self.loginManager loginWithRequestedScopes:requestedScopes presentingViewController:self completion:^(UBSDKAccessToken * _Nullable accessToken, NSError * _Nullable error) { + if (accessToken) { + [self _showMessage:UBSDKLOC(@"Saved access token!")]; + } else { + [self _showMessage:error.localizedDescription]; + } + }]; +} + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKLocalization.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKLocalization.h new file mode 100644 index 00000000..39f3c1f2 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKLocalization.h @@ -0,0 +1,26 @@ +// +// UBSDKLocalization.h +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// 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. + +#define UBSDKLOC(LOC_STRING) UBSDKLocalizedString(LOC_STRING) +#define UBSDKLocalizedString(key, ...) [NSString stringWithFormat:[[NSBundle bundleWithPath:[[NSBundle bundleForClass:[self class]] pathForResource:@"Localizations" ofType:@"bundle"]] localizedStringForKey:(key) value:@"" table:@"UberRides"], ##__VA_ARGS__, nil] diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.h b/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.h new file mode 100644 index 00000000..407de45e --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.h @@ -0,0 +1,34 @@ +// +// UBSDKRideRequestWidgetExampleViewController.h +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +/** + This class provides an example of how to use the UBSDKRideRequestButton to initiate + a ride request using the Ride Request Widget. Using the Ride Request Widget requires + the RideRequestControl scope, so be sure to add it to your app. + */ +@interface UBSDKRideRequestWidgetExampleViewController : UIViewController + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.m b/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.m new file mode 100644 index 00000000..d8001c6b --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/UBSDKRideRequestWidgetExampleViewController.m @@ -0,0 +1,122 @@ +// +// UBSDKRideRequestWidgetExampleViewController.m +// Obj-C SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UBSDKRideRequestWidgetExampleViewController.h" +#import "UBSDKLocalization.h" + +#import + +#import + +@interface UBSDKRideRequestWidgetExampleViewController () + +@property (nonatomic, readonly, nonnull) UBSDKRideRequestButton *rideRequestButton; +@property (nonatomic, readonly, nullable) CLLocationManager *locationManager; + +@end + +@implementation UBSDKRideRequestWidgetExampleViewController + +#pragma mark - UIViewController + +- (id)init { + self = [super init]; + if (self) { + [self _initialSetup]; + } + return self; +} + +#pragma mark - View Lifecycle + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; + self.navigationItem.title = UBSDKLOC(@"Ride Request Widget"); + + [self.view addSubview:self.rideRequestButton]; + + [self _addRequestButtonConstraints]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.locationManager requestWhenInUseAuthorization]; +} + +#pragma mark - Private + +- (void)_initialSetup { + _rideRequestButton = [self _buildRideRequestWidgetButton]; + _locationManager = [[CLLocationManager alloc] init]; +} + +- (UBSDKRideRequestButton *)_buildRideRequestWidgetButton { + UBSDKRideRequestViewRequestingBehavior *requestBehavior = [[UBSDKRideRequestViewRequestingBehavior alloc] initWithPresentingViewController:self]; + requestBehavior.modalRideRequestViewController.delegate = self; + + UBSDKRideParameters *rideParameters = [self _buildRideParameters]; + + return [[UBSDKRideRequestButton alloc] initWithRideParameters:rideParameters requestingBehavior:requestBehavior]; +} + +- (UBSDKRideParameters *)_buildRideParameters { + UBSDKRideParametersBuilder *parameterBuilder = [[UBSDKRideParametersBuilder alloc] init]; + [parameterBuilder setPickupToCurrentLocation]; + return [parameterBuilder build]; +} + +- (void)_addRequestButtonConstraints { + self.rideRequestButton.translatesAutoresizingMaskIntoConstraints = NO; + + // Center the button in the view + NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:self.rideRequestButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0]; + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.rideRequestButton + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterY + multiplier:1.0 + constant:0.0]; + [self.view addConstraints:@[centerXConstraint, centerYConstraint]]; +} + +#pragma mark - + +- (void)modalViewControllerDidDismiss:(UBSDKModalViewController *)modalViewController { + NSLog(@"did dismiss"); +} + +- (void)modalViewControllerWillDismiss:(UBSDKModalViewController *)modalViewController { + NSLog(@"will dismiss"); +} + +@end diff --git a/examples/Obj-C SDK/Obj-C SDK/ViewController.m b/examples/Obj-C SDK/Obj-C SDK/ViewController.m deleted file mode 100644 index b54fdead..00000000 --- a/examples/Obj-C SDK/Obj-C SDK/ViewController.m +++ /dev/null @@ -1,98 +0,0 @@ -// -// ViewController.m -// Obj-C SDK -// -// Copyright © 2015 Uber Technologies, Inc. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#import "ViewController.h" -@import UberRides; - -@interface ViewController () - -@end - -@implementation ViewController - -- (void)viewDidLoad { - [super viewDidLoad]; - - // create background views - UIView *topView = [[UIView alloc] init]; - [self.view addSubview:topView]; - UIView *bottomView = [[UIView alloc] init]; - [self.view addSubview:bottomView]; - - // add black request button with default configurations - RequestButton *blackRequestButton = [[RequestButton alloc] init]; - [topView addSubview:blackRequestButton]; - - // add white request button and add custom configurations - RequestButton *whiteRequestButton = [[RequestButton alloc] initWithColorStyle:RequestButtonColorStyleWhite]; - [whiteRequestButton setProductID:@"a1111c8c-c720-46c3-8534-2fcdd730040d"]; - [whiteRequestButton setPickupLocationWithLatitude:37.770 longitude:-122.466 nickname:@"California Academy of Sciences" address:nil]; - [whiteRequestButton setDropoffLocationWithLatitude:37.791 longitude:-122.405 nickname:@"Pier 39" address:nil]; - [bottomView addSubview:whiteRequestButton]; - - // position UIViews and request buttons - [self setUpBackgroundViewsWithTop:topView andBottom:bottomView]; - [self centerButton:blackRequestButton inView:topView]; - [self centerButton:whiteRequestButton inView:bottomView]; -} - -// set up two white and black background UIViews with autolayout constraints -- (void)setUpBackgroundViewsWithTop:(UIView*)topView andBottom:(UIView*)bottomView { - topView.backgroundColor = [UIColor whiteColor]; - bottomView.backgroundColor = [UIColor blackColor]; - topView.translatesAutoresizingMaskIntoConstraints = NO; - bottomView.translatesAutoresizingMaskIntoConstraints = NO; - - // pass views in dictionary - NSDictionary *views = @{@"top":topView, @"bottom":bottomView}; - - // position constraints - NSArray *horizontalTopConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[top]|" options:0 metrics:nil views:views]; - NSArray *horizontalBottomConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[bottom]|" options:0 metrics:nil views:views]; - NSArray *verticalConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[top(bottom)][bottom]|" options:NSLayoutFormatAlignAllLeading metrics:nil views:views]; - - // add constraints to view - [self.view addConstraints:horizontalTopConstraint]; - [self.view addConstraints:horizontalBottomConstraint]; - [self.view addConstraints:verticalConstraint]; -}; - -// center button position inside each background UIView -- (void)centerButton:(RequestButton*)button inView:(UIView*)view { - button.translatesAutoresizingMaskIntoConstraints = NO; - - // position constraints - NSLayoutConstraint *horizontalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]; - NSLayoutConstraint *verticalConstraint = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; - - // add constraints to view - [view addConstraints:[NSArray arrayWithObjects:horizontalConstraint, verticalConstraint, nil]]; -}; - -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -@end diff --git a/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/LaunchScreen.strings b/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/Main.strings b/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/Main.strings new file mode 100644 index 00000000..ea1301d8 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/hi-IN.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "SuO-Q8-doa"; */ +"SuO-Q8-doa.title" = "Root View Controller"; diff --git a/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/LaunchScreen.strings b/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/Main.strings b/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/Main.strings new file mode 100644 index 00000000..ea1301d8 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/zh-Hans.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "SuO-Q8-doa"; */ +"SuO-Q8-doa.title" = "Root View Controller"; diff --git a/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/LaunchScreen.strings b/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/Main.strings b/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/Main.strings new file mode 100644 index 00000000..ea1301d8 --- /dev/null +++ b/examples/Obj-C SDK/Obj-C SDK/zh-Hant.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "SuO-Q8-doa"; */ +"SuO-Q8-doa.title" = "Root View Controller"; diff --git a/examples/Swift SDK/Swift SDK.xcodeproj/project.pbxproj b/examples/Swift SDK/Swift SDK.xcodeproj/project.pbxproj index 1863bbb1..8ea07ebc 100644 --- a/examples/Swift SDK/Swift SDK.xcodeproj/project.pbxproj +++ b/examples/Swift SDK/Swift SDK.xcodeproj/project.pbxproj @@ -7,13 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 9950A42DC5E55EA5A03D13A9 /* Pods_Swift_SDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BA4ECB19AAC4C640E1A50AD /* Pods_Swift_SDK.framework */; }; AC0404A81BFAD0C800AC1501 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404A71BFAD0C800AC1501 /* AppDelegate.swift */; }; - AC0404AA1BFAD0C800AC1501 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404A91BFAD0C800AC1501 /* ViewController.swift */; }; + AC0404AA1BFAD0C800AC1501 /* DeeplinkExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404A91BFAD0C800AC1501 /* DeeplinkExampleViewController.swift */; }; AC0404AD1BFAD0C800AC1501 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC0404AB1BFAD0C800AC1501 /* Main.storyboard */; }; AC0404AF1BFAD0C800AC1501 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC0404AE1BFAD0C800AC1501 /* Assets.xcassets */; }; AC0404B21BFAD0C800AC1501 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AC0404B01BFAD0C800AC1501 /* LaunchScreen.storyboard */; }; AC0404BD1BFAD0C800AC1501 /* Swift_SDKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404BC1BFAD0C800AC1501 /* Swift_SDKTests.swift */; }; AC0404C81BFAD0C800AC1501 /* Swift_SDKUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404C71BFAD0C800AC1501 /* Swift_SDKUITests.swift */; }; + DC78EC781CB34AC600850814 /* ExampleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78EC771CB34AC600850814 /* ExampleTableViewController.swift */; }; + DC78EC7A1CB34D4800850814 /* RideRequestWidgetExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78EC791CB34D4800850814 /* RideRequestWidgetExampleViewController.swift */; }; + DC78EC7C1CB35AAB00850814 /* ImplicitGrantExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78EC7B1CB35AAB00850814 /* ImplicitGrantExampleViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,9 +38,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0BA4ECB19AAC4C640E1A50AD /* Pods_Swift_SDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Swift_SDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 936D353832B2D6A2F78B5456 /* Pods-Swift SDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Swift SDK.release.xcconfig"; path = "Pods/Target Support Files/Pods-Swift SDK/Pods-Swift SDK.release.xcconfig"; sourceTree = ""; }; AC0404A41BFAD0C800AC1501 /* Swift SDK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Swift SDK.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AC0404A71BFAD0C800AC1501 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - AC0404A91BFAD0C800AC1501 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + AC0404A91BFAD0C800AC1501 /* DeeplinkExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkExampleViewController.swift; sourceTree = ""; }; AC0404AC1BFAD0C800AC1501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; AC0404AE1BFAD0C800AC1501 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AC0404B11BFAD0C800AC1501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -47,6 +53,16 @@ AC0404C31BFAD0C800AC1501 /* Swift SDKUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Swift SDKUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; AC0404C71BFAD0C800AC1501 /* Swift_SDKUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swift_SDKUITests.swift; sourceTree = ""; }; AC0404C91BFAD0C800AC1501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C5ADDC8A8EE48350FDC38319 /* Pods-Swift SDK.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Swift SDK.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Swift SDK/Pods-Swift SDK.debug.xcconfig"; sourceTree = ""; }; + DC78EC771CB34AC600850814 /* ExampleTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleTableViewController.swift; sourceTree = ""; }; + DC78EC791CB34D4800850814 /* RideRequestWidgetExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestWidgetExampleViewController.swift; sourceTree = ""; }; + DC78EC7B1CB35AAB00850814 /* ImplicitGrantExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitGrantExampleViewController.swift; sourceTree = ""; }; + DC8FC3671CBDF5C100D58839 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + DC8FC3681CBDF5C100D58839 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + DC8FC3691CBDF5C700D58839 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; + DC8FC36A1CBDF5C700D58839 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/LaunchScreen.strings"; sourceTree = ""; }; + DC8FC36B1CBDF5D200D58839 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "hi-IN.lproj/Main.strings"; sourceTree = ""; }; + DC8FC36C1CBDF5D200D58839 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "hi-IN.lproj/LaunchScreen.strings"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,6 +70,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9950A42DC5E55EA5A03D13A9 /* Pods_Swift_SDK.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,6 +91,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 6AB8DBD1FAB9D5639A591D66 /* Pods */ = { + isa = PBXGroup; + children = ( + C5ADDC8A8EE48350FDC38319 /* Pods-Swift SDK.debug.xcconfig */, + 936D353832B2D6A2F78B5456 /* Pods-Swift SDK.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 823B0AE4697BBA3B456C5C93 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0BA4ECB19AAC4C640E1A50AD /* Pods_Swift_SDK.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; AC04049B1BFAD0C800AC1501 = { isa = PBXGroup; children = ( @@ -81,6 +115,8 @@ AC0404BB1BFAD0C800AC1501 /* Swift SDKTests */, AC0404C61BFAD0C800AC1501 /* Swift SDKUITests */, AC0404A51BFAD0C800AC1501 /* Products */, + 6AB8DBD1FAB9D5639A591D66 /* Pods */, + 823B0AE4697BBA3B456C5C93 /* Frameworks */, ); sourceTree = ""; }; @@ -98,7 +134,10 @@ isa = PBXGroup; children = ( AC0404A71BFAD0C800AC1501 /* AppDelegate.swift */, - AC0404A91BFAD0C800AC1501 /* ViewController.swift */, + DC78EC771CB34AC600850814 /* ExampleTableViewController.swift */, + DC78EC791CB34D4800850814 /* RideRequestWidgetExampleViewController.swift */, + AC0404A91BFAD0C800AC1501 /* DeeplinkExampleViewController.swift */, + DC78EC7B1CB35AAB00850814 /* ImplicitGrantExampleViewController.swift */, AC0404AB1BFAD0C800AC1501 /* Main.storyboard */, AC0404AE1BFAD0C800AC1501 /* Assets.xcassets */, AC0404B01BFAD0C800AC1501 /* LaunchScreen.storyboard */, @@ -132,9 +171,12 @@ isa = PBXNativeTarget; buildConfigurationList = AC0404CC1BFAD0C800AC1501 /* Build configuration list for PBXNativeTarget "Swift SDK" */; buildPhases = ( + 6FEA69134FB8E38A6A356E62 /* Check Pods Manifest.lock */, AC0404A01BFAD0C800AC1501 /* Sources */, AC0404A11BFAD0C800AC1501 /* Frameworks */, AC0404A21BFAD0C800AC1501 /* Resources */, + C6730F1AF1962CE782882276 /* Embed Pods Frameworks */, + 4C91B192F5D293C3FF882A6E /* Copy Pods Resources */, ); buildRules = ( ); @@ -253,13 +295,64 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 4C91B192F5D293C3FF882A6E /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Swift SDK/Pods-Swift SDK-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6FEA69134FB8E38A6A356E62 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + C6730F1AF1962CE782882276 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Swift SDK/Pods-Swift SDK-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ AC0404A01BFAD0C800AC1501 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AC0404AA1BFAD0C800AC1501 /* ViewController.swift in Sources */, + DC78EC781CB34AC600850814 /* ExampleTableViewController.swift in Sources */, + DC78EC7C1CB35AAB00850814 /* ImplicitGrantExampleViewController.swift in Sources */, + AC0404AA1BFAD0C800AC1501 /* DeeplinkExampleViewController.swift in Sources */, AC0404A81BFAD0C800AC1501 /* AppDelegate.swift in Sources */, + DC78EC7A1CB34D4800850814 /* RideRequestWidgetExampleViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -299,6 +392,9 @@ isa = PBXVariantGroup; children = ( AC0404AC1BFAD0C800AC1501 /* Base */, + DC8FC3671CBDF5C100D58839 /* zh-Hans */, + DC8FC3691CBDF5C700D58839 /* zh-Hant */, + DC8FC36B1CBDF5D200D58839 /* hi-IN */, ); name = Main.storyboard; sourceTree = ""; @@ -307,6 +403,9 @@ isa = PBXVariantGroup; children = ( AC0404B11BFAD0C800AC1501 /* Base */, + DC8FC3681CBDF5C100D58839 /* zh-Hans */, + DC8FC36A1CBDF5C700D58839 /* zh-Hant */, + DC8FC36C1CBDF5D200D58839 /* hi-IN */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -397,6 +496,7 @@ }; AC0404CD1BFAD0C800AC1501 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C5ADDC8A8EE48350FDC38319 /* Pods-Swift SDK.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -409,6 +509,7 @@ }; AC0404CE1BFAD0C800AC1501 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 936D353832B2D6A2F78B5456 /* Pods-Swift SDK.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; diff --git a/examples/Swift SDK/Swift SDK/AppDelegate.swift b/examples/Swift SDK/Swift SDK/AppDelegate.swift index 47bf9c80..d4612568 100644 --- a/examples/Swift SDK/Swift SDK/AppDelegate.swift +++ b/examples/Swift SDK/Swift SDK/AppDelegate.swift @@ -32,7 +32,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. - RidesClient.sharedInstance.configureClientID("YOUR_CLIENT_ID") + + // Uncomment if your app is registered in China + //Configuration.setRegion(.China) + + // Make requests to sandbox by default + Configuration.setSandboxEnabled(true) return true } diff --git a/examples/Swift SDK/Swift SDK/Base.lproj/LaunchScreen.storyboard b/examples/Swift SDK/Swift SDK/Base.lproj/LaunchScreen.storyboard index 497e9d79..c9b75643 100644 --- a/examples/Swift SDK/Swift SDK/Base.lproj/LaunchScreen.storyboard +++ b/examples/Swift SDK/Swift SDK/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + @@ -16,7 +16,6 @@ - diff --git a/examples/Swift SDK/Swift SDK/Base.lproj/Main.storyboard b/examples/Swift SDK/Swift SDK/Base.lproj/Main.storyboard index 1e3a9394..bc12bc29 100644 --- a/examples/Swift SDK/Swift SDK/Base.lproj/Main.storyboard +++ b/examples/Swift SDK/Swift SDK/Base.lproj/Main.storyboard @@ -1,26 +1,54 @@ - + - + - - + + - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/Swift SDK/Swift SDK/ViewController.swift b/examples/Swift SDK/Swift SDK/DeeplinkExampleViewController.swift similarity index 74% rename from examples/Swift SDK/Swift SDK/ViewController.swift rename to examples/Swift SDK/Swift SDK/DeeplinkExampleViewController.swift index 9332846a..21a92d9a 100644 --- a/examples/Swift SDK/Swift SDK/ViewController.swift +++ b/examples/Swift SDK/Swift SDK/DeeplinkExampleViewController.swift @@ -1,5 +1,5 @@ // -// ViewController.swift +// DeeplinkExampleViewController.swift // Swift SDK // // Copyright © 2015 Uber Technologies, Inc. All rights reserved. @@ -24,12 +24,17 @@ import UIKit import UberRides +import CoreLocation -class ViewController: UIViewController { +/// This class provides an example for using the RideRequestButton to initiate a deeplink +/// into the Uber app +class DeeplinkExampleViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.navigationItem.title = "Deeplink Buttons" + // create background UIViews let topView = UIView() view.addSubview(topView) @@ -37,14 +42,28 @@ class ViewController: UIViewController { view.addSubview(bottomView) // add black request button with default configurations - let blackRequestButton = RequestButton() + let blackRequestButton = RideRequestButton() topView.addSubview(blackRequestButton) // add white request button and add custom configurations - let whiteRequestButton = RequestButton(colorStyle: .White) - whiteRequestButton.setProductID("a1111c8c-c720-46c3-8534-2fcdd730040d") - whiteRequestButton.setPickupLocation(latitude: 37.770, longitude: -122.466, nickname: "California Academy of Sciences") - whiteRequestButton.setDropoffLocation(latitude: 37.791, longitude: -122.405, nickname: "Pier 39") + let whiteRequestButton = RideRequestButton() + + //Create the DeeplinkRequestingBehavior + //The RideRequestButton is initialized with this behavior by default, shown + //as an example + let deeplinkBehavior = DeeplinkRequestingBehavior() + whiteRequestButton.requestBehavior = deeplinkBehavior + + whiteRequestButton.colorStyle = .White + let parameterBuilder = RideParametersBuilder() + parameterBuilder.setProductID("a1111c8c-c720-46c3-8534-2fcdd730040d") + let pickupLocation = CLLocation(latitude: 37.770, longitude: -122.466) + parameterBuilder.setPickupLocation(pickupLocation, nickname: "California Academy of Sciences") + let dropoffLocation = CLLocation(latitude: 37.791, longitude: -122.405) + parameterBuilder.setDropoffLocation(dropoffLocation, nickname: "Pier 39") + + whiteRequestButton.rideParameters = parameterBuilder.build() + bottomView.addSubview(whiteRequestButton) // position UIViews and request buttons @@ -75,7 +94,7 @@ class ViewController: UIViewController { } // center button position inside each background UIView - func centerButton(forButton button: RequestButton, inView: UIView) { + func centerButton(forButton button: RideRequestButton, inView: UIView) { button.translatesAutoresizingMaskIntoConstraints = false // position constraints @@ -85,12 +104,5 @@ class ViewController: UIViewController { // add constraints to view inView.addConstraints([horizontalConstraint, verticalConstraint]) } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - } diff --git a/examples/Swift SDK/Swift SDK/ExampleTableViewController.swift b/examples/Swift SDK/Swift SDK/ExampleTableViewController.swift new file mode 100644 index 00000000..47d1e2b7 --- /dev/null +++ b/examples/Swift SDK/Swift SDK/ExampleTableViewController.swift @@ -0,0 +1,119 @@ +// +// ExampleTableViewController.swift +// Swift SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import UberRides + +class ExampleTableViewController: UITableViewController { + + override func viewDidLoad() { + super.viewDidLoad() + self.tableView.tableFooterView = UIView(frame: CGRectZero) + + self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "basicCell") + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("basicCell", forIndexPath: indexPath) + + switch indexPath.section { + case 0: + switch indexPath.row { + case 0: + cell.textLabel?.text = "Deeplink Request Buttons" + cell.textLabel?.textColor = UIColor.blackColor() + cell.accessoryType = .DisclosureIndicator + case 1: + cell.textLabel?.text = "Ride Request Widget Button" + cell.textLabel?.textColor = UIColor.blackColor() + cell.accessoryType = .DisclosureIndicator + case 2: + cell.textLabel?.text = "Implicit Grant / Login Manager" + cell.textLabel?.textColor = UIColor.blackColor() + cell.accessoryType = .DisclosureIndicator + default: + break + } + case 1: + fallthrough + default: + cell.textLabel?.text = "Logout" + cell.textLabel?.textColor = UIColor.redColor() + cell.accessoryType = .None + } + + + return cell + } + + //MARK: UITableViewDelegate + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + + tableView.deselectRowAtIndexPath(indexPath, animated: true) + + var viewControllerToPush: UIViewController? + + switch indexPath.section { + case 0: + switch indexPath.row { + case 0: + viewControllerToPush = DeeplinkExampleViewController() + case 1: + viewControllerToPush = RideRequestWidgetExampleViewController() + case 2: + viewControllerToPush = ImplicitGrantExampleViewController() + default: + break + } + case 1: + viewControllerToPush = nil + TokenManager.deleteToken() + default: + viewControllerToPush = nil + } + + + if let viewController = viewControllerToPush { + self.navigationController?.pushViewController(viewController, animated: true) + } + } + + //MARK: UITableViewDataSource Methods + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 2 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return 3 + case 1: + return 1 + default: + return 0 + } + } +} diff --git a/examples/Swift SDK/Swift SDK/ImplicitGrantExampleViewController.swift b/examples/Swift SDK/Swift SDK/ImplicitGrantExampleViewController.swift new file mode 100644 index 00000000..5943b40a --- /dev/null +++ b/examples/Swift SDK/Swift SDK/ImplicitGrantExampleViewController.swift @@ -0,0 +1,90 @@ +// +// ImplicitGrantExampleViewController.swift +// Swift SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import UberRides +import CoreLocation + +/// This class demonstrates how do use the LoginManager to complete Implicit Grant Authorization +class ImplicitGrantExampleViewController: UIViewController { + /// The LoginManager to use for login + let loginManager = LoginManager() + + let loginButton = UIButton(type: .RoundedRect) + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = UIColor.whiteColor() + self.navigationItem.title = "Implicit Grant / Login Manager" + + self.setupLoginButton() + } + + /** + Sets up the login button + */ + private func setupLoginButton() { + // Using autolayout + loginButton.setTitle("Login", forState: .Normal) + loginButton.sizeToFit() + loginButton.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(loginButton) + + // Center the button in the view + let centerXConstraint = NSLayoutConstraint(item: loginButton, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0) + let centerYConstraint = NSLayoutConstraint(item: loginButton, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0) + + self.view.addConstraints([ centerXConstraint, centerYConstraint ]) + + // Add our login action + loginButton.addTarget(self, action: Selector("loginButtonAction:"), forControlEvents: UIControlEvents.TouchUpInside) + } + + func loginButtonAction(button: UIButton) { + // Define which scopes we're requesting + // Need to be authorized on your developer dashboard at developer.uber.com + let requestedScopes = [ RidesScope.RideWidgets, RidesScope.Profile, RidesScope.Places ] + // Use your loginManager to login with the requested scopes, viewcontroller to present over, and completion block + loginManager.login(requestedScopes: requestedScopes, presentingViewController: self) { (accessToken, error) -> () in + if accessToken != nil { + //Success! AccessToken is automatically saved in keychain + self.showMessage("Got an AccessToken!") + } else { + // Error + if let error = error { + self.showMessage(error.localizedDescription) + } else { + self.showMessage("An Unknown Error Occured") + } + } + } + } + + private func showMessage(message: String) { + let alert = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.Alert) + let okayAction = UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default, handler: nil) + alert.addAction(okayAction) + self.presentViewController(alert, animated: true, completion: nil) + } +} diff --git a/examples/Swift SDK/Swift SDK/Info.plist b/examples/Swift SDK/Swift SDK/Info.plist index a0f6913a..bf20adf7 100644 --- a/examples/Swift SDK/Swift SDK/Info.plist +++ b/examples/Swift SDK/Swift SDK/Info.plist @@ -40,5 +40,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UberClientID + YOUR_CLIENT_ID + UberCallbackURI + callback://your_callback_uri + NSLocationWhenInUseUsageDescription + We use your location to make specifying a pickup spot easy as pie diff --git a/examples/Swift SDK/Swift SDK/RideRequestWidgetExampleViewController.swift b/examples/Swift SDK/Swift SDK/RideRequestWidgetExampleViewController.swift new file mode 100644 index 00000000..9f618979 --- /dev/null +++ b/examples/Swift SDK/Swift SDK/RideRequestWidgetExampleViewController.swift @@ -0,0 +1,132 @@ +// +// RideRequestWidgetExampleViewController.swift +// Swift SDK +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import UberRides +import CoreLocation + +/// This class provides an example of how to use the RideRequestButton to initiate +/// a ride request using the Ride Request Widget +class RideRequestWidgetExampleViewController: UIViewController { + /// The RideRequestButton instance + let rideRequestButton = RideRequestButton() + /// Location manger for getting user location + let locationManger:CLLocationManager = CLLocationManager() + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = UIColor.whiteColor() + self.navigationItem.title = "Ride Request Widget" + + // Create a RideRequestViewRequestingBehavior for the RideRequestButton + let requestBehavior = RideRequestViewRequestingBehavior(presentingViewController: self) + + // Optionally subscribe to the ModalRideRequestViewController delegate + requestBehavior.modalRideRequestViewController.delegate = self + // Set the RideRequestButton behavior + rideRequestButton.requestBehavior = requestBehavior + + // Subscribe to the CLLocationManager location updates + locationManger.delegate = self + + setupRideRequestButton() + + //Check location authorization + if !checkLocationServices() { + locationManger.requestWhenInUseAuthorization() + } + } + + /** + Sets up the RideRequestButton + */ + private func setupRideRequestButton() { + // Using autolayout + rideRequestButton.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(rideRequestButton) + + // Center the button in the view + let centerXConstraint = NSLayoutConstraint(item: rideRequestButton, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0) + let centerYConstraint = NSLayoutConstraint(item: rideRequestButton, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0) + + self.view.addConstraints([ centerXConstraint, centerYConstraint ]) + + // Setup our RideParameters. This button will be using the users current location + let parameterBuilder = RideParametersBuilder() + parameterBuilder.setPickupToCurrentLocation() + let rideParameters = parameterBuilder.build() + rideRequestButton.rideParameters = rideParameters + } + + private func checkLocationServices() -> Bool { + let locationEnabled = CLLocationManager.locationServicesEnabled() + let locationAuthorization = CLLocationManager.authorizationStatus() + let locationAuthorized = locationAuthorization == .AuthorizedWhenInUse || locationAuthorization == .AuthorizedAlways + + return locationEnabled && locationAuthorized + } + + private func showMessage(message: String) { + let alert = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.Alert) + let okayAction = UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default, handler: nil) + alert.addAction(okayAction) + self.presentViewController(alert, animated: true, completion: nil) + } + +} + +//MARK: ModalViewControllerDelegate + +extension RideRequestWidgetExampleViewController : ModalViewControllerDelegate { + // Fired when the modal is dismissed + func modalViewControllerDidDismiss(modalViewController: ModalViewController) { + print("did dismiss") + } + + // Fired right before the modal dismisses + func modalViewControllerWillDismiss(modalViewController: ModalViewController) { + print("will dismiss") + } +} + +//MARK: CLLocationManagerDelegate + +extension RideRequestWidgetExampleViewController : CLLocationManagerDelegate { + + func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { + if status == .Denied || status == .Restricted { + showMessage("Location Services disabled.") + } + } + + func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + locationManger.stopUpdatingLocation() + } + + func locationManager(manager: CLLocationManager, didFailWithError error: NSError) { + locationManger.stopUpdatingLocation() + showMessage("There was an error locating you.") + } +} \ No newline at end of file diff --git a/examples/Swift SDK/Swift SDK/hi-IN.lproj/LaunchScreen.strings b/examples/Swift SDK/Swift SDK/hi-IN.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Swift SDK/Swift SDK/hi-IN.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Swift SDK/Swift SDK/hi-IN.lproj/Main.strings b/examples/Swift SDK/Swift SDK/hi-IN.lproj/Main.strings new file mode 100644 index 00000000..1532930f --- /dev/null +++ b/examples/Swift SDK/Swift SDK/hi-IN.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "Baz-H6-Gak"; */ +"Baz-H6-Gak.title" = "Root View Controller"; diff --git a/examples/Swift SDK/Swift SDK/zh-Hans.lproj/LaunchScreen.strings b/examples/Swift SDK/Swift SDK/zh-Hans.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Swift SDK/Swift SDK/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Swift SDK/Swift SDK/zh-Hans.lproj/Main.strings b/examples/Swift SDK/Swift SDK/zh-Hans.lproj/Main.strings new file mode 100644 index 00000000..1532930f --- /dev/null +++ b/examples/Swift SDK/Swift SDK/zh-Hans.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "Baz-H6-Gak"; */ +"Baz-H6-Gak.title" = "Root View Controller"; diff --git a/examples/Swift SDK/Swift SDK/zh-Hant.lproj/LaunchScreen.strings b/examples/Swift SDK/Swift SDK/zh-Hant.lproj/LaunchScreen.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/Swift SDK/Swift SDK/zh-Hant.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/examples/Swift SDK/Swift SDK/zh-Hant.lproj/Main.strings b/examples/Swift SDK/Swift SDK/zh-Hant.lproj/Main.strings new file mode 100644 index 00000000..1532930f --- /dev/null +++ b/examples/Swift SDK/Swift SDK/zh-Hant.lproj/Main.strings @@ -0,0 +1,3 @@ + +/* Class = "UINavigationItem"; title = "Root View Controller"; ObjectID = "Baz-H6-Gak"; */ +"Baz-H6-Gak.title" = "Root View Controller"; diff --git a/source/Podfile b/source/Podfile new file mode 100644 index 00000000..a97622f0 --- /dev/null +++ b/source/Podfile @@ -0,0 +1,6 @@ +use_frameworks! + +pod 'ObjectMapper', '~> 1.1.0' + +target 'UberRidesTests' do +end diff --git a/source/Podfile.lock b/source/Podfile.lock new file mode 100644 index 00000000..8d0c2ba1 --- /dev/null +++ b/source/Podfile.lock @@ -0,0 +1,10 @@ +PODS: + - ObjectMapper (1.1.1) + +DEPENDENCIES: + - ObjectMapper (~> 1.1.0) + +SPEC CHECKSUMS: + ObjectMapper: 4143abed1dbd49b73d75092fe8fa1c88ed6434b7 + +COCOAPODS: 0.39.0 diff --git a/source/UberRides.xcodeproj/project.pbxproj b/source/UberRides.xcodeproj/project.pbxproj index c6ff71ee..362f27fe 100644 --- a/source/UberRides.xcodeproj/project.pbxproj +++ b/source/UberRides.xcodeproj/project.pbxproj @@ -7,18 +7,63 @@ objects = { /* Begin PBXBuildFile section */ + 64E80CE6D9A3D70ECDDFE7C3 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01CD6B39D5C3EB668427CE75 /* Pods.framework */; }; AC0404791BFACD1D00AC1501 /* UberRides.h in Headers */ = {isa = PBXBuildFile; fileRef = AC0404781BFACD1D00AC1501 /* UberRides.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC0404801BFACD1D00AC1501 /* UberRides.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC0404751BFACD1D00AC1501 /* UberRides.framework */; }; AC0404851BFACD1D00AC1501 /* UberRidesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404841BFACD1D00AC1501 /* UberRidesTests.swift */; }; - AC0404901BFACD6000AC1501 /* ColorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC04048F1BFACD6000AC1501 /* ColorUtil.swift */; }; - AC0404921BFACD7C00AC1501 /* RequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404911BFACD7C00AC1501 /* RequestButton.swift */; }; AC0404941BFACDAF00AC1501 /* RequestDeeplink.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404931BFACDAF00AC1501 /* RequestDeeplink.swift */; }; AC0404961BFACDC900AC1501 /* RidesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404951BFACDC900AC1501 /* RidesClient.swift */; }; AC04049A1BFACE5400AC1501 /* RequestDeeplinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0404991BFACE5400AC1501 /* RequestDeeplinkTests.swift */; }; - D8A196BB1C56FC250050A264 /* en.lproj in Resources */ = {isa = PBXBuildFile; fileRef = D8A196B71C56FC250050A264 /* en.lproj */; }; + AFD4AC45A3364734152F3911 /* Pods_UberRidesTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B095F1A046D6ABD97CBBFE1 /* Pods_UberRidesTests.framework */; }; + D81A9EC91C6A6C3A000F33F9 /* RidesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A9EC81C6A6C3A000F33F9 /* RidesError.swift */; }; + D829FD9C1C9A3E5200AC6578 /* RideRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829FD9B1C9A3E5200AC6578 /* RideRequestView.swift */; }; + D829FD9E1C9A3E5800AC6578 /* EndpointsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D829FD9D1C9A3E5800AC6578 /* EndpointsManager.swift */; }; + D844751B1C5B01FB00B03456 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D844751A1C5B01FB00B03456 /* Request.swift */; }; + D885CD3C1CADEF1E0055976D /* UberButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885CD3B1CADEF1E0055976D /* UberButton.swift */; }; + D89338F11C77CAE0005B5486 /* OAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89338F01C77CAE0005B5486 /* OAuthTests.swift */; }; + D8A0D71D1C72E3C400707DC6 /* RidesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A0D71C1C72E3C400707DC6 /* RidesClientTests.swift */; }; + D8A0D7231C73A74700707DC6 /* RidesScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A0D7221C73A74700707DC6 /* RidesScope.swift */; }; D8A196BC1C56FC250050A264 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D8A196B81C56FC250050A264 /* Media.xcassets */; }; - D8A196BD1C56FC250050A264 /* zh-Hans.lproj in Resources */ = {isa = PBXBuildFile; fileRef = D8A196B91C56FC250050A264 /* zh-Hans.lproj */; }; - D8A196BE1C56FC250050A264 /* zh-Hant.lproj in Resources */ = {isa = PBXBuildFile; fileRef = D8A196BA1C56FC250050A264 /* zh-Hant.lproj */; }; + D8A93F0D1C6E62D800AA5E58 /* RidesUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A93F0C1C6E62D800AA5E58 /* RidesUtil.swift */; }; + D8B49D7C1C72663F00B687D5 /* RequestButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B49D7B1C72663F00B687D5 /* RequestButtonTests.swift */; }; + D8B757841CA3485600D5025C /* RideRequestViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B757831CA3485600D5025C /* RideRequestViewTests.swift */; }; + D8BF670E1CA4BE0700FAEA30 /* RideRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BF670D1CA4BE0700FAEA30 /* RideRequestButton.swift */; }; + D8BF67101CA4BE0E00FAEA30 /* RideRequestingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BF670F1CA4BE0E00FAEA30 /* RideRequestingProtocol.swift */; }; + D8CC80FE1C7D3AA300385AD5 /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CC80FA1C7D3AA300385AD5 /* KeychainWrapper.swift */; }; + D8CC80FF1C7D3AA300385AD5 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CC80FB1C7D3AA300385AD5 /* LoginManager.swift */; }; + D8CC81001C7D3AA300385AD5 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CC80FC1C7D3AA300385AD5 /* AccessToken.swift */; }; + D8CC81011C7D3AA300385AD5 /* OAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CC80FD1C7D3AA300385AD5 /* OAuthViewController.swift */; }; + D8DAB6371C60240B007DE82C /* ModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DAB6361C60240B007DE82C /* ModelMapper.swift */; }; + DC1039731C96B1CE004854E3 /* RidesScopeExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1039721C96B1CE004854E3 /* RidesScopeExtensionsTests.swift */; }; + DC2730EF1CBC46520044AB04 /* RidesMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2730EE1CBC46520044AB04 /* RidesMocks.swift */; }; + DC2C77911CACDDD000A052BA /* ModalRideRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C77901CACDDD000A052BA /* ModalRideRequestViewController.swift */; }; + DC75DFE81CA51DB000071417 /* RideRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75DFE71CA51DB000071417 /* RideRequestViewController.swift */; }; + DC75DFEC1CA5EC1200071417 /* RideParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75DFEB1CA5EC1200071417 /* RideParameters.swift */; }; + DC75DFEE1CA5F5EB00071417 /* RideParametersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75DFED1CA5F5EB00071417 /* RideParametersTest.swift */; }; + DC75DFF01CA6282500071417 /* DeeplinkRequestingBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75DFEF1CA6282500071417 /* DeeplinkRequestingBehavior.swift */; }; + DC75E0301CA9EDBB00071417 /* RideRequestViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75E02F1CA9EDBB00071417 /* RideRequestViewControllerTests.swift */; }; + DC75E0321CAA179600071417 /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75E0311CAA179600071417 /* ModalViewController.swift */; }; + DC75E0341CAAEA5700071417 /* ModalViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC75E0331CAAEA5700071417 /* ModalViewControllerTests.swift */; }; + DC78EC741CB2F66500850814 /* LocalizationUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78EC731CB2F66500850814 /* LocalizationUtilTests.swift */; }; + DC78ECF71CB4877700850814 /* RidesAuthenticationErrorFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78ECF61CB4877700850814 /* RidesAuthenticationErrorFactoryTests.swift */; }; + DC78ED0B1CB4F22D00850814 /* RideRequestViewErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78ED0A1CB4F22D00850814 /* RideRequestViewErrorType.swift */; }; + DC78ED0F1CB502BF00850814 /* RideRequestViewErrorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78ED0E1CB502BF00850814 /* RideRequestViewErrorFactory.swift */; }; + DC78ED111CB50ED600850814 /* RideRequestViewErrorFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78ED101CB50ED600850814 /* RideRequestViewErrorFactoryTests.swift */; }; + DC78ED131CB578D100850814 /* RidesScopeFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC78ED121CB578D100850814 /* RidesScopeFactoryTests.swift */; }; + DC8E2AD01C9AA17400EDD74B /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E2ACF1C9AA17400EDD74B /* Configuration.swift */; }; + DC8E2AD21C9B19ED00EDD74B /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E2AD11C9B19ED00EDD74B /* ConfigurationTests.swift */; }; + DC8E2AD41C9B268600EDD74B /* testInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = DC8E2AD31C9B268600EDD74B /* testInfo.plist */; }; + DC8FC3631CBDF4D000D58839 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DC8FC3611CBDF4D000D58839 /* Localizable.strings */; }; + DC8FC3761CBE137A00D58839 /* LoginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8FC3751CBE137A00D58839 /* LoginManagerTests.swift */; }; + DC92DBFB1CA20D1C001A0DCC /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC92DBFA1CA20D1C001A0DCC /* TokenManager.swift */; }; + DC92DBFD1CA21379001A0DCC /* TokenManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC92DBFC1CA21379001A0DCC /* TokenManagerTests.swift */; }; + DC92DC001CA32CB7001A0DCC /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC92DBFF1CA32CB7001A0DCC /* LoginView.swift */; }; + DC92DC041CA38DBE001A0DCC /* WidgetsEndpointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC92DC031CA38DBE001A0DCC /* WidgetsEndpointTests.swift */; }; + DC92DC061CA3AC59001A0DCC /* OauthEndpointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC92DC051CA3AC59001A0DCC /* OauthEndpointTests.swift */; }; + DCB0D38E1CAD9D5800194DD5 /* RideRequestViewRequestingBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB0D38D1CAD9D5800194DD5 /* RideRequestViewRequestingBehavior.swift */; }; + DCB0D3901CADAA6300194DD5 /* RideRequestViewRequestingBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB0D38F1CADAA6300194DD5 /* RideRequestViewRequestingBehaviorTests.swift */; }; + DCED60F21C9724D4001A65E0 /* AccessTokenFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCED60F11C9724D4001A65E0 /* AccessTokenFactory.swift */; }; + DCED60F61C9770D9001A65E0 /* AccessTokenFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCED60F51C9770D9001A65E0 /* AccessTokenFactoryTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -32,21 +77,74 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 01CD6B39D5C3EB668427CE75 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D40D6AA81C808FAABB243C9 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 3B095F1A046D6ABD97CBBFE1 /* Pods_UberRidesTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UberRidesTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EA8F073A2E428216A862688 /* Pods-UberRidesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UberRidesTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-UberRidesTests/Pods-UberRidesTests.debug.xcconfig"; sourceTree = ""; }; AC0404751BFACD1D00AC1501 /* UberRides.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UberRides.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AC0404781BFACD1D00AC1501 /* UberRides.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UberRides.h; sourceTree = ""; }; AC04047A1BFACD1D00AC1501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC04047F1BFACD1D00AC1501 /* UberRidesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UberRidesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AC0404841BFACD1D00AC1501 /* UberRidesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UberRidesTests.swift; sourceTree = ""; }; AC0404861BFACD1D00AC1501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AC04048F1BFACD6000AC1501 /* ColorUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorUtil.swift; sourceTree = ""; }; - AC0404911BFACD7C00AC1501 /* RequestButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestButton.swift; sourceTree = ""; }; AC0404931BFACDAF00AC1501 /* RequestDeeplink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestDeeplink.swift; sourceTree = ""; }; AC0404951BFACDC900AC1501 /* RidesClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesClient.swift; sourceTree = ""; }; AC0404991BFACE5400AC1501 /* RequestDeeplinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestDeeplinkTests.swift; sourceTree = ""; }; - D8A196B71C56FC250050A264 /* en.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; name = en.lproj; path = Resources/en.lproj; sourceTree = ""; }; + C1C60712D3DA8E230F9B36D2 /* Pods-UberRidesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UberRidesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-UberRidesTests/Pods-UberRidesTests.release.xcconfig"; sourceTree = ""; }; + D81A9EC81C6A6C3A000F33F9 /* RidesError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RidesError.swift; path = Model/RidesError.swift; sourceTree = ""; }; + D829FD9B1C9A3E5200AC6578 /* RideRequestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestView.swift; sourceTree = ""; }; + D829FD9D1C9A3E5800AC6578 /* EndpointsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndpointsManager.swift; sourceTree = ""; }; + D844751A1C5B01FB00B03456 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + D885CD3B1CADEF1E0055976D /* UberButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UberButton.swift; sourceTree = ""; }; + D89338F01C77CAE0005B5486 /* OAuthTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthTests.swift; sourceTree = ""; }; + D8A0D71C1C72E3C400707DC6 /* RidesClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesClientTests.swift; sourceTree = ""; }; + D8A0D7221C73A74700707DC6 /* RidesScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RidesScope.swift; path = Model/RidesScope.swift; sourceTree = ""; }; D8A196B81C56FC250050A264 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Media.xcassets; path = Resources/Media.xcassets; sourceTree = ""; }; - D8A196B91C56FC250050A264 /* zh-Hans.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "zh-Hans.lproj"; path = "Resources/zh-Hans.lproj"; sourceTree = ""; }; - D8A196BA1C56FC250050A264 /* zh-Hant.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "zh-Hant.lproj"; path = "Resources/zh-Hant.lproj"; sourceTree = ""; }; + D8A93F0C1C6E62D800AA5E58 /* RidesUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesUtil.swift; sourceTree = ""; }; + D8B49D7B1C72663F00B687D5 /* RequestButtonTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestButtonTests.swift; sourceTree = ""; }; + D8B757831CA3485600D5025C /* RideRequestViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewTests.swift; sourceTree = ""; }; + D8BF670D1CA4BE0700FAEA30 /* RideRequestButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestButton.swift; sourceTree = ""; }; + D8BF670F1CA4BE0E00FAEA30 /* RideRequestingProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestingProtocol.swift; sourceTree = ""; }; + D8CC80FA1C7D3AA300385AD5 /* KeychainWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = KeychainWrapper.swift; path = OAuth/KeychainWrapper.swift; sourceTree = ""; }; + D8CC80FB1C7D3AA300385AD5 /* LoginManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginManager.swift; path = OAuth/LoginManager.swift; sourceTree = ""; }; + D8CC80FC1C7D3AA300385AD5 /* AccessToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AccessToken.swift; path = OAuth/AccessToken.swift; sourceTree = ""; }; + D8CC80FD1C7D3AA300385AD5 /* OAuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OAuthViewController.swift; path = OAuth/OAuthViewController.swift; sourceTree = ""; }; + D8DAB6361C60240B007DE82C /* ModelMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelMapper.swift; sourceTree = ""; }; + DC1039721C96B1CE004854E3 /* RidesScopeExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesScopeExtensionsTests.swift; sourceTree = ""; }; + DC2730EE1CBC46520044AB04 /* RidesMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesMocks.swift; sourceTree = ""; }; + DC2C77901CACDDD000A052BA /* ModalRideRequestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalRideRequestViewController.swift; sourceTree = ""; }; + DC75DFE71CA51DB000071417 /* RideRequestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewController.swift; sourceTree = ""; }; + DC75DFEB1CA5EC1200071417 /* RideParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RideParameters.swift; path = Model/RideParameters.swift; sourceTree = ""; }; + DC75DFED1CA5F5EB00071417 /* RideParametersTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideParametersTest.swift; sourceTree = ""; }; + DC75DFEF1CA6282500071417 /* DeeplinkRequestingBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeeplinkRequestingBehavior.swift; sourceTree = ""; }; + DC75E02D1CA98B3200071417 /* RequestURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestURLBuilder.swift; sourceTree = ""; }; + DC75E02F1CA9EDBB00071417 /* RideRequestViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewControllerTests.swift; sourceTree = ""; }; + DC75E0311CAA179600071417 /* ModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalViewController.swift; sourceTree = ""; }; + DC75E0331CAAEA5700071417 /* ModalViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalViewControllerTests.swift; sourceTree = ""; }; + DC78EC731CB2F66500850814 /* LocalizationUtilTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationUtilTests.swift; sourceTree = ""; }; + DC78ECF61CB4877700850814 /* RidesAuthenticationErrorFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesAuthenticationErrorFactoryTests.swift; sourceTree = ""; }; + DC78ED0A1CB4F22D00850814 /* RideRequestViewErrorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewErrorType.swift; sourceTree = ""; }; + DC78ED0E1CB502BF00850814 /* RideRequestViewErrorFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewErrorFactory.swift; sourceTree = ""; }; + DC78ED101CB50ED600850814 /* RideRequestViewErrorFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewErrorFactoryTests.swift; sourceTree = ""; }; + DC78ED121CB578D100850814 /* RidesScopeFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RidesScopeFactoryTests.swift; sourceTree = ""; }; + DC8E2ACF1C9AA17400EDD74B /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + DC8E2AD11C9B19ED00EDD74B /* ConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; + DC8E2AD31C9B268600EDD74B /* testInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = testInfo.plist; sourceTree = ""; }; + DC8FC3621CBDF4D000D58839 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/Localizable.strings; sourceTree = ""; }; + DC8FC3641CBDF4E800D58839 /* hi-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hi-IN"; path = "Resources/hi-IN.lproj/Localizable.strings"; sourceTree = ""; }; + DC8FC3651CBDF52200D58839 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Resources/zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + DC8FC3661CBDF52C00D58839 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "Resources/zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; + DC8FC3751CBE137A00D58839 /* LoginManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginManagerTests.swift; sourceTree = ""; }; + DC92DBFA1CA20D1C001A0DCC /* TokenManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenManager.swift; sourceTree = ""; }; + DC92DBFC1CA21379001A0DCC /* TokenManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenManagerTests.swift; sourceTree = ""; }; + DC92DBFF1CA32CB7001A0DCC /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginView.swift; path = OAuth/LoginView.swift; sourceTree = ""; }; + DC92DC031CA38DBE001A0DCC /* WidgetsEndpointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetsEndpointTests.swift; sourceTree = ""; }; + DC92DC051CA3AC59001A0DCC /* OauthEndpointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OauthEndpointTests.swift; sourceTree = ""; }; + DCB0D38D1CAD9D5800194DD5 /* RideRequestViewRequestingBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewRequestingBehavior.swift; sourceTree = ""; }; + DCB0D38F1CADAA6300194DD5 /* RideRequestViewRequestingBehaviorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideRequestViewRequestingBehaviorTests.swift; sourceTree = ""; }; + DCED60F11C9724D4001A65E0 /* AccessTokenFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AccessTokenFactory.swift; path = OAuth/AccessTokenFactory.swift; sourceTree = ""; }; + DCED60F51C9770D9001A65E0 /* AccessTokenFactoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessTokenFactoryTests.swift; sourceTree = ""; }; + EA01FB909C9A04DC5D83A4D9 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,6 +152,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 64E80CE6D9A3D70ECDDFE7C3 /* Pods.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -62,6 +161,7 @@ buildActionMask = 2147483647; files = ( AC0404801BFACD1D00AC1501 /* UberRides.framework in Frameworks */, + AFD4AC45A3364734152F3911 /* Pods_UberRidesTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,6 +174,8 @@ AC0404771BFACD1D00AC1501 /* UberRides */, AC0404831BFACD1D00AC1501 /* UberRidesTests */, AC0404761BFACD1D00AC1501 /* Products */, + E0C87DA8C44B500B36E07963 /* Pods */, + F036511333B3B5067F8B6DC8 /* Frameworks */, ); sourceTree = ""; }; @@ -89,13 +191,30 @@ AC0404771BFACD1D00AC1501 /* UberRides */ = { isa = PBXGroup; children = ( + D8CC80F91C7D3A5800385AD5 /* OAuth */, + D8874F271C5ABB780098DF99 /* Model */, D8A196B61C56FC1A0050A264 /* Resources */, AC0404781BFACD1D00AC1501 /* UberRides.h */, AC04047A1BFACD1D00AC1501 /* Info.plist */, - AC04048F1BFACD6000AC1501 /* ColorUtil.swift */, - AC0404911BFACD7C00AC1501 /* RequestButton.swift */, + D8A93F0C1C6E62D800AA5E58 /* RidesUtil.swift */, AC0404931BFACDAF00AC1501 /* RequestDeeplink.swift */, AC0404951BFACDC900AC1501 /* RidesClient.swift */, + D844751A1C5B01FB00B03456 /* Request.swift */, + D885CD3B1CADEF1E0055976D /* UberButton.swift */, + D8BF670D1CA4BE0700FAEA30 /* RideRequestButton.swift */, + D8BF670F1CA4BE0E00FAEA30 /* RideRequestingProtocol.swift */, + D829FD9D1C9A3E5800AC6578 /* EndpointsManager.swift */, + D829FD9B1C9A3E5200AC6578 /* RideRequestView.swift */, + DC78ED0A1CB4F22D00850814 /* RideRequestViewErrorType.swift */, + DC78ED0E1CB502BF00850814 /* RideRequestViewErrorFactory.swift */, + DC8E2ACF1C9AA17400EDD74B /* Configuration.swift */, + DC92DBFA1CA20D1C001A0DCC /* TokenManager.swift */, + DC75E0311CAA179600071417 /* ModalViewController.swift */, + DC75DFEF1CA6282500071417 /* DeeplinkRequestingBehavior.swift */, + DC75E02D1CA98B3200071417 /* RequestURLBuilder.swift */, + DC75DFE71CA51DB000071417 /* RideRequestViewController.swift */, + DC2C77901CACDDD000A052BA /* ModalRideRequestViewController.swift */, + DCB0D38D1CAD9D5800194DD5 /* RideRequestViewRequestingBehavior.swift */, ); path = UberRides; sourceTree = ""; @@ -103,24 +222,95 @@ AC0404831BFACD1D00AC1501 /* UberRidesTests */ = { isa = PBXGroup; children = ( + D8874F2C1C5AC52B0098DF99 /* Test Data */, AC0404841BFACD1D00AC1501 /* UberRidesTests.swift */, AC0404991BFACE5400AC1501 /* RequestDeeplinkTests.swift */, + D8B49D7B1C72663F00B687D5 /* RequestButtonTests.swift */, AC0404861BFACD1D00AC1501 /* Info.plist */, + D8A0D71C1C72E3C400707DC6 /* RidesClientTests.swift */, + D89338F01C77CAE0005B5486 /* OAuthTests.swift */, + DC8E2AD11C9B19ED00EDD74B /* ConfigurationTests.swift */, + DC1039721C96B1CE004854E3 /* RidesScopeExtensionsTests.swift */, + DC92DBFC1CA21379001A0DCC /* TokenManagerTests.swift */, + D8B757831CA3485600D5025C /* RideRequestViewTests.swift */, + DCED60F51C9770D9001A65E0 /* AccessTokenFactoryTests.swift */, + DC92DC031CA38DBE001A0DCC /* WidgetsEndpointTests.swift */, + DC92DC051CA3AC59001A0DCC /* OauthEndpointTests.swift */, + DC75DFED1CA5F5EB00071417 /* RideParametersTest.swift */, + DC75E0331CAAEA5700071417 /* ModalViewControllerTests.swift */, + DC75E02F1CA9EDBB00071417 /* RideRequestViewControllerTests.swift */, + DCB0D38F1CADAA6300194DD5 /* RideRequestViewRequestingBehaviorTests.swift */, + DC78EC731CB2F66500850814 /* LocalizationUtilTests.swift */, + DC78ECF61CB4877700850814 /* RidesAuthenticationErrorFactoryTests.swift */, + DC78ED101CB50ED600850814 /* RideRequestViewErrorFactoryTests.swift */, + DC78ED121CB578D100850814 /* RidesScopeFactoryTests.swift */, + DC2730EE1CBC46520044AB04 /* RidesMocks.swift */, + DC8FC3751CBE137A00D58839 /* LoginManagerTests.swift */, ); path = UberRidesTests; sourceTree = ""; }; + D8874F271C5ABB780098DF99 /* Model */ = { + isa = PBXGroup; + children = ( + D81A9EC81C6A6C3A000F33F9 /* RidesError.swift */, + D8A0D7221C73A74700707DC6 /* RidesScope.swift */, + D8DAB6361C60240B007DE82C /* ModelMapper.swift */, + DC75DFEB1CA5EC1200071417 /* RideParameters.swift */, + ); + name = Model; + sourceTree = ""; + }; + D8874F2C1C5AC52B0098DF99 /* Test Data */ = { + isa = PBXGroup; + children = ( + DC8E2AD31C9B268600EDD74B /* testInfo.plist */, + ); + name = "Test Data"; + sourceTree = ""; + }; D8A196B61C56FC1A0050A264 /* Resources */ = { isa = PBXGroup; children = ( - D8A196B71C56FC250050A264 /* en.lproj */, + DC8FC3611CBDF4D000D58839 /* Localizable.strings */, D8A196B81C56FC250050A264 /* Media.xcassets */, - D8A196B91C56FC250050A264 /* zh-Hans.lproj */, - D8A196BA1C56FC250050A264 /* zh-Hant.lproj */, ); name = Resources; sourceTree = ""; }; + D8CC80F91C7D3A5800385AD5 /* OAuth */ = { + isa = PBXGroup; + children = ( + D8CC80FA1C7D3AA300385AD5 /* KeychainWrapper.swift */, + D8CC80FB1C7D3AA300385AD5 /* LoginManager.swift */, + D8CC80FC1C7D3AA300385AD5 /* AccessToken.swift */, + D8CC80FD1C7D3AA300385AD5 /* OAuthViewController.swift */, + DCED60F11C9724D4001A65E0 /* AccessTokenFactory.swift */, + DC92DBFF1CA32CB7001A0DCC /* LoginView.swift */, + ); + name = OAuth; + sourceTree = ""; + }; + E0C87DA8C44B500B36E07963 /* Pods */ = { + isa = PBXGroup; + children = ( + EA01FB909C9A04DC5D83A4D9 /* Pods.debug.xcconfig */, + 0D40D6AA81C808FAABB243C9 /* Pods.release.xcconfig */, + 8EA8F073A2E428216A862688 /* Pods-UberRidesTests.debug.xcconfig */, + C1C60712D3DA8E230F9B36D2 /* Pods-UberRidesTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F036511333B3B5067F8B6DC8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 01CD6B39D5C3EB668427CE75 /* Pods.framework */, + 3B095F1A046D6ABD97CBBFE1 /* Pods_UberRidesTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -139,10 +329,12 @@ isa = PBXNativeTarget; buildConfigurationList = AC0404891BFACD1D00AC1501 /* Build configuration list for PBXNativeTarget "UberRides" */; buildPhases = ( + 410850AB00DCBB328EF1EF77 /* Check Pods Manifest.lock */, AC0404701BFACD1D00AC1501 /* Sources */, AC0404711BFACD1D00AC1501 /* Frameworks */, AC0404721BFACD1D00AC1501 /* Headers */, AC0404731BFACD1D00AC1501 /* Resources */, + 184D28F223EAC57721910BFB /* Copy Pods Resources */, ); buildRules = ( ); @@ -157,9 +349,12 @@ isa = PBXNativeTarget; buildConfigurationList = AC04048C1BFACD1D00AC1501 /* Build configuration list for PBXNativeTarget "UberRidesTests" */; buildPhases = ( + E8D9F26AA19446CC6F16BCD2 /* Check Pods Manifest.lock */, AC04047B1BFACD1D00AC1501 /* Sources */, AC04047C1BFACD1D00AC1501 /* Frameworks */, AC04047D1BFACD1D00AC1501 /* Resources */, + 6C3E62C4B0D1F2C541A9F192 /* Embed Pods Frameworks */, + C50596200B272FCB2440C40B /* Copy Pods Resources */, ); buildRules = ( ); @@ -177,7 +372,7 @@ AC04046C1BFACD1D00AC1501 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0710; + LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 0710; ORGANIZATIONNAME = Uber; TargetAttributes = { @@ -196,6 +391,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + "hi-IN", "zh-Hans", "zh-Hant", ); @@ -215,10 +411,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D8A196BE1C56FC250050A264 /* zh-Hant.lproj in Resources */, D8A196BC1C56FC250050A264 /* Media.xcassets in Resources */, - D8A196BD1C56FC250050A264 /* zh-Hans.lproj in Resources */, - D8A196BB1C56FC250050A264 /* en.lproj in Resources */, + DC8FC3631CBDF4D000D58839 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,20 +420,123 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC8E2AD41C9B268600EDD74B /* testInfo.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 184D28F223EAC57721910BFB /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 410850AB00DCBB328EF1EF77 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 6C3E62C4B0D1F2C541A9F192 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-UberRidesTests/Pods-UberRidesTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C50596200B272FCB2440C40B /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-UberRidesTests/Pods-UberRidesTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E8D9F26AA19446CC6F16BCD2 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ AC0404701BFACD1D00AC1501 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AC0404901BFACD6000AC1501 /* ColorUtil.swift in Sources */, + DC92DC001CA32CB7001A0DCC /* LoginView.swift in Sources */, AC0404941BFACDAF00AC1501 /* RequestDeeplink.swift in Sources */, + D8CC81001C7D3AA300385AD5 /* AccessToken.swift in Sources */, + D844751B1C5B01FB00B03456 /* Request.swift in Sources */, + D8CC81011C7D3AA300385AD5 /* OAuthViewController.swift in Sources */, + DC8E2AD01C9AA17400EDD74B /* Configuration.swift in Sources */, + DC75DFE81CA51DB000071417 /* RideRequestViewController.swift in Sources */, + D8A93F0D1C6E62D800AA5E58 /* RidesUtil.swift in Sources */, AC0404961BFACDC900AC1501 /* RidesClient.swift in Sources */, - AC0404921BFACD7C00AC1501 /* RequestButton.swift in Sources */, + DC78ED0B1CB4F22D00850814 /* RideRequestViewErrorType.swift in Sources */, + DC75DFEC1CA5EC1200071417 /* RideParameters.swift in Sources */, + DC2C77911CACDDD000A052BA /* ModalRideRequestViewController.swift in Sources */, + D8A0D7231C73A74700707DC6 /* RidesScope.swift in Sources */, + DCB0D38E1CAD9D5800194DD5 /* RideRequestViewRequestingBehavior.swift in Sources */, + DC78ED0F1CB502BF00850814 /* RideRequestViewErrorFactory.swift in Sources */, + D8DAB6371C60240B007DE82C /* ModelMapper.swift in Sources */, + D829FD9E1C9A3E5800AC6578 /* EndpointsManager.swift in Sources */, + DC92DBFB1CA20D1C001A0DCC /* TokenManager.swift in Sources */, + DCED60F21C9724D4001A65E0 /* AccessTokenFactory.swift in Sources */, + D81A9EC91C6A6C3A000F33F9 /* RidesError.swift in Sources */, + DC75E0321CAA179600071417 /* ModalViewController.swift in Sources */, + D8BF67101CA4BE0E00FAEA30 /* RideRequestingProtocol.swift in Sources */, + D8CC80FF1C7D3AA300385AD5 /* LoginManager.swift in Sources */, + D8BF670E1CA4BE0700FAEA30 /* RideRequestButton.swift in Sources */, + D829FD9C1C9A3E5200AC6578 /* RideRequestView.swift in Sources */, + D8CC80FE1C7D3AA300385AD5 /* KeychainWrapper.swift in Sources */, + D885CD3C1CADEF1E0055976D /* UberButton.swift in Sources */, + DC75DFF01CA6282500071417 /* DeeplinkRequestingBehavior.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -247,8 +544,28 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D8A0D71D1C72E3C400707DC6 /* RidesClientTests.swift in Sources */, + DC78ED111CB50ED600850814 /* RideRequestViewErrorFactoryTests.swift in Sources */, + DC8E2AD21C9B19ED00EDD74B /* ConfigurationTests.swift in Sources */, + D8B49D7C1C72663F00B687D5 /* RequestButtonTests.swift in Sources */, + DC75E0341CAAEA5700071417 /* ModalViewControllerTests.swift in Sources */, + D8B757841CA3485600D5025C /* RideRequestViewTests.swift in Sources */, AC0404851BFACD1D00AC1501 /* UberRidesTests.swift in Sources */, + DC1039731C96B1CE004854E3 /* RidesScopeExtensionsTests.swift in Sources */, + DC92DBFD1CA21379001A0DCC /* TokenManagerTests.swift in Sources */, + DC75E0301CA9EDBB00071417 /* RideRequestViewControllerTests.swift in Sources */, AC04049A1BFACE5400AC1501 /* RequestDeeplinkTests.swift in Sources */, + DC8FC3761CBE137A00D58839 /* LoginManagerTests.swift in Sources */, + DC78ED131CB578D100850814 /* RidesScopeFactoryTests.swift in Sources */, + DC92DC041CA38DBE001A0DCC /* WidgetsEndpointTests.swift in Sources */, + DC78ECF71CB4877700850814 /* RidesAuthenticationErrorFactoryTests.swift in Sources */, + DC78EC741CB2F66500850814 /* LocalizationUtilTests.swift in Sources */, + DCB0D3901CADAA6300194DD5 /* RideRequestViewRequestingBehaviorTests.swift in Sources */, + D89338F11C77CAE0005B5486 /* OAuthTests.swift in Sources */, + DC2730EF1CBC46520044AB04 /* RidesMocks.swift in Sources */, + DC75DFEE1CA5F5EB00071417 /* RideParametersTest.swift in Sources */, + DCED60F61C9770D9001A65E0 /* AccessTokenFactoryTests.swift in Sources */, + DC92DC061CA3AC59001A0DCC /* OauthEndpointTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -262,6 +579,20 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + DC8FC3611CBDF4D000D58839 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + DC8FC3621CBDF4D000D58839 /* en */, + DC8FC3641CBDF4E800D58839 /* hi-IN */, + DC8FC3651CBDF52200D58839 /* zh-Hans */, + DC8FC3661CBDF52C00D58839 /* zh-Hant */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ AC0404871BFACD1D00AC1501 /* Debug */ = { isa = XCBuildConfiguration; @@ -354,6 +685,7 @@ }; AC04048A1BFACD1D00AC1501 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EA01FB909C9A04DC5D83A4D9 /* Pods.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -373,6 +705,7 @@ }; AC04048B1BFACD1D00AC1501 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0D40D6AA81C808FAABB243C9 /* Pods.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -391,6 +724,7 @@ }; AC04048D1BFACD1D00AC1501 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8EA8F073A2E428216A862688 /* Pods-UberRidesTests.debug.xcconfig */; buildSettings = { INFOPLIST_FILE = UberRidesTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -401,6 +735,7 @@ }; AC04048E1BFACD1D00AC1501 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C1C60712D3DA8E230F9B36D2 /* Pods-UberRidesTests.release.xcconfig */; buildSettings = { INFOPLIST_FILE = UberRidesTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/source/UberRides.xcodeproj/xcshareddata/xcschemes/UberRides.xcscheme b/source/UberRides.xcodeproj/xcshareddata/xcschemes/UberRides.xcscheme index a4108364..26e6b7cd 100644 --- a/source/UberRides.xcodeproj/xcshareddata/xcschemes/UberRides.xcscheme +++ b/source/UberRides.xcodeproj/xcshareddata/xcschemes/UberRides.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/source/UberRides.xcworkspace/contents.xcworkspacedata b/source/UberRides.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8c828c79 --- /dev/null +++ b/source/UberRides.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/source/UberRides/Configuration.swift b/source/UberRides/Configuration.swift new file mode 100644 index 00000000..95addcd0 --- /dev/null +++ b/source/UberRides/Configuration.swift @@ -0,0 +1,251 @@ +// +// Configuration.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import WebKit + +/** + An enum to represent the region that the SDK should use for making requests + + - Default: The default region + - China: China, for apps that are based in China + */ +@objc public enum Region : Int { + case Default + case China +} + +/** + Class responsible for handling all of the SDK Configuration options. Provides + default values for Application-wide configuration properties. All properties are + configurable via the respective setter method +*/ +@objc(UBSDKConfiguration) public class Configuration : NSObject { + // MARK : Variables + + /// The .plist file to use, default is Info.plist + public static var plistName = "Info" + + /// The bundle that contains the .plist file. Default is the mainBundle() + public static var bundle = NSBundle.mainBundle() + + static var processPool = WKProcessPool() + + private static let clientIDKey = "UberClientID" + private static let callbackURIKey = "UberCallbackURI" + private static let accessTokenIdentifier = "RidesAccessTokenKey" + + private static var clientID : String? + private static var callbackURIString : String? + private static var defaultKeychainAccessGroup: String? + private static var defaultAccessTokenIdentifier: String? + private static var region : Region = .Default + private static var isSandbox : Bool = false + + /** + Resets all of the Configuration's values to default + */ + public static func restoreDefaults() { + setClientID(nil) + setCallbackURIString(nil) + setDefaultAccessTokenIdentifier(nil) + setDefaultKeychainAccessGroup(nil) + setRegion(Region.Default) + setSandboxEnabled(false) + resetProcessPool() + + plistName = "Info" + bundle = NSBundle.mainBundle() + } + + // MARK: Getters + + /** + Gets the client ID of this app. Defaults to the value stored in your Application's + plist if not set (UberClientID) + + - returns: The string to use for the Client ID + */ + public static func getClientID() -> String { + if clientID == nil { + guard let defaultValue = getDefaultValue(clientIDKey) else { + fatalConfigurationError("ClientID", key: clientIDKey) + } + clientID = defaultValue + } + + return clientID! + } + + /** + Gets the callback URIString of this app. Defaults to the value stored in your Application's + plist if not set (UberRedirectURL) + + - returns: The string to use for the Callback URI + */ + public static func getCallbackURIString() -> String + { + if callbackURIString == nil { + guard let defaultValue = getDefaultValue(callbackURIKey) else { + fatalConfigurationError("CallbackURIString", key: callbackURIKey) + } + callbackURIString = defaultValue + } + + return callbackURIString! + } + + /** + Gets the default keychain access group to save access tokens to. Advanced setting + for sharing access tokens between multiple of your apps. Defaults an empty string + + - returns: The default keychain access group to use + */ + public static func getDefaultKeychainAccessGroup() -> String { + guard let defaultKeychainAccessGroup = defaultKeychainAccessGroup else { + return "" + } + + return defaultKeychainAccessGroup + } + + /** + Gets the default key to use when saving access tokens to the keychain. Defaults + to using "RidesAccessTokenKey" + + - returns: The default access token identifier to use + */ + public static func getDefaultAccessTokenIdentifier() -> String { + guard let defaultAccessTokenIdentifier = defaultAccessTokenIdentifier else { + return accessTokenIdentifier + } + + return defaultAccessTokenIdentifier + } + + /** + Gets the current region the SDK is using. Defaults to Region.Default + + - returns: The Region the SDK is using + */ + public static func getRegion() -> Region { + return region + } + + /** + Returns if sandbox is enabled or not + + - returns: true if Sandbox is enabled, false otherwise + */ + public static func getSandboxEnabled() -> Bool { + return isSandbox + } + + //MARK: Setters + + /** + Sets a string to use as the Client ID. Overwrites the default value provided by + the plist. Setting clientID to nil will result in using the default value + + - parameter clientID: The client ID String to use + */ + public static func setClientID(clientID: String?) { + self.clientID = clientID + } + + /** + Sets a string to use as the Callback URI String. Overwrites the default value provided by + the plist. Setting to nil will result in using the default value. + If you're setting a custom value, be sure your app is configured to handle deeplinks + from this URI & you've added it to the redirect URIs on your Uber developer dashboard + + - parameter callbackURIString: The callback URI String to use + */ + public static func setCallbackURIString(callbackURIString: String?) { + self.callbackURIString = callbackURIString + } + + /** + Sets the default keychain access group to use. Access tokens will be saved + here by default, unless otherwise specified at the time of login + + - parameter keychainAccessGroup: The client ID String to use + */ + public static func setDefaultKeychainAccessGroup(keychainAccessGroup: String?) { + self.defaultKeychainAccessGroup = keychainAccessGroup + } + + /** + Sets the default key to use when saving access tokens to the keychain. Setting + to nil will result in using the default value + + - parameter accessTokenIdentifier: The access token identifier to use + */ + public static func setDefaultAccessTokenIdentifier(accessTokenIdentifier: String?) { + self.defaultAccessTokenIdentifier = accessTokenIdentifier + } + + /** + Set the region your app is registered in. Used to determine what endpoints to + send requests to. + + - parameter region: The region the SDK should use + */ + public static func setRegion(region: Region) { + self.region = region + } + + /** + Enables / Disables Sandbox mode. When the SDK is in sandbox mode, all requests + will go to the sandbox environment. + + - parameter enabled: Whether or not sandbox should be enabled + */ + public static func setSandboxEnabled(enabled: Bool) { + isSandbox = enabled + } + + // MARK: Internal + + static func resetProcessPool() { + processPool = WKProcessPool() + } + + // MARK: Private + + private static func getDefaultValue(key: String) -> String? { + guard let path = bundle.pathForResource(plistName, ofType: "plist"), + let dict = NSDictionary(contentsOfFile: path) as? [String: AnyObject], + let defaultValue = dict[key] as? String else { + return nil + } + + return defaultValue + } + + @noreturn private static func fatalConfigurationError(variableName: String, key: String ) { + fatalError("Unable to get your \(variableName). Did you forget to set it in your \(plistName).plist? (Should be under \(key) key)") + } + +} \ No newline at end of file diff --git a/source/UberRides/ColorUtil.swift b/source/UberRides/DeeplinkRequestingBehavior.swift similarity index 54% rename from source/UberRides/ColorUtil.swift rename to source/UberRides/DeeplinkRequestingBehavior.swift index 8a820a87..c89f204a 100644 --- a/source/UberRides/ColorUtil.swift +++ b/source/UberRides/DeeplinkRequestingBehavior.swift @@ -1,5 +1,5 @@ // -// ColorUtil.swift +// DeeplinkRequestingBehavior.swift // UberRides // // Copyright © 2015 Uber Technologies, Inc. All rights reserved. @@ -21,42 +21,25 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - - -import Foundation - -@objc internal enum UberButtonColor: Int { - case UberBlack - case UberWhite - case BlackHighlighted - case WhiteHighlighted -} - -private func hexCodeFromColor(color: UberButtonColor) -> String { - switch color { - case .UberBlack: - return "09091A" - case .UberWhite: - return "C0C0C8" - case .BlackHighlighted: - return "222231" - case .WhiteHighlighted: - return "CDCDD3" +@objc(UBSDKDeeplinkRequestingBehavior) public class DeeplinkRequestingBehavior : NSObject, RideRequesting { + + /** + Requests a ride using a RequestDeeplink that is constructed using the provided + rideParameters + + - parameter rideParameters: The RideParameters to use for building and executing + the deeplink + */ + @objc public func requestRide(rideParameters: RideParameters?) { + guard let rideParameters = rideParameters else { + return + } + let deeplink = createDeeplink(rideParameters) + deeplink.execute() } -} - -// convert hex color code into UIColor -internal func uberUIColor(color: UberButtonColor) -> UIColor { - let hexCode = hexCodeFromColor(color) - let scanner = NSScanner(string: hexCode) - var color: UInt32 = 0; - scanner.scanHexInt(&color) - let mask = 0x000000FF - - let redValue = CGFloat(Int(color >> 16)&mask)/255.0 - let greenValue = CGFloat(Int(color >> 8)&mask)/255.0 - let blueValue = CGFloat(Int(color)&mask)/255.0 + func createDeeplink(rideParameters: RideParameters) -> RequestDeeplink { + return RequestDeeplink(rideParameters: rideParameters) + } - return UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1.0) } diff --git a/source/UberRides/EndpointsManager.swift b/source/UberRides/EndpointsManager.swift new file mode 100644 index 00000000..4fec70ac --- /dev/null +++ b/source/UberRides/EndpointsManager.swift @@ -0,0 +1,184 @@ +// +// EndpointsManager.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/** + * Protocol for all endpoints to conform to. + */ +protocol UberAPI { + var HTTPMethod: Method { get } + var path: String { get } + var query: [NSURLQueryItem] { get } + var host: String { get} +} + +extension UberAPI { + var host: String { + if Configuration.getSandboxEnabled() { + switch Configuration.getRegion() { + case .China: + return "https://sandbox-api.uber.com.cn" + case .Default: + return "https://sandbox-api.uber.com" + } + } else { + switch Configuration.getRegion() { + case .China: + return "https://api.uber.com.cn" + case .Default: + return "https://api.uber.com" + } + } + } +} + +/** + Enum for HTTPMethods + */ +enum Method: String { + case GET = "GET" + case POST = "POST" + case PUT = "PUT" + case PATCH = "PATCH" + case DELETE = "DELETE" +} + +/** + Helper function to build array of NSURLQueryItems. A key-value pair with an empty string value is ignored. + + - parameter queries: tuples of key-value pairs + - returns: an array of NSURLQueryItems + */ +func queryBuilder(queries: (name: String, value: String)...) -> [NSURLQueryItem] { + var queryItems = [NSURLQueryItem]() + for query in queries { + if query.name.isEmpty || query.value.isEmpty { + continue + } + queryItems.append(NSURLQueryItem(name: query.name, value: query.value)) + } + return queryItems +} + +/** + Endpoints related to components. + - RideRequestWidget: Ride Request Widget endpoint. + */ +enum Components: UberAPI { + case RideRequestWidget(rideParameters: RideParameters?) + + var HTTPMethod: Method { + switch self { + case .RideRequestWidget: + return .GET + } + } + + var host: String { + switch Configuration.getRegion() { + case .China: + return "https://components.uber.com.cn" + case .Default: + return "https://components.uber.com" + } + } + + var path: String { + switch self { + case .RideRequestWidget: + return "/rides/" + } + } + + var query: [NSURLQueryItem] { + switch self { + case .RideRequestWidget(let rideParameters): + let environment = Configuration.getSandboxEnabled() ? "sandbox" : "production" + var queryItems = queryBuilder( ("env", "\(environment)") ) + + if let rideParameters = rideParameters { + do { + let url = try RequestURLUtil.buildURL(rideParameters) + if let urlComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), + let items = urlComponents.queryItems { + queryItems.appendContentsOf(items) + } + } catch { + return queryItems + } + } + return queryItems + } + } +} + +/** + OAuth endpoints. + + - Login: Used to login user and request access to specified scopes via implicit grant. + */ +enum OAuth: UberAPI { + case Login(clientID: String, scopes: [RidesScope], redirect: String) + + var HTTPMethod: Method { + switch self { + case .Login: + return .GET + } + } + + var host: String { + return regionHostString() + } + + func regionHostString(region: Region = Configuration.getRegion()) -> String { + switch region { + case .China: + return "https://login.uber.com.cn" + case .Default: + return "https://login.uber.com" + } + } + + var path: String { + switch self { + case .Login: + return "/oauth/v2/authorize" + } + } + + var query: [NSURLQueryItem] { + switch self { + case .Login(let clientID, let scopes, let redirect): + + return queryBuilder( + ("scope", scopes.toRidesScopeString()), + ("client_id", clientID), + ("redirect_uri", redirect), + ("response_type", "token"), + ("show_fb", "false")) + } + } +} diff --git a/source/UberRides/Info.plist b/source/UberRides/Info.plist index d3de8eef..c9410942 100644 --- a/source/UberRides/Info.plist +++ b/source/UberRides/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 0.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/source/UberRides/ModalRideRequestViewController.swift b/source/UberRides/ModalRideRequestViewController.swift new file mode 100644 index 00000000..c3c5faab --- /dev/null +++ b/source/UberRides/ModalRideRequestViewController.swift @@ -0,0 +1,90 @@ +// +// ModalRideRequestViewController.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/// Modal View Controller to use for presenting a RideRequestViewController. Handles errors & closing the modal for you +@objc(UBSDKModalRideRequestViewController) public class ModalRideRequestViewController : ModalViewController { + /// The RideRequestViewController this modal is wrapping + public internal(set) var rideRequestViewController : RideRequestViewController + + /** + Initializer for the ModalRideRequestViewController. Wraps the provided RideRequestViewController + and acts as it's delegate. Will handle errors coming in via the RideRequestViewControllerDelegate + and dismiss the modal appropriately + + - parameter rideRequestViewController: The RideRequestViewController to wrap + + - returns: An initialized ModalRideRequestViewController + */ + @objc public init(rideRequestViewController: RideRequestViewController) { + self.rideRequestViewController = rideRequestViewController + super.init(childViewController: rideRequestViewController, buttonStyle: ModalViewControllerButtonStyle.BackButton) + self.rideRequestViewController.delegate = self + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIViewController + + public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { + return [.Portrait, .PortraitUpsideDown] + } +} + +extension ModalRideRequestViewController : RideRequestViewControllerDelegate { + /** + ModalRideRequestViewController's implmentation for the RideRequestViewController delegate. + + - parameter rideRequestViewController: The RideRequestViewController that experienced an error + - parameter error: The RideRequestViewError that occured + */ + public func rideRequestViewController(rideRequestViewController: RideRequestViewController, didReceiveError error: NSError) { + let errorType = RideRequestViewErrorType(rawValue: error.code) ?? RideRequestViewErrorType.Unknown + var errorString: String? + navigationItem.title = LocalizationUtil.localizedString(forKey: "Sign in with Uber", comment: "Title of navigation bar during OAuth") + switch errorType { + case .AccessTokenExpired: + fallthrough + case .AccessTokenMissing: + errorString = LocalizationUtil.localizedString(forKey: "There was a problem authenticating you. Please try again.", comment: "RideRequestView error text for authentication error") + case .NetworkError: + break + default: + errorString = LocalizationUtil.localizedString(forKey: "The Ride Request Widget encountered a problem. Please try again.", comment: "RideRequestView error text for a generic error") + } + + if let errorString = errorString { + let actionString = LocalizationUtil.localizedString(forKey: "OK", comment: "OK button title") + let alert = UIAlertController(title: nil, message: errorString, preferredStyle: UIAlertControllerStyle.Alert) + let okayAction = UIAlertAction(title: actionString, style: UIAlertActionStyle.Default, handler: { (_) -> Void in + self.dismiss() + }) + alert.addAction(okayAction) + self.presentViewController(alert, animated: true, completion: nil) + } else { + self.dismiss() + } + } +} diff --git a/source/UberRides/ModalViewController.swift b/source/UberRides/ModalViewController.swift new file mode 100644 index 00000000..2673982f --- /dev/null +++ b/source/UberRides/ModalViewController.swift @@ -0,0 +1,207 @@ +// +// ModalViewController.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/** +Possible Styles for the ModalViewController + +- Empty: Presents the view modally without any BarButtonItems +- DoneButton: Presents the view mdoally with a Done BarButtonItem in the top right corner +*/ +@objc public enum ModalViewControllerButtonStyle : Int { + case Empty + case DoneButton + case BackButton +} + +/** + Possible color style for the ModalViewController + + - Default: Default dark style, dark navigation bar with light text + - Light: Light color style, light navigation bar with dark text + */ +@objc public enum ModalViewControllerColorStyle : Int { + case Default + case Light +} + +/** + * The ModalViewControllerDelegate protocol + */ +@objc(UBSDKModalViewControllerDelegate) public protocol ModalViewControllerDelegate { + /** + Called before the ModalViewController dismisses the modal. + + - parameter modalViewController: The ModalViewController that will be dismissed + */ + @objc func modalViewControllerWillDismiss(modalViewController: ModalViewController) + + /** + Called after the ModalViewController is dismissed. + + - parameter modalViewController: The ModalViewController that was dismissed + */ + @objc func modalViewControllerDidDismiss(modalViewController: ModalViewController) +} + +/// Convenience to wrap a ViewController in a UINavigationController and add the appropriate buttons. Allows you to modally present a view controller w/ Uber branding. +@objc(UBSDKModalViewController) public class ModalViewController : UIViewController { + /// The ModalViewControllerDelegate + public var delegate: ModalViewControllerDelegate? + + public var colorStyle: ModalViewControllerColorStyle = .Default { + didSet { + setupStyle() + } + } + + var buttonStyle: ModalViewControllerButtonStyle + var wrappedViewController: UIViewController + var wrappedNavigationController: UINavigationController + + //MARK: Initializers + + /** + Initializes a ModalViewController for the given childViewController and style inside a UINavigationController + with the appropriate buttons. + + - parameter childViewController: The child UIViewController to wrap + - parameter buttonStyle: The ModalViewControllerButtonStyle to use + + - returns: An initialized ModalViewController + */ + @objc public init(childViewController: UIViewController, buttonStyle: ModalViewControllerButtonStyle) { + wrappedViewController = childViewController + wrappedNavigationController = UINavigationController(rootViewController: childViewController) + self.buttonStyle = buttonStyle + super.init(nibName: nil, bundle: nil) + setupStyle() + } + + /** + Initializes a ModalViewController for the given childViewController and style inside a UINavigationController + with the appropriate buttons. + + Defaults to the .DoneButton ModalViewControllerButtonStyle style + + - parameter childViewController: The child UIViewController to wrap + + - returns: An initialized ModalViewController + */ + @objc public convenience init(childViewController: UIViewController) { + self.init(childViewController: childViewController, buttonStyle: .DoneButton) + } + + /** + Unavailable. ModalViewController doesn't support being initialized via + init(coder:) + + - throws: Fatal Error + */ + @objc required public init?(coder aDecoder: NSCoder) { + fatalError("ModalViewController doesn't support init(coder:)") + } + + //MARK: View Lifecycle + + public override func viewDidLoad() { + super.viewDidLoad() + + self.addChildViewController(wrappedNavigationController) + self.view.addSubview(self.wrappedNavigationController.view) + + self.wrappedNavigationController.didMoveToParentViewController(self) + } + + public override func viewDidDisappear(animated: Bool) { + super.viewDidDisappear(animated) + self.delegate?.modalViewControllerDidDismiss(self) + } + + //MARK: Public + + /** + Function to dimiss the modalViewController. + */ + public func dismiss() { + self.delegate?.modalViewControllerWillDismiss(self) + self.dismissViewControllerAnimated(true, completion: nil) + } + + public override func preferredStatusBarStyle() -> UIStatusBarStyle { + switch colorStyle { + case .Light: + return UIStatusBarStyle.Default + case .Default: + return UIStatusBarStyle.LightContent + } + + } + + //MARK: Button Actions + + func doneButtonPressed(button: UIButton) { + dismiss() + } + + func backButtonPressed(button: UIButton) { + dismiss() + } + + //MARK: Private Helpers + + private func setupStyle() { + let bundle = NSBundle(forClass: RideRequestButton.self) + self.wrappedViewController.navigationItem.leftBarButtonItem = nil + self.wrappedViewController.navigationItem.rightBarButtonItem = nil + var iconTintColor = UIColor.whiteColor() + switch colorStyle { + case .Light: + iconTintColor = UIColor.blackColor() + wrappedViewController.navigationController?.navigationBar.barStyle = .Default + case .Default: + wrappedViewController.navigationController?.navigationBar.barStyle = .Black + break + } + + switch buttonStyle { + case .Empty: + break + case .DoneButton: + let doneButton = UIBarButtonItem(barButtonSystemItem: .Done , target: self, action: Selector("doneButtonPressed:")) + doneButton.tintColor = iconTintColor + self.wrappedViewController.navigationItem.rightBarButtonItem = doneButton + case .BackButton: + let backImage = UIImage(named: "ic_back_arrow_white", inBundle: bundle, compatibleWithTraitCollection: nil) + let backButton = UIBarButtonItem(image: backImage, style: .Plain, target: self, action: Selector("backButtonPressed:")) + backButton.tintColor = iconTintColor + self.wrappedViewController.navigationItem.leftBarButtonItem = backButton + } + + let logoImage = UIImage(named: "ic_logo_white", inBundle: bundle, compatibleWithTraitCollection: nil)?.imageWithRenderingMode(.AlwaysTemplate) + let logoImageView = UIImageView(image: logoImage) + logoImageView.tintColor = iconTintColor + + wrappedViewController.navigationItem.titleView = logoImageView + } +} diff --git a/source/UberRides/Model/RideParameters.swift b/source/UberRides/Model/RideParameters.swift new file mode 100644 index 00000000..88545c8a --- /dev/null +++ b/source/UberRides/Model/RideParameters.swift @@ -0,0 +1,284 @@ +// +// RideParameters.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import MapKit + +/// Object to represent the parameters needed to request a ride. Should be built using a RideParametersBuilder +@objc(UBSDKRideParameters) public class RideParameters : NSObject { + + /// True if the pickup location should use the device's current location, false if a location has been set + public let useCurrentLocationForPickup: Bool + + /// ProductID to use for the ride + public let productID: String? + + /// The pickup location to use for the ride + public let pickupLocation: CLLocation? + + /// The nickname of the pickup location of the ride + public let pickupNickname: String? + + /// The address of the pickup location of the ride + public let pickupAddress: String? + + /// The dropoff location to use for the ride + public let dropoffLocation: CLLocation? + + /// The nickname of the dropoff location for the ride + public let dropoffNickname: String? + + /// The adress of the dropoff location of the ride + public let dropoffAddress: String? + + var userAgent: String { + var userAgentString: String = "" + if let versionNumber: String = NSBundle(forClass: self.dynamicType).objectForInfoDictionaryKey("CFBundleShortVersionString") as? String { + userAgentString = "rides-ios-v\(versionNumber)" + if let source = source { + userAgentString = "\(userAgentString)-\(source)" + } + } + return userAgentString + } + + var source: String? + + private init(useCurrentLocationForPickup: Bool, + productID: String?, + pickupLocation: CLLocation?, + pickupNickname: String?, + pickupAddress: String?, + dropoffLocation: CLLocation?, + dropoffNickname: String?, + dropoffAddress: String?, + source: String?) { + + self.useCurrentLocationForPickup = useCurrentLocationForPickup + self.productID = productID + self.pickupLocation = pickupLocation + self.pickupNickname = pickupNickname + self.pickupAddress = pickupAddress + self.dropoffLocation = dropoffLocation + self.dropoffNickname = dropoffNickname + self.dropoffAddress = dropoffAddress + self.source = source + } + +} + +/// Builder for a RideParameters object. +@objc(UBSDKRideParametersBuilder) public class RideParametersBuilder : NSObject { + + private var useCurrentLocationForPickup: Bool + private var productID: String? + private var pickupLocation: CLLocation? + private var pickupNickname: String? + private var pickupAddress: String? + private var dropoffLocation: CLLocation? + private var dropoffNickname: String? + private var dropoffAddress: String? + private var source: String? + + @objc public convenience override init() { + self.init(rideParameters: nil) + } + + @objc public init(rideParameters: RideParameters?) { + if let rideParameters = rideParameters { + useCurrentLocationForPickup = rideParameters.useCurrentLocationForPickup + productID = rideParameters.productID + pickupLocation = rideParameters.pickupLocation + pickupNickname = rideParameters.pickupNickname + pickupAddress = rideParameters.pickupAddress + dropoffLocation = rideParameters.dropoffLocation + dropoffNickname = rideParameters.dropoffNickname + dropoffAddress = rideParameters.dropoffAddress + source = rideParameters.source + } else { + useCurrentLocationForPickup = true + } + } + + /** + Set the product ID for the ride parameters. + + - parameter productID: The unique ID of the product being requested. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setProductID(productID: String) -> RideParametersBuilder { + self.productID = productID + + return self + } + + /** + Sets the builder to use your current location for pickup. Will clear any set + pickupLocation, pickupNickname, and pickupAddress. + */ + public func setPickupToCurrentLocation() -> RideParametersBuilder { + useCurrentLocationForPickup = true + pickupLocation = nil + pickupNickname = nil + pickupAddress = nil + + return self + } + + /** + Set pickup location information for the ride parameters. + + - parameter location: CLLocation of pickup. + - parameter nickname: Optional nickname of pickup location. + - parameter address: Optional address of pickup location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setPickupLocation(location: CLLocation, nickname: String?, address: String?) -> RideParametersBuilder { + useCurrentLocationForPickup = false + pickupLocation = location + pickupNickname = nickname + pickupAddress = address + + return self + } + + /** + Set pickup location information for the ride parameters. + + - parameter location: CLLocation of pickup. + - parameter address: Optional address of pickup location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setPickupLocation(location: CLLocation, address: String?) -> RideParametersBuilder { + return self.setPickupLocation(location, nickname: nil, address: address) + } + + /** + Set pickup location information for the ride parameters. + + - parameter location: CLLocation of pickup. + - parameter nickname: Optional nickname of pickup location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setPickupLocation(location: CLLocation, nickname: String?) -> RideParametersBuilder { + return self.setPickupLocation(location, nickname: nickname, address: nil) + } + + /** + Set pickup location information for the ride parameters. + + - parameter location: CLLocation of pickup. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setPickupLocation(location: CLLocation) -> RideParametersBuilder { + return self.setPickupLocation(location, nickname: nil, address: nil) + } + + /** + Set dropoff location information for the ride parameters. + + - parameter location: CLLocation of dropoff. + - parameter nickname: Optional nickname of dropoff location. + - parameter address: Optional address of dropoff location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setDropoffLocation(location: CLLocation, nickname: String?, address: String?) -> RideParametersBuilder { + dropoffLocation = location + dropoffNickname = nickname + dropoffAddress = address + + return self + } + + /** + Set dropoff location information for the ride parameters. + + - parameter location: CLLocation of dropoff. + - parameter address: Optional address of dropoff location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setDropoffLocation(location: CLLocation, address: String?) -> RideParametersBuilder { + return self.setDropoffLocation(location, nickname: nil, address: address) + } + + /** + Set dropoff location information for the ride parameters. + + - parameter location: CLLocation of dropoff. + - parameter nickname: Optional nickname of dropoff location. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setDropoffLocation(location: CLLocation, nickname: String?) -> RideParametersBuilder { + return self.setDropoffLocation(location, nickname: nickname, address: nil) + } + + /** + Set dropoff location information for the ride parameters. + + - parameter location: CLLocation of dropoff. + + - returns: RideParametersBuilder to continue chaining. + */ + public func setDropoffLocation(location: CLLocation) -> RideParametersBuilder { + return self.setDropoffLocation(location, nickname: nil, address: nil) + } + + /** + Set the source to use for attributing the ride + + - parameter source: The source string to use + + - returns: RideParametersBuilder to continue chaining. + */ + func setSource(source: String?) -> RideParametersBuilder { + self.source = source + + return self + } + + /** + Build the ride parameter object. + + - returns: An initialized RideParameters object + */ + public func build() -> RideParameters { + return RideParameters(useCurrentLocationForPickup: useCurrentLocationForPickup, + productID: productID, + pickupLocation: pickupLocation, + pickupNickname: pickupNickname, + pickupAddress: pickupAddress, + dropoffLocation: dropoffLocation, + dropoffNickname: dropoffNickname, + dropoffAddress: dropoffAddress, + source: source) + } + +} diff --git a/source/UberRides/Model/RidesError.swift b/source/UberRides/Model/RidesError.swift new file mode 100644 index 00000000..92d8347c --- /dev/null +++ b/source/UberRides/Model/RidesError.swift @@ -0,0 +1,247 @@ +// +// RidesError.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import ObjectMapper + +// MARK: RidesError + +/// Base class for errors that can be mapped from HTTP responses. +@objc(UBSDKRidesError) public class RidesError : NSObject { + /// HTTP status code for error. + public internal(set) var status: Int? + + /// Human readable message which corresponds to the client error. + public internal(set) var title: String? + + /// Underscore delimited string. + public internal(set) var code: String? + + /// Additional information about errors. Can be "fields" or "meta" as the key. + public internal(set) var meta: [String: AnyObject]? + + /// List of additional errors. This can be populated instead of status/code/title. + public internal(set) var errors: [RidesError]? + + public required init?(_ map: Map) { + } +} + +extension RidesError: UberModel { + public func mapping(map: Map) { + code <- map["code"] + status <- map["status"] + errors <- map["errors"] + + if map["message"].currentValue != nil { + title <- map["message"] + } else if map["title"].currentValue != nil { + title <- map["title"] + } + + if map["fields"].currentValue != nil { + meta <- map["fields"] + } else if map["meta"].currentValue != nil { + meta <- map["meta"] + } + + if map["error"].currentValue != nil { + title <- map["error"] + } + } +} + +// MARK: RidesError subclasses + +/// Client error 4xx. +@objc(UBSDKRidesClientError) public class RidesClientError: RidesError { + + public required init?(_ map: Map) { + super.init(map) + } +} + +/// Server error 5xx. +@objc(UBSDKRidesServerError) public class RidesServerError: RidesError { + + public required init?(_ map: Map) { + super.init(map) + } +} + +/// Unknown error type. +@objc(UBSDKRidesUnknownError) public class RidesUnknownError: RidesError { + + public required init?(_ map: Map) { + super.init(map) + } +} + +// MARK: RidesAuthenticationError + +/** +Possible authentication errors. + +- InvalidClientID: Invalid client ID provided for authentication. +- InvalidRedirect: Redirect URI provided was invalid +- InvalidRequest: General case for invalid requests. +- InvalidResponse: The response from the server was un-parseable +- InvalidScope: Scopes provided contains an invalid scope. +- MismatchingRedirect: Redirect URI provided doesn't match one registered for client ID. +- NetworkError: A network error occured +- ServerError: A server error occurred during authentication. +- UnableToPresentLogin Unable to present the login screen +- UnableToSaveAccessToken There was a problem saving the access token +- Unavailable: Authentication services temporarily unavailable. +- UserCancelled: User cancelled the auth process +*/ +@objc public enum RidesAuthenticationErrorType: Int { + case InvalidClientID + case InvalidRedirect + case InvalidRequest + case InvalidResponse + case InvalidScope + case MismatchingRedirect + case NetworkError + case ServerError + case UnableToPresentLogin + case UnableToSaveAccessToken + case Unavailable + case UserCancelled + + func toString() -> String { + switch self { + case .InvalidClientID: + return "invalid_client_id" + case .InvalidRedirect: + return "invalid_redirect_uri" + case .InvalidRequest: + return "invalid_parameters" + case .InvalidResponse: + return "invalid_response" + case .InvalidScope: + return "invalid_scope" + case .MismatchingRedirect: + return "mismatching_redirect_uri" + case .NetworkError: + return "network_error" + case .ServerError: + return "server_error" + case .UnableToPresentLogin: + return "present_login_failed" + case .UnableToSaveAccessToken: + return "token_not_saved" + case .Unavailable: + return "temporarily_unavailable" + case .UserCancelled: + return "cancelled" + } + } + + var localizedDescriptionKey: String { + switch self { + case .InvalidClientID: + return "Invalid Client ID provided." + case .InvalidRedirect: + return "Invalid Redirect URI provided." + case .InvalidRequest: + return "The server was unable to understand your request." + case .InvalidResponse: + return "Unable to interpret the response from the server." + case .InvalidScope: + return "Your app is not authorized for the requested scopes." + case .MismatchingRedirect: + return "The Redirect URI provided did not match what was expected." + case .NetworkError: + return "A network error occured." + case .ServerError: + return "A server error occurred." + case .UnableToPresentLogin: + return "Unable to present the login view." + case .UnableToSaveAccessToken: + return "Unable to save the access token." + case .Unavailable: + return "Login is temporarily unavailable." + case .UserCancelled: + return "User cancelled the login process." + } + } + + func toLocalizedDescription() -> String { + return LocalizationUtil.localizedString(forKey: self.localizedDescriptionKey, comment: self.toString()) + } +} + +class RidesAuthenticationErrorFactory : NSObject { + + static let errorDomain = "com.uber.rides-ios-sdk.ridesAuthenticationError" + + /** + Creates a RidesAuthenticationError for the provided RidesAuthenticationErrorType + + - parameter ridesAuthenticationErrorType: the RidesAuthenticationErrorType of error to create + + - returns: An initialized RidesAuthenticationError + */ + static func errorForType(ridesAuthenticationErrorType ridesAuthenticationErrorType : RidesAuthenticationErrorType) -> NSError { + return NSError(domain: errorDomain, code: ridesAuthenticationErrorType.rawValue, userInfo: [NSLocalizedDescriptionKey : ridesAuthenticationErrorType.toLocalizedDescription()]) + } + + static func createRidesAuthenticationError(rawValue rawValue: String) -> NSError? { + guard let ridesAuthenticationErrorType = ridesAuthenticationErrorType(rawValue) else { + return nil + } + return RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: ridesAuthenticationErrorType) + } + + static func ridesAuthenticationErrorType(rawValue: String) -> RidesAuthenticationErrorType? { + switch rawValue { + case "cancelled": + return .UserCancelled + case "invalid_client_id": + return .InvalidClientID + case "invalid_parameters": + return .InvalidRequest + case "invalid_redirect_uri": + return .InvalidRedirect + case "invalid_response": + return .InvalidResponse + case "invalid_scope": + return .InvalidScope + case "mismatching_redirect_uri": + return .MismatchingRedirect + case "network_error": + return .NetworkError + case "present_login_failed": + return .UnableToPresentLogin + case "server_error": + return .ServerError + case "temporarily_unavailable": + return .Unavailable + case "token_not_saved": + return .UnableToSaveAccessToken + default: + return nil + } + } +} diff --git a/source/UberRides/Model/RidesScope.swift b/source/UberRides/Model/RidesScope.swift new file mode 100644 index 00000000..f34e27ad --- /dev/null +++ b/source/UberRides/Model/RidesScope.swift @@ -0,0 +1,210 @@ +// +// RidesScope.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/** + Category of scope that describes its level of access. + + - General: scopes that can be used without review. + - Privileged: scopes that require approval before opened to your users in production. + */ +@objc public enum ScopeType : Int { + case General, Privileged +} + +/** + Scopes control the various API endpoints your application can access. + + - AllTrips: Get details of the trip the user is currently taking. + - History: Pull trip data of a user's historical pickups and drop-offs. + - HistoryLite: Same as History without city information. + - PaymentMethods: Retrieve user's available registered payment methods. + - Places: Save and retrieve user's favorite places. + - Profile: Access basic profile information on a user's Uber account. + - RideWidgets: The scope for using the Ride Request Widget + + */ +@objc public enum RidesScopeType: Int { + case History + case HistoryLite + case PaymentMethods + case Places + case Profile + case RideWidgets + + + var type: ScopeType { + switch(self) { + case History: fallthrough + case HistoryLite: fallthrough + case PaymentMethods: fallthrough + case Places: fallthrough + case Profile: fallthrough + case .RideWidgets: + return .General + } + } + + func toString() -> String { + switch self { + case History: + return "history" + case HistoryLite: + return "history_lite" + case PaymentMethods: + return "payment_methods_readonly" + case Places: + return "places" + case Profile: + return "profile" + case RideWidgets: + return "ride_widgets" + } + } +} + +/** + * Object representing an access scope to the Uber API + */ +@objc(UBSDKRidesScope) public class RidesScope : NSObject { + /// The RidesScopeType of this RidesScope + public let ridesScopeType: RidesScopeType + /// The ScopeType of this RidesScope (General / Privileged) + public let scopeType : ScopeType + /// The String raw value of the scope + public let rawValue : String + + public init(ridesScopeType: RidesScopeType) { + self.ridesScopeType = ridesScopeType + scopeType = ridesScopeType.type + rawValue = ridesScopeType.toString() + } + + override public func isEqual(object: AnyObject?) -> Bool { + if let object = object as? RidesScope { + return self.ridesScopeType == object.ridesScopeType + } else { + return false + } + } + + override public var hash: Int { + return ridesScopeType.rawValue.hashValue + } + + /// Convenience variable for the History scope + public static let History = RidesScope(ridesScopeType: .History) + + /// Convenience variable for the HistoryLite scope + public static let HistoryLite = RidesScope(ridesScopeType: .HistoryLite) + + /// Convenience variable for the PaymentMethods scope + public static let PaymentMethods = RidesScope(ridesScopeType: .PaymentMethods) + + /// Convenience variable for the Places scope + public static let Places = RidesScope(ridesScopeType: .Places) + + /// Convenience variable for the Profile scope + public static let Profile = RidesScope(ridesScopeType: .Profile) + + /// Convenience variable for the RideWidgets scope + public static let RideWidgets = RidesScope(ridesScopeType: .RideWidgets) + +} + +class RidesScopeFactory : NSObject { + static func ridesScopeForType(ridesScopeType: RidesScopeType) -> RidesScope { + return RidesScope(ridesScopeType: ridesScopeType) + } + + static func ridesScopeForString(rawString: String) -> RidesScope? { + guard let type = ridesScopeTypeForRawValue(rawString) else { + return nil + } + return ridesScopeForType(type) + } + + static func ridesScopeTypeForRawValue(rawValue: String) -> RidesScopeType? { + switch rawValue.lowercaseString { + case RidesScopeType.History.toString(): + return .History + case RidesScopeType.HistoryLite.toString(): + return .HistoryLite + case RidesScopeType.PaymentMethods.toString(): + return .PaymentMethods + case RidesScopeType.Places.toString(): + return .Places + case RidesScopeType.Profile.toString(): + return .Profile + case RidesScopeType.RideWidgets.toString(): + return .RideWidgets + default: + return nil + } + } +} + +/** + Extending String to allow for easy conversion from space delminated scope string + to array of RidesScopes + */ +extension String { + /** + Converts a string of space delimited scopes into an array of RideScopes + - returns: An array of RidesScope representing the string + */ + func toRidesScopesArray() -> [RidesScope] + { + var scopesArray = [RidesScope]() + for scopeString in self.componentsSeparatedByString(" ") { + guard let scope = RidesScopeFactory.ridesScopeForString(scopeString) else { + continue + } + scopesArray.append(scope) + } + return scopesArray + } +} + +/** + Extends SequenceType of RidesScope to allow for easy conversion to a space + delimited scope string + */ +extension SequenceType where Generator.Element == RidesScope { + /** + Converts an array of RidesScopes into a space delimited String + - parameter scopes: The array of RidesScopes to convert + - returns: A string representing the scopes + */ + func toRidesScopeString() -> String + { + var scopeStringArray = [String]() + for scope in self { + scopeStringArray.append(scope.rawValue) + } + + return scopeStringArray.joinWithSeparator(" ") + } +} diff --git a/source/UberRides/ModelMapper.swift b/source/UberRides/ModelMapper.swift new file mode 100644 index 00000000..449b079c --- /dev/null +++ b/source/UberRides/ModelMapper.swift @@ -0,0 +1,45 @@ +// +// ModelMapper.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import ObjectMapper + +protocol UberModel: Mappable { + init?(_ map: Map) + mutating func mapping(map: Map) +} + +/** + * Layer between models and external callers mapping JSON to and from models. + */ +struct ModelMapper { + /** + Map a JSON string representation to a model that conforms to the Mappable protocol. + + - parameter json: string representing the JSON information. + - returns: an object that conforms to the Mappable protocol. + */ + func mapFromJSON(json: NSString) -> U? { + return Mapper().map(json) + } +} diff --git a/source/UberRides/OAuth/AccessToken.swift b/source/UberRides/OAuth/AccessToken.swift new file mode 100644 index 00000000..2f19fe51 --- /dev/null +++ b/source/UberRides/OAuth/AccessToken.swift @@ -0,0 +1,111 @@ +// +// AccessToken.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import ObjectMapper + +/// Stores information about an access token used for authorizing requests. +@objc(UBSDKAccessToken) public class AccessToken: NSObject, NSCoding, UberModel { + + /// String containing the bearer token. + public private(set) var tokenString: String? + + /// String containing the refresh token. + public private(set) var refreshToken: String? + + /// The expiration date for this access token + public private(set) var expirationDate: NSDate? + + /// The scopes this token is valid for + public private(set) var grantedScopes: [RidesScope]? + + /** + Initializes an AccessToken with the provided tokenString + + - parameter tokenString: The tokenString to use for this AccessToken + + - returns: an initialized AccessToken object + */ + @objc public init(tokenString: String) { + super.init() + self.tokenString = tokenString + } + + /** + Initializer to build an accessToken from the provided NSCoder. Allows for + serialization of an AccessToken + + - parameter decoder: The NSCoder to decode the AcccessToken from + + - returns: An initialized AccessToken, or nil if something went wrong + */ + @objc public required init?(coder decoder: NSCoder) { + super.init() + guard let token = decoder.decodeObjectForKey("tokenString") as? String else { + return nil + } + tokenString = token + refreshToken = decoder.decodeObjectForKey("refreshToken") as? String + expirationDate = decoder.decodeObjectForKey("expirationDate") as? NSDate + if let scopesString = decoder.decodeObjectForKey("grantedScopes") as? String { + grantedScopes = scopesString.toRidesScopesArray() + } + } + + /** + Required initializer for ObjectMapper + + - parameter map: The Map to build an AccessToken from + + - returns: An initialized AccessToken, or nil if someting went wrong + */ + public required init?(_ map: Map) { + } + + /** + Encodes the AccessToken. Required to allow for serialization + + - parameter coder: The NSCoder to encode the access token on + */ + @objc public func encodeWithCoder(coder: NSCoder) { + coder.encodeObject(self.tokenString, forKey: "tokenString") + coder.encodeObject(self.refreshToken, forKey: "refreshToken") + coder.encodeObject(self.expirationDate, forKey: "expirationDate") + coder.encodeObject(self.grantedScopes?.toRidesScopeString(), forKey: "grantedScopes") + } + + /** + Mapping function used by ObjectMapper. Builds an AccessToken using the provided + Map data + + - parameter map: The Map to use for populatng this AccessToken. + */ + public func mapping(map: Map) { + tokenString <- map["access_token"] + refreshToken <- map["refresh_token"] + expirationDate <- (map["expiration_date"], DateTransform()) + var scopesString = String() + scopesString <- map["scope"] + grantedScopes = scopesString.toRidesScopesArray() + } +} diff --git a/source/UberRides/OAuth/AccessTokenFactory.swift b/source/UberRides/OAuth/AccessTokenFactory.swift new file mode 100644 index 00000000..5549ab90 --- /dev/null +++ b/source/UberRides/OAuth/AccessTokenFactory.swift @@ -0,0 +1,82 @@ +// +// AccessTokenFactory.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/** +Factory class to build access tokens +*/ +class AccessTokenFactory { + /** + Builds an AccessToken from the provided redirect URL + + - throws: RidesAuthenticationError + - parameter url: The URL to parse the token from + - returns: An initialized AccessToken, or nil if one couldn't be created + */ + static func createAccessTokenFromRedirectURL(url : NSURL) throws -> AccessToken { + guard let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) else { + throw RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidResponse) + } + + var finalQueryArray = [String]() + if let existingQuery = components.query { + finalQueryArray.append(existingQuery) + } + if let existingFragment = components.fragment { + finalQueryArray.append(existingFragment) + } + components.fragment = nil + components.query = finalQueryArray.joinWithSeparator("&") + + guard let queryItems = components.queryItems else { + throw RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidRequest) + } + var queryDictionary = [String : String]() + for queryItem in queryItems { + guard let value = queryItem.value else { + continue + } + queryDictionary[queryItem.name] = value + } + if let error = queryDictionary["error"] { + guard let error = RidesAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: error) else { + throw RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidRequest) + } + throw error + } else { + if let expiresInString = queryDictionary["expires_in"] as String? { + let expiresInSeconds = NSTimeInterval(atof(expiresInString)) + let expirationDateSeconds = NSDate().timeIntervalSince1970 + expiresInSeconds + queryDictionary["expiration_date"] = "\(expirationDateSeconds)" + queryDictionary.removeValueForKey("expires_in") + } + if let token = AccessToken(JSON: queryDictionary) { + return token + } + } + throw RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidResponse) + } +} diff --git a/source/UberRides/OAuth/KeychainWrapper.swift b/source/UberRides/OAuth/KeychainWrapper.swift new file mode 100644 index 00000000..52b76931 --- /dev/null +++ b/source/UberRides/OAuth/KeychainWrapper.swift @@ -0,0 +1,135 @@ +// +// KeychainWrapper.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/// Wraps saving and retrieving objects from keychain. +class KeychainWrapper: NSObject { + private static let serviceName = "com.uber.rides-ios-sdk" + private var accessGroup = "" + + private let Class = kSecClass as String + private let AttrAccount = kSecAttrAccount as String + private let AttrService = kSecAttrService as String + private let AttrAccessGroup = kSecAttrAccessGroup as String + private let AttrGeneric = kSecAttrGeneric as String + private let AttrAccessible = kSecAttrAccessible as String + private let ReturnData = kSecReturnData as String + private let ValueData = kSecValueData as String + private let MatchLimit = kSecMatchLimit as String + + /** + Set the access group for keychain to use. + + - parameter group: String representing name of keychain access group. + */ + func setAccessGroup(accessGroup: String) { + self.accessGroup = accessGroup + } + + /** + Save an object to keychain. + + - parameter object: object conforming to NSCoding to save to keychain. + - parameter key: key for the object. + + - returns: true if object was successfully added to keychain. + */ + func setObject(object: NSCoding, key: String) -> Bool { + var keychainItemData = getKeychainItemData(key) + + let value = NSKeyedArchiver.archivedDataWithRootObject(object) + keychainItemData[AttrAccessible] = kSecAttrAccessibleWhenUnlocked + keychainItemData[ValueData] = value + + var result: OSStatus = SecItemAdd(keychainItemData, nil) + + if result == errSecDuplicateItem { + result = SecItemUpdate(keychainItemData, [ValueData: value]) + } + + return result == errSecSuccess + } + + /** + Get an object from the keychain. + + - parameter key: the key associated to the object to retrieve. + + - returns: the object in keychain or nil if none exists for the given key. + */ + func getObjectForKey(key: String) -> NSCoding? { + var keychainItemData = getKeychainItemData(key) + + keychainItemData[MatchLimit] = kSecMatchLimitOne + keychainItemData[ReturnData] = kCFBooleanTrue + + var data: AnyObject? + let result = withUnsafeMutablePointer(&data) { + SecItemCopyMatching(keychainItemData, UnsafeMutablePointer($0)) + } + + var object: AnyObject? + + if let data = data as? NSData { + object = NSKeyedUnarchiver.unarchiveObjectWithData(data) + } + + return result == noErr ? object as? NSCoding : nil + } + + /** + Remove an object from keychain + + - parameter key: key for object to remove. + + - returns: true if object was successfully deleted. + */ + func deleteObjectForKey(key: String) -> Bool { + let keychainItemData = getKeychainItemData(key) + + let result = SecItemDelete(keychainItemData) + + return result == noErr + } + + /** + Helper method to build keychain query dictionary. + + - returns: dictionary of base attributes for keychain query. + */ + private func getKeychainItemData(key: String) -> [String: AnyObject] { + var keychainItemData = [String: AnyObject]() + + let identifier = key.dataUsingEncoding(NSUTF8StringEncoding) + keychainItemData[AttrGeneric] = identifier + keychainItemData[AttrAccount] = identifier + keychainItemData[AttrService] = self.dynamicType.serviceName + keychainItemData[Class] = kSecClassGenericPassword + + if !accessGroup.isEmpty { + keychainItemData[AttrAccount] = accessGroup + } + + return keychainItemData + } +} diff --git a/source/UberRides/OAuth/LoginManager.swift b/source/UberRides/OAuth/LoginManager.swift new file mode 100644 index 00000000..bdb581a1 --- /dev/null +++ b/source/UberRides/OAuth/LoginManager.swift @@ -0,0 +1,191 @@ +// +// LoginManager.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/** +The type of behaviour that login manager should use for authentication. + +- Implicit: Implicit grant (only valid for general scope endpoints). +*/ +@objc public enum LoginBehavior : Int { + case Implicit +} + +/// Manages user login via implicit grant. +@objc(UBSDKLoginManager) public class LoginManager: NSObject { + + /// Completion handler for when login has succeeded or retrieved an error. + var loginCompletion: ((accessToken: AccessToken?, error: NSError?) -> Void)? + + var accessTokenIdentifier: String + var keychainAccessGroup: String + var loginBehavior: LoginBehavior + var oauthViewController: OAuthViewController? + + private var currentScopes: [RidesScope]? + private var currentPresentingViewController: UIViewController? + + + /** + Create instance of login manager to authenticate user and retreive access token. + + - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() + - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.getDefaultKeychainAccessGroup() + - parameter loginBehavior: The login behavior to use for logging in, defaults to Implicit + + - returns: An initialized LoginManager + */ + @objc public init(accessTokenIdentifier: String, keychainAccessGroup: String?, loginBehavior: LoginBehavior) { + + self.accessTokenIdentifier = accessTokenIdentifier + self.keychainAccessGroup = keychainAccessGroup ?? Configuration.getDefaultKeychainAccessGroup() + self.loginBehavior = loginBehavior + self.loginCompletion = nil + + super.init() + } + + /** + Create instance of login manager to authenticate user and retreive access token. + Uses the Implicit Login Behavior + + - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() + - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.getDefaultKeychainAccessGroup() + + - returns: An initialized LoginManager + */ + @objc public convenience init(accessTokenIdentifier: String, keychainAccessGroup: String?) { + let accessGroup = keychainAccessGroup ?? Configuration.getDefaultKeychainAccessGroup() + self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: accessGroup, loginBehavior: LoginBehavior.Implicit) + } + + /** + Create instance of login manager to authenticate user and retreive access token. + Uses the Implicit Login Behavior & your Configuration's keychain access group + + - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() + + - returns: An initialized LoginManager + */ + @objc public convenience init(accessTokenIdentifier: String) { + self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: nil) + } + + /** + Create instance of login manager to authenticate user and retreive access token. + Uses the provided LoginBehavior, with the accessTokenIdentifier & keychainAccessGroup defined + in your Configuration + + - parameter loginBehavior: The login behavior to use for logging in + + - returns: An initialized LoginManager + */ + @objc public convenience init(loginBehavior: LoginBehavior) { + self.init(accessTokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), keychainAccessGroup: Configuration.getDefaultAccessTokenIdentifier(), loginBehavior: loginBehavior) + } + + /** + Create instance of login manager to authenticate user and retreive access token. + Uses the Implicit LoginBehavior, with the accessTokenIdentifier & keychainAccessGroup defined + in your Configuration + + - returns: An initialized LoginManager + */ + @objc public convenience override init() { + self.init(accessTokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), keychainAccessGroup: Configuration.getDefaultAccessTokenIdentifier(), loginBehavior: LoginBehavior.Implicit) + } + + /** + Launches view for user to log into Uber account and grant access to requested scopes. + Access token (or error) is passed into completion handler. + + - parameter scopes: scopes being requested. + - parameter presentingViewController: The presenting view controller present the login view controller over. + - parameter completion: The LoginManagerRequestTokenHandler completion handler for login success/failure. + */ + @objc public func login(requestedScopes scopes: [RidesScope], presentingViewController: UIViewController? = nil , completion: ((accessToken: AccessToken?, error: NSError?) -> Void)? = nil) { + self.loginCompletion = completion + + switch(self.loginBehavior) { + case .Implicit: + guard let presentingViewController = presentingViewController else { + completion?(accessToken: nil, error: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .UnableToPresentLogin)) + self.loginCompletion = nil + return + } + + self.loginCompletion = { token, error in + presentingViewController.dismissViewControllerAnimated(true, completion: { () -> Void in + completion?(accessToken: token, error: error) + }) + }; + + let oauthViewController = OAuthViewController(scopes: scopes) + oauthViewController.loginView.delegate = self + let navController = UINavigationController(rootViewController: oauthViewController) + self.oauthViewController = oauthViewController + + presentingViewController.presentViewController(navController, animated: true, completion: nil) + } + } + + // MARK: Private + + private func displayNetworkErrorAlert() { + guard let oauthViewController = oauthViewController else { + self.loginCompletion?(accessToken: nil, error: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .NetworkError)) + return + } + oauthViewController.loginView.cancelLoad() + let alertController = UIAlertController(title: nil, message: LocalizationUtil.localizedString(forKey: "The Ride Request Widget encountered a problem.", comment: "The Ride Request Widget encountered a problem."), preferredStyle: .Alert) + let tryAgainAction = UIAlertAction(title: LocalizationUtil.localizedString(forKey: "Try Again", comment: "Try Again"), style: .Default, handler: { (UIAlertAction) -> Void in + oauthViewController.loginView.load() + }) + let cancelAction = UIAlertAction(title: LocalizationUtil.localizedString(forKey: "Cancel", comment: "Cancel"), style: .Cancel, handler: { (UIAlertAction) -> Void in + self.loginCompletion?(accessToken: nil, error: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .NetworkError)) + }) + alertController.addAction(tryAgainAction) + alertController.addAction(cancelAction) + oauthViewController.presentViewController(alertController, animated: true, completion: nil) + } +} + +// MARK: LoginViewDelegate + +extension LoginManager: LoginViewDelegate { + @objc public func loginView(loginWebView: LoginView, didSucceedWithToken accessToken: AccessToken) { + if !TokenManager.saveToken(accessToken, tokenIdentifier: accessTokenIdentifier, accessGroup: keychainAccessGroup) { + print("Error: access token failed to save to keychain") + } + self.loginCompletion?(accessToken: accessToken, error: nil) + } + + @objc public func loginView(loginWebView: LoginView, didFailWithError error: NSError) { + guard error.code != RidesAuthenticationErrorType.NetworkError.rawValue else { + displayNetworkErrorAlert() + return + } + + self.loginCompletion?(accessToken: nil, error: error) + } +} \ No newline at end of file diff --git a/source/UberRides/OAuth/LoginView.swift b/source/UberRides/OAuth/LoginView.swift new file mode 100644 index 00000000..016e3b7d --- /dev/null +++ b/source/UberRides/OAuth/LoginView.swift @@ -0,0 +1,219 @@ +// +// LoginWebView.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import WebKit + +/** + * LoginWebViewDelegate protocol. Alerts delegate to login outcome with either + a valid access token or an error describing what went wrong + */ +@objc public protocol LoginViewDelegate { + + /** + Called after an access token has been successfully retrieved. + + - parameter loginView: The LoginView that completed log in + - parameter accessToken: The AccessToken received from login + */ + @objc func loginView(loginView: LoginView, didSucceedWithToken accessToken: AccessToken) + + /** + Called if the LoginView experienced an error when attempting to log in. The + included authentication error provides more details + + - parameter loginView: The LoginView that experienced the error + - parameter error: An NSError describing what went wrong, code contains the RidesAuthenticationError that occured. + */ + @objc func loginView(loginView: LoginView, didFailWithError error: NSError) +} + +/// Login Web View class. Wrapper around a WKWebView to handle Login flow for Implicit Grant +@objc(UBSDKLoginView) public class LoginView : UIView { + + public var delegate : LoginViewDelegate? + public var scopes: [RidesScope]? + + var callbackURIString = Configuration.getCallbackURIString() + var clientID = Configuration.getClientID() + let webView: WKWebView + + //MARK: Initializers + + /** + Creates a LoginWebView for obtaining an access token + + - parameter scopes: Array of RidesScope that you would like to request + - parameter frame: The frame to use for the view, defaults to CGRectZero + + - returns: An initialized LoginWebView + */ + @objc public init(scopes: [RidesScope], frame: CGRect) { + let configuration = WKWebViewConfiguration() + configuration.processPool = Configuration.processPool + webView = WKWebView.init(frame: frame, configuration: configuration) + + var filteredScopes = [RidesScope]() + for scope in scopes { + if scope.scopeType == .General { + filteredScopes.append(scope) + } + } + + if filteredScopes.count < scopes.count { + print("Warning: can not request access to privileged scopes via implicit grant.") + } + + self.scopes = filteredScopes + super.init(frame: frame) + webView.navigationDelegate = self + self.addSubview(webView) + setupWebView() + } + + /** + Creates a LoginWebView for obtaining an access token. + Defaults to a CGRectZero Frame + + - parameter scopes: Array of RidesScope that you would like to request + + - returns: An initialized LoginWebView + */ + @objc public convenience init(scopes: [RidesScope]) { + self.init(scopes: scopes, frame: CGRectZero) + } + + /** + Initializer for adding a LoginWebView via Storyboard. If using this constructor, + you must add the scopes you want before attempting to call loadLoginPage() + + - parameter aDecoder: The coder to use + + - returns: An initialized loginWebView + */ + required public init?(coder aDecoder: NSCoder) { + webView = WKWebView() + callbackURIString = Configuration.getCallbackURIString() + super.init(coder: aDecoder) + webView.navigationDelegate = self + self.addSubview(webView) + setupWebView() + } + + //MARK: View setup + + func setupWebView() { + self.webView.scrollView.bounces = false + self.webView.translatesAutoresizingMaskIntoConstraints = false + + let views = ["webView": webView] + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[webView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[webView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + + addConstraints(horizontalConstraints) + addConstraints(verticalConstraints) + } + + //MARK: Actions + + /** + Loads the login page + */ + public func load() { + guard let scopes = scopes else { + print("Must set request scopes") + return + } + + // Create URL for request + let endpoint = OAuth.Login(clientID: clientID, scopes: scopes, redirect: callbackURIString) + let request = Request(session: nil, endpoint: endpoint) + request.prepare() + + guard let _ = request.requestURL() else { + self.delegate?.loginView(self, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType:.InvalidRequest)) + return + } + + // Load request in web view + self.webView.loadRequest(request.urlRequest) + } + + /** + Stops loading the login page and clears the view. + If the login page has already loaded, calling this still clears the view. + */ + public func cancelLoad() { + webView.stopLoading() + if let url = NSURL(string: "about:blank") { + webView.loadRequest(NSURLRequest(URL: url)) + } + } +} + +extension LoginView : WKNavigationDelegate { + + public func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) { + + var shouldLoad = true + + if navigationAction.request.URL?.absoluteString.lowercaseString.hasPrefix(callbackURIString.lowercaseString) == true { + do { + let accessToken = try AccessTokenFactory.createAccessTokenFromRedirectURL(navigationAction.request.URL!) + self.delegate?.loginView(self, didSucceedWithToken: accessToken) + } catch let ridesError as NSError { + self.delegate?.loginView(self, didFailWithError: ridesError) + } catch { + self.delegate?.loginView(self, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidResponse)) + } + shouldLoad = false + } + + if shouldLoad { + if navigationAction.request.URL?.absoluteString.containsString("errors") == true { + let authError = OAuthUtil.parseAuthenticationErrorFromURL(navigationAction.request.URL!) + + self.delegate?.loginView(self, didFailWithError: authError) + shouldLoad = false + } + } + + if (shouldLoad) { + decisionHandler(WKNavigationActionPolicy.Allow) + } else { + decisionHandler(WKNavigationActionPolicy.Cancel) + } + } + + public func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) { + self.delegate?.loginView(self, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .NetworkError)) + } + + public func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) { + if error.code != 102 { + self.delegate?.loginView(self, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .NetworkError)) + } + } +} + diff --git a/source/UberRides/OAuth/OAuthViewController.swift b/source/UberRides/OAuth/OAuthViewController.swift new file mode 100644 index 00000000..4c1c2ec8 --- /dev/null +++ b/source/UberRides/OAuth/OAuthViewController.swift @@ -0,0 +1,109 @@ +// +// OAuthViewController.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +// MARK: View Controller Lifecycle + +// View controller to users to enter credentials for OAuth. +@objc(UBSDKOAuthViewController) +class OAuthViewController: UIViewController { + + var scopes: [RidesScope]? { + didSet { + loginView.scopes = scopes + } + } + + var hasLoaded = false + var loginView: LoginView + + /** + Initializes the web view controller with the necessary information. + + - parameter scopes: An array of scopes to request the user to authorize. (see RidesScope) + + - returns: An initialized OAuthWebViewController + */ + @objc init(scopes: [RidesScope]) { + loginView = LoginView(scopes: scopes) + super.init(nibName: nil, bundle: nil) + } + + /** + Initializer for storyboard. If using this, you must set your scopes before attempting + to present this view controller + + - parameter aDecoder: the coder to use + + - returns: An initialized OAuthWebViewController, or nil if something went wrong + */ + @objc required init?(coder aDecoder: NSCoder) { + guard let loginView = LoginView(coder: aDecoder) else { + self.loginView = LoginView(scopes: []) + super.init(nibName: nil, bundle: nil) + return nil + } + + self.loginView = loginView + super.init(coder: aDecoder) + } + + override func viewDidLoad() { + super.viewDidLoad() + self.edgesForExtendedLayout = UIRectEdge.None + self.view.addSubview(loginView) + self.setupLoginView() + // Set up navigation item + let cancelButton = UIBarButtonItem(barButtonSystemItem: .Cancel, target: self, action: Selector("cancel")) + navigationItem.leftBarButtonItem = cancelButton + navigationItem.title = LocalizationUtil.localizedString(forKey: "Sign in with Uber", comment: "Title of navigation bar during OAuth") + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + if !hasLoaded { + self.loginView.load() + } + } + + func cancel() { + self.loginView.delegate?.loginView(self.loginView, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .UserCancelled)) + dismissViewControllerAnimated(true, completion: nil) + } + + //MARK: View Setup + + func setupLoginView() { + self.loginView.translatesAutoresizingMaskIntoConstraints = false + + let views = ["loginView": self.loginView] + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[loginView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[loginView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + + self.view.addConstraints(horizontalConstraints) + self.view.addConstraints(verticalConstraints) + } +} diff --git a/source/UberRides/Request.swift b/source/UberRides/Request.swift new file mode 100644 index 00000000..7048240e --- /dev/null +++ b/source/UberRides/Request.swift @@ -0,0 +1,81 @@ +// +// Request.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/// Class to create and execute NSURLRequests. +class Request: NSObject { + let session: NSURLSession? + let endpoint: UberAPI + let urlRequest: NSMutableURLRequest + let bearerToken: NSString? + + /** + Initialize a request object. + + - parameter hostURL: Host URL string for API. + - parameter session: NSURLSession to execute request with. + - parameter endpoint: UberAPI conforming endpoint. + */ + init(session: NSURLSession?, endpoint: UberAPI, bearerToken: NSString? = nil) { + self.session = session + self.endpoint = endpoint + self.urlRequest = NSMutableURLRequest() + self.bearerToken = bearerToken + } + + /** + Creates a URL based off the endpoint requested. Function asserts for valid URL. + + - returns: constructed NSURL or nil if construction failed. + */ + func requestURL() -> NSURL? { + let components = NSURLComponents(string: endpoint.host)! + components.path = endpoint.path + components.queryItems = endpoint.query + + return components.URL + } + + /** + Adds HTTP Headers to the request. + */ + private func addHeaders() { + if let token = bearerToken { + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + } + + private func addGzip() { + urlRequest.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding") + } + + /** + Prepares the NSURLRequest by adding necessary fields. + */ + func prepare() { + urlRequest.URL = requestURL() + urlRequest.HTTPMethod = endpoint.HTTPMethod.rawValue + addHeaders() + addGzip() + } +} diff --git a/source/UberRides/RequestButton.swift b/source/UberRides/RequestButton.swift deleted file mode 100644 index d13c8d32..00000000 --- a/source/UberRides/RequestButton.swift +++ /dev/null @@ -1,250 +0,0 @@ -// -// RequestButton.swift -// UberRides -// -// Copyright © 2015 Uber Technologies, Inc. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - - -import UIKit - -// RequestButton implements a button on the touch screen to request a ride. -public class RequestButton: UIButton { - var deeplink: RequestDeeplink? - var contentWidth: CGFloat = 0 - var contentHeight: CGFloat = 0 - let padding: CGFloat = 8 - let imageSize: CGFloat = 28 - var buttonStyle: RequestButtonColorStyle - - let uberImageView: UIImageView! - let uberTitleLabel: UILabel! - - // initializer to use in storyboard - required public init?(coder aDecoder: NSCoder) { - uberImageView = UIImageView() - uberTitleLabel = UILabel() - buttonStyle = .Black - super.init(coder: aDecoder) - setUp(.Black) - } - - public convenience init() { - self.init(colorStyle: .Black) - } - - // swift-style initializer - public init(colorStyle: RequestButtonColorStyle) { - uberImageView = UIImageView() - uberTitleLabel = UILabel() - buttonStyle = colorStyle - super.init(frame: CGRectZero) - setUp(colorStyle) - } - - private func setUp(colorStyle: RequestButtonColorStyle) { - do { - try setDeeplink() - addTarget(self, action: "uberButtonTapped:", forControlEvents: .TouchUpInside) - } catch RequestButtonError.NullClientID { - print("No Client ID attached to the deeplink.") - } catch let error { - print(error) - } - - setContent() - setConstraints() - setColorStyle(colorStyle) - } - - // build and attach a deeplink to the button - private func setDeeplink() throws { - guard RidesClient.sharedInstance.hasClientID() else { - throw RequestButtonError.NullClientID - } - - let clientID = RidesClient.sharedInstance.clientID - deeplink = RequestDeeplink(withClientID: clientID!, fromSource: .Button) - } - - /** - Set the user's current location as a default pickup location. - */ - public func setPickupLocationToCurrentLocation() { - if RidesClient.sharedInstance.hasClientID() { - deeplink!.setPickupLocationToCurrentLocation() - } - } - - /** - Set deeplink pickup location information. - - - parameter latitude: The latitude coordinate for pickup - - parameter longitude: The longitude coordinate for pickup - - parameter nickname: Optional pickup location name - - parameter address: Optional pickup location address - */ - public func setPickupLocation(latitude latitude: Double, longitude: Double, nickname: String? = nil, address: String? = nil) { - if RidesClient.sharedInstance.hasClientID() { - deeplink!.setPickupLocation(latitude: latitude, longitude: longitude, nickname: nickname, address: address) - } - } - - /** - Set deeplink dropoff location information. - - - parameter latitude: The latitude coordinate for dropoff - - parameter longitude: The longitude coordinate for dropoff - - parameter nickname: Optional dropoff location name - - parameter address: Optional dropoff location address - */ - public func setDropoffLocation(latitude latitude: Double, longitude: Double, nickname: String? = nil, address: String? = nil) { - if RidesClient.sharedInstance.hasClientID() { - deeplink!.setDropoffLocation(latitude: latitude, longitude: longitude, nickname: nickname, address: address) - } - } - - /** - Add a specific product ID to the deeplink. You can see product ID's for a given - location with the Rides API `GET /v1/products` endpoint. - - - parameter productID: Unique identifier of the product to populate in pickup - */ - public func setProductID(productID: String) { - if RidesClient.sharedInstance.hasClientID() { - deeplink!.setProductID(productID) - } - } - - // add title, image, and sizing configuration - private func setContent() { - // add title label - let bundle = NSBundle(forClass: RequestButton.self) - uberTitleLabel.text = NSLocalizedString("RequestButton.TitleText", bundle: bundle, comment: "Request button description") - uberTitleLabel.font = UIFont.systemFontOfSize(17) - uberTitleLabel.numberOfLines = 1; - - // add image - let badge = getImage("Badge") - uberImageView.image = badge - - // update content sizes - let titleSize = uberTitleLabel!.intrinsicContentSize() - contentWidth += titleSize.width + badge.size.width - contentHeight = max(titleSize.height, badge.size.height) - - // rounded corners - clipsToBounds = true - layer.cornerRadius = 5 - - // set to false for constraint-based layouts - translatesAutoresizingMaskIntoConstraints = false - } - - // get image from media directory - private func getImage(name: String) -> UIImage { - let bundle = NSBundle(forClass: RequestButton.self) - let image = UIImage(named: name, inBundle: bundle, compatibleWithTraitCollection: nil) - return image! - } - - private func setConstraints() { - addSubview(uberImageView) - addSubview(uberTitleLabel) - - // store constraints and metrics in dictionaries - let views = ["image": uberImageView!, "label": uberTitleLabel!] - let metrics = ["padding": padding, "imageSize": imageSize] - - // set to false for constraint-based layouts - uberImageView?.translatesAutoresizingMaskIntoConstraints = false - uberTitleLabel?.translatesAutoresizingMaskIntoConstraints = false - - // prioritize constraints - uberTitleLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal) - - // create layout constraints - let horizontalConstraint: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("H:|-padding-[image(24)]-padding-[label]-padding-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views) - let imageVerticalViewConstraint: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("V:|-[image(24)]-|", options: NSLayoutFormatOptions.AlignAllLeading, metrics: nil, views: views) - let labelVerticalViewConstraint: NSArray = NSLayoutConstraint.constraintsWithVisualFormat("V:|-padding-[label]-padding-|", options: NSLayoutFormatOptions.AlignAllLeading, metrics: metrics, views: views) - - // add layout constraints - addConstraints(horizontalConstraint as! [NSLayoutConstraint]) - addConstraints(imageVerticalViewConstraint as! [NSLayoutConstraint]) - addConstraints(labelVerticalViewConstraint as! [NSLayoutConstraint]) - } - - // set color scheme, default is black background with white font - private func setColorStyle(style: RequestButtonColorStyle) { - buttonStyle = style - - switch style { - case .Black: - uberTitleLabel.textColor = uberUIColor(.UberWhite) - backgroundColor = uberUIColor(.UberBlack) - case .White : - uberTitleLabel.textColor = uberUIColor(.UberBlack) - backgroundColor = uberUIColor(.UberWhite) - } - } - - // override to maintain fit-to-content size - public override func intrinsicContentSize() -> CGSize { - let width = (3 * padding) + contentWidth - let height = (2 * padding) + contentHeight - return CGSizeMake(width, height) - } - - // override to change colors when button is tapped - override public var highlighted: Bool { - didSet { - if buttonStyle == .Black { - if highlighted { - backgroundColor = uberUIColor(.BlackHighlighted) - } else { - backgroundColor = uberUIColor(.UberBlack) - } - } else if buttonStyle == .White { - if highlighted { - backgroundColor = uberUIColor(.WhiteHighlighted) - } else { - backgroundColor = uberUIColor(.UberWhite) - } - } - } - } - - // initiate deeplink when button is tapped - public func uberButtonTapped(sender: UIButton) { - if RidesClient.sharedInstance.hasClientID() { - deeplink!.build() - deeplink!.execute() - } - } -} - -@objc public enum RequestButtonColorStyle: Int { - case Black - case White -} - -private enum RequestButtonError: ErrorType { - case NullClientID -} diff --git a/source/UberRides/RequestDeeplink.swift b/source/UberRides/RequestDeeplink.swift index 30389ad8..f0da7650 100644 --- a/source/UberRides/RequestDeeplink.swift +++ b/source/UberRides/RequestDeeplink.swift @@ -23,254 +23,55 @@ // THE SOFTWARE. -import Foundation +import CoreLocation import UIKit -// RequestDeeplink builds and executes a deeplink to the native Uber app. -public class RequestDeeplink: NSObject { - private var parameters: QueryParameters - private var clientID: String - private var deeplinkURI: String? - private var source: RequestDeeplink.SourceParameter +/// Builds and executes a deeplink to the native Uber app. +@objc(UBSDKRequestDeeplink) public class RequestDeeplink: NSObject { + private let parameters: RideParameters + private let clientID: String - public init(withClientID: String, fromSource: SourceParameter = .Deeplink) { - parameters = QueryParameters() - clientID = withClientID - source = fromSource - parameters.setParameter(.ClientID, parameterValue: clientID) - } + let deeplinkURL: NSURL? - /** - Build a deeplink URI. - */ - public func build() -> String { - if !pickupLocationSet() { - setPickupLocationToCurrentLocation() - } - - if !parameters.pendingChanges { - return deeplinkURI! - } - - let components = NSURLComponents() - components.scheme = "uber" - components.host = "" - components.queryItems = parameters.getQueryItems() - - parameters.pendingChanges = false; - - deeplinkURI = components.string?.stringByRemovingPercentEncoding - return deeplinkURI! - } + static let sourceString = "deeplink" - /** - Execute deeplink to launch the Uber app. Redirect to the app store if the app is not installed. - */ - public func execute() { - let deeplinkURL = createURL(deeplinkURI!) - let appstoreURL = createURL("https://m.uber.com/sign-up?client_id=" + clientID) - - if UIApplication.sharedApplication().canOpenURL(deeplinkURL) { - UIApplication.sharedApplication().openURL(deeplinkURL) - } else { - UIApplication.sharedApplication().openURL(appstoreURL) - } - } - - /** - Set the user's current location as a default pickup location. - */ - public func setPickupLocationToCurrentLocation() { - parameters.setParameter(.Action, parameterValue: "setPickup") - parameters.setParameter(.PickupDefault, parameterValue: "my_location") - parameters.deleteParameters([.PickupLatitude, .PickupLongitude, .PickupAddress, .PickupNickname]) - } - - /** - Set deeplink pickup location information. - - - parameter latitude: The latitude coordinate for pickup. - - parameter longitude: The longitude coordinate for pickup. - - parameter nickname: A URL-encoded string of the pickup location name. (Optional) - - parameter address: A URL-encoded string of the pickup address. (Optional) - */ - public func setPickupLocation(latitude latitude: Double, longitude: Double, nickname: String? = nil, address: String? = nil) { - parameters.deleteParameters([.PickupNickname, .PickupAddress]) - parameters.setParameter(.Action, parameterValue: "setPickup") - parameters.setParameter(.PickupLatitude, parameterValue: "\(latitude)") - parameters.setParameter(.PickupLongitude, parameterValue: "\(longitude)") + @objc public init(rideParameters: RideParameters = RideParametersBuilder().build()) { + parameters = rideParameters + clientID = Configuration.getClientID() - if nickname != nil { - parameters.setParameter(.PickupNickname, parameterValue: nickname!) + if rideParameters.source == nil { + rideParameters.source = RequestDeeplink.sourceString } - if address != nil { - parameters.setParameter(.PickupAddress, parameterValue: address!) - } - - parameters.deleteParameters([.PickupDefault]) - } - - /** - Set deeplink dropoff location information. - - - parameter latitude: The latitude coordinate for dropoff. - - parameter longitude: The longitude coordinate for dropoff. - - parameter nickname: A URL-encoded string of the dropoff location name. (Optional) - - parameter address: A URL-encoded string of the dropoff address. (Optional) - */ - public func setDropoffLocation(latitude latitude: Double, longitude: Double, nickname: String? = nil, address: String? = nil) { - parameters.deleteParameters([.DropoffNickname, .DropoffAddress]) - parameters.setParameter(.DropoffLatitude, parameterValue: "\(latitude)") - parameters.setParameter(.DropoffLongitude, parameterValue: "\(longitude)") - if nickname != nil { - parameters.setParameter(.DropoffNickname, parameterValue: nickname!) - } - if address != nil { - parameters.setParameter(.DropoffAddress, parameterValue: address!) + do { + try deeplinkURL = RequestURLUtil.buildURL(rideParameters) + } catch { + deeplinkURL = nil } } /** - Add a specific product ID to the deeplink. You can see product ID's for a given - location with the Rides API `GET /v1/products` endpoint. - */ - public func setProductID(productID: String) { - parameters.setParameter(.ProductID, parameterValue: productID) - } - - /** - Return true if deeplink has set pickup latitude and longitude, false otherwise. - */ - internal func pickupLocationSet() -> Bool { - return (parameters.doesParameterExist(.PickupLatitude) && parameters.doesParameterExist(.PickupLongitude)) || parameters.doesParameterExist(.PickupDefault) - } - - /** - Possible sources for the deeplink. - */ - @objc public enum SourceParameter: Int { - case Button - case Deeplink - } - - /** - Create an NSURL from a String. Add parameter for tracking and affiliation program. - */ - func createURL(var url: String) -> NSURL { - switch source { - case .Button: - url += "&user-agent=rides-button-v0.1.0" - case .Deeplink: - url += "&user-agent=rides-deeplink-v0.1.0" - } - return NSURL(string: url)! - } -} - - -// Store mapping of parameter names to values -private class QueryParameters: NSObject { - private var params = [String: String]() - private var pendingChanges: Bool - - private override init() { - pendingChanges = false; - } - - /** - QueryParameterName is a set of query parameters than can be sent - in a deeplink. `clientID` is a required query parameter. + Execute deeplink to launch the Uber app. Redirect to the app store if the app is not installed. - Optional query parameters can be used to automatically pass additional - information, like a user's destination, over to the native Uber App. + - returns: true if the deeplink was executed, false otherwise. Note that an appstore redirect is considered success */ - private enum QueryParameterName: Int { - case Action - case ClientID - case ProductID - case PickupDefault - case PickupLatitude - case PickupLongitude - case PickupNickname - case PickupAddress - case DropoffLatitude - case DropoffLongitude - case DropoffNickname - case DropoffAddress - } - - /** - Adds a query parameter. If parameterName has already been assigned a value, - its overwritten with parameterValue. - */ - private func setParameter(parameterName: QueryParameterName, parameterValue: String) { - params[stringFromParameterName(parameterName)] = stringFromParameterValue(parameterValue) - pendingChanges = true - } - - /** - Removes key-value pair of all query parameters in array of parameter names. - */ - private func deleteParameters(parameters: Array) { - for name in parameters { - params.removeValueForKey(stringFromParameterName(name)) + @objc public func execute() -> Bool { + if let deeplinkURL = deeplinkURL where UIApplication.sharedApplication().canOpenURL(deeplinkURL) { + return UIApplication.sharedApplication().openURL(deeplinkURL) } - pendingChanges = true - } - - /** - - returns: An array containing an NSURLQueryItem for every parameter - */ - private func getQueryItems() -> Array { - var queryItems = [NSURLQueryItem]() - for (parameterName, parameterValue) in params { - let queryItem = NSURLQueryItem(name: parameterName, value: parameterValue) - queryItems.append(queryItem) + if let appstoreURL = createURL("https://m.uber.com/sign-up") { + return UIApplication.sharedApplication().openURL(appstoreURL) } - return queryItems - } - - /** - - returns: true if given query parameter has been set; false otherwise. - */ - private func doesParameterExist(parameterName: QueryParameterName) -> Bool { - return params[stringFromParameterName(parameterName)] != nil - } - - private func stringFromParameterName(name: QueryParameterName) -> String { - switch name { - case .Action: - return "action" - case .ClientID: - return "client_id" - case .ProductID: - return "product_id" - case .PickupDefault: - return "pickup" - case .PickupLatitude: - return "pickup[latitude]" - case .PickupLongitude: - return "pickup[longitude]" - case .PickupNickname: - return "pickup[nickname]" - case .PickupAddress: - return "pickup[formatted_address]" - case .DropoffLatitude: - return "dropoff[latitude]" - case .DropoffLongitude: - return "dropoff[longitude]" - case .DropoffNickname: - return "dropoff[nickname]" - case .DropoffAddress: - return "dropoff[formatted_address]" - } + return false } - private func stringFromParameterValue(value: String) -> String { - let customAllowedChars = NSCharacterSet(charactersInString: " =\"#%/<>?@\\^`{|}!$&'()*+,:;[]%").invertedSet - return value.stringByAddingPercentEncodingWithAllowedCharacters(customAllowedChars)! + func createURL(url: String) -> NSURL? { + let clientIDItem = NSURLQueryItem(name: RequestURLUtil.clientIDKey, value: clientID) + let userAgentItem = NSURLQueryItem(name: RequestURLUtil.userAgentKey, value: parameters.userAgent) + let urlComponents = NSURLComponents(string: url) + urlComponents?.queryItems = [ clientIDItem, userAgentItem ] + return urlComponents?.URL } } diff --git a/source/UberRides/RequestURLBuilder.swift b/source/UberRides/RequestURLBuilder.swift new file mode 100644 index 00000000..590d9d57 --- /dev/null +++ b/source/UberRides/RequestURLBuilder.swift @@ -0,0 +1,110 @@ +// +// RequestURLBuilder.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreLocation + +class RequestURLBuilder { + + private enum LocationType: String { + case Pickup = "pickup" + case Dropoff = "dropoff" + } + + static let actionKey = "action" + static let setPickupValue = "setPickup" + static let clientIDKey = "client_id" + static let productIDKey = "product_id" + static let currentLocationValue = "my_location" + static let latitudeKey = "[latitude]" + static let longitudeKey = "[longitude]" + static let nicknameKey = "[nickname]" + static let formattedAddressKey = "[formatted_address]" + static let deeplinkScheme = "uber" + static let userAgentKey = "user-agent" + + private let clientID: String + private var rideParameters: RideParameters? + + init() { + clientID = Configuration.getClientID() + } + + func setRideParameters(rideParameters: RideParameters) -> RequestURLBuilder { + self.rideParameters = rideParameters + + return self + } + + func build() -> NSURL? { + guard let rideParameters = rideParameters else { + return nil + } + let urlComponents = NSURLComponents() + + urlComponents.scheme = RequestURLBuilder.deeplinkScheme + urlComponents.host = "" + + var queryItems = [NSURLQueryItem]() + queryItems.append(NSURLQueryItem(name: RequestURLBuilder.actionKey, value: RequestURLBuilder.setPickupValue)) + queryItems.append(NSURLQueryItem(name: RequestURLBuilder.clientIDKey, value: clientID)) + + if let productID = rideParameters.productID { + queryItems.append(NSURLQueryItem(name: RequestURLBuilder.productIDKey, value: productID)) + } + + if let location = rideParameters.pickupLocation { + queryItems.appendContentsOf(addLocation(LocationType.Pickup, location: location, nickname: rideParameters.pickupNickname, address: rideParameters.pickupAddress)) + } else { + queryItems.append(NSURLQueryItem(name: LocationType.Pickup.rawValue, value: RequestURLBuilder.currentLocationValue)) + } + + if let location = rideParameters.dropoffLocation { + queryItems.appendContentsOf(addLocation(LocationType.Dropoff, location: location, nickname: rideParameters.dropoffNickname, address: rideParameters.dropoffAddress)) + } + + queryItems.append(NSURLQueryItem(name: RequestURLBuilder.userAgentKey, value: rideParameters.userAgent)) + + urlComponents.queryItems = queryItems + + return urlComponents.URL + } + + private func addLocation(locationType: LocationType, location: CLLocation, nickname: String?, address: String?) -> [NSURLQueryItem] { + var queryItems = [NSURLQueryItem]() + + let locationPrefix = locationType.rawValue + let latitudeString = "\(location.coordinate.latitude)" + let longitudeString = "\(location.coordinate.longitude)" + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLBuilder.latitudeKey, value: latitudeString)) + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLBuilder.longitudeKey, value: longitudeString)) + if let nickname = nickname { + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLBuilder.nicknameKey, value: nickname)) + } + if let address = address { + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLBuilder.formattedAddressKey, value: address)) + } + + return queryItems + } +} diff --git a/source/UberRides/Resources/Media.xcassets/Badge.imageset/Contents.json b/source/UberRides/Resources/Media.xcassets/Badge.imageset/Contents.json index c883a615..fe62a208 100644 --- a/source/UberRides/Resources/Media.xcassets/Badge.imageset/Contents.json +++ b/source/UberRides/Resources/Media.xcassets/Badge.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "uber_badge_24.png", + "filename" : "ios_rides-api_badge.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "uber_badge_24@2x.png", + "filename" : "ios_rides-api_badge@2x.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "uber_badge_24@3x.png", + "filename" : "ios_rides-api_badge@3x.png", "scale" : "3x" } ], diff --git a/source/UberRides/Resources/Media.xcassets/Badge.imageset/ios_rides-api_badge.png b/source/UberRides/Resources/Media.xcassets/Badge.imageset/ios_rides-api_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..0526da19d62afa8d3d87928a609423b61bf7dd8b GIT binary patch literal 608 zcmV-m0-ybfP)Px%8c9S!R7efAR!fR%K@6?yZ=|5;Km-+a9d899I1&VL0gV%J<`Nt!La)P#i*WqL zerZ(vpr{}!TAfqfeLtFeyC2UE6eg+ENh(R@s!1vT?DzZEe!uTOpU+Z z)&D1=EEdym0Wcb9 z`aYP^Xe6%Jt3?Nay+OCzwHQ)|i0O1H#^bSC;Gr{9r_(Wu2G12al}gF&c5AMoBaa6c z3WeArvp2w4C>D!sPL0q$IMkwAtqOWK_gfz_cLUl{(Ui+&$5F9fuVpryeTe+)yux;J z07H6kve9Trn(Zo07$0~iH0)$N9=8R@*&jLhiUz-Kw_8%LJA#_+;ec&ZGGYmB$h9JC u{Hb)|1MOt+KYI%Mw?FzCRqj{+w!8zHntG}Y@V55=0000Px(bxA})RA>e5T3aZ+SrlK#T{L1aFESp^c#*=qkc1bFyb+QIMT!RxUhps@H7{PM zQ6!fr;YrEMjFMX z*4mfvIEYCI@eNnMa`}nN4_q8vJ|hV>YM;v=Tz+;q95;?Pp<5XT=PBAet_JQJP7R}Ag+KU$( zE;>3o#O38BR4EaI5^Zg5B04%+dtBHILORk0keiz;j*pL_LW@pLPDEZ_o)!oLfVK-Y zH8s)d>MEtCrfR!GN=gbXFE3MLW23eXEdX_Obu=(AK;hxx+5;Pc5fKqIG&DresY8|l z6crWG$jAusZct}DzcK((SXk({-(QC%0338QJUmQcVc3D#OPC2{si>$ZONuQ4;A}%q zr_<5^R+!V$(x|ny)rtry1Hc>N$hV|ihq=AIogyP6l|_|(0Si!4Qj%2h&dv_q-roA; z!(x|}mF2U~co-KKN2R5uG&?(MJTk470hE=MnJPjjjL*)_Hs#yi-j;%>s;W{3pzK09 zIXR|=q0`@+_ZC#)k-Y?OeM&Qcl)1RLNGmHV-m>r7*zedu2xg4Q%ez#a5&$S_X=#xv zvx@<#G%+!eE-o&lDwSPGs>x=C`}=z(jY>0s>Fn$4qtMV$Q>AdBo}M049+l4L=V!XR zyHjB$B>-@t;^Jb;$jC5Ny0*4v3xLzpQ`2$0JC$8%Yir9}&Sx8WUPdZBNEoW5Gy_mJ zKR++sF=CNIp7)n3RTeUgwXm>&N{N&K%*@Qt`T02|CnuXKmaVj=!rY0>GdDLUWApSY zyU^3q6AcaydISU2=(VZWrq7>xplM zEe)c=jMQ0OT}?MPH!AEN9!mfqg;d$l(4e%;dq$BojFoD4>Ib1LKk98nPuNC8TRS2Gj-s;f;+A z_ujs3AswPJL1RP62T zL9K`Qno;cT?ux0YDN#{TA^3Reku&72*2cw_Rt)f9?+ACH{MpA{&>0dE;=WPx*8A(JzRCodHoXblbNgT&(jOgZ~Xrd^-Q1GBfL0A%tz^ zf53yghdspAi~a|`A_fr@A18BA@PUt;XjT)Igai>4b!va#9!q+}O;1;K_eiFH@UzD5 zuKHDd>en^(n({E6aL&E-@9(5PlO~XQNW(!m74n4iJ?Y;bk7rR7S_42}exeJ1lKx5> zE>Hx6`ypWUk#-ZP3jyd2T>u>Z_U+q$2L}h!wzs$0&CLy6`tjw-lP4@ECx^ASw|}Ur zs!F92gUcHN^c%x#Me2h#H#Z;B4G;`l1p~o@LN5SKkn*vyu@B)4Mql>v@$r2yA(;T6 zBYdi;s0drH(Zj)Ku(*{Y0K)6VGcz+?K(|c`Slo&)f{1Aib+|OhR9-{TvG4$pE8ijD z0{RXd+;RcAfD8y2+;%<41!O?L;C7_JeaCJ;efpFoB_*L>#y)=h$SyB09kcyCLm>f_ zkdVO2%E}n^SXo|Po(3j1HZ~xiU%q_NKy7YrvdPIwHZ?WHaLsWMUB>kEbX$AUG&1J( z_4RydX^B%uOqdNiyuQBXi;Ih#29rD@BEr_Zf`t&cv?g`|0+_nGI=->7AxtTUYinz~ zva-@PNR9zYOibi6Gc(FS{x5;~`FWn4oNPHLM*w-fUVeCpsTF$#HVO&~EQMq{ptiO) zjyA(7p>I6?`t@teAlV8C?Tt($tP0cT^YO;UMoS>s2I$3$7aX-2D!I70;Dv>S%8+aU zl#-IdzkT}_DmHg^PfkvhC;#+-EE|jU_4Tosm>9?VarX^AfBu~H_xInwEdRei4{DT( zlvY<)i)0>6SYBQ(RTePNdysNRF)}ie@9&F6fJeiJZ*6UHw6rvN z7q%%T76NHlqMW)ippK3XKvPqba?(np`HYMVR$5vrl?fVnvV((zpyJXuB%_{r zP|xZgqHYI!_Usw^{Q0x~vboQq)sShVJ3Bi?bW`EN!h-3x0dZu6*4s_nvaAilT#k$c z>pA+gW#S+{yhufq+8|j#Dj7B#H!Ca4Y?OfKrQ0BE8NnbP7|cIPZ9*^xbkBqg z5SC}Nv$J(sd7K~P%4gYxJ-NV$;~fUuJTi*}(FnpopBI5quRiE`Wrq|^_h#O~zy_?We}wkmb& z?-G;zfZjbO>Zzh6Q+ObnsBzU-jFQ%Bt+uw7)7VEO>sW~0Bv$S4u;mpKfDkq`G&s8b z1)COXYHE~TD}DZL2Lxj;U%up@K7F#?gRo(uq@=_$ILNyrfMAF=65xk=wm}*l9p$O1 zsg}ZV6cCI-TUuK9&d!cypwKaW_3D-7U_^c$^B`dmA$qka6b*2tK%o(S_wF6Xmq1ih zlx=X3S3MvPa!VH(mXT$aa1ht5&@cK}ad9y#Dk{=6H#e8vpAk4cJ!P~*gJC22>gp;R z9v)`YrkOP*^*jM$dwYBSxS~2ijjn`5;a&7o1RfU`r)}%CV{5OOvgtiYX+B7*dLD=b zCq6$v*YttgA-!Z-PdcQfu+g{+$N;Vj$OU9Tz~Hv)K`tNz0tUC?L0U%<`+Z#T6VS2X z0Hn<>VcV=LegZlcCjf+FskFm|{I}h|IB4mX!h6g~dr5sz_kbma-^2zSu%xXS6P&vv zy6AABgQ20JRQHH3{}EjR5Dt0!i}V*#a|`XazW4(L_|p1^^bZxx!nTOFX3! zrBfkSQJUi{3^Ab*H8)mws_%+Ai6+3&sizBg|-1?)OH`fjuC8EZL8DacDa#*fvq*s;sTl|tdB zo$|rwYnwZ;ySoQxXTR8D0_cbm3Iu!Jlnr~L+qhjk=ku4LR4PKb{1BbJLI=MYc&=94 zjsTq(T&<_@xIDp}JaW@W3r7Aw!O`~N<0qslmqA0AmVtq5khLxeP!P6O>o5RE8n8*H z*g9SZj*m|Qhwalg*CQhYGQk%XUV;=`3cIwl0^2+9xqY;F2PP*UMAppBJ!ejwU`KfQ z4iqgsa@KddU2vQqAm=^L3!-(L6KFQS`D1{=!RugI7H=kv%mLmE&WZ4h;ERi|V>#d7 z{|bAbzXUdW{sjyV-!j2{oHTCD5EN*T(@6yd{~fA<3;HNJfJnB87m|U?XYmd(g!gv< zkyKRCBm<|u(8fDHPCA${JX28O{~MeL0JT1_6QzN1h;%*^fEzSw;30~_>dS2>04FLb zbZ{9or^txJr^m`>6M|Fd>$5_diTS9=nrD7^n3G=MmX&2|Z-9PasY#I;WwY60r?$(t z{d8tZIr$6t?CcB&>U2CfIA|EbYPHA|bZhG!E434Fw=~LHkr%NJD;1!nAJU>nzOk_x zBY03M%xtN?SqH*wveL=c(A@v?3NATt46*H4asMv)Ctyb4yb@eR_4c(4SMcKl% zNN_1~26@8Te%k&SD`>sZFb-if730DC`>zJI$i;7HuWUIFI6jY!-9uVkJO>s0K;t(w ze)%W^4FwOYh$jC9s?~hGOg@_{d1X{v*HubhRQTDl_nu P00000NkvXXu0mjf0H|TF diff --git a/source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@2x.png b/source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@2x.png deleted file mode 100644 index c0c0e076b9e95677e4c6d0633618df2bc7feaece..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1522 zcmV&m=x$ zjDhP&l9(sWo~~xe22J~ie;)GmE4A9;j(9jh;2ZpOO@NOXJWa=fT;zpl$^xYDE8>g$ z`~SkTjZOaiO?-`+#XnASU}pwc7j5 ziPUUfh??Kn_!aKmU*+KYGL!fm1AzLB6pV>$<*Z(3c|rPp8+`H`d7T!vR4PHE@fpmD@k^ z!t4tRJROPJN1Govv(e%4y2o2$ZSAQrqsd6T06Xls!;ACw%N^;NBJrZc+l)Uv{$YO* z-l#zRt`ELgEW*^(ds;#0?-G!mo!3tIBh!biE>q+w?%SOX^m~0PJVbP;6P_uesi4@u%iCx*jqpFM{(N9>)8@9_ zZg;HS=F^2UX#uF-vcao35p8y-t8|)KSXZf-krSRNCK!=+^!j-P8b$#RMjx=jYt@-` zV%p>FL5e9-J#>)D0wF1h4PI*n&5hdO^|6Jt&wvru@&*q)m~PZ3518)u7z-{Bn4$?fm7}Vf@fgVz71YH zQ{n&>PlF6bMh_2UaHYX(Efvwg;gbin50p!#pzwH|OdFE-xZpKiX3;{-G4Q5CzNw(_ zcvKHn%vfNB2Qw1q$%AAXBl{(eS#hp16g=j2s9IIPdcB?l-e`e*d4MA+oX!)qg3XN+ zts&s?eD-XE=UG-FO>H#ZZ{;8YH9x%Oi3;jCH$O;WA-1<)!otECZa%Zhvd=K^ z*kRG;(QI;SULTuh1z^gwU0#stcK6_i8I`xTMYK_w6`{43m6!3U`{`@%_{2oqhZS9hUZtm-!tj9~4xEeTaHIMML_Hm&* z-fF-L(cs^bTMbYW;8G^82KHq^{O(ad8|ww0GZ?diSsH?)%_BIK9k|tCW~SDZn?1I+ zUdzoM<{l5`*pOjgT^T0bv#&&Vo$+Q5gjaT}q=l&B@;%*dGe+?7b{qb=!r|q1o3{cC Y07NByaWr}RNB{r;07*qoM6N<$f>5FK9smFU diff --git a/source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@3x.png b/source/UberRides/Resources/Media.xcassets/Badge.imageset/uber_badge_24@3x.png deleted file mode 100644 index e7d8f7ebf8a89e0c6e056a4d49ec272e9f1268e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2147 zcmV-p2%PtcP)E`moq6x~X5O2PJL&Eulby`J-S^q|e!t)M_ujlV&@2lJ zPu>tOqhj4B)||f|5y51sorV_Y#-FdgJ=SxsE7oao-c)z@EBCp3v@u`^#zX~-^oC6Z zM?QC2tS1F1R|`nM85A$q#rk}U!SR=W#5y7%6%;@fI5)YQluBjz{f|H4@xrgQw+h@j zxKDo^L!Wc`FxHMq<-oxC82+OiXU|tkkJVdjdTk@*y1F2f$#BT%&ro;w#zGZPrCHsR zYficRFI>C!HQc-Zz}xoT#4i}CP-?Y|IWMrURu*|5hTsMFRksuEL*+}%W}bt-d>@RB zz6058M*ga)CiMtNt`F$CY;*1lC|9n038ivbU+DiS9RGvSXC^q~$B#mPzE`eF(*n>) zTg6-5li^(YLm2{_sRCD$EmpTP_v?%(@JpY_2R|Lo~@P4*Qg^m zEwVYy-mGLe%Z{i7pI`Yxw(a|zU=ToK}V7M@kPL&V>P?6wtLY9AF}Fhl?az6%I1;q+NFWws1N-U#x`e zC>1~j<{vK$XW%~l%?QNWju)V#V=Z$$=x#71M`|{u3`O^;IUH{vh9ph9PQjVLIdWta zcJ9~~D`*i_!FcP|k1#p;S!^xYwQC2w{nox%fvXd!Hw(AC7{?5bdLJh#9I81<;m|TL zupN5)dW@P?QaH2>?izqxuFDJ#^En;)wx%{GsDy3d_{X|BUwWo+{9{@1S&iVJdcDJc zcte9+2OKF^p>1k^EiTptr&f3qIO-STs7BhtTRmeq^jj$W0CRKmiQ$CPP0?_$Qoi8z z?ZKNOfQ2jP0bITMRmDR^CA5~e7Z?9Fa^+{wo`X%BUe$Ule^;)n6UN34vUE(~a7Ak8 z+P6`hgM95EIFBC9uXvpE%iN>*I7d@;OyRIbI+R03iy%xpL4~L1Q z(L#743kNGdsR>W6plAk%E0@FF%S4rH`{EGNRpk*KhOuy@ilm98%~BSu-w1sZY3Fm0 zZDLVyfN7?VhL2G`hja6*4i46$;5azQ7BNex~qXgwHWbPUvBI0!m7Zv!VXVWvuj z$OOA4m5~}8)FxPr;jlF*5gZ1RG!&=daCB`eWhueof<#mXV8z3c zO^%ZYjz-+_M!G?8*n}USQ8b0aB<=0;Ic&8|4GvZv2dBr(;Doi9Dm5&1RHP_tI2_P= zv0yukf`hS1duttqz((RJz(I`{QlUh^@r6G|^ov>PD9#q81cy(Q+rf#9gM!`CruiIw z7fc#(A_ifA864DUvCxG!$u2gviK)QB2#F>yl0b4*f2VcSdc;p%I+8 z!3)M7Op|}<%5|m!hnDs0b9ygk1Sdv|*?2J2B&RyocBBG_w%4ueSOGW~t;I}@bh%Vw zlT-P8Z*}BHBjIE-S=h2A&%#++DjC7ih}C5I92&Xh54;ZEnC!s*{UZ&9L%#=*By zW#K$|QZ$5Ppv7$RId|^-%!c*D!+T(GaA(8d?A|>Hd-o2r&Fc2;8Dluy^$y#oWtg(~ z_Pg)3-7Lq)kHWiS2fe3io@KK-e0U5_oH(wHfo|OR#uyF;fAHKqFW4N!K46(nG&EcAr|Nc~asCk*gQ3Kz0vuo7%XV1j%OeULLBp7RX0 z4ni7mPM`ijye?V6(P}X^U1}jbsi~bg^HJz=qO{?7o+}X?jF1|LiIZLA_Q%KH^XyGp zaLDEyI&>^C9B#~I>_BoG;khJg(E0Npd*hOa-`oTJ`8@YL=!(LjIETQHx3V?J`h6^* zl}nQrT^BB1m=yaaVZDHrAxK(pCFP2x+)SttQYO1*Ns>_goQ=pB|6kW5k^8!+MimoW3sO_JRIp1uyFSIx} zPSkRGoITl_qRWS)#6gv5sd{A3)*^7GE6u79+}WnP$%6;;>@LOuWh002ovPDHLkV1lRK43+=@ diff --git a/source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/Contents.json b/source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/Contents.json new file mode 100644 index 00000000..6457d886 --- /dev/null +++ b/source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "surgeIconDark.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/surgeIconDark.pdf b/source/UberRides/Resources/Media.xcassets/Surge-BlackOutline.imageset/surgeIconDark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6458ead898552b7278700f81a215ad5e632e24ad GIT binary patch literal 4146 zcma)hz8U0=k6BP*c#u4``S;0f^JSN! zC)gq*OleNyr&h-jT8&9PKS>6GL*Nff$2hO-7MGLA7W$!rr!);gfEYEwz_l5y=}fW&r?=AiY*`R5Z$m- z5fm`=D7}=hF1;|_&@phrxn^N~ktwqhd@$_VE~!7Vz`d%Gu14@@R~|P(aqK?9Xa!oD z`(?m$qVX2jr+e*~k)^#&r+6!NVvdX0h{VK=ze4g9TJj>+N^Cp3pZXCKSdd9D`KM#( z3k+{_9U4q6^=0WSiutBL8W|}%;238_mJYhD{4NtS;eTH*`h0O)_fmHEJS6q>Ocv1% zT;nz&;#ac)wF_6T!w9?WP7yZa_@wXg5 z+$Z@hO7uE;o1$_Q2ilYE2NG4lxmaOwS}6Jd!I+O+Z)gv)2a&2IsydJJg1~Fs9t@y& zm#?w`G=r6WHE06;`CSwaDX|_C)HSaU>E*t-j;C+Jc8!c+8Em*(q?j-P02+@#@Tp=Q#ft{pQs z2cWhTXnOXlq~hbBUu`bhSQ7Cys~i%l(cpLLJMf9*l|b?xrUEMVieK^O=a{64(JpG@ zf*t_guEh#M1I3{1DGaH(yyJ}pwi}sK7b;uhi2M^N-1;3{ui;5yl^P=n;IXtYK#EZu z_@hPI9Xedq4dn!?$<1@!-CIvosUWQJrtQ=GDYn5-MOLn*;8hwb`j|$^*N#k~+me<^ zRBjm-6Cr?LWlYAg{y3}2nK$M;D@EGd7_kd64QgZe-Bd|y#YY}MID8B6j})oF`gZY_ zk&Agei;MbwQ(f|M+6u{7Zz-tsFQ6(F3d>mCf~)stqGLejBi`o z)xDg-A>;R#G8+u1IGK@|p`y^KfQ+{WYENw(i7;~nrwVbUjr!Zx(HwtBM|cW^`-wnk zgl-+>@rB){9RX4K`(9eqM_x^aY3u^2f))q{QD?KbatTHU(3w-JoX`D19 zUSMT%@LDi-`@H@6JF&d6=CRbV5!tFb_+HlD#B954^&W^dzct!=(0aF5BqyAdNpejh zC7QwyCia5n3M+y=%6F{S8|I|uqPY{etGT(jb-0P}W&~G)eL`!(WWqGOrP$Jdu>@c+ zX)s-40*Q@ZYQJF)E((=Q$;)5N6z-CKHk>1shkah8XOVWjsN;ljl!&2(T$xdswyV6l zim0BbX@TZ)inL~OUh=aVRE?p}+uBah`&*%MkL#YNgrwAr2PE?Lk4sTV{9_SglajSw{V~q7`3A7%^7IkC{ zPnG0l36HweEP7o|FfJTsue@GK+F;t?-R0jEqmv493#ww;93Z%ytMvEkJvKixR^G7^ zHclttDe$6OIde2ozeBL&Xh$035suFsJ;I!hT@GXAJdTkfW+GvxZnsl+ z)%wGyT^j8ghX}qC#6@GNY%tNsK1>7*H<495>S0~+e6>niV_ z5q)RMXVk+^Bfp1q3An<5IAYAUADY(bJDfj#^317GmV@9E^;wgu`D>=HTqgnj(Un2FQPa&Aro&f^ zw{Uxr0HXg(|1Je$qj4}+IbWGgd7MUFuH>WhOVd@qiL981F=>LZtgfuTt^A9(io4~( z=#at-#xN;?D$xdb8U$JK)~a_&=R~S(SETAWCw=qExCdWV*RO{0JqF*?XDqUQIbwmr zCBM~dKQ^^CkuYK3cB=JD>#jG#9O3G0QSI_IaIX%rWW$sf(3fvC^y<2+ab3mOdi8vS z9mwwL;O91_ws73ZQTvdQ9?hqkh1It#ido^PtL9h{b4M(fi#~wS5af(W9p-@cy%R`gbF~Vx70^C_g?Pl zmv}A_BVHdK)%;@Ip#{JHaUJP-B4<_umiB53aPh3<%BPmCGs{Uwt_-?|cxBHD>?d!O zb{d2jr1X903od;oZsn=!IXdgxop`F_l(5X>gF9dauei z2dsn!a|B(wNsz86o_Lp-Uzk7A=0_|rS$XA{HB_~`r$fxK$d@#}?3L(MwHUY`-gmk( z>5=Jzx7BRPvR&2e?V|lh<9QFN4|z@R81IUXY*ctnJ~&7`NgH~2XGx^m^T6bT)U4OY zeivO9vx!W*$HjfieeA+`ssK>^d{VYd-hS^^zj;>G+QQTRTco1<77>BvNYR7X?E>|N zL!>Oy1*Kx8IaD5s6t*2U?D1l)>2q5cv3oqAM(}ItW^7kZvbXDQl?A4?@v}|We7wxH zk>}NtJAE5=(`GXh-L6*48JQs?In^>7e)}oA*tM3(*#-42_1NTa8P_`ud#i5zcIH!~ zoBm&R|DY6aWUvmSf%hQMI%G5G7>3|cEXu~C;+PkGX5DtLF=DP{ENJu{(qLH zK(aHL$HhRRDpqDrWO$~4J2~5LfGkEq?|)`Qq6uhQyv28@cltr|zmQr??8l6AYG^D- zT^pna5`&0{ksCjdrZd`+-~l2N`Zv^fBMAQ7x7g2?;he%HC`3#g0u_^hKp{|sgaHI1 zK>q!T{B1vE>Nj)7l6%B&Ymz$#B)K&tSmEszD9HXTC#8+RoBjU(*G}CWappiuV*!Nx z-vts!AYcfPIp~K56GxD{5y}O!|EWRXlH^nQrv`&V$))(G27y4ypY<;dE5z|bL$D5Zpyv0t641<^&UoXd$k@b;f|l}%wu{9SV4n%)f^Cy&AKOGEYk5^4Pqw$cXavqw>Oh&hGKK{%?Dob56_ z!>k!|_t64pgMm&?{@v!}Bc2r<)*+{sqDD$+cYKZ74pM?;o%V4bBl3M+@;ViYYF2LE zO_w(_9Ahz`c~`>#9B#S?)2D7GHX!_~{(3g-bJe+Y`ME}q2q9stQ8L^oWZfw<#h|vZD*pi+~6U=WTC=(^FCSA6$CR$3TCWEu4TC z!@oVo4e)Mq`q2XKBvn~Kx)EwYIuv0ceBP)d$}G$RSF8Za+DNT`2BflsqUigImrhMBRBf-^S`iMit{V|-)Zz0OI1G&E7E|P*A_FV+ zBc%fP*rzFy^efvqL=Qo)HQF4lG4PvM)&;xz8&sE21n z*XfL<@lNDKgR;!-^M16=xk-(~8>pp`O>dv??cI5(Nd{p_wRkhjm*o^8s>H&v60uG} zMw8SkQSZ(WwJTwlLFSupI~56vP{Zak58k(*o_l4pw^pgYi-lfHYPmdd$5)fESw(*r z!X8xadQXX**VsALE`GU`YkAptV8(8@ad`Z6fc}B>E5CiiHNGW6hJ18}@wn5{V-Y+X z*%pte8+wKmg7v{R)8XVx3H!e~TLd3w zzGPnuoio&(OQH>lu&WD|3O0%pwg!g;_D=+!K=M;_1f)B%*m+ry`G-pA)TmLva+M}i zJV(jnBI`%tzi7$?1;1*jw=0b=_lOns`!>6GhR!U~WkOS)TYqMYKGxcuH(QV+XFSBI ziQ-rswdW%+JXjb)A!vD&D+qRtatt5~2~uUEyhkalN?|VtdMYn7LIq;(5D z>xi>Zwkbx$F(t_DzU3=Cap87xmZFaI<~dK!S{n52y+_n<0XWqGxqk@reTrVp(IK+u zLBkc2Tr?`pT^~`ttPiQ#Uye+u*RaVjR5!Cvocv5C7;4d+NF(=(hO$T5i00%PN+HgO zhn)GWV?1{*MUhfk97FEO7cr{wZc~(0;aQmbC)nCKCc}-RC$LvA16XxP|9z|bS2WpY zkTy)P;N4q!*#0N^M*NRuy=jrm4`PH`uUN)fnpUJdp&Y_2NpdlCyyW=AbuFOggqf%d zq6}2cI2c_QwAgZVf^W^Y{=yi`>Mh15kI&efXB)tQT!E*@W6!iV^2gDdfE;fzp1lX~ zIPn-Tanj7v9E318!*zoTwLQ7Rn69^)VFjMt9g?qCvox_J-Z6PG6uKYbbP^ zB`r7RLu~}9he?RUUXt~ zU3BaQeC7pVk}!fSw>Lkm7f9Z{;Bw(+GIz2~GI?@rv8JJGKTCglv2(F@AH;#r0pl?2 zu-`9S5<@5;_+${$E#QY!U&9wFY9st=_8h*nEJ!aTa;9-MaB^@Oa;CuB5gchQX`N}) zX|wQ-DmxRpXCRYlli6qHkmS^rHwHGml~EE|rDe+nLOn9Mqb1U%R)v*DwmDjr-N(@h z!lq*K)n?WDJ__0zB1R$><+`g`GP;?inYoQBji!OGU%mz2TSm!0Xe!K#%xc7BU~(p` z&K1X(^jyd>$&qXqw|edtxh6Ad%2yGQu9lz{(|K8csBg@`vX)Rqpm&_HN>J3 zJ{TK6RvvgH(IWNgX3q5oyBF!Cql}``*+$tc1eXQ%g%$@S&yt{7#j)2btEF>G<~^X)^arNe#0@eJETp5Eu{ zLj3!g7w0Bwy4Rv7srhg4Kj~E~7*99u7U(|OokK^@{+Yc`=!AQZ+e8hQd%Uop8}#l1I|2V-WvTb)}+hopxOYW6QOYB0vDM0F$;B+haY=*;h=lwXa%T3(&7 zja_wEEs(h+6Co2LbG*U3p~PQg2XjDi;Jr7xG5C3QZ*X55Bo2B^I|t4MDTA(1%g~9^ z9y{U%YH1>Gs=IYY~`Vw$uposG(I0j=W92N zQnAM7!*9zzqD!aWtY@!R*Iiv|xPmctvt+c7wT`V4EqsbsFLzMsFIdQ32R* zpc1N**U=p7)i2pMC{`$zglvvUXn!*4*5P{a@r&Y(<0bPtu$&h=An~&jYo9uH&a7t8 zOATL-^e>+0KgisA{?;VYBx~TqK*aN0r2P%e8{_jqz3HdAPYKCBIJ~(Ty_K3)`Px>4 z5Z}%E@PS08Pr$lDd+1tJ1bg_UtDZ8oRa5WM%PPvox`I>6&DUPI7meWezZ#~L*p^A4 zCH>R=@ylTcF$1UTGVWO{1=!C&TXn|IU#mR0H(7eO;fVk29(rG7Y^&CP`tD)+Ny;dy zy%phx8;9l}r04y|4tl7I7|mth_=z9b9at?*X7hu!FJu(UmLBx)4B8aoH)(_;$#~{=cc3M4{hN8VMEs1*;@}C22&+RZ~<{z{ zNND|wiT@^VkN=;gNs#PCnZSS-_T!?;XLY+CmM3ioS1I@p&=o}5cQ`9gF}e<=1&cR6#MtOF!6u&gAtGJpJQPNv43kw(cjkfbjR2` z;oQHca(#P0;`tE+ke;h6(H|ra5D!w##m1FrmLF{t(JoS090n%_!9hi>MJ2=#I5D^t o4u*jvtr1WgIFcyKfd6}!A9nNbBwFKpL11EVQ7|vBlD0DVKiuUS(*OVf literal 0 HcmV?d00001 diff --git a/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/Contents.json b/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/Contents.json new file mode 100644 index 00000000..cbe307d1 --- /dev/null +++ b/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "back_arrow@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "back_arrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "back_arrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@1x.png b/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..b66e15e34fd2de4088141747978fc22b39a65a86 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp@KrF(+1|-9bw(A2a#^NA%Cx&(BWL^R}t)4E9Ar^vf zCtc)hFyL{KE(|K1z;|iaoaQULgFFtnuY8yH`HGG-L8gD+ad*lk9g)J@^&qAO=rYKbLh*2~7ZT Ci&DY> literal 0 HcmV?d00001 diff --git a/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@2x.png b/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f8bfbd6c23244fa8a3cb1d53d9aa17b70615616b GIT binary patch literal 260 zcmV+f0sH=mP)Px#zDYzuR9Fe^RY4AeFbt#Pu($2FAN5Dv*dO+xX{XL9WE2T9+C`ikDQcyZ*fB12 zopYp=uE-G?om(&M3J`2__=BOXPBLSB7e6oqMA2fu4BoaA5Ii{`Z$^igQTtswFo{eg z#t<@XL69olC>P~|2X=#(aqhqiG%* zga&aj$lw2?5W*Ww?#PG1=xNmIN{01gU8e)5H^BTslt%%!0{H;TMr+WLLZA8o0000< KMNUMnLSTYkV`&Qj literal 0 HcmV?d00001 diff --git a/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@3x.png b/source/UberRides/Resources/Media.xcassets/ic_back_arrow_white.imageset/back_arrow@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf80d7d43cca366fa70a874c4e748b290b8e5e3d GIT binary patch literal 387 zcmV-}0et?6P)Px$JxN4CRA>e5n9&V_Fc5}8pG|NUS8yIrko-*=Npo#K zzH5unG!;t-;VktjwXcYgPW=NWmqXbGprA~~My~i%7jOY=BCG>w0ILHCfJMM#x>Wz) zqF^%WUFroG{RG7NLLJ7`oE|TFL*)v=P`5NjW89Up3xL9NObsA~#%RoZ&dS2-q;Ap} z!OUsYK&BGXMPsP0JY$_>I(wIl#a|5UV8@@04705z8lCnY+OGspGUi?Y)d*5nH%P!_ zZCM*6ulkRK2Cc%9I2ufpfDJSV3QNMB2K5DL)khtA%n&f9nkkMdoELjB==p51>Mkar za|LF7&R~iM>*Fql*I@2@t10De+rB06D)nXR@C~{FvH78Z!9IUB#|Q?%I$+biECAL8 hTL9b!*bLw-*asdSlQ`nPC7A#K002ovPDHLkV1jLJqhVP)Px&&q+i+ECqbf0tX4L7xtzk98J{d?_o_c>?Z zbDK)1)8jnPn+Ig|sZ{FJZ@2wlv;$$+_?cU1Yr-lto#AGJ!Qq@Ae`9t6*vTN_a1hNlLdGKw ziR!JkG)e9~1eLBj;>h&`ElEYJ{7?Lm;d*e3EKQR85y5U#BipND^x2I4Ezt-3Mk|)9 zJ8TH(2J~TSX=p50?E!w+dP4ND$X3gDxu>af-;wBTaqsb)tXQt@s=hw7=04?pt#h?R4jZm|}Wt2IZ1 z1q!goK#yCJ6;jLjXfW|7SN}v(fuub3C!y)(>sqM!GT$3Cip6%uG^VSyfkr;YXiTar z&QTMD#?!>vZcrr~aUe9`9MZEaUXNSW?-tg)oZYX?cm}>(GmG&1fdWuN!)w{3{Z>B8 z)rLL=s%cEzb`xYV>e03zZ`YK|kX1_P4*DX;H#Ht-=Ju1uZZc}lBN7w%@NdHcHf0K& z8qwGO4qa7raWy}qt3cy=9UzsHT}OQfXme$Az~GGMd0Sayo9|~f=%oo5hgrVM9d40& z^Gvcj*XC#v{0%mx9(Oz_0fWFp5M_`4(dJyG&QyC5`dCbS+~<}N??RKgSe+oRT4Eil z)2%pg;dKVA3<%HDRYESxtnn)8}% zQ;$>=HNP`P8uj$WFf>xoHlcr^sMj_|jRJ^LpVSMWjt-}xok&8*imc%mI&TUa(4y?; P00000NkvXXu0mjfftU5v literal 0 HcmV?d00001 diff --git a/source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@2x.png b/source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bc72863557ef841048582d2afef2aa487263cd67 GIT binary patch literal 1897 zcmV-v2bTDWP)Px+BS}O-RCodHn`@|4RTRg&d9x@}OS6a1u0{lfqKPONlwT?o!sy{6B=qPGi%S?bIy!&$C(BH zd-hp-t+m%)v(A2K5Cb429Ho4V!gVBm$1OM!`?@sy=s}lLVr_TnGQa~z*7z*O$4i^;^Bxxz$R8-QFziJGn!Zh zyS!Qq*e8ylXgbeXAUfn~iTxp+wYKkym8O03@KF47nRLeCBApl2sVxGGLOMitO~<_? zR*NH0@Z_|`D0(?az)nON^>Ppa9WGjQftan+*dDlt(PZF*61bUrLzN(J2Ll;w@J^V6c=Q4e5H26tb-SKN> z0_BtF+>0;&CeSUZ3jQlzvmHiT@bQr9sy%@;vZQ%|sUK_x!C0VRl#n`OOD~LzrDZ9T z$MulhKtlZC;f(adgT(tO>(J1mcS2^hCs5drq*n!OIKdsx52rWul_c$W=B0E%HY}QJ zF%$CiRl}lCw-y)CVc&JtNHGa~NfJFQMg?gFN&IaE+xKZ&XF`+kGrP4vS4JSc=h+L6 z7KnmAMd%Ck#=4Us_XXNdKp{Va_^j^$0I>`2EFiV9FOp5Pa#Aj>NMaId2?I#qwurV`Yw<8qw0`v{A2OKvXnhfs=gMS&kwe%-1c_bT7VFxH%3Dj3YDQJL0 zS3`O!DAAgMU>qY+KE?sj4J$PIRlcT zOF@g_u{I#)Q9zhLuJ=G%1e=ORBUu(n*MpncSw?PQb?$A&?yS*K&Xj6>R_#JJIKokV>af_;VU03c>vZ;z--*m^WcLvd z*-D9Qpy8J*)p&t+-z-p9&MJ*U*6F#n`Ok(8=OmDZFYqXpQ$Y58M<|@&+KSE+FcQ0I z>{%U2;Eq#8)AH^3c*ZGFnToIR>6=P4O}pD@_bo@;QyGN{Ptgc;1Jl74P%Irl;UzHG z;XqMr?EFxy10M3N$gBrCZb(Gv_5{9DIHE<9bu#Sq73=DcMFkHLT?R2U0GzN4!doRh zt*qN68A3?g-kU%Z&>X16t4?lpEUrT+o!)K-dSdkCle&ZTOh_Koj3aU~3F;Z5*gdL^>;5fRFOmi}0mCg4W2e6*~vLjKqYnITqH)7+bf8 ju{qD+*ID%l_($MBSHwyse~tQq00000NkvXXu0mjfQd5%{ literal 0 HcmV?d00001 diff --git a/source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@3x.png b/source/UberRides/Resources/Media.xcassets/ic_logo_white.imageset/uber_logotype_white@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a863a8b1c1ca541019c1d52431f87bf01922c92f GIT binary patch literal 2813 zcmVPxG1p-znrO=jO6%}lJRZ$V7K2UrFUj#9!7{en` zLIc`VO$D_n)j|;jHMIduq{JAlVAK>FL?K0rH7FKnX?v}J(b860xP@MS|I3+^bI#qf z^X=~Jp2wXp`R?w{eDB$BXLe@x+<}1zF#<6HrA1&+l1D~Hj^r>8JVe)RVE^FY;KDX_ zL5i{CbOfE6B5W@xg6S3Dgv`vz3?mc|gU^D`VDFA(vT955Q*EtwKCD{J ziQ2Xkm)(gq#zOp=2AyS0#_L9P9xF7shi+LjJcckJ2}ay@tR;svFK#kOq8#pku>4b&#ds^&Wm~uh)m=oL*4GWhCm)bo7HCkBGYEHjDF>g7r$S+vvR>EhxvzuBrAbt;7j* zYCkOlsAtm9@716M&Z8H*7C-NE#4*8KB$AI;5-c@VZXn=#XV81r)u8X?d=~U?MdzP# zD{N-enw$@|p-)59eu80q(!((b&?c`zklODwgTRkNQ%|rsL^MRK2nh|n5BwoAzHF59 zdLvyVt0)4r--D>q%=29am1j26!~|=MM!DzLA>J}XI=WmlqVzlwAj$NqV0lPt6jE=) zm|%ILor8x4(!0+62G*$tnn==_2>cPXPa9NEaWpZ(TqLSFK4wr;WD(XNK9b~(01rvM z(>`I~f4QTH3FabE&C!xP|Ek86s3m6v9yjpSXN_Zm8KM=CEq$53-c2j%|9J7rkLFLMXvRKq55uh!#9cr4voW~qZOfVOTfa-WA%=E|7N)Huy$;|AXcsXKPF_ z7m4KKcan>Pm#R_D;ifG8qt*n=^+c`@X%g!56zwKyC*zMkz3&UapV#5YB7-yc;&*+v z*q4n4d6A@G1lGepn?|WW^$N=C2KF2KA-PDZgTRx(#ROH0%!e9+ds8D#?7;3Ce67A0FO$e^>7W^0rE;XA#O)U9lY_mzUQO|Gfx#JMg7Pzvh-hcxAKF5Nx6_qSs zTZt^Qr6rru5%E7GxmL7ljtSNpkLbt>1r3$5fc$#^)VoC1 z4^0)98^G}dn*Ch}UQK(c_Z|Z-!4fN5sI^{#oMb^Qw0SR`J?;1{cDCcqTdprz7vp!K zJ%YqeiKg8g-vrYVP5am`1J4JGf;C?MK+z@mczaQWop#A11kyO7kxyQdgLf>%SbfMZr$PG`2=(qz*EJ#n0lVG z(4{2nSCEfG;2tn-^r7G2sDCWUc@k`;a1QZ$CaJQu%q7^9trcyql>nVguz$C)tE}Sd zLkp&1eogYMLGHU#WUIG(Gx&!Tnow11`Z&3MOOP~TSc{L|-kf%s39a5CHMDOB$51gJ z{6;&1#&S2wVZCuRty~J=oMdbLAq}i{rpd@sS7a?EP0!tFm9@HA2AU8ge-9y~3uS1y zJD0@zy(PJ1KXiKUy;0{LxSadd*VB!Pa0zDZ!QSXmT%=l0Y#&seV0(QA<*eUk;G1Hg z2|-epk0ER;B4dFyv09XQfBNTMab5K=Opa zNH=^pkVcZ02xxLbcpk4(*~ zoonFJn7SWevcZYx3^b7>IRg4Wa#~u+3s=Et8P#3Gz3V!uL|u)Qs}m`j3pI=Q`R1Y^ zSM!i^gh!HQI`8q&^h&i}lzpJJs0ba10LiH#;v$3UlcD)s8eP@|ldP|iwOTX{0?0ZM zL5mH-euLxc`uhRiW^lst1T~0>Bsn9X$qr3=_&~dF+K%cP6KoxlHu|JduNSXpdT!8d`L zE&Me2GSC8965k>(3(a-dUSwF>xEE1FVQqu5fQG}SodOnN9-686Xc@A4#F(qqp7-FN zcj4D8u_Qv-2waBvH69hpqh;V+58c230%w9dBsnMShi?}+1a7OHt1K^QFJMr>kgw)7 zrXs%ghj18M-&>KrS=2lZaed^J)lYb58Cg2_5;%_F}XdGyJLb|LgD+>C$f#d--Y$5@NaOUc5fQOcMElh*`5 zL%sKad6P_Hih_onpY6fMDB|}J%oi0A&;z_Hd%(sf6S)UBP;g6AVe-Wcikmuw!~_#> zEy-qcYFQr>ODNchpo_tC$60c1!RL44vyS}=>+$ceJ?h*-?6xdkFQ6AY%K6wf!9+kX zHiIAJRAbA{Dk~B6(t9&_0y_0?I4xarUUT;3Wh{YDe%=7;m!&oZmfuZ&%C(;Byk%{`oSLjTd?~u-gfhN>oPWamUGTNx zJ$ykTvJVGCwiV)X!!Vp$Gb#kgm}amb=p=_T7`@hbYEo}>-|nDe&T@oVFLHB0?dWi* z@u#NV4+N7yySASM9|Sd~)lgv_Plks%Ru1^^A(U#l;FVz0i)mnEG~KQS7wR2LehW90 z+)JZkbXIsahYP`J-~_M}pk7)(3UgHm<+R-y$eEcUOaHd_2PyTrtI+*Xn6;jnHu}?O zO#k0|b*= UIImage { + let bundle = NSBundle(forClass: RideRequestButton.self) + let image = UIImage(named: name, inBundle: bundle, compatibleWithTraitCollection: nil) + return image! + } + + override func setConstraints() { + addSubview(uberImageView) + addSubview(uberTitleLabel) + + uberTitleLabel?.translatesAutoresizingMaskIntoConstraints = false + uberImageView?.translatesAutoresizingMaskIntoConstraints = false + + let imageSize = uberImageView?.image?.size.height ?? 0 + + let views = ["image": uberImageView!, "label": uberTitleLabel!] + let metrics = ["imagePadding": horizontalImagePadding, "verticalPadding": verticalPadding, "labelPadding": horizontalLabelPadding, "middlePadding":imageLabelPadding, "imageSize": imageSize] + + uberTitleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal) + uberTitleLabel.setContentHuggingPriority(UILayoutPriorityDefaultHigh, forAxis: .Vertical) + + let horizontalConstraint: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat("H:|-imagePadding-[image]-middlePadding-[label]-(>=labelPadding)-|", options: .AlignAllCenterY, metrics: metrics, views: views) + let verticalConstraint: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat("V:|-verticalPadding-[image]-verticalPadding-|", options: .AlignAllLeading, metrics: metrics, views: views) + + addConstraints(horizontalConstraint) + addConstraints(verticalConstraint) + addConstraint(NSLayoutConstraint(item: self, attribute: .CenterY, relatedBy: .Equal, toItem: uberImageView, attribute: .CenterY, multiplier: 1.0, constant: 0)) + } + + override public func sizeThatFits(size: CGSize) -> CGSize { + let logoSize = uberImageView.image?.size ?? CGSizeZero + let titleSize = uberTitleLabel.intrinsicContentSize() + let width: CGFloat = horizontalLabelPadding + horizontalImagePadding + imageLabelPadding + logoSize.width + titleSize.width + let height: CGFloat = 2 * verticalPadding + max(logoSize.height, titleSize.height) + return CGSizeMake(width, height) + } + + // initiate deeplink when button is tapped + func uberButtonTapped(sender: UIButton) { + rideParameters.source = RideRequestButton.sourceString + + requestBehavior.requestRide(rideParameters) + } +} + +// MARK: RideRequestButton structures + +@objc public enum RequestButtonColorStyle: Int { + case Black + case White +} diff --git a/source/UberRides/RideRequestView.swift b/source/UberRides/RideRequestView.swift new file mode 100644 index 00000000..a8a5cad3 --- /dev/null +++ b/source/UberRides/RideRequestView.swift @@ -0,0 +1,250 @@ +// +// RideRequestView.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import WebKit +import CoreLocation + +/** + * Delegates are informed of events that occur in the RideRequestView such as errors. + */ +@objc(UBSDKRideRequestViewDelegate) public protocol RideRequestViewDelegate { + /** + An error has occurred in the Ride Request Control. + + - parameter rideRequestView: the RideRequestView + - parameter error: the NSError that occured, with a code of RideRequestViewErrorType + */ + func rideRequestView(rideRequestView: RideRequestView, didReceiveError error: NSError) +} + +/// A view that shows the embedded Uber experience. +@objc(UBSDKRideRequestView) public class RideRequestView: UIView { + /// The RideRequestViewDelegate of this view. + public var delegate: RideRequestViewDelegate? + + /// The access token used to authorize the web view + public var accessToken: AccessToken? + + /// Ther RideParameters to use for prefilling the RideRequestView + public var rideParameters: RideParameters + + var webView: WKWebView + let redirectURL = "uberconnect://oauth" + + static let sourceString = "ride_request_view" + + /** + Initializes to show the embedded Uber ride request view. + + - parameter rideParameters: The RideParameters to use for presetting values; defaults to using the current location for pickup + - parameter accessToken: specific access token to use with web view; defaults to using TokenManager's default token + - parameter frame: frame of the view. Defaults to CGRectZero + + - returns: An initialized RideRequestView + */ + @objc public required init(rideParameters: RideParameters, accessToken: AccessToken?, frame: CGRect) { + self.rideParameters = rideParameters + self.accessToken = accessToken + let configuration = WKWebViewConfiguration() + configuration.processPool = Configuration.processPool + webView = WKWebView(frame: CGRectZero, configuration: configuration) + super.init(frame: frame) + initialSetup() + } + + /** + Initializes to show the embedded Uber ride request view. + Uses the TokenManager's default accessToken + + - parameter rideParameters: The RideParameters to use for presetting values + - parameter frame: frame of the view + + - returns: An initialized RideRequestView + */ + @objc public convenience init(rideParameters: RideParameters, frame: CGRect) { + self.init(rideParameters: rideParameters, accessToken: TokenManager.fetchToken(), frame: frame) + } + + /** + Initializes to show the embedded Uber ride request view. + Frame defaults to CGRectZero + Uses the TokenManager's default accessToken + + - parameter rideParameters: The RideParameters to use for presetting values + + - returns: An initialized RideRequestView + */ + @objc public convenience init(rideParameters: RideParameters) { + self.init(rideParameters: rideParameters, accessToken: TokenManager.fetchToken(), frame: CGRectZero) + } + + /** + Initializes to show the embedded Uber ride request view. + Uses the current location for pickup + Uses the TokenManager's default accessToken + + - parameter frame: frame of the view + + - returns: An initialized RideRequestView + */ + @objc public convenience override init(frame: CGRect) { + self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager.fetchToken(), frame: frame) + } + + /** + Initializes to show the embedded Uber ride request view. + Uses the current location for pickup + Uses the TokenManager's default accessToken + Frame defaults to CGRectZero + + - returns: An initialized RideRequestView + */ + @objc public convenience init() { + self.init(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager.fetchToken(), frame: CGRectZero) + } + + required public init?(coder aDecoder: NSCoder) { + rideParameters = RideParametersBuilder().build() + let configuration = WKWebViewConfiguration() + configuration.processPool = Configuration.processPool + webView = WKWebView(frame: CGRectZero, configuration: configuration) + super.init(coder: aDecoder) + initialSetup() + } + + deinit { + webView.scrollView.delegate = nil + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + // MARK: Public + + /** + Load the Uber Ride Request Widget view. + Requires that the access token has been retrieved. + */ + public func load() { + guard let accessToken = accessToken else { + self.delegate?.rideRequestView(self, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenMissing)) + return + } + + let tokenString = accessToken.tokenString + + if rideParameters.source == nil { + rideParameters.source = RideRequestView.sourceString + } + + let endpoint = Components.RideRequestWidget(rideParameters: rideParameters) + let request = Request(session: nil, endpoint: endpoint, bearerToken: tokenString) + request.prepare() + let urlRequest = request.urlRequest + urlRequest.cachePolicy = .ReturnCacheDataElseLoad + webView.loadRequest(urlRequest) + } + + /** + Stop loading the Ride Request Widget View and clears the view. + If the view has already loaded, calling this still clears the view. + */ + public func cancelLoad() { + webView.stopLoading() + if let url = NSURL(string: "about:blank") { + webView.loadRequest(NSURLRequest(URL: url)) + } + } + + // MARK: Private + + private func initialSetup() { + webView.navigationDelegate = self + webView.scrollView.delegate = self + + NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillAppear:"), name: UIKeyboardWillShowNotification, object: nil) + NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardDidAppear:"), name: UIKeyboardDidShowNotification, object: nil) + + setupWebView() + } + + private func setupWebView() { + addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + webView.scrollView.bounces = false + + let views = ["webView": webView] + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[webView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[webView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + + addConstraints(horizontalConstraints) + addConstraints(verticalConstraints) + } + + // MARK: Keyboard Notifications + + func keyboardWillAppear(notification: NSNotification) { + webView.scrollView.scrollEnabled = false + } + + func keyboardDidAppear(notification: NSNotification) { + webView.scrollView.scrollEnabled = true + } +} + +// MARK: WKNavigationDelegate + +extension RideRequestView: WKNavigationDelegate { + public func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.URL { + if url.absoluteString.lowercaseString.hasPrefix(redirectURL.lowercaseString) { + let error = OAuthUtil.parseRideWidgetErrorFromURL(url) + delegate?.rideRequestView(self, didReceiveError: error) + decisionHandler(.Cancel) + return + } + } + + decisionHandler(.Allow) + } + + public func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) { + delegate?.rideRequestView(self, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + } + + public func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) { + guard error.code != 102 else { + return + } + delegate?.rideRequestView(self, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + } +} + +// MARK: UIScrollViewDelegate + +extension RideRequestView : UIScrollViewDelegate { + public func scrollViewDidScroll(scrollView: UIScrollView) { + if !scrollView.scrollEnabled { + scrollView.bounds = self.webView.bounds + } + } +} diff --git a/source/UberRides/RideRequestViewController.swift b/source/UberRides/RideRequestViewController.swift new file mode 100644 index 00000000..c86f84ca --- /dev/null +++ b/source/UberRides/RideRequestViewController.swift @@ -0,0 +1,261 @@ +// +// RideRequestViewController.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import MapKit + +/** + * Delegate Protocol to pass errors from the internal RideRequestView outward if necessary. + * For example, you might want to dismiss the View Controller if it experiences an error + */ +@objc(UBSDKRideRequestViewControllerDelegate) public protocol RideRequestViewControllerDelegate { + /** + Delegate method to pass on errors from the RideRequestView that can't be handled + by the RideRequestViewController + + - parameter rideRequestViewController: The RideRequestViewController that experienced the error + - parameter error: The NSError that was experienced, with a code related to the appropriate RideRequestViewErrorType + */ + @objc func rideRequestViewController(rideRequestViewController: RideRequestViewController, didReceiveError error: NSError) +} + +// View controller to wrap the RideRequestView +@objc (UBSDKRideRequestViewController) public class RideRequestViewController: UIViewController { + /// The RideRequestViewControllerDelegate to handle the errors + public var delegate: RideRequestViewControllerDelegate? + + /// The LoginManager to use for managing the login process + public var loginManager: LoginManager { + didSet { + loginView.delegate = loginManager + loginManager.loginCompletion = loginCompletion + accessTokenIdentifier = loginManager.accessTokenIdentifier + keychainAccessGroup = loginManager.keychainAccessGroup + } + } + + lazy var rideRequestView: RideRequestView = RideRequestView() + lazy var loginView: LoginView = LoginView(scopes: [RidesScope.RideWidgets]) + + static let sourceString = "ride_request_widget" + + private var accessTokenWasUnauthorizedOnPreviousAttempt = false + private var accessTokenIdentifier: String + private var keychainAccessGroup: String + private var loginCompletion: ((accessToken: AccessToken?, error: NSError?) -> Void)? + + /** + Initializes a RideRequestViewController using the provided coder. By default, + uses the default token identifier and access group + + - parameter aDecoder: The Coder to use + + - returns: An initialized RideRequestViewController, or nil if something went wrong + */ + @objc public required init?(coder aDecoder: NSCoder) { + loginManager = LoginManager() + accessTokenIdentifier = loginManager.accessTokenIdentifier + keychainAccessGroup = loginManager.keychainAccessGroup + + super.init(coder: aDecoder) + + let builder = RideParametersBuilder() + builder.setSource(RideRequestViewController.sourceString) + let defaultRideParameters = builder.build() + + rideRequestView.rideParameters = defaultRideParameters + } + + /** + Designated initializer for the RideRequestViewController. + + - parameter rideParameters: The RideParameters to use for prefilling the RideRequestView. + - parameter loginManager: The LoginManger to use for logging in (if required). Also uses its values for token identifier & access group to check for an access token + + - returns: An initialized RideRequestViewController + */ + @objc public init(rideParameters: RideParameters, loginManager: LoginManager) { + self.loginManager = loginManager + accessTokenIdentifier = loginManager.accessTokenIdentifier + keychainAccessGroup = loginManager.keychainAccessGroup + + super.init(nibName: nil, bundle: nil) + + if rideParameters.source == nil { + rideParameters.source = RideRequestViewController.sourceString + } + + rideRequestView.rideParameters = rideParameters + rideRequestView.accessToken = TokenManager.fetchToken(accessTokenIdentifier, accessGroup: keychainAccessGroup) + } + + // MARK: View Lifecycle + + public override func viewDidLoad() { + super.viewDidLoad() + self.edgesForExtendedLayout = UIRectEdge.None + self.view.backgroundColor = UIColor.whiteColor() + loginCompletion = { token, error in + guard let token = token else { + if error?.code == RidesAuthenticationErrorType.NetworkError.rawValue { + self.displayNetworkErrorAlert() + } else { + self.delegate?.rideRequestViewController(self, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenMissing)) + } + return + } + self.loginView.hidden = true + self.rideRequestView.accessToken = token + self.rideRequestView.hidden = false + self.load() + } + + setupRideRequestView() + setupLoginView() + } + + public override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + self.load() + } + + public override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + stopLoading() + accessTokenWasUnauthorizedOnPreviousAttempt = false + } + + // MARK: UIViewController + + public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { + return [.Portrait, .PortraitUpsideDown] + } + + // MARK: Internal + + func load() { + if let accessToken = TokenManager.fetchToken(accessTokenIdentifier, accessGroup: keychainAccessGroup) { + rideRequestView.accessToken = accessToken + rideRequestView.hidden = false + loginView.hidden = true + rideRequestView.load() + } else { + loginManager.loginCompletion = loginCompletion + loginView.hidden = false + rideRequestView.hidden = true + loginView.load() + } + } + + func stopLoading() { + loginView.cancelLoad() + rideRequestView.cancelLoad() + } + + func displayNetworkErrorAlert() { + self.rideRequestView.cancelLoad() + self.loginView.cancelLoad() + let alertController = UIAlertController(title: nil, message: LocalizationUtil.localizedString(forKey: "The Ride Request Widget encountered a problem.", comment: "The Ride Request Widget encountered a problem."), preferredStyle: .Alert) + let tryAgainAction = UIAlertAction(title: LocalizationUtil.localizedString(forKey: "Try Again", comment: "Try Again"), style: .Default, handler: { (UIAlertAction) -> Void in + self.load() + }) + let cancelAction = UIAlertAction(title: LocalizationUtil.localizedString(forKey: "Cancel", comment: "Cancel"), style: .Cancel, handler: { (UIAlertAction) -> Void in + self.delegate?.rideRequestViewController(self, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + }) + alertController.addAction(tryAgainAction) + alertController.addAction(cancelAction) + self.presentViewController(alertController, animated: true, completion: nil) + } + + //MARK: Private + + private func setupRideRequestView() { + self.view.addSubview(rideRequestView) + + rideRequestView.translatesAutoresizingMaskIntoConstraints = false + + let views = ["rideRequestView": rideRequestView] + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[rideRequestView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[rideRequestView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + + self.view.addConstraints(horizontalConstraints) + self.view.addConstraints(verticalConstraints) + + rideRequestView.delegate = self + } + + private func setupLoginView() { + switch loginManager.loginBehavior { + case .Implicit: + setupImplicitLoginView() + break + } + } + + private func setupImplicitLoginView() { + let loginView = LoginView(scopes: [RidesScope.RideWidgets]) + self.view.addSubview(loginView) + loginView.hidden = true + loginView.translatesAutoresizingMaskIntoConstraints = false + + let views = ["loginView": loginView] + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|[loginView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|[loginView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views) + + self.view.addConstraints(horizontalConstraints) + self.view.addConstraints(verticalConstraints) + + loginView.delegate = loginManager + loginManager.loginCompletion = self.loginCompletion + self.loginView = loginView + } +} + +//MARK: RideRequestView Delegate + +extension RideRequestViewController : RideRequestViewDelegate { + public func rideRequestView(rideRequestView: RideRequestView, didReceiveError error: NSError) { + let errorType = RideRequestViewErrorType(rawValue: error.code) ?? .Unknown + switch errorType { + case .NetworkError: + self.displayNetworkErrorAlert() + break + case .AccessTokenMissing: + fallthrough + case .AccessTokenExpired: + if accessTokenWasUnauthorizedOnPreviousAttempt { + fallthrough + } + accessTokenWasUnauthorizedOnPreviousAttempt = true + self.rideRequestView.hidden = true + self.loginView.hidden = false + self.loginView.load() + break + default: + self.delegate?.rideRequestViewController(self, didReceiveError: error) + break + } + } +} diff --git a/source/UberRides/RideRequestViewErrorFactory.swift b/source/UberRides/RideRequestViewErrorFactory.swift new file mode 100644 index 00000000..a925ab04 --- /dev/null +++ b/source/UberRides/RideRequestViewErrorFactory.swift @@ -0,0 +1,66 @@ +// +// RideRequestViewErrorFactory.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/// Factory class for creating RideRequestViewErrors +class RideRequestViewErrorFactory { + + static let errorDomain = "com.uber.rides-ios-sdk.riderequesterror" + + /** + Creates a NSError using the provided RideRequestViewErrorType + + - parameter rideRequestViewErrorType: The RideRequestViewErrorType to create the error with + + - returns: An initialized NSError + */ + static func errorForType(rideRequestViewErrorType: RideRequestViewErrorType) -> NSError { + return NSError(domain: errorDomain, code: rideRequestViewErrorType.rawValue, userInfo: nil) + } + + /** + Creates a RideRequestViewError using the provided error String. The error string + should match the string provided from the Ride Request Widget endpoint + + - parameter rawValue: The error string to use + + - returns: An initialized RideRequestViewError + */ + static func errorForString(rawValue: String) -> NSError { + let errorType = rideRequestViewErrorType(rawValue) + return RideRequestViewErrorFactory.errorForType(errorType) + } + + static func rideRequestViewErrorType(rawValue: String) -> RideRequestViewErrorType { + switch rawValue { + case "network_error": + return .NetworkError + case "no_access_token": + return .AccessTokenMissing + case "unauthorized": + return .AccessTokenExpired + default: + return .Unknown + } + } +} diff --git a/source/UberRides/RideRequestViewErrorType.swift b/source/UberRides/RideRequestViewErrorType.swift new file mode 100644 index 00000000..bb053d7d --- /dev/null +++ b/source/UberRides/RideRequestViewErrorType.swift @@ -0,0 +1,51 @@ +// +// RideRequestViewErrorType.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/** +Possible errors that can occur in the RideRequestView. + +- AccessTokenMissing: There is no access token to make the request with +- AccessTokenExpired: Access token has expired. +- NetworkError: A network error occured +- Unknown: Unknown error occured. +*/ +@objc public enum RideRequestViewErrorType: Int { + case AccessTokenExpired + case AccessTokenMissing + case NetworkError + case Unknown + + func toString() -> String { + switch self { + case .AccessTokenExpired: + return "unauthorized" + case .AccessTokenMissing: + return "no_access_token" + case .NetworkError: + return "network_error" + case .Unknown: + return "unknown" + } + } +} diff --git a/source/UberRides/RideRequestViewRequestingBehavior.swift b/source/UberRides/RideRequestViewRequestingBehavior.swift new file mode 100644 index 00000000..8b53b25f --- /dev/null +++ b/source/UberRides/RideRequestViewRequestingBehavior.swift @@ -0,0 +1,95 @@ +// +// RideRequestViewRequestingBehavior.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// 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. + + +/// A RideRequesting object for requesting a ride via the RideRequestViewController +@objc(UBSDKRideRequestViewRequestingBehavior) public class RideRequestViewRequestingBehavior : NSObject { + + /// The UIViewController to present the RideRequestViewController over + unowned public var presentingViewController: UIViewController + + /** + The LoginManager to use with the RideRequestViewController. Uses the + accessTokenIdentifier & keychainAccessGroup to get an AccessToken. Will be used + to log a user in, if necessary + */ + public var loginManager: LoginManager { + get { + return self.modalRideRequestViewController.rideRequestViewController.loginManager + } + set { + self.modalRideRequestViewController.rideRequestViewController.loginManager = newValue + } + } + + /// The ModalRideRequestViewController that is created by this behavior, only exists after requestRide() is called + public internal(set) var modalRideRequestViewController: ModalRideRequestViewController + + /** + Creates the RideRequestViewRequestingBehavior with the given presenting view controller. + This view controller will be used to modally present the ModalRideRequestViewController + when this behavior is executed + + - parameter presentingViewController: The UIViewController to present the ModalRideRequestViewController over + - parameter loginManager: The LoginManager to use for managing the AccessToken for the RideRequestView + + - returns: An initialized RideRequestViewRequestingBehavior object + */ + @objc public init(presentingViewController: UIViewController, loginManager: LoginManager) { + self.presentingViewController = presentingViewController + let rideRequestViewController = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManager) + modalRideRequestViewController = ModalRideRequestViewController(rideRequestViewController: rideRequestViewController) + } + + /** + Creates the RideRequestViewRequestingBehavior with the given presenting view controller. + This view controller will be used to modally present the ModalRideRequestViewController + when this behavior is executed + + Uses a default LoginManager() for login & token management + + - parameter presentingViewController: The UIViewController to present the ModalRideRequestViewController over + + - returns: An initialized RideRequestViewRequestingBehavior object + */ + @objc public convenience init(presentingViewController: UIViewController) { + self.init(presentingViewController: presentingViewController, loginManager: LoginManager()) + } +} + +extension RideRequestViewRequestingBehavior : RideRequesting { + /** + Requests a ride by presenting a RideRequestView that is constructed using the provided + rideParameters + + - parameter rideParameters: The RideParameters to use for building and prefilling + the RideRequestView + */ + public func requestRide(rideParameters: RideParameters?) { + if let rideParameters = rideParameters { + modalRideRequestViewController.rideRequestViewController.rideRequestView.rideParameters = rideParameters + } + presentingViewController.presentViewController(modalRideRequestViewController, animated: true, completion: nil) + } +} diff --git a/source/UberRides/RideRequestingProtocol.swift b/source/UberRides/RideRequestingProtocol.swift new file mode 100644 index 00000000..078b8998 --- /dev/null +++ b/source/UberRides/RideRequestingProtocol.swift @@ -0,0 +1,36 @@ +// +// RideRequestingProtocol.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// 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. + +/** + * Protocol for an object that defines how to request a ride. Is expected to be used + * by any control that can request a ride for a user. + */ +@objc(UBSDKRideRequesting) public protocol RideRequesting { + /** + Requests a ride using the provided RideParameters. + + - parameter rideParameters: The RideParameters to use for the ride request + */ + @objc func requestRide(rideParameters: RideParameters?) +} diff --git a/source/UberRides/RidesClient.swift b/source/UberRides/RidesClient.swift index 827ad2e2..bea59b05 100644 --- a/source/UberRides/RidesClient.swift +++ b/source/UberRides/RidesClient.swift @@ -25,19 +25,125 @@ import Foundation +/// API client for the Uber Rides API. +@objc(UBSDKRidesClient) public class RidesClient: NSObject { + + /// Application client ID. Required for every instance of RidesClient. + var clientID: String = Configuration.getClientID() + + /// The Access Token Identifier. The identifier to use for looking up this client's accessToken + let accessTokenIdentifier: String + + /// The Keychain Access Group. The access group to use when looking up this client's accessToken + let keychainAccessGroup: String + + /// NSURLSession used to make requests to Uber API. Default session configuration unless otherwise initialized. + var session: NSURLSession + + /** + Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + + - parameter accessTokenIdentifier: The accessTokenIdentifier to use. This identifier + is used (along with keychainAccessGroup) to fetch the appropriate AccessToken. Defaults + to the value set in your Configuration struct + - parameter sessionConfiguration: Configuration to use for NSURLSession. Defaults to defaultSessionConfiguration. + - parameter keychainAccessGroup: The keychain access group to use. Uses this group + (along with the accessTokenIdentifier) to fetch the appropriate AccessToken. Defaults + to the value set in yoru Configuration struct + + - returns: An initialized RidesClient -public class RidesClient: NSObject { - var clientID: String? + */ + @objc public init(accessTokenIdentifier: String, sessionConfiguration: NSURLSessionConfiguration, keychainAccessGroup: String) { + self.accessTokenIdentifier = accessTokenIdentifier + self.keychainAccessGroup = keychainAccessGroup + self.session = NSURLSession(configuration: sessionConfiguration) + } - static public let sharedInstance = RidesClient() + /** + Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + By default, uses NSURLSessionConfiguration.defaultSessionConfiguration() for the URL requests + + - parameter accessTokenIdentifier: Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + By default, it is initialized using the keychainAccessGroup default from your Configuration object + Also uses NSURLSessionConfiguration.defaultSessionConfiguration() for the URL requests + - parameter keychainAccessGroup: The keychain access group to use. Uses this group + (along with the accessTokenIdentifier) to fetch the appropriate AccessToken. Defaults + to the value set in yoru Configuration struct + + - returns: An initialized RidesClient + */ + @objc public convenience init(accessTokenIdentifier: String, keychainAccessGroup: String) { + self.init(accessTokenIdentifier: accessTokenIdentifier, + sessionConfiguration: NSURLSessionConfiguration.defaultSessionConfiguration(), + keychainAccessGroup: keychainAccessGroup) + } + + /** + Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + By default, it is initialized using the keychainAccessGroup default from your Configuration object + + - parameter accessTokenIdentifier: The accessTokenIdentifier to use. This identifier + is used (along with keychainAccessGroup) to fetch the appropriate AccessToken + - parameter sessionConfiguration: Configuration to use for NSURLSession. Defaults to defaultSessionConfiguration. + + - returns: An initialized RidesClient + */ + @objc public convenience init(accessTokenIdentifier: String, sessionConfiguration: NSURLSessionConfiguration) { + self.init(accessTokenIdentifier: accessTokenIdentifier, + sessionConfiguration: sessionConfiguration, + keychainAccessGroup: Configuration.getDefaultKeychainAccessGroup()) + } - private override init() {} + /** + Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + By default, it is initialized using the keychainAccessGroup default from your Configuration object + Also uses NSURLSessionConfiguration.defaultSessionConfiguration() for the URL requests + + - parameter accessTokenIdentifier: The accessTokenIdentifier to use. This identifier + is used (along with keychainAccessGroup) to fetch the appropriate AccessToken + + - returns: An initialized RidesClient + */ + @objc public convenience init(accessTokenIdentifier: String) { + self.init(accessTokenIdentifier: accessTokenIdentifier, + sessionConfiguration: NSURLSessionConfiguration.defaultSessionConfiguration(), + keychainAccessGroup: Configuration.getDefaultKeychainAccessGroup()) + } - public func configureClientID(id: String) { - clientID = id + /** + Initializer for the RidesClient. The RidesClient handles making reqeusts to the API + for you. + By default, it is initialized using the accessTokenIdentifier & keychainAccessGroup + defaults from your Configuration object + Also uses NSURLSessionConfiguration.defaultSessionConfiguration() for the URL requests + + - returns: An initialized RidesClient + */ + @objc public convenience override init() { + self.init(accessTokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), + sessionConfiguration: NSURLSessionConfiguration.defaultSessionConfiguration(), + keychainAccessGroup: Configuration.getDefaultKeychainAccessGroup()) } - public func hasClientID() -> Bool { - return clientID != nil && clientID != "YOUR_CLIENT_ID" + + + /** + Retrieves the token used by this rides client. + + Currently pulls from the keychain each time. + + - returns: an AccessToken object, or nil if one can't be located + */ + @objc public func getAccessToken() -> AccessToken? { + guard let accessToken = TokenManager.fetchToken(accessTokenIdentifier, accessGroup: keychainAccessGroup) else { + return nil + } + return accessToken } } diff --git a/source/UberRides/RidesUtil.swift b/source/UberRides/RidesUtil.swift new file mode 100644 index 00000000..778d5d35 --- /dev/null +++ b/source/UberRides/RidesUtil.swift @@ -0,0 +1,260 @@ +// +// RidesUtil.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation +import CoreLocation + +@objc enum UberButtonColor: Int { + case UberBlack + case UberWhite + case BlackHighlighted + case WhiteHighlighted +} + +class ColorUtil { + /// Convert hex color code into UIColor + static func uberUIColor(color: UberButtonColor) -> UIColor { + let hexCode = hexCodeFromColor(color) + let scanner = NSScanner(string: hexCode) + var color: UInt32 = 0; + scanner.scanHexInt(&color) + + let mask = 0x000000FF + + let redValue = CGFloat(Int(color >> 16)&mask)/255.0 + let greenValue = CGFloat(Int(color >> 8)&mask)/255.0 + let blueValue = CGFloat(Int(color)&mask)/255.0 + + return UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1.0) + } + + private static func hexCodeFromColor(color: UberButtonColor) -> String { + switch color { + case .UberBlack: + return "000000" + case .UberWhite: + return "FFFFFF" + case .BlackHighlighted: + return "282727" + case .WhiteHighlighted: + return "E5E5E4" + } + } +} + +class FontUtil { + static func loadFontWithName(name: String, familyName: String) -> Bool { + if let path = NSBundle(forClass: FontUtil.self).pathForResource(name, ofType: "otf") { + if let inData = NSData(contentsOfFile: path) { + var error: Unmanaged? + let cfdata = CFDataCreate(nil, UnsafePointer(inData.bytes), inData.length) + if let provider = CGDataProviderCreateWithCFData(cfdata) { + if let font = CGFontCreateWithDataProvider(provider) { + if (CTFontManagerRegisterGraphicsFont(font, &error)) { + return true + } + print("Failed to load font with error: \(error)") + } + } + } + } + return false + } +} + +class LocalizationUtil { + static func localizedString(forKey key: String, comment: String) -> String { + var localizationBundle = NSBundle(forClass: self) + if let frameworkPath = NSBundle.mainBundle().privateFrameworksPath, let frameworkBundle = NSBundle(path: "\(frameworkPath)/UberRides.framework") { + localizationBundle = frameworkBundle + } + return NSLocalizedString(key, bundle: localizationBundle, comment: comment) + } +} + +class OAuthUtil { + + static let ErrorKey = "error" + + /** + Parses a URL returned from an authentication request to find the error described in the query parameters. + + - parameter url: the URL to be parsed, most likely from a webview. + + - returns: an NSError, who's code contains the RidesAuthenticationErrorType that occured. If none recognized, defaults to InvalidRequest. + */ + static func parseAuthenticationErrorFromURL(url: NSURL) -> NSError { + let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)! + if let params = components.allItems() { + for param in params { + if param.name == "error" { + guard let rawValue = param.value, let error = RidesAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: rawValue) else { + return RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidRequest) + } + return error + } + } + } + return RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .InvalidRequest) + } + + static func parseRideWidgetErrorFromURL(url: NSURL) -> NSError { + let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)! + if let fragment = components.fragment { + components.fragment = nil + components.query = fragment + for item in components.queryItems! where item.name == ErrorKey { + if let value = item.value { + return RideRequestViewErrorFactory.errorForString(value) + } + } + } + + return RideRequestViewErrorFactory.errorForType(.Unknown) + } +} + +class RequestURLUtil { + + enum RequestURLUtilError : ErrorType { + case UnableToBuildURLError + } + + private enum LocationType: String { + case Pickup = "pickup" + case Dropoff = "dropoff" + } + + static let actionKey = "action" + static let setPickupValue = "setPickup" + static let clientIDKey = "client_id" + static let productIDKey = "product_id" + static let currentLocationValue = "my_location" + static let latitudeKey = "[latitude]" + static let longitudeKey = "[longitude]" + static let nicknameKey = "[nickname]" + static let formattedAddressKey = "[formatted_address]" + static let deeplinkScheme = "uber" + static let userAgentKey = "user-agent" + + static func buildURL(rideParameters: RideParameters) throws -> NSURL { + let urlComponents = NSURLComponents() + + urlComponents.scheme = RequestURLUtil.deeplinkScheme + urlComponents.host = "" + + var queryItems = [NSURLQueryItem]() + queryItems.append(NSURLQueryItem(name: RequestURLUtil.actionKey, value: RequestURLUtil.setPickupValue)) + queryItems.append(NSURLQueryItem(name: RequestURLUtil.clientIDKey, value: Configuration.getClientID())) + + if let productID = rideParameters.productID { + queryItems.append(NSURLQueryItem(name: RequestURLUtil.productIDKey, value: productID)) + } + + if let location = rideParameters.pickupLocation { + queryItems.appendContentsOf(addLocation(LocationType.Pickup, location: location, nickname: rideParameters.pickupNickname, address: rideParameters.pickupAddress)) + } else { + queryItems.append(NSURLQueryItem(name: LocationType.Pickup.rawValue, value: RequestURLUtil.currentLocationValue)) + } + + if let location = rideParameters.dropoffLocation { + queryItems.appendContentsOf(addLocation(LocationType.Dropoff, location: location, nickname: rideParameters.dropoffNickname, address: rideParameters.dropoffAddress)) + } + + queryItems.append(NSURLQueryItem(name: RequestURLUtil.userAgentKey, value: rideParameters.userAgent)) + + urlComponents.queryItems = queryItems + + if let url = urlComponents.URL { + return url + } else { + throw RequestURLUtilError.UnableToBuildURLError + } + } + + private static func addLocation(locationType: LocationType, location: CLLocation, nickname: String?, address: String?) -> [NSURLQueryItem] { + var queryItems = [NSURLQueryItem]() + + let locationPrefix = locationType.rawValue + let latitudeString = "\(location.coordinate.latitude)" + let longitudeString = "\(location.coordinate.longitude)" + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLUtil.latitudeKey, value: latitudeString)) + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLUtil.longitudeKey, value: longitudeString)) + if let nickname = nickname { + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLUtil.nicknameKey, value: nickname)) + } + if let address = address { + queryItems.append(NSURLQueryItem(name: locationPrefix + RequestURLUtil.formattedAddressKey, value: address)) + } + + return queryItems + } +} + +/** + Extension for NSURLComponents to easily extract key value pairs from the fragment + + Adds functionality to extract key value pairs (as NSURLQueryItem) from the fragment + Adds functionality to extract key value pairs (as NSURLQueryItem) from both fragment and query + */ +extension NSURLComponents +{ + /** + Converts key value pairs in the fragment into NSURLQueryItems + This is done by setting the query to the value of the fragment, calling .queryItems + then restoring the original value of query + - returns: An array of NSURLQueryItems, or nil if there was no fragment + */ + func fragmentItems() -> [NSURLQueryItem]? + { + objc_sync_enter(self) + let holdQuery = self.query + self.query = self.fragment + let fragmentItems = self.queryItems + self.query = holdQuery + objc_sync_exit(self) + return fragmentItems + } + + /** + Converts key value pairs in the fragment into NSURLQueryItems and appends them + to the NSURLQuery items returned from .queryItems + - returns: An array of NSURLQueryItems, or nil if there was no fragment and no query + */ + func allItems() -> [NSURLQueryItem]? + { + var finalItemArray = [NSURLQueryItem]() + if let queryItems = self.queryItems { + finalItemArray.appendContentsOf(queryItems) + } + if let fragmentItems = self.fragmentItems() { + finalItemArray.appendContentsOf(fragmentItems) + } + guard finalItemArray.count > 0 else { + return nil + } + return finalItemArray + } +} diff --git a/source/UberRides/TokenManager.swift b/source/UberRides/TokenManager.swift new file mode 100644 index 00000000..022f3227 --- /dev/null +++ b/source/UberRides/TokenManager.swift @@ -0,0 +1,186 @@ +// +// TokenManager.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Manager class for saving and deleting AccessTokens. Allows you to manage tokens based on token identifier & keychain access group +@objc(UBSDKTokenManager) public class TokenManager: NSObject { + + private static let keychainWrapper = KeychainWrapper() + + //MARK: Get + + /** + Gets the AccessToken for the given tokenIdentifier and accessGroup. + + - parameter tokenIdentifier: The token identifier string to use + - parameter accessGroup: The keychain access group to use + + - returns: An AccessToken, or nil if one wasn't found + */ + @objc public static func fetchToken(tokenIdentifier: String, accessGroup: String) -> AccessToken? { + keychainWrapper.setAccessGroup(accessGroup) + guard let token = keychainWrapper.getObjectForKey(tokenIdentifier) as? AccessToken else { + return nil + } + return token + } + + /** + Gets the AccessToken for the given tokenIdentifier. + Uses the default value for keychain access group, as defined by your Configuration. + + - parameter tokenIdentifier: The token identifier string to use + + - returns: An AccessToken, or nil if one wasn't found + */ + @objc public static func fetchToken(tokenIdentifier: String) -> AccessToken? { + return self.fetchToken(tokenIdentifier, + accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + /** + Gets the AccessToken using the default tokenIdentifier and accessGroup. + These values are the defined in your Configuration + + - returns: An AccessToken, or nil if one wasn't found + */ + @objc public static func fetchToken() -> AccessToken? { + return self.fetchToken(Configuration.getDefaultAccessTokenIdentifier(), + accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + //MARK: Save + + /** + Saves the given AccessToken using the provided tokenIdentifier and acessGroup.If no values + are supplied, it uses the defaults defined in your Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.getDefaultAccessTokenIdentifier()) + - parameter accessGroup: The keychain access group to use (defaults to Configuration.getDefaultKeychainAccessGroup()) + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @objc public static func saveToken(accessToken: AccessToken, tokenIdentifier: String, accessGroup: String) -> Bool { + keychainWrapper.setAccessGroup(accessGroup) + return keychainWrapper.setObject(accessToken, key: tokenIdentifier) + } + + /** + Saves the given AccessToken using the provided tokenIdentifier. + Uses the default keychain access group defined by your Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + - parameter tokenIdentifier: The token identifier string to use + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @objc public static func saveToken(accessToken: AccessToken, tokenIdentifier: String) -> Bool { + return self.saveToken(accessToken, tokenIdentifier: tokenIdentifier, accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + /** + Saves the given AccessToken. + Uses the default access token identifier & keychain access group defined by your + Configuration. + + Access Token is saved syncronously + + - parameter accessToken: The AccessToken to save + + - returns: true if the accessToken was saved successfully, false otherwise + */ + @objc public static func saveToken(accessToken: AccessToken) -> Bool { + return self.saveToken(accessToken, tokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + //MARK: Delete + + /** + Deletes the AccessToken for the givent tokenIdentifier and accessGroup. If no values + are supplied, it uses the defaults defined in your Configuration. + + - parameter tokenIdentifier: The token identifier string to use (defaults to Configuration.getDefaultAccessTokenIdentifier()) + - parameter accessGroup: The keychain access group to use (defaults to Configuration.getDefaultKeychainAccessGroup()) + + - returns: true if the token was deleted, false otherwise + */ + @objc public static func deleteToken(tokenIdentifier: String, accessGroup: String) -> Bool { + keychainWrapper.setAccessGroup(accessGroup) + deleteCookies() + return keychainWrapper.deleteObjectForKey(tokenIdentifier) + } + + /** + Deletes the AccessToken for the given tokenIdentifier. + Uses the default keychain access group defined in your Configuration. + + - parameter tokenIdentifier: The token identifier string to use + + - returns: true if the token was deleted, false otherwise + */ + @objc public static func deleteToken(tokenIdentifier: String) -> Bool { + return self.deleteToken(tokenIdentifier, accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + /** + Deletes an AccessToken. + Uses the default token identifier defined in your Configuration. + Uses the default keychain access group defined in your Configuration. + + - returns: true if the token was deleted, false otherwise + */ + @objc public static func deleteToken() -> Bool { + return self.deleteToken(Configuration.getDefaultAccessTokenIdentifier(), accessGroup: Configuration.getDefaultKeychainAccessGroup()) + } + + // MARK: Private Interface + + private static func deleteCookies() { + Configuration.resetProcessPool() + let loginEndpoint = OAuth.Login(clientID: Configuration.getClientID(), scopes: [], redirect: Configuration.getCallbackURIString()) + var urlsToClear = [NSURL]() + if let loginURL = NSURL(string: loginEndpoint.regionHostString(.Default)) { + urlsToClear.append(loginURL) + } + if let loginURL = NSURL(string: loginEndpoint.regionHostString(.China)) { + urlsToClear.append(loginURL) + } + + let sharedCookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage() + + for url in urlsToClear { + if let cookies = sharedCookieStorage.cookiesForURL(url) { + for cookie in cookies { + sharedCookieStorage.deleteCookie(cookie) + } + } + } + } +} diff --git a/source/UberRides/UberButton.swift b/source/UberRides/UberButton.swift new file mode 100644 index 00000000..3d11a4a8 --- /dev/null +++ b/source/UberRides/UberButton.swift @@ -0,0 +1,112 @@ +// +// UberButton.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +/// Base class for Uber buttons that sets up colors and some constraints. +@objc(UBSDKUberButton) public class UberButton: UIButton { + let horizontalImagePadding: CGFloat = 12 + let horizontalLabelPadding: CGFloat = 19 + let imageLabelPadding: CGFloat = 9 + let verticalPadding: CGFloat = 10 + + let uberImageView: UIImageView! = UIImageView() + let uberTitleLabel: UILabel! = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setColorStyle(.Black) + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setColorStyle(.Black) + } + + override public var highlighted: Bool { + // Change colors when button is highlighted + didSet { + var color: UberButtonColor + switch colorStyle { + case .Black: + color = highlighted ? .BlackHighlighted : .UberBlack + case .White: + color = highlighted ? .WhiteHighlighted : .UberWhite + } + backgroundColor = ColorUtil.uberUIColor(color) + } + } + + /// Set color scheme, default is black background with white font. + public var colorStyle: RequestButtonColorStyle = .Black { + didSet { + setColorStyle(colorStyle) + } + } + + private func setColorStyle(style: RequestButtonColorStyle) { + switch colorStyle { + case .Black: + backgroundColor = ColorUtil.uberUIColor(.UberBlack) + uberTitleLabel.textColor = ColorUtil.uberUIColor(.UberWhite) + case .White : + backgroundColor = ColorUtil.uberUIColor(.UberWhite) + uberTitleLabel.textColor = ColorUtil.uberUIColor(.UberBlack) + } + } + + public func setImage(image: UIImage) { + uberImageView.image = image + } + + public func setText(text: String, font: UIFont?) { + uberTitleLabel.text = text + uberTitleLabel.font = font + } + + func setContent() { + self.dynamicType.loadFonts() + clipsToBounds = true + layer.cornerRadius = 5 + } + + private static func loadFonts() { + struct DispatchOnce { static var token: dispatch_once_t = 0} + dispatch_once(&DispatchOnce.token, { + FontUtil.loadFontWithName("ClanPro-Book", familyName: "Clan Pro") + FontUtil.loadFontWithName("ClanPro-Medium", familyName: "Clan Pro") + }) + } + + func setConstraints() { + let views = ["imageView": uberImageView, "titleView": uberTitleLabel] + let metrics = ["horizontalImagePadding": horizontalImagePadding, "horizontalLabelPadding": horizontalLabelPadding, "verticalPadding": verticalPadding] + + let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-horizontalImagePadding-[imageView]-imageLabelPadding-[titleLabel]-horizontalLabelPadding-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views) + let verticalContraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-verticalPadding-[imageView]-verticalPadding-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrics, views: views) + + addConstraints(horizontalConstraints) + addConstraints(verticalContraints) + } +} diff --git a/source/UberRidesTests/AccessTokenFactoryTests.swift b/source/UberRidesTests/AccessTokenFactoryTests.swift new file mode 100644 index 00000000..e4307780 --- /dev/null +++ b/source/UberRidesTests/AccessTokenFactoryTests.swift @@ -0,0 +1,212 @@ +// +// AccessTokenFactorytests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class AccessTokenFactoryTests: XCTestCase { + private let redirectURI = "http://localhost:1234/" + private let tokenString = "token" + private let refreshTokenString = "refreshToken" + private let expirationTime = 10030.23 + private let allowedScopesString = "profile history" + private let errorString = "invalid_parameters" + + private let maxExpirationDifference = 2.0 + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testParseTokenFromURL_withSuccess() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)&refresh_token=\(refreshTokenString)&expires_in=\(expirationTime)&scope=\(allowedScopesString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + let expectedExpirationInterval = NSDate().timeIntervalSince1970 + expirationTime + + let token : AccessToken = try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + XCTAssertNotNil(token) + XCTAssertEqual(token.tokenString, tokenString) + XCTAssertEqual(token.refreshToken, refreshTokenString) + XCTAssertEqual(token.grantedScopes?.toRidesScopeString(), allowedScopesString) + + guard let expiration = token.expirationDate?.timeIntervalSince1970 else { + XCTAssert(false) + return + } + + let timeDiff = abs(expiration - expectedExpirationInterval) + XCTAssertLessThanOrEqual(timeDiff, maxExpirationDifference) + + } catch _ as NSError { + XCTAssert(false) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withError() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)&refresh_token=\(refreshTokenString)&expires_in=\(expirationTime)&scope=\(allowedScopesString)&error=\(errorString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + } catch let error as NSError { + XCTAssertEqual(error.code, RidesAuthenticationErrorType.InvalidRequest.rawValue) + XCTAssertEqual(error.domain, RidesAuthenticationErrorFactory.errorDomain) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withOnlyError() { + let components = NSURLComponents() + components.fragment = "error=\(errorString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + } catch let error as NSError { + XCTAssertEqual(error.code, RidesAuthenticationErrorType.InvalidRequest.rawValue) + XCTAssertEqual(error.domain, RidesAuthenticationErrorFactory.errorDomain) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withPartialParameters() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + let token : AccessToken = try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + XCTAssertNotNil(token) + XCTAssertEqual(token.tokenString, tokenString) + XCTAssertNil(token.refreshToken) + XCTAssertNil(token.expirationDate) + XCTAssertEqual(token.grantedScopes!, [RidesScope]()) + } catch _ as NSError { + XCTAssert(false) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withFragmentAndQuery_withError() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)" + components.query = "error=\(errorString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + } catch let error as NSError { + XCTAssertEqual(error.code, RidesAuthenticationErrorType.InvalidRequest.rawValue) + XCTAssertEqual(error.domain, RidesAuthenticationErrorFactory.errorDomain) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withFragmentAndQuery_withSuccess() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)&refresh_token=\(refreshTokenString)" + components.query = "expires_in=\(expirationTime)&scope=\(allowedScopesString)" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + let expectedExpirationInterval = NSDate().timeIntervalSince1970 + expirationTime + + let token : AccessToken = try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + XCTAssertNotNil(token) + XCTAssertEqual(token.tokenString, tokenString) + XCTAssertEqual(token.refreshToken, refreshTokenString) + XCTAssertEqual(token.grantedScopes?.toRidesScopeString(), allowedScopesString) + + guard let expiration = token.expirationDate?.timeIntervalSince1970 else { + XCTAssert(false) + return + } + + let timeDiff = abs(expiration - expectedExpirationInterval) + XCTAssertLessThanOrEqual(timeDiff, maxExpirationDifference) + + } catch _ as NSError { + XCTAssert(false) + } catch { + XCTAssert(false) + } + } + + func testParseTokenFromURL_withInvalidFragment() { + let components = NSURLComponents() + components.fragment = "access_token=\(tokenString)&refresh_token" + components.host = redirectURI + guard let url = components.URL else { + XCTAssert(false) + return + } + do { + let token : AccessToken = try AccessTokenFactory.createAccessTokenFromRedirectURL(url) + XCTAssertNotNil(token) + XCTAssertEqual(token.tokenString, tokenString) + XCTAssertNil(token.refreshToken) + XCTAssertNil(token.expirationDate) + XCTAssertEqual(token.grantedScopes!, [RidesScope]()) + } catch _ as NSError { + XCTAssert(false) + } catch { + XCTAssert(false) + } + } + +} diff --git a/source/UberRidesTests/ConfigurationTests.swift b/source/UberRidesTests/ConfigurationTests.swift new file mode 100644 index 00000000..110a1813 --- /dev/null +++ b/source/UberRidesTests/ConfigurationTests.swift @@ -0,0 +1,196 @@ +// +// ConfigurationTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class ConfigurationTests: XCTestCase { + + private let defaultClientID = "testClientID" + private let defaultCallbackString = "testUri://uberConnect" + private let defaultAccessTokenIdentifier = "RidesAccessTokenKey" + private let defaultRegion = Region.Default + private let defaultSandbox = false + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + //MARK: Reset Test + + func testConfiguration_restoreDefaults() { + + let newClientID = "newID" + let newCallback = "newCallback://" + let newGroup = "new group" + let newTokenId = "newTokenID" + let newRegion = Region.China + let newSandbox = true + + Configuration.setClientID(newClientID) + Configuration.setCallbackURIString(newCallback) + Configuration.setDefaultKeychainAccessGroup(newGroup) + Configuration.setDefaultAccessTokenIdentifier(newTokenId) + Configuration.setRegion(newRegion) + Configuration.setSandboxEnabled(newSandbox) + + XCTAssertEqual(newClientID, Configuration.getClientID()) + XCTAssertEqual(newCallback, Configuration.getCallbackURIString()) + XCTAssertEqual(newGroup, Configuration.getDefaultKeychainAccessGroup()) + XCTAssertEqual(newTokenId, Configuration.getDefaultAccessTokenIdentifier()) + XCTAssertEqual(newRegion, Configuration.getRegion()) + XCTAssertEqual(newSandbox, Configuration.getSandboxEnabled()) + Configuration.restoreDefaults() + + XCTAssertEqual(Configuration.plistName, "Info") + XCTAssertEqual(Configuration.bundle, NSBundle.mainBundle()) + + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + + XCTAssertEqual(Configuration.getClientID(), defaultClientID) + XCTAssertEqual(defaultCallbackString, Configuration.getCallbackURIString()) + XCTAssertEqual("", Configuration.getDefaultKeychainAccessGroup()) + XCTAssertEqual(defaultAccessTokenIdentifier, Configuration.getDefaultAccessTokenIdentifier()) + XCTAssertEqual(defaultRegion, Configuration.getRegion()) + XCTAssertEqual(defaultSandbox, Configuration.getSandboxEnabled()) + } + + //MARK: Client ID Tests + + func testClientID_getDefault() { + XCTAssertEqual(defaultClientID, Configuration.getClientID()) + } + + func testClientID_overwriteDefault() { + let clientID = "clientID" + Configuration.setClientID(clientID) + XCTAssertEqual(clientID, Configuration.getClientID()) + } + + func testClientID_resetDefault() { + Configuration.setClientID("alternateClient") + + Configuration.setClientID(nil) + + XCTAssertEqual(defaultClientID, Configuration.getClientID()) + } + + //MARK: Callback URI String Tests + + func testCallbackURIString_getDefault() { + XCTAssertEqual(defaultCallbackString, Configuration.getCallbackURIString()) + } + + func testCallbackURIString_overwriteDefault() { + let callbackURIString = "callback://test" + Configuration.setCallbackURIString(callbackURIString) + + XCTAssertEqual(callbackURIString, Configuration.getCallbackURIString()) + } + + func testCallbackURIString_resetDefault() { + Configuration.setCallbackURIString("testCallback://asdf") + + Configuration.setCallbackURIString(nil) + + XCTAssertEqual(defaultCallbackString, Configuration.getCallbackURIString()) + } + + //MARK: Keychain Access Group Tests + + func testDefaultKeychainAccessGroup_getDefault() { + XCTAssertEqual("", Configuration.getDefaultKeychainAccessGroup()) + } + + func testDefaultKeychainAccessGroup_overwriteDefault() { + let defaultKeychainAccessGroup = "accessGroup" + Configuration.setDefaultKeychainAccessGroup(defaultKeychainAccessGroup) + + XCTAssertEqual(defaultKeychainAccessGroup, Configuration.getDefaultKeychainAccessGroup()) + } + + func testDefaultKeychainAccessGroup_resetDefault() { + Configuration.setDefaultKeychainAccessGroup("accessGroup") + + Configuration.setDefaultKeychainAccessGroup(nil) + + XCTAssertEqual("", Configuration.getDefaultKeychainAccessGroup()) + } + + //MARK: Access token identifier tests + + func testDefaultAccessTokenIdentifier_getDefault() { + XCTAssertEqual(defaultAccessTokenIdentifier, Configuration.getDefaultAccessTokenIdentifier()) + } + + func testDefaultAccessTokenIdentifier_overwriteDefault() { + let newIdentifier = "newIdentifier" + Configuration.setDefaultAccessTokenIdentifier(newIdentifier) + + XCTAssertEqual(newIdentifier, Configuration.getDefaultAccessTokenIdentifier()) + } + + func testDefaultAccessTokenIdentifier_resetDefault() { + Configuration.setDefaultAccessTokenIdentifier("newIdentifier") + + Configuration.setDefaultAccessTokenIdentifier(nil) + + XCTAssertEqual(defaultAccessTokenIdentifier, Configuration.getDefaultAccessTokenIdentifier()) + } + + //MARK: Region Tests + + func testRegion_getDefault() { + XCTAssertEqual(defaultRegion, Configuration.getRegion()) + } + + func testRegion_overwriteDefault() { + let newRegion = Region.China + Configuration.setRegion(newRegion) + + XCTAssertEqual(newRegion, Configuration.getRegion()) + } + + //MARK: Sandbox Tests + + func testSandbox_getDefault() { + XCTAssertEqual(defaultSandbox, Configuration.getSandboxEnabled()) + } + + func testSandbox_overwriteDefault() { + let newSandbox = true + Configuration.setSandboxEnabled(newSandbox) + + XCTAssertEqual(newSandbox, Configuration.getSandboxEnabled()) + } +} diff --git a/source/UberRidesTests/Info.plist b/source/UberRidesTests/Info.plist index ba72822e..e82cdb5f 100644 --- a/source/UberRidesTests/Info.plist +++ b/source/UberRidesTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.0 + 0.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/source/UberRidesTests/LocalizationUtilTests.swift b/source/UberRidesTests/LocalizationUtilTests.swift new file mode 100644 index 00000000..f43d2aa4 --- /dev/null +++ b/source/UberRidesTests/LocalizationUtilTests.swift @@ -0,0 +1,73 @@ +// +// LocalizationUtilTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +@testable import UberRides + +class LocalizationUtilTests : XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testRequestButtonTitleText() { + let localizationKey = "Ride there with Uber"; + let expectedString = "Ride there with Uber"; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + + func testAuthorizationTitleTest() { + let localizationKey = "Sign in with Uber" + let expectedString = "Sign in with Uber"; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + + func testAuthenticationErrorMessageText() { + let localizationKey = "There was a problem authenticating you. Please try again." + let expectedString = "There was a problem authenticating you. Please try again."; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + + func testRideRequestUnknownError() { + let localizationKey = "The Ride Request Widget encountered a problem. Please try again." + let expectedString = "The Ride Request Widget encountered a problem. Please try again."; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + + func testOkayButtonTitleText() { + let localizationKey = "OK" + let expectedString = "OK"; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + + func testDoneButtonTitleText() { + let localizationKey = "Done" + let expectedString = "Done"; + XCTAssertEqual(LocalizationUtil.localizedString(forKey: localizationKey, comment: "asdf"), expectedString) + } + +} diff --git a/source/UberRidesTests/LoginManagerTests.swift b/source/UberRidesTests/LoginManagerTests.swift new file mode 100644 index 00000000..361f43d2 --- /dev/null +++ b/source/UberRidesTests/LoginManagerTests.swift @@ -0,0 +1,78 @@ +// +// LoginManagerTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +import WebKit +@testable import UberRides + +class LoginManagerTests: XCTestCase { + private let timeout: Double = 2 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + func testPresentNetworkErrorAlert_cancelsLoad_presentsAlertView() { + let expectation = expectationWithDescription("Test presentNetworkAlert() call") + let loginLoadExpecation = expectationWithDescription("LoginView cancelLoad() call") + + let presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ()) = { (viewController, flag, completion) in + expectation.fulfill() + XCTAssertTrue(viewController.dynamicType == UIAlertController.self) + } + + let loginClosure: () -> () = { + loginLoadExpecation.fulfill() + } + + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManager = LoginManager(accessTokenIdentifier: testIdentifier) + + let loginViewMock = LoginViewMock(scopes: [], testClosure: loginClosure) + let oauthViewControllerMock = OAuthViewControllerMock(loginView: loginViewMock, presentViewControllerClosure: presentViewControllerClosure) + + oauthViewControllerMock.loginView = loginViewMock + + loginManager.oauthViewController = oauthViewControllerMock + + loginManager.loginView(oauthViewControllerMock.loginView, didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .NetworkError)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } +} diff --git a/source/UberRidesTests/ModalViewControllerTests.swift b/source/UberRidesTests/ModalViewControllerTests.swift new file mode 100644 index 00000000..fda77011 --- /dev/null +++ b/source/UberRidesTests/ModalViewControllerTests.swift @@ -0,0 +1,106 @@ +// +// ModalViewControllerTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class ModalViewControllerTests: XCTestCase { + + private let timeout = 2.0 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + func testDelegate_willDismiss() { + @objc class ModalViewControllerDelegateMock : NSObject, ModalViewControllerDelegate { + var testClosure: () -> () + init(testClosure: () -> ()) { + self.testClosure = testClosure + } + @objc func modalViewControllerWillDismiss(modalViewController: ModalViewController) { + testClosure() + } + @objc func modalViewControllerDidDismiss(modalViewController: ModalViewController) { + //intentionally left blank + } + } + + let expectation = expectationWithDescription("Test willDismiss() is called") + + let testVC = UIViewController() + let ridesModal = ModalViewController(childViewController: testVC) + let testClosure = { + expectation.fulfill() + } + let modalDelegateMock = ModalViewControllerDelegateMock(testClosure: testClosure) + ridesModal.delegate = modalDelegateMock + + ridesModal.dismiss() + + waitForExpectationsWithTimeout(timeout) { (error) -> Void in + XCTAssertNil(error) + } + } + + func testDelegate_didDismiss() { + @objc class ModalViewControllerDelegateMock : NSObject, ModalViewControllerDelegate { + var testClosure: () -> () + init(testClosure: () -> ()) { + self.testClosure = testClosure + } + @objc func modalViewControllerWillDismiss(modalViewController: ModalViewController) { + //intentionally left blank + } + @objc func modalViewControllerDidDismiss(modalViewController: ModalViewController) { + testClosure() + } + } + + let expectation = expectationWithDescription("Test willDismiss() is called") + + let testVC = UIViewController() + let ridesModal = ModalViewController(childViewController: testVC) + let testClosure = { + expectation.fulfill() + } + let modalDelegateMock = ModalViewControllerDelegateMock(testClosure: testClosure) + ridesModal.delegate = modalDelegateMock + + ridesModal.viewDidDisappear(false) + + waitForExpectationsWithTimeout(timeout) { (error) -> Void in + XCTAssertNil(error) + } + } +} diff --git a/source/UberRidesTests/OAuthTests.swift b/source/UberRidesTests/OAuthTests.swift new file mode 100644 index 00000000..4e5bd031 --- /dev/null +++ b/source/UberRidesTests/OAuthTests.swift @@ -0,0 +1,317 @@ +// +// OAuthTests.swift +// UberRidesTests +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class OAuthTests: XCTestCase { + var expectation: XCTestExpectation! + var accessToken: AccessToken? + var error: NSError? + let timeout: NSTimeInterval = 10 + let tokenString = "accessToken1234" + private var redirectURI: String? + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + redirectURI = Configuration.getCallbackURIString() + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + /** + Test for parsing successful access token retrieval. + */ + func testParseAccessTokenFromRedirect() { + expectation = expectationWithDescription("success access token") + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)#access_token=\(tokenString)") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + } + + XCTAssertNotNil(self.accessToken) + XCTAssertEqual(self.accessToken!.tokenString, self.tokenString) + }) + } + + /** + Test for empty access token string (this should never happen though). + */ + func testParseEmptyAccessTokenFromRedirect() { + expectation = expectationWithDescription("empty access token") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)#access_token=") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.accessToken) + XCTAssertEqual(self.accessToken!.tokenString, "") + }) + } + + /** + Test error mapping when redirect URI doesn't match what's expected for client ID. + */ + func testMismatchingRedirectError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=mismatching_redirect_uri") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.MismatchingRedirect.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + /** + Test error mapping when redirect URI is invalid. + */ + func testInvalidRedirectError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=invalid_redirect_uri") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.InvalidRedirect.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + /** + Test error mapping when client ID is invalid. + */ + func testInvalidClientIDError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=invalid_client_id") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.InvalidClientID.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + /** + Test error mapping when scope provided is invalid. + */ + func testInvalidScopeError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=invalid_scope") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.InvalidScope.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + /** + Test error mapping when parameters are generally invalid. + */ + func testInvalidParametersError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=invalid_parameters") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.InvalidRequest.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + /** + Test error mapping when server error is encountered. + */ + func testServerError() { + expectation = expectationWithDescription("errors") + + let loginView = LoginView(scopes: [.Profile]) + loginView.delegate = self + + let url = NSURL(string: "\(redirectURI!)/errors?error=server_error") + loginView.webView.loadRequest(NSURLRequest(URL: url!)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + if error != nil { + print("Error: \(error)") + return + } + + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RidesAuthenticationErrorType.ServerError.rawValue) + XCTAssertEqual(self.error?.domain, RidesAuthenticationErrorFactory.errorDomain) + }) + } + + func testBuildinigWithString() { + let tokenString = "accessTokenString" + let token = AccessToken(tokenString: tokenString) + XCTAssertEqual(token.tokenString, tokenString) + } + + /** + Test saving and object in keychain and retrieving it. + */ + func testSaveRetrieveObjectFromKeychain() { + guard let token = tokenFixture() else { + XCTAssert(false) + return + } + + let keychain = KeychainWrapper() + let key = "AccessTokenKey" + XCTAssertTrue(keychain.setObject(token, key: key)) + + let result = keychain.getObjectForKey(key) as! AccessToken + XCTAssertEqual(result.tokenString, token.tokenString) + XCTAssertEqual(result.refreshToken, token.refreshToken) + XCTAssertEqual(result.grantedScopes!, token.grantedScopes!) + + XCTAssertTrue(keychain.deleteObjectForKey(key)) + + // Make sure object was actually deleted + XCTAssertNil(keychain.getObjectForKey(key)) + } + + /** + Test saving a duplicate key with different value and verify that value is updated. + */ + func testSaveDuplicateObjectInKeychain() { + guard let token = tokenFixture(), newToken = tokenFixture("newTokenString") else { + XCTAssert(false) + return + } + + let keychain = KeychainWrapper() + let key = "AccessTokenKey" + XCTAssertTrue(keychain.setObject(token, key: key)) + + XCTAssertTrue(keychain.setObject(newToken, key: key)) + + let result = keychain.getObjectForKey(key) as! AccessToken + XCTAssertEqual(result.tokenString, newToken.tokenString) + XCTAssertEqual(result.refreshToken, newToken.refreshToken) + XCTAssertEqual(result.grantedScopes!, newToken.grantedScopes!) + + XCTAssertTrue(keychain.deleteObjectForKey(key)) + + // Make sure object was actually deleted + XCTAssertNil(keychain.getObjectForKey(key)) + } +} + +// Mark: Helper + +func tokenFixture(accessToken: String = "token") -> AccessToken? +{ + var jsonDictionary = [String : AnyObject]() + jsonDictionary["access_token"] = accessToken + jsonDictionary["refresh_token"] = "refresh" + jsonDictionary["expires_in"] = "10030.23" + jsonDictionary["scope"] = "profile history" + return AccessToken(JSON: jsonDictionary) +} + +extension OAuthTests: LoginViewDelegate { + func loginView(loginView: LoginView, didSucceedWithToken accessToken: AccessToken) { + self.accessToken = accessToken + expectation.fulfill() + } + + func loginView(loginView: LoginView, didFailWithError error: NSError) { + self.error = error + expectation.fulfill() + } +} diff --git a/source/UberRidesTests/OauthEndpointTests.swift b/source/UberRidesTests/OauthEndpointTests.swift new file mode 100644 index 00000000..984d359d --- /dev/null +++ b/source/UberRidesTests/OauthEndpointTests.swift @@ -0,0 +1,150 @@ +// +// OauthEndpointTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class OauthEndpointTests: XCTestCase { + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + func testLogin_withRegionDefault_withSandboxEnabled() { + Configuration.setSandboxEnabled(true) + Configuration.setRegion(Region.Default) + + let scopes = [ RidesScope.Profile, RidesScope.History ] + let expectedHost = "https://login.uber.com" + let expectedPath = "/oauth/v2/authorize" + let expectedScopes = scopes.toRidesScopeString() + let expectedClientID = Configuration.getClientID() + let expectedRedirect = Configuration.getCallbackURIString() + let expectedTokenType = "token" + let expectedShowFB = "false" + + let expectedQueryItems = queryBuilder( + ("scope", expectedScopes), + ("client_id", expectedClientID), + ("redirect_uri", expectedRedirect), + ("response_type", expectedTokenType), + ("show_fb", expectedShowFB)) + + let login = OAuth.Login(clientID: expectedClientID, scopes: scopes, redirect: expectedRedirect) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(login.query, expectedQueryItems) + } + + func testLogin_withRegionChina_withSandboxEnabled() { + Configuration.setSandboxEnabled(true) + Configuration.setRegion(Region.China) + + let scopes = [ RidesScope.Profile, RidesScope.History ] + let expectedHost = "https://login.uber.com.cn" + let expectedPath = "/oauth/v2/authorize" + let expectedScopes = scopes.toRidesScopeString() + let expectedClientID = Configuration.getClientID() + let expectedRedirect = Configuration.getCallbackURIString() + let expectedTokenType = "token" + let expectedShowFB = "false" + + let expectedQueryItems = queryBuilder( + ("scope", expectedScopes), + ("client_id", expectedClientID), + ("redirect_uri", expectedRedirect), + ("response_type", expectedTokenType), + ("show_fb", expectedShowFB)) + + let login = OAuth.Login(clientID: expectedClientID, scopes: scopes, redirect: expectedRedirect) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(login.query, expectedQueryItems) + } + + func testLogin_withRegionDefault_withSandboxDisabled() { + Configuration.setSandboxEnabled(false) + Configuration.setRegion(Region.Default) + + let scopes = [ RidesScope.Profile, RidesScope.History ] + let expectedHost = "https://login.uber.com" + let expectedPath = "/oauth/v2/authorize" + let expectedScopes = scopes.toRidesScopeString() + let expectedClientID = Configuration.getClientID() + let expectedRedirect = Configuration.getCallbackURIString() + let expectedTokenType = "token" + let expectedShowFB = "false" + + let expectedQueryItems = queryBuilder( + ("scope", expectedScopes), + ("client_id", expectedClientID), + ("redirect_uri", expectedRedirect), + ("response_type", expectedTokenType), + ("show_fb", expectedShowFB)) + + let login = OAuth.Login(clientID: expectedClientID, scopes: scopes, redirect: expectedRedirect) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(login.query, expectedQueryItems) + } + + func testLogin_withRegionChina_withSandboxDisabled() { + Configuration.setSandboxEnabled(false) + Configuration.setRegion(Region.China) + + let scopes = [ RidesScope.Profile, RidesScope.History ] + let expectedHost = "https://login.uber.com.cn" + let expectedPath = "/oauth/v2/authorize" + let expectedScopes = scopes.toRidesScopeString() + let expectedClientID = Configuration.getClientID() + let expectedRedirect = Configuration.getCallbackURIString() + let expectedTokenType = "token" + let expectedShowFB = "false" + + let expectedQueryItems = queryBuilder( + ("scope", expectedScopes), + ("client_id", expectedClientID), + ("redirect_uri", expectedRedirect), + ("response_type", expectedTokenType), + ("show_fb", expectedShowFB)) + + let login = OAuth.Login(clientID: expectedClientID, scopes: scopes, redirect: expectedRedirect) + + XCTAssertEqual(login.host, expectedHost) + XCTAssertEqual(login.path, expectedPath) + XCTAssertEqual(login.query, expectedQueryItems) + } + +} diff --git a/source/UberRidesTests/RequestButtonTests.swift b/source/UberRidesTests/RequestButtonTests.swift new file mode 100644 index 00000000..1e325c1b --- /dev/null +++ b/source/UberRidesTests/RequestButtonTests.swift @@ -0,0 +1,155 @@ +// +// RequestButtonTests.swift +// UberRidesTests +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +import WebKit +@testable import UberRides + +class RequestButtonTests: XCTestCase { + var button: RideRequestButton! + var expectation: XCTestExpectation! + let timeout: Double = 2 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + /** + Test that title is initialized properly to default value. + */ + func testInitRequestButtonDefaultText() { + button = RideRequestButton() + XCTAssertEqual(button.uberTitleLabel.text!, "Ride there with Uber") + } + + func testCorrectSource_whenRideRequestViewRequestingBehavior() { + let expectation = expectationWithDescription("Test RideRequestView source parameter") + + let expectationClosure: (NSURLRequest) -> () = { request in + expectation.fulfill() + guard let url = request.URL, let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), let items = components.queryItems else { + XCTAssert(false) + return + } + XCTAssertTrue(items.count > 0) + var foundUserAgent = false + for item in items { + if (item.name == "user-agent") { + if let value = item.value { + foundUserAgent = true + XCTAssertTrue(value.containsString(RideRequestButton.sourceString)) + break + } + } + } + XCTAssert(foundUserAgent) + } + + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let baseViewController = UIViewControllerMock() + let requestBehavior = RideRequestViewRequestingBehavior(presentingViewController: baseViewController) + let button = RideRequestButton(rideParameters: RideParametersBuilder().build(), requestingBehavior: requestBehavior) + + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + XCTAssertNotNil(rideRequestVC.view) + + let webViewMock = WebViewMock(frame: CGRectZero, configuration: WKWebViewConfiguration(), testClosure: expectationClosure) + rideRequestVC.rideRequestView.webView = webViewMock + + requestBehavior.modalRideRequestViewController.rideRequestViewController = rideRequestVC + + button.uberButtonTapped(button) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertTrue(rideRequestVC.loginView.hidden) + XCTAssertFalse(rideRequestVC.rideRequestView.hidden) + }) + } + + func testCorrectSource_whenDeeplinkRequestingBehavior() { + let expectation = expectationWithDescription("Test Deeplink source parameter") + + let expectationClosure: (NSURL?) -> (Bool) = { url in + expectation.fulfill() + guard let url = url, let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), let items = components.queryItems else { + XCTAssert(false) + return false + } + XCTAssertTrue(items.count > 0) + var foundUserAgent = false + for item in items { + if (item.name == "user-agent") { + if let value = item.value { + foundUserAgent = true + XCTAssertTrue(value.containsString(RideRequestButton.sourceString)) + break + } + } + } + XCTAssert(foundUserAgent) + return false + } + + let requestBehavior = DeeplinkRequestingBehaviorMock(testClosure: expectationClosure) + let button = RideRequestButton(rideParameters: RideParametersBuilder().build(), requestingBehavior: requestBehavior) + + button.uberButtonTapped(button) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + +} + +private class UIViewControllerMock : UIViewController { + override func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { + viewControllerToPresent.viewWillAppear(flag) + viewControllerToPresent.viewDidAppear(flag) + + if let modal = viewControllerToPresent as? ModalRideRequestViewController { + modal.rideRequestViewController.viewWillAppear(flag) + modal.rideRequestViewController.viewDidAppear(flag) + } + + return + } +} diff --git a/source/UberRidesTests/RequestDeeplinkTests.swift b/source/UberRidesTests/RequestDeeplinkTests.swift index 41928826..2bb67979 100644 --- a/source/UberRidesTests/RequestDeeplinkTests.swift +++ b/source/UberRidesTests/RequestDeeplinkTests.swift @@ -24,9 +24,11 @@ import XCTest +import CoreLocation @testable import UberRides let clientID = "clientID1234" +let redirectURI = "http://localhost:1234/" let productID = "productID1234" let pickupLat = 37.770 let pickupLong = -122.466 @@ -54,75 +56,86 @@ struct ExpectedDeeplink { } class UberRidesDeeplinkTests: XCTestCase { + private var versionNumber: String? + private var expectedDeeplinkUserAgent: String? + private var expectedButtonUserAgent: String? + let timeout: Double = 2 override func setUp() { super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setClientID(clientID) + Configuration.setSandboxEnabled(true) + versionNumber = NSBundle(forClass: RideParameters.self).objectForInfoDictionaryKey("CFBundleShortVersionString") as? String + expectedDeeplinkUserAgent = "rides-ios-v\(versionNumber!)-deeplink" + expectedButtonUserAgent = "rides-ios-v\(versionNumber!)-button" } override func tearDown() { + Configuration.restoreDefaults() super.tearDown() } - /** - Test that PickupLocationSet check returns False if no Pickup Parameters were added - */ - func testPickupLocationSetIsFalseWithNoPickupParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - XCTAssertFalse(deeplink.pickupLocationSet()) - } - - /** - Test that PickupLocationSet check returns True if Pickup Parameters are added - */ - func testPickupLocationSetIsTrueWithPickupParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong) - XCTAssertTrue(deeplink.pickupLocationSet()) - } - /** Test to build an UberDeeplink with no PickupLatLng and assign user's current location as default */ func testBuildDeeplinkWithClientIDHasDefaultParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - let uri = deeplink.build() + let deeplink = RequestDeeplink() + guard let uri = deeplink.deeplinkURL?.absoluteString else { + XCTAssert(false) + return + } XCTAssertTrue(uri.containsString(ExpectedDeeplink.uberScheme)) let components = NSURLComponents(string: uri) - XCTAssertEqual(components?.queryItems?.count, 3) + XCTAssertEqual(components?.queryItems?.count, 4) let query = components?.query XCTAssertTrue(query!.containsString(ExpectedDeeplink.clientIDQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.setPickupAction)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.defaultPickupQuery)) + XCTAssertTrue(query!.containsString(expectedDeeplinkUserAgent!)) } /** Test to build an UberDeeplink with a Pickup Latitude and Longitude. */ func testBuildDeeplinkWithPickupLatLng() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong) + let location = CLLocation(latitude: pickupLat, longitude: pickupLong) + let rideParams = RideParametersBuilder().setPickupLocation(location).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) - let components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 4) + guard let uri = deeplink.deeplinkURL else { + XCTAssert(false) + return + } + let components = NSURLComponents(URL: uri, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 5) let query = components?.query XCTAssertTrue(query!.containsString(ExpectedDeeplink.clientIDQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.setPickupAction)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.pickupLatQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.pickupLongQuery)) + XCTAssertTrue(query!.containsString(expectedDeeplinkUserAgent!)) } /** Test to build an UberDeeplink with all optional Pickup Parameters. */ func testBuildDeeplinkWithAllPickupParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong, nickname: pickupNickname, address: pickupAddress) + let location = CLLocation(latitude: pickupLat, longitude: pickupLong) + let rideParams = RideParametersBuilder().setPickupLocation(location, nickname: pickupNickname, address: pickupAddress).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) - let components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 6) + guard let uri = deeplink.deeplinkURL else { + XCTAssert(false) + return + } + let components = NSURLComponents(URL: uri, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 7) let query = components?.query XCTAssertTrue(query!.containsString(ExpectedDeeplink.clientIDQuery)) @@ -131,17 +144,23 @@ class UberRidesDeeplinkTests: XCTestCase { XCTAssertTrue(query!.containsString(ExpectedDeeplink.pickupLongQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.pickupNicknameQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.pickupAddressQuery)) + XCTAssertTrue(query!.containsString(expectedDeeplinkUserAgent!)) } /** Test to build an UberDeeplink with only Dropoff Parameters (set default Pickup Parameters). */ func testBuildDeeplinkWithoutPickupParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setDropoffLocation(latitude: dropoffLat, longitude: dropoffLong) + let location = CLLocation(latitude: dropoffLat, longitude: dropoffLong) + let rideParams = RideParametersBuilder().setDropoffLocation(location).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) - let components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 5) + guard let uri = deeplink.deeplinkURL else { + XCTAssert(false) + return + } + let components = NSURLComponents(URL: uri, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 6) let query = components?.query XCTAssertTrue(query!.containsString(ExpectedDeeplink.clientIDQuery)) @@ -149,19 +168,25 @@ class UberRidesDeeplinkTests: XCTestCase { XCTAssertTrue(query!.containsString(ExpectedDeeplink.defaultPickupQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.dropoffLatQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.dropoffLongQuery)) + XCTAssertTrue(query!.containsString(expectedDeeplinkUserAgent!)) } /** Test to build an UberDeeplink with all possible query parameters. */ func testBuildDeeplinkWithAllParameters() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setProductID(productID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong, nickname: pickupNickname, address: pickupAddress) - deeplink.setDropoffLocation(latitude: dropoffLat, longitude: dropoffLong, nickname: dropoffNickname, address: dropoffAddress) + let pickupLocation = CLLocation(latitude: pickupLat, longitude: pickupLong) + let dropoffLocation = CLLocation(latitude: dropoffLat, longitude: dropoffLong) + let rideParams = RideParametersBuilder().setProductID(productID).setPickupLocation(pickupLocation, nickname: pickupNickname, address: pickupAddress) + .setDropoffLocation(dropoffLocation, nickname: dropoffNickname, address: dropoffAddress).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) - let components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 11) + guard let uri = deeplink.deeplinkURL else { + XCTAssert(false) + return + } + let components = NSURLComponents(URL: uri, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 12) let query = components?.query XCTAssertTrue(query!.containsString(ExpectedDeeplink.clientIDQuery)) @@ -175,103 +200,80 @@ class UberRidesDeeplinkTests: XCTestCase { XCTAssertTrue(query!.containsString(ExpectedDeeplink.dropoffLongQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.dropoffNicknameQuery)) XCTAssertTrue(query!.containsString(ExpectedDeeplink.dropoffAddressQuery)) + XCTAssertTrue(query!.containsString(expectedDeeplinkUserAgent!)) } /** - Test to set deeplink to default location, then override with another location. - Test ensures deeplink removes original default location parameter. - */ - func testOverrideDefaultPickupWithPickupLocation() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocationToCurrentLocation() - - var components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 3) - - var query = components?.query - XCTAssertTrue(query!.containsString(ExpectedDeeplink.defaultPickupQuery)) - - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong) - components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - query = components?.query - - XCTAssertEqual(components?.queryItems?.count, 4) - XCTAssertFalse(query!.containsString(ExpectedDeeplink.defaultPickupQuery)) - } - - /** - Test to set deeplink to pickup location, then override with current location. - Test ensures deeplink removes original pickup location parameters. - */ - func testOverridePickupLocationWithDefault() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong, nickname: pickupNickname, address: pickupAddress) - - var components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - XCTAssertEqual(components?.queryItems?.count, 6) - - deeplink.setPickupLocationToCurrentLocation() - components = NSURLComponents(URL: NSURL(string: deeplink.build())!, resolvingAgainstBaseURL: false) - let query = components?.query - - XCTAssertEqual(components?.queryItems?.count, 3) - XCTAssertTrue(query!.containsString(ExpectedDeeplink.defaultPickupQuery)) - } - - /** - Test to rebuild deep link without making changes and verify that the same string is returned. - */ - func testRebuildingDeeplinkWithoutChanges() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong, nickname: pickupNickname, address: pickupAddress) - - let originalAddress = unsafeAddressOf(deeplink.build()) - let rebuiltAddress = unsafeAddressOf(deeplink.build()) - - XCTAssertEqual(originalAddress, rebuiltAddress) - } - - /** - Test to rebuild deep link after making changes and verify that the deep link has been built again. - */ - func testRebuildingDeeplinWithChanges() { - let deeplink = RequestDeeplink(withClientID: clientID) - deeplink.setPickupLocation(latitude: pickupLat, longitude: pickupLong, nickname: pickupNickname, address: pickupAddress) - - let originalAddress = unsafeAddressOf(deeplink.build()) - deeplink.setProductID(productID) - let rebuiltAddress = unsafeAddressOf(deeplink.build()) - - XCTAssertNotEqual(originalAddress, rebuiltAddress) - } - - /** - * Test createURL with source button. - */ + * Test createURL with source button. + */ func testCreateURLWithButtonSource() { - let urlString = "https://m.uber.com/sign-up?client_id=\(clientID)" - let deeplink = RequestDeeplink(withClientID: clientID, fromSource: .Button) - let url = deeplink.createURL(urlString) + let expectedUrlString = "https://m.uber.com/sign-up?client_id=\(clientID)&user-agent=\(expectedButtonUserAgent!)" + let urlString = "https://m.uber.com/sign-up" + let rideParams = RideParametersBuilder().setSource(RideRequestButton.sourceString).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) + guard let url = deeplink.createURL(urlString) else { + XCTAssert(false) + return + } let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) XCTAssertNotNil(components) + XCTAssertEqual(expectedUrlString, url.absoluteString) XCTAssertEqual(components!.queryItems!.count, 2) - XCTAssertTrue(components!.query!.containsString("&user-agent=rides-button-v0.1.0")) + XCTAssertTrue(components!.query!.containsString("&user-agent=\(expectedButtonUserAgent!)")) } /** * Test createURL with source deeplink. */ func testCreateURLWithDeeplinkSource() { - let urlString = "https://m.uber.com/sign-up?client_id=\(clientID)" - let deeplink = RequestDeeplink(withClientID: clientID, fromSource: .Deeplink) - let url = deeplink.createURL(urlString) + let expectedUrlString = "https://m.uber.com/sign-up?client_id=\(clientID)&user-agent=\(expectedDeeplinkUserAgent!)" + let urlString = "https://m.uber.com/sign-up" + let rideParams = RideParametersBuilder().setSource(RequestDeeplink.sourceString).build() + let deeplink = RequestDeeplink(rideParameters: rideParams) + guard let url = deeplink.createURL(urlString) else { + XCTAssert(false) + return + } let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) XCTAssertNotNil(components) + XCTAssertEqual(expectedUrlString, url.absoluteString) XCTAssertEqual(components!.queryItems!.count, 2) - XCTAssertTrue(components!.query!.containsString("&user-agent=rides-deeplink-v0.1.0")) + XCTAssertTrue(components!.query!.containsString("&user-agent=\(expectedDeeplinkUserAgent!)")) + } + + func testDeeplinkDefaultSource() { + let expectation = expectationWithDescription("Test Deeplink source parameter") + let expectationClosure: (NSURL?) -> (Bool) = { url in + expectation.fulfill() + guard let url = url, let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), let items = components.queryItems else { + XCTAssert(false) + return false + } + XCTAssertTrue(items.count > 0) + var foundUserAgent = false + for item in items { + if (item.name == "user-agent") { + if let value = item.value { + foundUserAgent = true + XCTAssertTrue(value.containsString(RequestDeeplink.sourceString)) + break + } + } + } + XCTAssert(foundUserAgent) + return false + } + + let deeplink = RequestDeeplinkMock(rideParameters: RideParametersBuilder().build(), testClosure: expectationClosure) + + deeplink.execute() + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) } } diff --git a/source/UberRidesTests/RideParametersTest.swift b/source/UberRidesTests/RideParametersTest.swift new file mode 100644 index 00000000..19e2ac8e --- /dev/null +++ b/source/UberRidesTests/RideParametersTest.swift @@ -0,0 +1,158 @@ +// +// RideParametersTest.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import MapKit +@testable import UberRides + +class RideParametersTest: XCTestCase { + private var versionNumber: String? + private var baseUserAgent: String? + + private var builder: RideParametersBuilder = RideParametersBuilder() + + override func setUp() { + super.setUp() + builder = RideParametersBuilder() + versionNumber = NSBundle(forClass: RideParameters.self).objectForInfoDictionaryKey("CFBundleShortVersionString") as? String + baseUserAgent = "rides-ios-v\(versionNumber!)" + } + + func testBuilder_withNoParams() { + let params = builder.build() + XCTAssertNotNil(params) + XCTAssertTrue(params.useCurrentLocationForPickup) + XCTAssertNil(params.pickupLocation) + XCTAssertNil(params.pickupAddress) + XCTAssertNil(params.pickupNickname) + XCTAssertNil(params.dropoffLocation) + XCTAssertNil(params.dropoffAddress) + XCTAssertNil(params.dropoffNickname) + XCTAssertNil(params.productID) + XCTAssertEqual(params.userAgent, baseUserAgent) + } + + func testBuilder_correctUseCurrentLocation() { + let testPickup = CLLocation(latitude: 32.0, longitude: -32.0) + builder.setPickupLocation(testPickup) + let params = builder.build() + XCTAssertFalse(params.useCurrentLocationForPickup) + XCTAssertEqual(testPickup, params.pickupLocation) + XCTAssertNil(params.pickupAddress) + XCTAssertNil(params.pickupNickname) + XCTAssertNil(params.dropoffLocation) + XCTAssertNil(params.dropoffAddress) + XCTAssertNil(params.dropoffNickname) + XCTAssertNil(params.productID) + XCTAssertEqual(params.userAgent, baseUserAgent) + } + + func testBuilder_withAllParameters() { + + let testPickupLocation = CLLocation(latitude: 32.0, longitude: -32.0) + let testDropoffLocation = CLLocation(latitude: 62.0, longitude: -62.0) + let testPickupNickname = "testPickup" + let testPickupAddress = "123 pickup address" + let testDropoffNickname = "testDropoff" + let testDropoffAddress = "123 dropoff address" + let testProductID = "test ID" + let testSource = "test source" + let expectedUserAgent = "\(baseUserAgent!)-\(testSource)" + builder.setPickupLocation(testPickupLocation, nickname: testPickupNickname, address: testPickupAddress) + builder.setDropoffLocation(testDropoffLocation, nickname: testDropoffNickname, address: testDropoffAddress) + builder.setProductID(testProductID).setSource(testSource) + let params = builder.build() + + XCTAssertFalse(params.useCurrentLocationForPickup) + XCTAssertEqual(params.pickupLocation, testPickupLocation) + XCTAssertEqual(params.pickupAddress, testPickupAddress) + XCTAssertEqual(params.pickupNickname, testPickupNickname) + XCTAssertEqual(params.dropoffLocation, testDropoffLocation) + XCTAssertEqual(params.dropoffAddress, testDropoffAddress) + XCTAssertEqual(params.dropoffNickname, testDropoffNickname) + XCTAssertEqual(params.productID, testProductID) + XCTAssertEqual(params.userAgent, expectedUserAgent) + } + + func testBuilder_updateParameter() { + let testPickupLocation1 = CLLocation(latitude: 32.0, longitude: -32.0) + let testPickupLocation2 = CLLocation(latitude: 62.0, longitude: -62.0) + builder.setPickupLocation(testPickupLocation1) + builder.setPickupLocation(testPickupLocation2) + let params = builder.build() + XCTAssertFalse(params.useCurrentLocationForPickup) + XCTAssertEqual(params.pickupLocation, testPickupLocation2) + XCTAssertNil(params.pickupAddress) + XCTAssertNil(params.pickupNickname) + XCTAssertNil(params.dropoffLocation) + XCTAssertNil(params.dropoffAddress) + XCTAssertNil(params.dropoffNickname) + XCTAssertNil(params.productID) + XCTAssertEqual(params.userAgent, baseUserAgent) + } + + func testBuilder_useCurrentLocation() { + let testPickupLocation = CLLocation(latitude: 32.0, longitude: -32.0) + let testPickupNickname = "testPickup nickname" + let testPickupAddress = "123 test pickup st" + builder.setPickupLocation(testPickupLocation, nickname: testPickupNickname, address: testPickupAddress) + builder.setPickupToCurrentLocation() + let params = builder.build() + XCTAssertTrue(params.useCurrentLocationForPickup) + XCTAssertNil(params.pickupLocation) + XCTAssertNil(params.pickupAddress) + XCTAssertNil(params.pickupNickname) + XCTAssertNil(params.dropoffLocation) + XCTAssertNil(params.dropoffAddress) + XCTAssertNil(params.dropoffNickname) + XCTAssertNil(params.productID) + XCTAssertEqual(params.userAgent, baseUserAgent) + } + + func testBuilder_withExistingParameters() { + let expectedBuilder = RideParametersBuilder() + let testPickupLocation = CLLocation(latitude: 32.0, longitude: -32.0) + let testDropoffLocation = CLLocation(latitude: 62.0, longitude: -62.0) + let testPickupNickname = "testPickup" + let testPickupAddress = "123 pickup address" + let testDropoffNickname = "testDropoff" + let testDropoffAddress = "123 dropoff address" + let testProductID = "test ID" + let testSource = "test source" + + expectedBuilder.setPickupLocation(testPickupLocation, nickname: testPickupNickname, address: testPickupAddress) + expectedBuilder.setDropoffLocation(testDropoffLocation, nickname: testDropoffNickname, address: testDropoffAddress) + expectedBuilder.setProductID(testProductID).setSource(testSource) + let expectedParams = expectedBuilder.build() + let params = RideParametersBuilder(rideParameters: expectedParams).build() + + XCTAssertEqual(params.useCurrentLocationForPickup, expectedParams.useCurrentLocationForPickup) + XCTAssertEqual(params.pickupLocation, expectedParams.pickupLocation) + XCTAssertEqual(params.pickupAddress, expectedParams.pickupAddress) + XCTAssertEqual(params.pickupNickname, expectedParams.pickupNickname) + XCTAssertEqual(params.dropoffLocation, expectedParams.dropoffLocation) + XCTAssertEqual(params.dropoffAddress, expectedParams.dropoffAddress) + XCTAssertEqual(params.dropoffNickname, expectedParams.dropoffNickname) + XCTAssertEqual(params.productID, expectedParams.productID) + XCTAssertEqual(params.userAgent, expectedParams.userAgent) + } +} diff --git a/source/UberRidesTests/RideRequestViewControllerTests.swift b/source/UberRidesTests/RideRequestViewControllerTests.swift new file mode 100644 index 00000000..f3ffdc30 --- /dev/null +++ b/source/UberRidesTests/RideRequestViewControllerTests.swift @@ -0,0 +1,406 @@ +// +// RideRequestViewControllerTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +import WebKit +@testable import UberRides + +class RideRequestViewControllerTests: XCTestCase { + private let timeout: Double = 2 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + func testAccessTokenMissing_whenNoAccessToken_loginFailed() { + let expectation = expectationWithDescription("Test Token Missing delegate call") + + let expectationClosure: (RideRequestViewController, NSError) -> () = {vc, error in + XCTAssertEqual(error.code, RideRequestViewErrorType.AccessTokenMissing.rawValue) + XCTAssertEqual(error.domain, RideRequestViewErrorFactory.errorDomain) + expectation.fulfill() + } + let loginManager = LoginManager() + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManager) + let rideRequestVCDelegateMock = RideRequestViewControllerDelegateMock(testClosure: expectationClosure) + rideRequestVC.delegate = rideRequestVCDelegateMock + XCTAssertNotNil(rideRequestVC.view) + rideRequestVC.loginManager.loginView(LoginView(scopes: [ RidesScope.RideWidgets ]), didFailWithError: RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .UnableToSaveAccessToken)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + + func testRideRequestViewLoads_withValidAccessToken() { + let expectation = expectationWithDescription("Test RideRequestView load() call") + + let expectationClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + rideRequestVC.rideRequestView = RideRequestViewMock(rideRequestView: rideRequestVC.rideRequestView, testClosure: expectationClosure) + XCTAssertNotNil(rideRequestVC.view) + rideRequestVC.load() + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertTrue(rideRequestVC.loginView.hidden) + XCTAssertFalse(rideRequestVC.rideRequestView.hidden) + }) + } + + func testLoginViewLoads_withNoAccessToken() { + let expectation = expectationWithDescription("Test LoginView load() call") + + let expectationClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + + XCTAssertNotNil(rideRequestVC.view) + XCTAssertNotNil(rideRequestVC.loginView) + rideRequestVC.loginView = LoginViewMock(scopes: rideRequestVC.loginView.scopes!, testClosure: expectationClosure) + + rideRequestVC.load() + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertFalse(rideRequestVC.loginView.hidden) + XCTAssertTrue(rideRequestVC.rideRequestView.hidden) + }) + } + + func testLoginViewLoads_whenRideRequestViewErrors() { + let expectation = expectationWithDescription("Test LoginView load() call") + + let expectationClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + + XCTAssertNotNil(rideRequestVC.view) + XCTAssertNotNil(rideRequestVC.loginView) + rideRequestVC.loginView = LoginViewMock(scopes: rideRequestVC.loginView.scopes!, testClosure: expectationClosure) + + rideRequestVC.rideRequestView(rideRequestVC.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenExpired)) + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertFalse(rideRequestVC.loginView.hidden) + XCTAssertTrue(rideRequestVC.rideRequestView.hidden) + }) + } + + func testLoginViewLoads_whenAuthenticationFails_whenViewControllerIsDismissed() { + let expectation = expectationWithDescription("Test LoginView load() call") + + let expectationClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + + XCTAssertNotNil(rideRequestVC.view) + XCTAssertNotNil(rideRequestVC.loginView) + let loginMock = LoginViewMock(scopes: rideRequestVC.loginView.scopes!, testClosure: nil) + rideRequestVC.loginView = loginMock + + rideRequestVC.rideRequestView(rideRequestVC.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenExpired)) + rideRequestVC.viewWillDisappear(false) + rideRequestVC.viewDidDisappear(false) + + loginMock.testClosure = expectationClosure + + rideRequestVC.viewWillAppear(false) + rideRequestVC.viewDidAppear(false) + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertFalse(rideRequestVC.loginView.hidden) + XCTAssertTrue(rideRequestVC.rideRequestView.hidden) + }) + } + + func testLoginViewSkipsLoad_whenAuthenticationFailsTwice() { + let expectation = expectationWithDescription("Test LoginView load() call") + + let expectationClosure: () -> () = { + expectation.fulfill() + } + + let failureClosure: () -> () = { + XCTAssert(false) + } + + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + + XCTAssertNotNil(rideRequestVC.view) + XCTAssertNotNil(rideRequestVC.loginView) + let loginMock = LoginViewMock(scopes: rideRequestVC.loginView.scopes!, testClosure: expectationClosure) + rideRequestVC.loginView = loginMock + + rideRequestVC.rideRequestView(rideRequestVC.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenExpired)) + + loginMock.testClosure = failureClosure + + rideRequestVC.rideRequestView(rideRequestVC.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenExpired)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertFalse(rideRequestVC.loginView.hidden) + XCTAssertTrue(rideRequestVC.rideRequestView.hidden) + }) + } + + + func testLoginViewStopsLoading_whenRideRequestViewControllerDismissed() { + let loadExpectation = expectationWithDescription("Test LoginView load() call") + let cancelLoadExpectation = expectationWithDescription("Test LoginView cancelLoad() call") + + let loadExpectationClosure: () -> () = { + loadExpectation.fulfill() + } + + let cancelLoadExpectationClosure: () -> () = { + cancelLoadExpectation.fulfill() + } + + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + + XCTAssertNotNil(rideRequestVC.view) + XCTAssertNotNil(rideRequestVC.loginView) + let loginMock = LoginViewMock(scopes: rideRequestVC.loginView.scopes!, testClosure: loadExpectationClosure) + rideRequestVC.loginView = loginMock + + rideRequestVC.rideRequestView(rideRequestVC.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.AccessTokenExpired)) + loginMock.testClosure = cancelLoadExpectationClosure + + rideRequestVC.viewWillDisappear(false) + rideRequestVC.viewDidDisappear(false) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertFalse(rideRequestVC.loginView.hidden) + XCTAssertTrue(rideRequestVC.rideRequestView.hidden) + }) + } + + func testRequestViewStopsLoading_whenRideRequestViewControllerDismissed() { + let loadExpectation = expectationWithDescription("Test RideRequestView load() call") + let cancelLoadExpectation = expectationWithDescription("Test RideRequestView cancelLoad() call") + + let loadExpectationClosure: () -> () = { + loadExpectation.fulfill() + } + + let cancelLoadExpectationClosure: () -> () = { + cancelLoadExpectation.fulfill() + } + + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManager = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManager) + let requestViewMock = RideRequestViewMock(rideRequestView: rideRequestVC.rideRequestView, testClosure: loadExpectationClosure) + rideRequestVC.rideRequestView = requestViewMock + XCTAssertNotNil(rideRequestVC.view) + rideRequestVC.load() + + requestViewMock.testClosure = cancelLoadExpectationClosure + + rideRequestVC.viewWillDisappear(false) + rideRequestVC.viewDidDisappear(false) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertTrue(rideRequestVC.loginView.hidden) + XCTAssertFalse(rideRequestVC.rideRequestView.hidden) + }) + } + + func testRequestUsesCorrectSource_whenPresented() { + let expectation = expectationWithDescription("Test RideRequestView load() call") + + let expectationClosure: (NSURLRequest) -> () = { request in + expectation.fulfill() + guard let url = request.URL, let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), let items = components.queryItems else { + XCTAssert(false) + return + } + XCTAssertTrue(items.count > 0) + var foundUserAgent = false + for item in items { + if (item.name == "user-agent") { + if let value = item.value { + foundUserAgent = true + XCTAssertTrue(value.containsString(RideRequestViewController.sourceString)) + break + } + } + } + XCTAssert(foundUserAgent) + } + + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManger = LoginManager(accessTokenIdentifier: testIdentifier) + let rideRequestVC = RideRequestViewController(rideParameters: RideParametersBuilder().build(), loginManager: loginManger) + XCTAssertNotNil(rideRequestVC.view) + + let webViewMock = WebViewMock(frame: CGRectZero, configuration: WKWebViewConfiguration(), testClosure: expectationClosure) + rideRequestVC.rideRequestView.webView = webViewMock + + rideRequestVC.load() + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertTrue(rideRequestVC.loginView.hidden) + XCTAssertFalse(rideRequestVC.rideRequestView.hidden) + }) + } + + func testPresentNetworkErrorAlert_whenValidAccessToken_whenNetworkError() { + let expectation = expectationWithDescription("Test presentNetworkAlert() call") + + let networkClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManager = LoginManager(accessTokenIdentifier: testIdentifier) + + let rideRequestViewControllerMock = RideRequestViewControllerMock(rideParameters: RideParametersBuilder().build(), loginManager: loginManager, loadClosure: nil, networkClosure: networkClosure, presentViewControllerClosure: nil) + + rideRequestViewControllerMock.rideRequestView(rideRequestViewControllerMock.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + + func testPresentNetworkErrorAlert_whenNoAccessToken_whenNetworkError() { + let expectation = expectationWithDescription("Test presentNetworkAlert() call") + + let networkClosure: () -> () = { + expectation.fulfill() + } + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + + let loginManager = LoginManager(accessTokenIdentifier: testIdentifier) + + let rideRequestViewControllerMock = RideRequestViewControllerMock(rideParameters: RideParametersBuilder().build(), loginManager: loginManager, loadClosure: nil, networkClosure: networkClosure, presentViewControllerClosure: nil) + + rideRequestViewControllerMock.rideRequestView(rideRequestViewControllerMock.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + + func testPresentNetworkErrorAlert_cancelsLoads_presentsAlertView() { + let expectation = expectationWithDescription("Test presentNetworkAlert() call") + let loginLoadExpecation = expectationWithDescription("LoginView cancelLoad() call") + let requestViewExpectation = expectationWithDescription("RequestView cancelLoad() call") + + let presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ()) = { (viewController, flag, completion) in + expectation.fulfill() + XCTAssertTrue(viewController.dynamicType == UIAlertController.self) + } + let testIdentifier = "testAccessTokenIdentifier" + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + let loginManager = LoginManager(accessTokenIdentifier: testIdentifier) + + let rideRequestViewControllerMock = RideRequestViewControllerMock(rideParameters: RideParametersBuilder().build(), loginManager: loginManager, loadClosure: nil, networkClosure: nil, presentViewControllerClosure: presentViewControllerClosure) + + let loginViewMock = LoginViewMock(scopes: []) { () -> () in + loginLoadExpecation.fulfill() + } + + let requestViewMock = RideRequestViewMock(rideRequestView: rideRequestViewControllerMock.rideRequestView) { () -> () in + requestViewExpectation.fulfill() + } + + rideRequestViewControllerMock.rideRequestView = requestViewMock + rideRequestViewControllerMock.loginView = loginViewMock + + rideRequestViewControllerMock.rideRequestView(rideRequestViewControllerMock.rideRequestView, didReceiveError: RideRequestViewErrorFactory.errorForType(.NetworkError)) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + +} diff --git a/source/UberRidesTests/RideRequestViewErrorFactoryTests.swift b/source/UberRidesTests/RideRequestViewErrorFactoryTests.swift new file mode 100644 index 00000000..6ffc1b89 --- /dev/null +++ b/source/UberRidesTests/RideRequestViewErrorFactoryTests.swift @@ -0,0 +1,59 @@ +// +// RideRequestViewErrorFactoryTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class RideRequestViewErrorFactoryTests: XCTestCase { + let expectedErrorToStringMapping = [ + RideRequestViewErrorType.AccessTokenExpired : RideRequestViewErrorType.AccessTokenExpired.toString(), + RideRequestViewErrorType.AccessTokenMissing : RideRequestViewErrorType.AccessTokenMissing.toString(), + RideRequestViewErrorType.Unknown : RideRequestViewErrorType.Unknown.toString() + ] + + func testCreateErrorsByErrorType() { + + for errorType in expectedErrorToStringMapping.keys { + let rideRequestViewError = RideRequestViewErrorFactory.errorForType(errorType) + + XCTAssertNotNil(rideRequestViewError) + XCTAssertEqual(rideRequestViewError.code , errorType.rawValue) + XCTAssertEqual(rideRequestViewError.domain , RideRequestViewErrorFactory.errorDomain) + } + } + + func testCreateErrorsByRawValue() { + for (errorType, rawValue) in expectedErrorToStringMapping { + let rideRequestViewError = RideRequestViewErrorFactory.errorForString(rawValue) + + XCTAssertNotNil(rideRequestViewError) + XCTAssertEqual(rideRequestViewError.code , errorType.rawValue) + XCTAssertEqual(rideRequestViewError.domain , RideRequestViewErrorFactory.errorDomain) + } + } + + func testCreateErrorByRawValue_withUnknownValue() { + let rideRequestViewError = RideRequestViewErrorFactory.errorForString("not.real.error") + + XCTAssertEqual(rideRequestViewError.code, RideRequestViewErrorType.Unknown.rawValue) + } +} diff --git a/source/UberRidesTests/RideRequestViewRequestingBehaviorTests.swift b/source/UberRidesTests/RideRequestViewRequestingBehaviorTests.swift new file mode 100644 index 00000000..a6e1fdfa --- /dev/null +++ b/source/UberRidesTests/RideRequestViewRequestingBehaviorTests.swift @@ -0,0 +1,105 @@ +// +// RideRequestViewRequestingBehaviorTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +@testable import UberRides + +class RideRequestViewRequestingBehaviorTests : XCTestCase { + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + super.tearDown() + Configuration.restoreDefaults() + } + + func testUpdateLoginManager() { + let baseVC = UIViewController() + let initialLoginManger = LoginManager() + let behavior = RideRequestViewRequestingBehavior(presentingViewController: baseVC, loginManager: initialLoginManger) + XCTAssertNotNil(behavior.loginManager) + XCTAssertEqual(behavior.modalRideRequestViewController.rideRequestViewController.loginManager, initialLoginManger) + + let newLoginManager = LoginManager(accessTokenIdentifier: "testToken") + behavior.loginManager = newLoginManager + XCTAssertNotNil(behavior.loginManager) + XCTAssertEqual(behavior.modalRideRequestViewController.rideRequestViewController.loginManager, newLoginManager) + } + + func testRideParametersUpdated() { + class UIViewControllerMock : UIViewController { + override func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { + return + } + } + + let baseVC = UIViewControllerMock() + let initialLoginManger = LoginManager() + let behavior = RideRequestViewRequestingBehavior(presentingViewController: baseVC, loginManager: initialLoginManger) + XCTAssertNotNil(behavior.modalRideRequestViewController) + XCTAssertNotNil(behavior.modalRideRequestViewController.rideRequestViewController) + let pickupLocation = CLLocation(latitude: -32.0, longitude: 42.2) + let newRideParams = RideParametersBuilder().setPickupLocation(pickupLocation).build() + behavior.requestRide(newRideParams) + XCTAssertTrue(behavior.modalRideRequestViewController.rideRequestViewController.rideRequestView.rideParameters === newRideParams) + } + + func testPresentModal() { + class UIViewControllerMock : UIViewController { + let testClosure: (UIViewController) -> () + private init(testClosure: (UIViewController) -> ()) { + self.testClosure = testClosure + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { + self.testClosure(viewControllerToPresent) + return + } + } + + let expectation = expectationWithDescription("ModalRideViewController is presented") + let expectationClosure: (UIViewController) -> () = { viewController in + XCTAssertTrue(viewController.isKindOfClass(ModalRideRequestViewController)) + expectation.fulfill() + } + + let baseVC = UIViewControllerMock(testClosure: expectationClosure) + let initialLoginManger = LoginManager() + let behavior = RideRequestViewRequestingBehavior(presentingViewController: baseVC, loginManager: initialLoginManger) + behavior.requestRide(RideParametersBuilder().build()) + waitForExpectationsWithTimeout(2.0) {error in + XCTAssertNil(error) + } + } +} diff --git a/source/UberRidesTests/RideRequestViewTests.swift b/source/UberRidesTests/RideRequestViewTests.swift new file mode 100644 index 00000000..ded0789a --- /dev/null +++ b/source/UberRidesTests/RideRequestViewTests.swift @@ -0,0 +1,193 @@ +// +// RideRequestViewTests.swift +// UberRides +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import WebKit +@testable import UberRides + +class RideRequestViewTests: XCTestCase { + var expectation: XCTestExpectation! + var error: NSError? + let timeout: NSTimeInterval = 10 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setSandboxEnabled(true) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + /** + Test that access token expiration is routed to delegate. + */ + func testAccessTokenExpired() { + expectation = expectationWithDescription("access token expired delegate call") + let view = RideRequestView(rideParameters: RideParametersBuilder().build()) + view.delegate = self + let request = NSURLRequest(URL: NSURL(string: "uberConnect://oauth#error=unauthorized")!) + view.webView.loadRequest(request) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } + + /** + Test the an unknown error message is routed to delegate. + */ + func testUnkownError() { + expectation = expectationWithDescription("unknown error delegate call") + let view = RideRequestView() + view.delegate = self + let request = NSURLRequest(URL: NSURL(string: "uberConnect://oauth#error=on_fire")!) + view.webView.loadRequest(request) + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + XCTAssertNotNil(self.error) + XCTAssertEqual(self.error?.code, RideRequestViewErrorType.Unknown.rawValue) + XCTAssertEqual(self.error?.domain, RideRequestViewErrorFactory.errorDomain) + }) + } + + /** + Test that no exception is thrown for authorization if custom access token is passed. + */ + func testAuthorizeWithCustomAccessToken() { + let tokenString = "accessToken1234" + let tokenData = ["access_token" : tokenString] + let token = AccessToken(JSON: tokenData) + let view = RideRequestView(rideParameters: RideParametersBuilder().build(), accessToken: token, frame: CGRectZero) + XCTAssertNotNil(view.accessToken) + XCTAssertEqual(view.accessToken, token) + } + + /** + Test that authorization passes with token in token manager. + */ + func testAuthorizeWithTokenManagerAccessToken() { + let tokenString = "accessToken1234" + let tokenData = ["access_token" : tokenString] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + TokenManager.saveToken(token) + + let view = RideRequestView() + XCTAssertNotNil(view.accessToken) + XCTAssertEqual(view.accessToken?.tokenString, TokenManager.fetchToken()?.tokenString) + + TokenManager.deleteToken() + } + + /** + Test that load is successful when access token is set after initialization. + */ + func testAuthorizeWithTokenSetAfterInitialization() { + let tokenString = "accessToken1234" + let tokenData = ["access_token" : tokenString] + let token = AccessToken(JSON: tokenData) + let view = RideRequestView() + view.accessToken = token + XCTAssertNotNil(view.accessToken) + } + + /** + Test that exception is thrown without passing in custom access token (and none in TokenManager). + */ + func testAuthorizeFailsWithoutAccessToken() { + expectation = expectationWithDescription("access token missing delegate call") + let view = RideRequestView() + view.delegate = self + TokenManager.deleteToken() + + view.load() + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertEqual(self.error?.code, RideRequestViewErrorType.AccessTokenMissing.rawValue) + XCTAssertEqual(self.error?.domain, RideRequestViewErrorFactory.errorDomain) + XCTAssertNil(error) + }) + } + + func testRequestUsesCorrectSource_whenPresented() { + let expectation = expectationWithDescription("Test RideRequestView source call") + + let expectationClosure: (NSURLRequest) -> () = { request in + expectation.fulfill() + guard let url = request.URL, let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false), let items = components.queryItems else { + XCTAssert(false) + return + } + XCTAssertTrue(items.count > 0) + var foundUserAgent = false + for item in items { + if (item.name == "user-agent") { + if let value = item.value { + foundUserAgent = true + XCTAssertTrue(value.containsString(RideRequestView.sourceString)) + break + } + } + } + XCTAssert(foundUserAgent) + } + + let testIdentifier = "testAccessTokenIdentifier" + TokenManager.deleteToken(testIdentifier) + let testToken = AccessToken(JSON: ["access_token" : "testTokenString"]) + TokenManager.saveToken(testToken!, tokenIdentifier: testIdentifier) + defer { + TokenManager.deleteToken(testIdentifier) + } + + let rideRequestView = RideRequestView(rideParameters: RideParametersBuilder().build(), accessToken: TokenManager.fetchToken(testIdentifier), frame: CGRectZero) + XCTAssertNotNil(rideRequestView) + + let webViewMock = WebViewMock(frame: CGRectZero, configuration: WKWebViewConfiguration(), testClosure: expectationClosure) + rideRequestView.webView.scrollView.delegate = nil + rideRequestView.webView = webViewMock + + rideRequestView.load() + + + waitForExpectationsWithTimeout(timeout, handler: { error in + XCTAssertNil(error) + }) + } +} + +extension RideRequestViewTests: RideRequestViewDelegate { + func rideRequestView(rideRequestView: RideRequestView, didReceiveError error: NSError) { + self.error = error + expectation.fulfill() + } +} diff --git a/source/UberRidesTests/RidesAuthenticationErrorFactoryTests.swift b/source/UberRidesTests/RidesAuthenticationErrorFactoryTests.swift new file mode 100644 index 00000000..e5c35b74 --- /dev/null +++ b/source/UberRidesTests/RidesAuthenticationErrorFactoryTests.swift @@ -0,0 +1,74 @@ +// +// RidesAuthenticationErrorFactoryTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class RidesAuthenticationErrorFactoryTests: XCTestCase { + + let expectedErrorToStringMapping = [ + RidesAuthenticationErrorType.MismatchingRedirect : "mismatching_redirect_uri", + RidesAuthenticationErrorType.InvalidRedirect : "invalid_redirect_uri", + RidesAuthenticationErrorType.InvalidRequest : "invalid_parameters", + RidesAuthenticationErrorType.InvalidScope : "invalid_scope", + RidesAuthenticationErrorType.ServerError : "server_error", + RidesAuthenticationErrorType.InvalidClientID : "invalid_client_id", + RidesAuthenticationErrorType.Unavailable : "temporarily_unavailable", + RidesAuthenticationErrorType.UserCancelled : "cancelled", + RidesAuthenticationErrorType.InvalidResponse : "invalid_response", + RidesAuthenticationErrorType.UnableToSaveAccessToken : "token_not_saved", + RidesAuthenticationErrorType.UnableToPresentLogin : "present_login_failed", + ] + + func testCreateErrorsByErrorType() { + + for errorType in expectedErrorToStringMapping.keys { + let ridesAuthenticationError = RidesAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: errorType) + + XCTAssertNotNil(ridesAuthenticationError) + XCTAssertEqual(ridesAuthenticationError.code , errorType.rawValue) + XCTAssertEqual(ridesAuthenticationError.domain , RidesAuthenticationErrorFactory.errorDomain) + XCTAssertEqual(ridesAuthenticationError.localizedDescription, errorType.localizedDescriptionKey) + } + } + + func testCreateErrorsByRawValue() { + for (errorType, rawValue) in expectedErrorToStringMapping { + let ridesAuthenticationError = RidesAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: rawValue) + + XCTAssertNotNil(ridesAuthenticationError) + if let ridesAuthenticationError = ridesAuthenticationError { + XCTAssertEqual(ridesAuthenticationError.code , errorType.rawValue) + XCTAssertEqual(ridesAuthenticationError.domain, RidesAuthenticationErrorFactory.errorDomain) + XCTAssertEqual(ridesAuthenticationError.localizedDescription, errorType.localizedDescriptionKey) + } + } + } + + func testCreateErrorByRawValue_withUnknownValue() { + let ridesAuthenticationError = RidesAuthenticationErrorFactory.createRidesAuthenticationError(rawValue: "not.real.error") + + XCTAssertNil(ridesAuthenticationError) + } +} diff --git a/source/UberRidesTests/RidesClientTests.swift b/source/UberRidesTests/RidesClientTests.swift new file mode 100644 index 00000000..6ed05365 --- /dev/null +++ b/source/UberRidesTests/RidesClientTests.swift @@ -0,0 +1,225 @@ +// +// RidesClientTests.swift +// UberRidesTests +// +// Copyright © 2016 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class RidesClientTests: XCTestCase { + var client: RidesClient! + let timeout: Double = 10 + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + Configuration.setClientID(clientID) + Configuration.setSandboxEnabled(true) + client = RidesClient() + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + /** + Test to check getting the access token when using the default settings + and the token exists + */ + func testGetAccessTokenSuccess_defaultId_defaultGroup() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + + let keychainHelper = KeychainWrapper() + + let tokenKey = Configuration.getDefaultAccessTokenIdentifier() + let tokenGroup = Configuration.getDefaultKeychainAccessGroup() + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient() + guard let accessToken = ridesClient.getAccessToken() else { + XCTAssert(false) + return + } + XCTAssertEqual(accessToken.tokenString, token.tokenString) + } + + /** + Test to check getting the access token when using the default settings + and the token doesn't exist + */ + func testGetAccessTokenFail_defaultId_defaultGroup() { + let ridesClient = RidesClient() + let accessToken = ridesClient.getAccessToken() + XCTAssertNil(accessToken) + } + + /** + Test to check getting the access token when using a custom ID and default group + and the token exists + */ + func testGetAccessTokenSuccess_customId_defaultGroup() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + let keychainHelper = KeychainWrapper() + + let tokenKey = "newTokenKey" + let tokenGroup = Configuration.getDefaultKeychainAccessGroup() + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient(accessTokenIdentifier:tokenKey) + guard let accessToken = ridesClient.getAccessToken() else { + XCTAssert(false) + return + } + XCTAssertEqual(accessToken.tokenString, token.tokenString) + } + + /** + Test to check getting the access token when using the default ID and cusom group + and the token exists + */ + func testGetAccessTokenSuccess_defaultId_customGroup() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + let keychainHelper = KeychainWrapper() + + let tokenKey = Configuration.getDefaultAccessTokenIdentifier() + let tokenGroup = "newTokenGroup" + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient(accessTokenIdentifier: tokenKey, keychainAccessGroup:tokenGroup) + guard let accessToken = ridesClient.getAccessToken() else { + XCTAssert(false) + return + } + XCTAssertEqual(accessToken.tokenString, token.tokenString) + } + + /** + Test to check getting the access token when using custom settings + and the token exists + */ + func testGetAccessTokenSuccess_customId_customGroup() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + let keychainHelper = KeychainWrapper() + + let tokenKey = "newTokenID" + let tokenGroup = "newTokenGroup" + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient(accessTokenIdentifier: tokenKey, keychainAccessGroup:tokenGroup) + guard let accessToken = ridesClient.getAccessToken() else { + XCTAssert(false) + return + } + XCTAssertEqual(accessToken.tokenString, token.tokenString) + } + + /** + Test to check getting the access token when using custom settings + and the token doesn't exist + */ + func testGetAccessTokenFailure_customId_customGroup() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + let keychainHelper = KeychainWrapper() + + let tokenKey = "newTokenID" + let tokenGroup = "newTokenGroup" + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient() + let accessToken = ridesClient.getAccessToken() + XCTAssertNil(accessToken) + } + + /** + Test to check getting the access token fails when using a matching ID but different + group + */ + func testGetAccessTokenFailure_groupMismatch() { + let tokenData = [ "access_token" : "testAccessToken" ] + guard let token = AccessToken(JSON: tokenData) else { + XCTAssert(false) + return + } + let keychainHelper = KeychainWrapper() + + let tokenKey = "newTokenID" + let tokenGroup = "newTokenGroup" + + keychainHelper.setAccessGroup(tokenGroup) + keychainHelper.setObject(token, key: tokenKey) + defer { + keychainHelper.deleteObjectForKey(tokenKey) + } + + let ridesClient = RidesClient(accessTokenIdentifier: tokenKey) + let accessToken = ridesClient.getAccessToken() + XCTAssertNil(accessToken) + } +} diff --git a/source/UberRidesTests/RidesMocks.swift b/source/UberRidesTests/RidesMocks.swift new file mode 100644 index 00000000..c15bf8cd --- /dev/null +++ b/source/UberRidesTests/RidesMocks.swift @@ -0,0 +1,185 @@ +// +// RidesMocks.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import WebKit +@testable import UberRides + +class RideRequestViewMock : RideRequestView { + var testClosure: (() -> ())? + init(rideRequestView: RideRequestView, testClosure: (() -> ())?) { + self.testClosure = testClosure + super.init(rideParameters: rideRequestView.rideParameters, accessToken: rideRequestView.accessToken, frame: rideRequestView.frame) + } + + required init(rideParameters: RideParameters?, accessToken: AccessToken?, frame: CGRect) { + fatalError("init(rideParameters:accessToken:frame:) has not been implemented") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func load() { + self.testClosure?() + } + + override func cancelLoad() { + self.testClosure?() + } +} + +class RideRequestViewControllerMock : RideRequestViewController { + var loadClosure: (() -> ())? + var networkClosure: (() -> ())? + var presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ())? + init(rideParameters: RideParameters, loginManager: LoginManager, loadClosure: (() -> ())?, networkClosure: (() -> ())?, presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ())?) { + self.loadClosure = loadClosure + self.networkClosure = networkClosure + self.presentViewControllerClosure = presentViewControllerClosure + super.init(rideParameters: rideParameters, loginManager: loginManager) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func load() { + if let loadClosure = loadClosure { + loadClosure() + } else { + super.load() + } + } + + override func displayNetworkErrorAlert() { + if let networkClosure = networkClosure { + networkClosure() + } else { + super.displayNetworkErrorAlert() + } + } + + override func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { + if let presentViewControllerClosure = presentViewControllerClosure { + presentViewControllerClosure(viewControllerToPresent, flag, completion) + } else { + super.presentViewController(viewControllerToPresent, animated: flag, completion: completion) + } + } + +} + +class LoginViewMock : LoginView { + var testClosure: (() -> ())? + init(scopes: [RidesScope], testClosure: (() -> ())?) { + self.testClosure = testClosure + super.init(scopes: scopes, frame: CGRectZero) + } + + required override init(scopes: [RidesScope], frame: CGRect) { + fatalError("init(scopes:frame:) has not been implemented") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func load() { + self.testClosure?() + } + + override func cancelLoad() { + self.testClosure?() + } +} + +class OAuthViewControllerMock : OAuthViewController { + var presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ())? + + init(loginView: LoginView, presentViewControllerClosure: ((UIViewController, Bool, (() -> Void)?) -> ())?) { + self.presentViewControllerClosure = presentViewControllerClosure + super.init(scopes: loginView.scopes!) + self.loginView = loginView + } + + @objc required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { + if let presentViewControllerClosure = presentViewControllerClosure { + presentViewControllerClosure(viewControllerToPresent, flag, completion) + } else { + super.presentViewController(viewControllerToPresent, animated: flag, completion: completion) + } + } +} + +class WebViewMock : WKWebView { + var testClosure: ((NSURLRequest) -> ())? + init(frame: CGRect, configuration: WKWebViewConfiguration, testClosure: ((NSURLRequest) -> ())?) { + self.testClosure = testClosure + super.init(frame: frame, configuration: configuration) + } + + override func loadRequest(request: NSURLRequest) -> WKNavigation? { + testClosure?(request) + return nil + } +} + +class RequestDeeplinkMock : RequestDeeplink { + var testClosure: ((NSURL?) -> (Bool))? + init(rideParameters: RideParameters, testClosure: ((NSURL?) -> (Bool))?) { + self.testClosure = testClosure + super.init(rideParameters: rideParameters) + } + + override func execute() -> Bool { + guard let testClosure = testClosure else { + return false + } + return testClosure(deeplinkURL) + } +} + +class DeeplinkRequestingBehaviorMock : DeeplinkRequestingBehavior { + var testClosure: ((NSURL?) -> (Bool))? + init(testClosure: ((NSURL?) -> (Bool))?) { + self.testClosure = testClosure + super.init() + } + + override func createDeeplink(rideParameters: RideParameters) -> RequestDeeplink { + return RequestDeeplinkMock(rideParameters: rideParameters, testClosure: testClosure) + } +} + +@objc class RideRequestViewControllerDelegateMock : NSObject, RideRequestViewControllerDelegate { + let testClosure: (RideRequestViewController, NSError) -> () + init(testClosure: (RideRequestViewController, NSError) -> ()) { + self.testClosure = testClosure + } + @objc func rideRequestViewController(rideRequestViewController: RideRequestViewController, didReceiveError error: NSError) { + self.testClosure(rideRequestViewController, error) + } +} diff --git a/source/UberRidesTests/RidesScopeExtensionsTests.swift b/source/UberRidesTests/RidesScopeExtensionsTests.swift new file mode 100644 index 00000000..feb5539b --- /dev/null +++ b/source/UberRidesTests/RidesScopeExtensionsTests.swift @@ -0,0 +1,131 @@ +// +// RidesScopeUtilTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class RidesScopeExtensionsTests: XCTestCase { + + func testRidesScopeToString_withValidScopes() + { + let scopes : [RidesScope] = Array(arrayLiteral: RidesScope.Profile, RidesScope.Places) + + let expectedString = "\(RidesScope.Profile.rawValue) \(RidesScope.Places.rawValue)" + let scopeString = scopes.toRidesScopeString() + + XCTAssertEqual(expectedString, scopeString) + } + + func testRidesScopeToString_withNoScopes() + { + let scopes : [RidesScope] = [RidesScope]() + + let expectedString = "" + let scopeString = scopes.toRidesScopeString() + + XCTAssertEqual(expectedString, scopeString) + } + + func testRidesScopeToString_withValidScopesUsingSet() + { + let scopes : Set = Set(arrayLiteral: RidesScope.Profile, RidesScope.Places) + + let scopeString = scopes.toRidesScopeString() + + var testSet : Set = Set() + for scopeString in scopeString.componentsSeparatedByString(" ") { + guard let scope = RidesScopeFactory.ridesScopeForString(scopeString) else { + continue + } + testSet.insert(scope) + } + + XCTAssertEqual(scopes, testSet) + } + + func testRidesScopeToString_withNoScopes_usingSet() + { + let scopes : Set = Set() + + let expectedString = "" + let scopeString = scopes.toRidesScopeString() + + XCTAssertEqual(expectedString, scopeString) + } + + func testStringToRidesScope_withValidScopes() + { + let expectedScopes : [RidesScope] = Array(arrayLiteral: RidesScope.Profile, RidesScope.Places) + + let scopeString = "\(RidesScope.Profile.rawValue) \(RidesScope.Places.rawValue)" + + let scopes = scopeString.toRidesScopesArray() + + XCTAssertEqual(scopes, expectedScopes) + } + + func testStringToRidesScope_withInvalidScopes() + { + let expectedScopes : [RidesScope] = [RidesScope]() + + let scopeString = "not actual values" + + let scopes = scopeString.toRidesScopesArray() + + XCTAssertEqual(scopes, expectedScopes) + } + + func testStringToRidesScope_withNoScopes() + { + let expectedScopes : [RidesScope] = [RidesScope]() + + let scopeString = "" + + let scopes = scopeString.toRidesScopesArray() + + XCTAssertEqual(scopes, expectedScopes) + } + + func testStringToRidesScope_withInvalidAndValidScopes() + { + let expectedScopes : [RidesScope] = Array(arrayLiteral: RidesScope.Places) + + let scopeString = "not actual values \(RidesScope.Places.rawValue)" + + let scopes = scopeString.toRidesScopesArray() + + XCTAssertEqual(scopes, expectedScopes) + } + + func testStringToRidesScope_caseInsensitive() + { + let expectedScopes : [RidesScope] = Array(arrayLiteral: RidesScope.Places, RidesScope.History) + + let scopeString = "plAcEs HISTORY" + + let scopes = scopeString.toRidesScopesArray() + + XCTAssertEqual(scopes, expectedScopes) + } +} diff --git a/source/UberRidesTests/RidesScopeFactoryTests.swift b/source/UberRidesTests/RidesScopeFactoryTests.swift new file mode 100644 index 00000000..41855cd7 --- /dev/null +++ b/source/UberRidesTests/RidesScopeFactoryTests.swift @@ -0,0 +1,65 @@ +// +// RidesScopeFactoryTests.swift +// UberRides +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class RidesScopeFactoryTests: XCTestCase { + let expectedRidesScopeToStringMapping = [ + RidesScopeType.Profile : RidesScopeType.Profile.toString(), + RidesScopeType.History : RidesScopeType.History.toString(), + RidesScopeType.HistoryLite : RidesScopeType.HistoryLite.toString(), + RidesScopeType.Places : RidesScopeType.Places.toString(), + RidesScopeType.PaymentMethods : RidesScopeType.PaymentMethods.toString(), + RidesScopeType.RideWidgets : RidesScopeType.RideWidgets.toString(), + ] + + func testCreateRidesScopeByRidesScopeType() { + + for (ridesScopeType, rawValue) in expectedRidesScopeToStringMapping { + let ridesScope = RidesScopeFactory.ridesScopeForType(ridesScopeType) + + XCTAssertNotNil(ridesScope) + XCTAssertEqual(ridesScope.ridesScopeType, ridesScopeType) + XCTAssertEqual(ridesScope.rawValue, rawValue) + XCTAssertEqual(ridesScope.scopeType, ridesScopeType.type) + } + } + + func testCreateRidesScopeByRawValue() { + for (ridesScopeType, rawValue) in expectedRidesScopeToStringMapping { + guard let ridesScope = RidesScopeFactory.ridesScopeForString(rawValue) else { + XCTAssert(false) + return + } + + XCTAssertEqual(ridesScope.ridesScopeType, ridesScopeType) + XCTAssertEqual(ridesScope.rawValue, rawValue) + XCTAssertEqual(ridesScope.scopeType, ridesScopeType.type) + } + } + + func testCreateRidesScopeByRawValue_withUnknownValue() { + let ridesScope = RidesScopeFactory.ridesScopeForString("not.real.error") + XCTAssertNil(ridesScope) + } +} diff --git a/source/UberRidesTests/TokenManagerTests.swift b/source/UberRidesTests/TokenManagerTests.swift new file mode 100644 index 00000000..02b8128c --- /dev/null +++ b/source/UberRidesTests/TokenManagerTests.swift @@ -0,0 +1,201 @@ +// +// TokenManagerTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import UberRides + +class TokenManagerTests: XCTestCase { + + private var keychain: KeychainWrapper? + + override func setUp() { + super.setUp() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + keychain = KeychainWrapper() + } + + override func tearDown() { + Configuration.restoreDefaults() + keychain = nil + super.tearDown() + } + + + func testSave() { + let identifier = "testIdentifier" + let accessGroup = "testAccessGroup" + + let token = getTestToken() + + XCTAssertTrue(TokenManager.saveToken(token, tokenIdentifier:identifier, accessGroup: accessGroup)) + + keychain?.setAccessGroup(accessGroup) + guard let actualToken = keychain?.getObjectForKey(identifier) as? AccessToken else { + XCTAssert(false) + return + } + XCTAssertEqual(actualToken.tokenString, token.tokenString) + + + keychain?.deleteObjectForKey(identifier) + + } + + func testGet() { + + let identifier = "testIdentifier" + let accessGroup = "testAccessGroup" + + let token = getTestToken() + + keychain?.setAccessGroup(accessGroup) + keychain?.setObject(token, key: identifier) + + let actualToken = TokenManager.fetchToken(identifier, accessGroup: accessGroup) + XCTAssertNotNil(actualToken) + + XCTAssertEqual(actualToken?.tokenString, token.tokenString) + + keychain?.deleteObjectForKey(identifier) + } + + func testGet_nonExistent() { + let identifer = "there.is.no.token.named.this.123412wfdasd3o" + + XCTAssertNil(TokenManager.fetchToken(identifer)) + } + + func testDelete() { + let identifier = "testIdentifier" + let accessGroup = "testAccessGroup" + + let token = getTestToken() + + keychain?.setAccessGroup(accessGroup) + keychain?.setObject(token, key: identifier) + + XCTAssertTrue(TokenManager.deleteToken(identifier, accessGroup: accessGroup)) + + let actualToken = keychain?.getObjectForKey(identifier) as? AccessToken + guard actualToken == nil else { + XCTAssert(false) + keychain?.deleteObjectForKey(identifier) + return + } + } + + func testDelete_nonExistent() { + let identifier = "there.is.no.token.named.this.123412wfdasd3o" + + XCTAssertFalse(TokenManager.deleteToken(identifier)) + + } + + func testCookiesCleared_whenTokenDeleted() { + guard let usUrl = NSURL(string: "https://login.uber.com"), let chinaURL = NSURL(string: "https://login.uber.com.cn") else { + XCTAssertFalse(false) + return + } + + let cookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage() + + if let cookies = cookieStorage.cookies { + for cookie in cookies { + cookieStorage.deleteCookie(cookie) + } + } + + + cookieStorage.setCookies(createTestUSCookies(), forURL: usUrl, mainDocumentURL: nil) + cookieStorage.setCookies(createTestChinaCookies(), forURL: chinaURL, mainDocumentURL: nil) + NSUserDefaults.standardUserDefaults().synchronize() + XCTAssertEqual(cookieStorage.cookies?.count, 4) + XCTAssertEqual(cookieStorage.cookiesForURL(usUrl)?.count, 2) + XCTAssertEqual(cookieStorage.cookiesForURL(chinaURL)?.count, 2) + + let identifier = "testIdentifier" + let accessGroup = "testAccessGroup" + + let token = getTestToken() + + keychain?.setAccessGroup(accessGroup) + keychain?.setObject(token, key: identifier) + + XCTAssertTrue(TokenManager.deleteToken(identifier, accessGroup: accessGroup)) + + let actualToken = keychain?.getObjectForKey(identifier) as? AccessToken + guard actualToken == nil else { + XCTAssert(false) + keychain?.deleteObjectForKey(identifier) + return + } + + let testCookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage() + XCTAssertEqual(testCookieStorage.cookies?.count, 0) + + } + + + //MARK: Helpers + + func getTestToken() -> AccessToken! { + let tokenData = ["access_token" : "testTokenString"] + return AccessToken(JSON: tokenData) + } + + func createTestUSCookies() -> [NSHTTPCookie] { + let secureUSCookie = NSHTTPCookie(properties: [NSHTTPCookieDomain: ".uber.com", + NSHTTPCookiePath : "/", + NSHTTPCookieName : "us_login_secure", + NSHTTPCookieValue : "some_value", + NSHTTPCookieSecure : true]) + let unsecureUSCookie = NSHTTPCookie(properties: [NSHTTPCookieDomain: ".uber.com", + NSHTTPCookiePath : "/", + NSHTTPCookieName : "us_login_unecure", + NSHTTPCookieValue : "some_value", + NSHTTPCookieSecure : false]) + if let secureUSCookie = secureUSCookie, let unsecureUSCookie = unsecureUSCookie { + return [secureUSCookie, unsecureUSCookie] + } + return [] + } + + func createTestChinaCookies() -> [NSHTTPCookie] { + let secureChinaCookie = NSHTTPCookie(properties: [NSHTTPCookieDomain : ".uber.com.cn", + NSHTTPCookiePath : "/", + NSHTTPCookieName : "cn_login_secure", + NSHTTPCookieValue : "some_value", + NSHTTPCookieSecure : true]) + let unsecureChinaCookie = NSHTTPCookie(properties: [NSHTTPCookieDomain : ".uber.com.cn", + NSHTTPCookiePath : "/", + NSHTTPCookieName : "cn_login_unsecure", + NSHTTPCookieValue : "some_value", + NSHTTPCookieSecure : false]) + if let secureChinaCookie = secureChinaCookie, let unsecureChinaCookie = unsecureChinaCookie { + return [secureChinaCookie, unsecureChinaCookie] + } + return [] + } +} diff --git a/source/UberRidesTests/WidgetsEndpointTests.swift b/source/UberRidesTests/WidgetsEndpointTests.swift new file mode 100644 index 00000000..8cd24f37 --- /dev/null +++ b/source/UberRidesTests/WidgetsEndpointTests.swift @@ -0,0 +1,126 @@ +// +// WidgetsEndpointTests.swift +// UberRides +// +// Copyright © 2015 Uber Technologies, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import CoreLocation +@testable import UberRides + +class WidgetsEndpointTests: XCTestCase { + + override func setUp() { + super.setUp() + Configuration.restoreDefaults() + Configuration.plistName = "testInfo" + Configuration.bundle = NSBundle(forClass: self.dynamicType) + } + + override func tearDown() { + Configuration.restoreDefaults() + super.tearDown() + } + + func testERRC_withNoLocation() { + Configuration.setSandboxEnabled(true) + Configuration.setRegion(Region.Default) + + let expectedHost = "https://components.uber.com" + let expectedPath = "/rides/" + let expectedQueryItems = queryBuilder( ("env", "sandbox") ) + + let rideRequestWidget = Components.RideRequestWidget(rideParameters: nil) + + XCTAssertEqual(rideRequestWidget.host, expectedHost) + XCTAssertEqual(rideRequestWidget.path, expectedPath) + XCTAssertEqual(rideRequestWidget.query, expectedQueryItems) + } + + func testERRC_withRegionDefault_withSandboxEnabled() { + Configuration.setSandboxEnabled(true) + Configuration.setRegion(Region.Default) + + let expectedLat = 33.2 + let expectedLong = -41.2 + let expectedHost = "https://components.uber.com" + let expectedPath = "/rides/" + let expectedQueryItems = queryBuilder( ("env", "sandbox"), + ("pickup[latitude]", "\(expectedLat)" ), + ("pickup[longitude]", "\(expectedLong)") + ) + let rideParameters = RideParametersBuilder().setPickupLocation(CLLocation(latitude: expectedLat, longitude: expectedLong)).build() + let rideRequestWidget = Components.RideRequestWidget(rideParameters: rideParameters) + + XCTAssertEqual(rideRequestWidget.host, expectedHost) + XCTAssertEqual(rideRequestWidget.path, expectedPath) + + for item in expectedQueryItems { + XCTAssertTrue(rideRequestWidget.query.contains(item)) + } + } + + func testERRC_withRegionChina_withSandboxEnabled() { + Configuration.setSandboxEnabled(true) + Configuration.setRegion(Region.China) + + let expectedHost = "https://components.uber.com.cn" + let expectedPath = "/rides/" + let expectedQueryItems = queryBuilder( ("env", "sandbox") ) + + let rideRequestWidget = Components.RideRequestWidget(rideParameters: nil) + + XCTAssertEqual(rideRequestWidget.host, expectedHost) + XCTAssertEqual(rideRequestWidget.path, expectedPath) + XCTAssertEqual(rideRequestWidget.query, expectedQueryItems) + } + + func testERRC_withRegionDefault_withSandboxDisabled() { + Configuration.setSandboxEnabled(false) + Configuration.setRegion(Region.Default) + + let expectedHost = "https://components.uber.com" + let expectedPath = "/rides/" + let expectedQueryItems = queryBuilder( ("env", "production") ) + + let rideRequestWidget = Components.RideRequestWidget(rideParameters: nil) + + XCTAssertEqual(rideRequestWidget.host, expectedHost) + XCTAssertEqual(rideRequestWidget.path, expectedPath) + XCTAssertEqual(rideRequestWidget.query, expectedQueryItems) + } + + func testERRC_withRegionChina_withSandboxDisabled() { + Configuration.setSandboxEnabled(false) + Configuration.setRegion(Region.China) + + let expectedHost = "https://components.uber.com.cn" + let expectedPath = "/rides/" + let expectedQueryItems = queryBuilder( ("env", "production") ) + + let rideRequestWidget = Components.RideRequestWidget(rideParameters: nil) + + XCTAssertEqual(rideRequestWidget.host, expectedHost) + XCTAssertEqual(rideRequestWidget.path, expectedPath) + XCTAssertEqual(rideRequestWidget.query, expectedQueryItems) + } + +} diff --git a/source/UberRidesTests/testInfo.plist b/source/UberRidesTests/testInfo.plist new file mode 100644 index 00000000..074e69d4 --- /dev/null +++ b/source/UberRidesTests/testInfo.plist @@ -0,0 +1,10 @@ + + + + + UberClientID + testClientID + UberCallbackURI + testUri://uberConnect + +