From f1913e1454d3b06207239a6830bab7568e7ae257 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 8 Oct 2015 11:30:32 -0700 Subject: [PATCH] =?UTF-8?q?(=E3=81=A3=CB=98=E2=96=BD=CB=98)=E3=81=A3=20:cl?= =?UTF-8?q?oud:=20=E2=8A=82(=E2=97=95=E3=80=82=E2=97=95=E2=8A=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 71 ++ .gitignore | 20 + .travis.yml | 27 + CONTRIBUTING.md | 67 ++ .../ParseTwitterTestApplication.xcconfig | 17 + .../ParseTwitterUtils-Tests.xcconfig | 24 + Configurations/ParseTwitterUtils-iOS.xcconfig | 24 + Configurations/Shared/Common.xcconfig | 21 + Configurations/Shared/Platform/OSX.xcconfig | 11 + Configurations/Shared/Platform/iOS.xcconfig | 21 + .../Shared/Platform/watchOS.xcconfig | 14 + .../Shared/Product/Application.xcconfig | 14 + .../Shared/Product/Framework.xcconfig | 19 + .../Shared/Product/UnitTest.xcconfig | 15 + Configurations/Shared/Project/Debug.xcconfig | 22 + .../Shared/Project/Release.xcconfig | 18 + Configurations/Shared/Warnings.xcconfig | 43 + Gemfile | 4 + Gemfile.lock | 65 ++ LICENSE | 30 + PATENTS | 33 + ParseTwitterUtils.podspec | 32 + ParseTwitterUtils.xcodeproj/project.pbxproj | 789 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/ParseTwitterUtils-iOS.xcscheme | 128 +++ .../contents.xcworkspacedata | 10 + .../Internal/Dialog/PFOAuth1FlowDialog.h | 88 ++ .../Internal/Dialog/PFOAuth1FlowDialog.m | 585 +++++++++++++ .../Internal/OAuthCore/PF_OAuthCore.h | 63 ++ .../Internal/OAuthCore/PF_OAuthCore.m | 190 +++++ .../Internal/PFTwitterAlertView.h | 22 + .../Internal/PFTwitterAlertView.m | 109 +++ .../PFTwitterAuthenticationProvider.h | 38 + .../PFTwitterAuthenticationProvider.m | 113 +++ .../Internal/PFTwitterPrivateUtilities.h | 45 + .../Internal/PFTwitterPrivateUtilities.m | 75 ++ .../Internal/PFTwitterUtils_Private.h | 19 + .../Internal/PF_Twitter_Private.h | 38 + ParseTwitterUtils/PFTwitterUtils.h | 320 +++++++ ParseTwitterUtils/PFTwitterUtils.m | 222 +++++ ParseTwitterUtils/PF_Twitter.h | 86 ++ ParseTwitterUtils/PF_Twitter.m | 447 ++++++++++ ParseTwitterUtils/ParseTwitterUtils.h | 11 + Podfile | 6 + Podfile.lock | 10 + README.md | 88 ++ Resources/Info.plist | 29 + Resources/Localizable.strings | Bin 0 -> 250 bytes Tests/Other/PFTwitterTestMacros.h | 26 + Tests/Other/TestCase/PFTwitterTestCase.h | 78 ++ Tests/Other/TestCase/PFTwitterTestCase.m | 63 ++ Tests/Resources/Info.plist | 24 + Tests/TestApplication/Classes/main.m | 31 + Tests/TestApplication/Resources/Info.plist | 47 ++ Tests/Unit/OAuth1FlowDialogTests.m | 141 ++++ Tests/Unit/OAuthCoreTests.m | 168 ++++ .../Unit/TwitterAuthenticationProviderTests.m | 140 ++++ Tests/Unit/TwitterTests.m | 625 ++++++++++++++ Tests/Unit/TwitterUtilsTests.m | 71 ++ third_party_licenses.txt | 35 + 60 files changed, 5599 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Configurations/ParseTwitterTestApplication.xcconfig create mode 100644 Configurations/ParseTwitterUtils-Tests.xcconfig create mode 100644 Configurations/ParseTwitterUtils-iOS.xcconfig create mode 100644 Configurations/Shared/Common.xcconfig create mode 100644 Configurations/Shared/Platform/OSX.xcconfig create mode 100644 Configurations/Shared/Platform/iOS.xcconfig create mode 100644 Configurations/Shared/Platform/watchOS.xcconfig create mode 100644 Configurations/Shared/Product/Application.xcconfig create mode 100644 Configurations/Shared/Product/Framework.xcconfig create mode 100644 Configurations/Shared/Product/UnitTest.xcconfig create mode 100644 Configurations/Shared/Project/Debug.xcconfig create mode 100644 Configurations/Shared/Project/Release.xcconfig create mode 100644 Configurations/Shared/Warnings.xcconfig create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 PATENTS create mode 100644 ParseTwitterUtils.podspec create mode 100644 ParseTwitterUtils.xcodeproj/project.pbxproj create mode 100644 ParseTwitterUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ParseTwitterUtils.xcodeproj/xcshareddata/xcschemes/ParseTwitterUtils-iOS.xcscheme create mode 100644 ParseTwitterUtils.xcworkspace/contents.xcworkspacedata create mode 100644 ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.h create mode 100644 ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.m create mode 100644 ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.h create mode 100644 ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.m create mode 100644 ParseTwitterUtils/Internal/PFTwitterAlertView.h create mode 100644 ParseTwitterUtils/Internal/PFTwitterAlertView.m create mode 100644 ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.h create mode 100644 ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.m create mode 100644 ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.h create mode 100644 ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.m create mode 100644 ParseTwitterUtils/Internal/PFTwitterUtils_Private.h create mode 100644 ParseTwitterUtils/Internal/PF_Twitter_Private.h create mode 100644 ParseTwitterUtils/PFTwitterUtils.h create mode 100644 ParseTwitterUtils/PFTwitterUtils.m create mode 100644 ParseTwitterUtils/PF_Twitter.h create mode 100644 ParseTwitterUtils/PF_Twitter.m create mode 100644 ParseTwitterUtils/ParseTwitterUtils.h create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 README.md create mode 100644 Resources/Info.plist create mode 100644 Resources/Localizable.strings create mode 100644 Tests/Other/PFTwitterTestMacros.h create mode 100644 Tests/Other/TestCase/PFTwitterTestCase.h create mode 100644 Tests/Other/TestCase/PFTwitterTestCase.m create mode 100644 Tests/Resources/Info.plist create mode 100644 Tests/TestApplication/Classes/main.m create mode 100644 Tests/TestApplication/Resources/Info.plist create mode 100644 Tests/Unit/OAuth1FlowDialogTests.m create mode 100644 Tests/Unit/OAuthCoreTests.m create mode 100644 Tests/Unit/TwitterAuthenticationProviderTests.m create mode 100644 Tests/Unit/TwitterTests.m create mode 100644 Tests/Unit/TwitterUtilsTests.m create mode 100644 third_party_licenses.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..bdf01dc --- /dev/null +++ b/.clang-format @@ -0,0 +1,71 @@ +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +--- +Language: Cpp +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: true +AlignEscapedNewlinesLeft: true +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakTemplateDeclarations: false +AlwaysBreakBeforeMultilineStrings: false +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +BinPackParameters: true +BinPackArguments: true +ColumnLimit: 0 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +DerivePointerAlignment: true +ExperimentalAutoDetectBinPacking: true +IndentCaseLabels: true +IndentWrappedFunctionNames: true +IndentFunctionDeclarationAfterType: true +MaxEmptyLinesToKeep: 1 +KeepEmptyLinesAtTheStartOfBlocks: true +NamespaceIndentation: None +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakString: 1000 +PenaltyBreakFirstLessLess: 140 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 120 +PointerAlignment: Right +SpacesBeforeTrailingComments: 1 +Cpp11BracedListStyle: true +Standard: Cpp11 +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +BreakBeforeBraces: Attach +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: false +SpaceInEmptyParentheses: false +SpacesInCStyleCastParentheses: false +SpaceAfterCStyleCast: false +SpacesInContainerLiterals: true +SpaceBeforeAssignmentOperators: true +ContinuationIndentWidth: 4 +CommentPragmas: '^ IWYU pragma:' +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +SpaceBeforeParens: ControlStatements +DisableFormat: false +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94c2ed3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.DS_Store + +*.pbxuser +*.perspective +*.perspectivev3 + +*.mode1v3 +*.mode2v3 + +*.xcodeproj/xcuserdata/*.xcuserdatad + +*.xccheckout +*.xcscmblueprint +*.xcuserdatad + +Pods + +DerivedData +build +Vendor/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7a9b6f6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +branches: + only: + - master +language: objective-c +os: osx +osx_image: xcode7 +env: + global: + - LC_CTYPE=en_US.UTF-8 + - LANG=en_US.UTF-8 + matrix: + - TEST_TYPE=ios + - TEST_TYPE=podspecs +script: +- | + if [ "$TEST_TYPE" = ios ]; then + set -o pipefail + xcodebuild test -workspace ParseTwitterUtils.xcworkspace -sdk iphonesimulator -scheme ParseTwitterUtils-iOS -configuration Debug -destination "platform=iOS Simulator,name=iPhone 4s" -destination "platform=iOS Simulator,name=iPhone 6 Plus" GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty + elif [ "$TEST_TYPE" = podspecs ]; then + pod lib lint ParseTwitterUtils.podspec + pod lib lint --use-libraries ParseTwitterUtils.podspec + fi +after_success: +- | + if [ "$TEST_TYPE" = ios ]; then + bash <(curl -s https://codecov.io/bash) + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..611e478 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to Parse Twitter Utils for iOS +We want to make contributing to this project as easy and transparent as possible. + +## Code of Conduct +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. + +## Our Development Process +Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. + +### `master` is unsafe +Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. + +### Pull Requests +We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +1. Fork the repo and create your branch from `master`. +4. Add unit tests for any new code you add. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +### Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Bugs +Although we try to keep developing on Parse easy, you still may run into some issues. General questions should be asked on [Google Groups][google-group], technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. + +### Known Issues +We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues +Not all issues are SDK issues. If you're unsure whether your bug is with the SDK or backend, you can test to see if it reproduces with our [REST API][rest-api] and [Parse API Console][parse-api-console]. If it does, you can report backend bugs [here][bug-reports]. + +To view the REST API network requests issued by the Parse SDK, please check out our [Network Debugging Tool][network-debugging-tool]. + +Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: + +* A description. What did you expect to happen and what actually happened? Why do you think that was wrong? +* A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. +* What version does this reproduce on? What version did it last work on? +* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. +* Anything else you find relevant. + + +### Security Bugs +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. + +## Style Guide +We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following: + +* Most importantly, match the existing code style as much as possible. +* Try to keep lines under 120 characters, if possible. + +## License +By contributing to Parse Twitter Utils for iOS, you agree that your contributions will be licensed under its license. + + [google-group]: https://groups.google.com/forum/#!forum/parse-developers + [stack-overflow]: http://stackoverflow.com/tags/parse.com + [bug-reports]: https://www.parse.com/help#report + [rest-api]: https://www.parse.com/docs/rest/guide + [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ + [network-debugging-tool]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/wiki/Network-Debug-Tool + [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg + [tests-dir]: /Tests/Unit/ diff --git a/Configurations/ParseTwitterTestApplication.xcconfig b/Configurations/ParseTwitterTestApplication.xcconfig new file mode 100644 index 0000000..35fd6d8 --- /dev/null +++ b/Configurations/ParseTwitterTestApplication.xcconfig @@ -0,0 +1,17 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/iOS.xcconfig" +#include "Shared/Product/Application.xcconfig" + +PRODUCT_NAME = ParseTwitterTestApplication + +INFOPLIST_FILE = $(SRCROOT)/Tests/TestApplication/Resources/Info.plist + +CLANG_ENABLE_MODULES = YES diff --git a/Configurations/ParseTwitterUtils-Tests.xcconfig b/Configurations/ParseTwitterUtils-Tests.xcconfig new file mode 100644 index 0000000..9af5cf1 --- /dev/null +++ b/Configurations/ParseTwitterUtils-Tests.xcconfig @@ -0,0 +1,24 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/iOS.xcconfig" +#include "Shared/Product/UnitTest.xcconfig" +#include "Pods/Target Support Files/Pods-ParseTwitterUtils-Tests/Pods-ParseTwitterUtils-Tests.debug.xcconfig" + +PRODUCT_NAME = ParseTwitterUtils-Tests + +CLANG_ENABLE_MODULES = YES + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(SRCROOT)/Vendor +LIBRARY_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) +HEADER_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) + +INFOPLIST_FILE = $(PROJECT_DIR)/Tests/Resources/Info.plist + +TEST_HOST = $(BUILT_PRODUCTS_DIR)/ParseTwitterTestApplication.app/ParseTwitterTestApplication diff --git a/Configurations/ParseTwitterUtils-iOS.xcconfig b/Configurations/ParseTwitterUtils-iOS.xcconfig new file mode 100644 index 0000000..4cea9bd --- /dev/null +++ b/Configurations/ParseTwitterUtils-iOS.xcconfig @@ -0,0 +1,24 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/iOS.xcconfig" +#include "Shared/Product/Framework.xcconfig" + +PRODUCT_NAME = ParseTwitterUtils + +MACH_O_TYPE = staticlib +DEFINES_MODULE = YES + +CLANG_ENABLE_MODULES = YES + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(SRCROOT)/Vendor + +INFOPLIST_FILE = $(SRCROOT)/Resources/Info.plist + +OTHER_CFLAGS[sdk=iphoneos9.0] = $(inherited) -fembed-bitcode diff --git a/Configurations/Shared/Common.xcconfig b/Configurations/Shared/Common.xcconfig new file mode 100644 index 0000000..de2de31 --- /dev/null +++ b/Configurations/Shared/Common.xcconfig @@ -0,0 +1,21 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Warnings.xcconfig" + +// Language Settings +CLANG_ENABLE_OBJC_ARC = YES +GCC_C_LANGUAGE_STANDARD = gnu11 +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +CLANG_CXX_LIBRARY = libstdc++ + +// Search Paths +PARSE_DIR = $(PROJECT_DIR) +VENDOR_DIR = $(PARSE_DIR)/Vendor +ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Configurations/Shared/Platform/OSX.xcconfig b/Configurations/Shared/Platform/OSX.xcconfig new file mode 100644 index 0000000..db20c6a --- /dev/null +++ b/Configurations/Shared/Platform/OSX.xcconfig @@ -0,0 +1,11 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = macosx +MACOSX_DEPLOYMENT_TARGET = 10.9 diff --git a/Configurations/Shared/Platform/iOS.xcconfig b/Configurations/Shared/Platform/iOS.xcconfig new file mode 100644 index 0000000..5c5affb --- /dev/null +++ b/Configurations/Shared/Platform/iOS.xcconfig @@ -0,0 +1,21 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = iphoneos +IPHONEOS_DEPLOYMENT_TARGET = 7.0 + +GCC_THUMB_SUPPORT = NO + +ARCHS = $(ARCHS_STANDARD) armv7s +DSTROOT = /tmp/$(PRODUCT_NAME).dst + +CODE_SIGN_IDENTITY = +CODE_SIGNING_REQUIRED = NO + +TARGETED_DEVICE_FAMILY = 1,2 diff --git a/Configurations/Shared/Platform/watchOS.xcconfig b/Configurations/Shared/Platform/watchOS.xcconfig new file mode 100644 index 0000000..7287106 --- /dev/null +++ b/Configurations/Shared/Platform/watchOS.xcconfig @@ -0,0 +1,14 @@ +// +// Copyright (c) 2014, Facebook, Inc. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = watchos +WATCHOS_DEPLOYMENT_TARGET = 2.0 + +CODE_SIGN_IDENTITY = +CODE_SIGNING_REQUIRED = NO diff --git a/Configurations/Shared/Product/Application.xcconfig b/Configurations/Shared/Product/Application.xcconfig new file mode 100644 index 0000000..59b9917 --- /dev/null +++ b/Configurations/Shared/Product/Application.xcconfig @@ -0,0 +1,14 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks + +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon +ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage +CLANG_ENABLE_MODULES = YES diff --git a/Configurations/Shared/Product/Framework.xcconfig b/Configurations/Shared/Product/Framework.xcconfig new file mode 100644 index 0000000..9d5cca2 --- /dev/null +++ b/Configurations/Shared/Product/Framework.xcconfig @@ -0,0 +1,19 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +ENABLE_NS_ASSERTIONS = NO +MTL_ENABLE_DEBUG_INFO = NO + +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 + +SKIP_INSTALL = YES + +CLANG_MODULES_AUTOLINK = NO +CLANG_ENABLE_MODULES = YES diff --git a/Configurations/Shared/Product/UnitTest.xcconfig b/Configurations/Shared/Product/UnitTest.xcconfig new file mode 100644 index 0000000..a4ab7b9 --- /dev/null +++ b/Configurations/Shared/Product/UnitTest.xcconfig @@ -0,0 +1,15 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +OTHER_LDFLAGS = $(inherited) -ObjC -framework XCTest +BUNDLE_LOADER = $(TEST_HOST) + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/Frameworks @executable_path/Frameworks +USER_HEADER_SEARCH_PATHS = $(value) $(PARSE_DIR)/Tests/** +CLANG_ENABLE_MODULES = YES diff --git a/Configurations/Shared/Project/Debug.xcconfig b/Configurations/Shared/Project/Debug.xcconfig new file mode 100644 index 0000000..c5882bc --- /dev/null +++ b/Configurations/Shared/Project/Debug.xcconfig @@ -0,0 +1,22 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "../Common.xcconfig" + +GCC_OPTIMIZATION_LEVEL = 0 +SWIFT_OPTIMIZATION_LEVEL = -Onone + +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 +ONLY_ACTIVE_ARCH = YES + +ENABLE_TESTABILITY = YES + +SANITIZE_FLAGS = -fsanitize-undefined-trap-on-error -fsanitize=undefined-trap +OTHER_CFLAGS = $(value) $(SANITIZE_FLAGS) +OTHER_LDFLAGS = $(value) $(SANITIZE_FLAGS) diff --git a/Configurations/Shared/Project/Release.xcconfig b/Configurations/Shared/Project/Release.xcconfig new file mode 100644 index 0000000..8570b87 --- /dev/null +++ b/Configurations/Shared/Project/Release.xcconfig @@ -0,0 +1,18 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "../Common.xcconfig" + +GCC_OPTIMIZATION_LEVEL = s +SWIFT_OPTIMIZATION_LEVEL = -O + +VALIDATE_PRODUCT = YES + +DEPLOYMENT_POSTPROCESSING = YES +STRIP_STYLE = debugging diff --git a/Configurations/Shared/Warnings.xcconfig b/Configurations/Shared/Warnings.xcconfig new file mode 100644 index 0000000..75ebeb5 --- /dev/null +++ b/Configurations/Shared/Warnings.xcconfig @@ -0,0 +1,43 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +ENABLE_STRICT_OBJC_MSGSEND = YES + +GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +GCC_WARN_UNKNOWN_PRAGMAS = YES +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES + +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES +CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES + +// Errors +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..be60713 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'xcpretty' +gem 'cocoapods', '~> 0.39.0.rc.1' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..8a823ee --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,65 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (4.2.4) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + claide (0.9.1) + cocoapods (0.39.0.rc.1) + activesupport (>= 4.0.2) + claide (~> 0.9.1) + cocoapods-core (= 0.39.0.rc.1) + cocoapods-downloader (~> 0.9.3) + cocoapods-plugins (~> 0.4.2) + cocoapods-search (~> 0.1.0) + cocoapods-stats (~> 0.6.2) + cocoapods-trunk (~> 0.6.4) + cocoapods-try (~> 0.5.1) + colored (~> 1.2) + escape (~> 0.0.4) + molinillo (~> 0.4.0) + nap (~> 1.0) + xcodeproj (~> 0.28.1) + cocoapods-core (0.39.0.rc.1) + activesupport (>= 4.0.2) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-downloader (0.9.3) + cocoapods-plugins (0.4.2) + nap + cocoapods-search (0.1.0) + cocoapods-stats (0.6.2) + cocoapods-trunk (0.6.4) + nap (>= 0.8, < 2.0) + netrc (= 0.7.8) + cocoapods-try (0.5.1) + colored (1.2) + escape (0.0.4) + fuzzy_match (2.0.4) + i18n (0.7.0) + json (1.8.3) + minitest (5.8.1) + molinillo (0.4.0) + nap (1.0.0) + netrc (0.7.8) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + xcodeproj (0.28.1) + activesupport (>= 3) + claide (~> 0.9.1) + colored (~> 1.2) + xcpretty (0.1.12) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (~> 0.39.0.rc.1) + xcpretty + +BUNDLED WITH + 1.10.6 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..66d8ad4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Parse Twitter Utils for iOS software + +Copyright (c) 2015-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 0000000..fc84d0d --- /dev/null +++ b/PATENTS @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the Parse Twitter Utils for iOS software distributed by Parse, LLC. + +Parse, LLC. ("Parse") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Parse’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Parse or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Parse or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Parse or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Parse that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/ParseTwitterUtils.podspec b/ParseTwitterUtils.podspec new file mode 100644 index 0000000..a310680 --- /dev/null +++ b/ParseTwitterUtils.podspec @@ -0,0 +1,32 @@ +Pod::Spec.new do |s| + s.name = 'ParseTwitterUtils' + s.version = '1.9.0' + s.license = { :type => 'Commercial', :text => "See https://www.parse.com/about/terms" } + s.homepage = 'https://www.parse.com/' + s.summary = 'Parse is a complete technology stack to power your app\'s backend.' + s.authors = 'Parse' + + s.source = { :git => "https://github.com/ParsePlatform/ParseTwitterUtils-iOS.git", :tag => s.version.to_s } + + s.platform = :ios + s.ios.deployment_target = '7.0' + s.requires_arc = true + + s.public_header_files = 'ParseTwitterUtils/*.h' + s.source_files = 'ParseTwitterUtils/**/*.{h,m}' + + s.frameworks = 'AudioToolbox', + 'CFNetwork', + 'CoreGraphics', + 'CoreLocation', + 'QuartzCore', + 'Security', + 'StoreKit', + 'SystemConfiguration' + s.weak_frameworks = 'Accounts', + 'Social' + s.libraries = 'z', 'sqlite3' + + s.dependency 'Bolts/Tasks', '>= 1.3.0' + s.dependency 'Parse', '~> 1.9' +end diff --git a/ParseTwitterUtils.xcodeproj/project.pbxproj b/ParseTwitterUtils.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2f35b28 --- /dev/null +++ b/ParseTwitterUtils.xcodeproj/project.pbxproj @@ -0,0 +1,789 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 77DE89F60F51105C769BEC0B /* libPods-ParseTwitterUtils-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F31BB2625FB261014BCD4FB6 /* libPods-ParseTwitterUtils-Tests.a */; }; + 8135E4951B4B6A0E0092F452 /* PF_Twitter_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8135E48E1B4B6A0E0092F452 /* PF_Twitter_Private.h */; }; + 8135E4961B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8135E48F1B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.h */; }; + 8135E4971B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8135E4901B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.m */; }; + 8135E4981B4B6A0E0092F452 /* PF_Twitter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8135E4911B4B6A0E0092F452 /* PF_Twitter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8135E4991B4B6A0E0092F452 /* PF_Twitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8135E4921B4B6A0E0092F452 /* PF_Twitter.m */; }; + 8135E49A1B4B6A0E0092F452 /* PFTwitterUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 8135E4931B4B6A0E0092F452 /* PFTwitterUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8135E49B1B4B6A0E0092F452 /* PFTwitterUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8135E4941B4B6A0E0092F452 /* PFTwitterUtils.m */; }; + 813DFC931AB2515A00F25A08 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 813DFC921AB2515A00F25A08 /* Bolts.framework */; }; + 813DFC951AB251F700F25A08 /* Parse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 813DFC941AB251F700F25A08 /* Parse.framework */; }; + 813DFC981AB2526000F25A08 /* third_party_licenses.txt in Resources */ = {isa = PBXBuildFile; fileRef = 813DFC971AB2526000F25A08 /* third_party_licenses.txt */; }; + 813E54A41BB5DDEF00C727E8 /* PFTwitterUtils_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 813E54A31BB5DDEF00C727E8 /* PFTwitterUtils_Private.h */; settings = {ASSET_TAGS = (); }; }; + 813E54A91BB5E5FA00C727E8 /* PFTwitterPrivateUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 813E54A61BB5E52000C727E8 /* PFTwitterPrivateUtilities.m */; settings = {ASSET_TAGS = (); }; }; + 813E54AA1BB5E5FF00C727E8 /* PFTwitterPrivateUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 813E54A51BB5E52000C727E8 /* PFTwitterPrivateUtilities.h */; settings = {ASSET_TAGS = (); }; }; + 815F18401B4B730E0066E996 /* PFOAuth1FlowDialog.h in Headers */ = {isa = PBXBuildFile; fileRef = 815F183E1B4B730E0066E996 /* PFOAuth1FlowDialog.h */; }; + 815F18411B4B730E0066E996 /* PFOAuth1FlowDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = 815F183F1B4B730E0066E996 /* PFOAuth1FlowDialog.m */; }; + 81665C731BBDE27D00AE923F /* OAuth1FlowDialogTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C6E1BBDE27D00AE923F /* OAuth1FlowDialogTests.m */; settings = {ASSET_TAGS = (); }; }; + 81665C741BBDE27D00AE923F /* OAuthCoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C6F1BBDE27D00AE923F /* OAuthCoreTests.m */; settings = {ASSET_TAGS = (); }; }; + 81665C751BBDE27D00AE923F /* TwitterAuthenticationProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C701BBDE27D00AE923F /* TwitterAuthenticationProviderTests.m */; settings = {ASSET_TAGS = (); }; }; + 81665C761BBDE27D00AE923F /* TwitterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C711BBDE27D00AE923F /* TwitterTests.m */; settings = {ASSET_TAGS = (); }; }; + 81665C771BBDE27D00AE923F /* TwitterUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C721BBDE27D00AE923F /* TwitterUtilsTests.m */; settings = {ASSET_TAGS = (); }; }; + 81665C7C1BBDE2EE00AE923F /* PFTwitterTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 81665C7B1BBDE2EE00AE923F /* PFTwitterTestCase.m */; settings = {ASSET_TAGS = (); }; }; + 8166FB921B4F1DC5003841A2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FB8F1B4F1DC5003841A2 /* main.m */; }; + 817A37CB1B4B741A00129AFA /* PF_OAuthCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 817A37C91B4B741A00129AFA /* PF_OAuthCore.h */; }; + 817A37CC1B4B741A00129AFA /* PF_OAuthCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 817A37CA1B4B741A00129AFA /* PF_OAuthCore.m */; }; + 819DAAD61BB5EC79002BDE2B /* PFTwitterAlertView.h in Headers */ = {isa = PBXBuildFile; fileRef = 819DAAD41BB5EC79002BDE2B /* PFTwitterAlertView.h */; settings = {ASSET_TAGS = (); }; }; + 819DAAD71BB5EC79002BDE2B /* PFTwitterAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = 819DAAD51BB5EC79002BDE2B /* PFTwitterAlertView.m */; settings = {ASSET_TAGS = (); }; }; + 81B3F22B1AC9CA5300A92677 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 81B3F22A1AC9CA5300A92677 /* Localizable.strings */; }; + 81CB98CC1AB7905D00136FA5 /* ParseTwitterUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AAC07E0554694100DB518D /* ParseTwitterUtils.framework */; }; + 81CB98D81AB791FB00136FA5 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 813DFC921AB2515A00F25A08 /* Bolts.framework */; }; + 81CB98DB1AB7920E00136FA5 /* Parse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 813DFC941AB251F700F25A08 /* Parse.framework */; }; + 81CB98DD1AB7921C00136FA5 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 81CB98DC1AB7921C00136FA5 /* libsqlite3.dylib */; }; + 81CB98DF1AB7922600136FA5 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81CB98DE1AB7922600136FA5 /* AudioToolbox.framework */; }; + 81CB98E11AB7922C00136FA5 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81CB98E01AB7922C00136FA5 /* SystemConfiguration.framework */; }; + 81D342A11B4C7DA500B6C124 /* ParseTwitterUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D342A01B4C7DA500B6C124 /* ParseTwitterUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8166FB941B4F1E9A003841A2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8166FB661B4F1D77003841A2; + remoteInfo = ParseTwitterTestApplication; + }; + 81CB98CD1AB7905D00136FA5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2AAC07D0554694100DB518D; + remoteInfo = "ParseFacebookUtils_v4-iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 8135E48E1B4B6A0E0092F452 /* PF_Twitter_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PF_Twitter_Private.h; sourceTree = ""; }; + 8135E48F1B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterAuthenticationProvider.h; sourceTree = ""; }; + 8135E4901B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTwitterAuthenticationProvider.m; sourceTree = ""; }; + 8135E4911B4B6A0E0092F452 /* PF_Twitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PF_Twitter.h; sourceTree = ""; }; + 8135E4921B4B6A0E0092F452 /* PF_Twitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PF_Twitter.m; sourceTree = ""; }; + 8135E4931B4B6A0E0092F452 /* PFTwitterUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterUtils.h; sourceTree = ""; }; + 8135E4941B4B6A0E0092F452 /* PFTwitterUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTwitterUtils.m; sourceTree = ""; }; + 813DFC921AB2515A00F25A08 /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Bolts.framework; path = Vendor/Bolts.framework; sourceTree = ""; }; + 813DFC941AB251F700F25A08 /* Parse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Parse.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 813DFC971AB2526000F25A08 /* third_party_licenses.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = third_party_licenses.txt; sourceTree = SOURCE_ROOT; }; + 813E54A31BB5DDEF00C727E8 /* PFTwitterUtils_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterUtils_Private.h; sourceTree = ""; }; + 813E54A51BB5E52000C727E8 /* PFTwitterPrivateUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterPrivateUtilities.h; sourceTree = ""; }; + 813E54A61BB5E52000C727E8 /* PFTwitterPrivateUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTwitterPrivateUtilities.m; sourceTree = ""; }; + 815F183E1B4B730E0066E996 /* PFOAuth1FlowDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOAuth1FlowDialog.h; sourceTree = ""; }; + 815F183F1B4B730E0066E996 /* PFOAuth1FlowDialog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOAuth1FlowDialog.m; sourceTree = ""; }; + 81665C6E1BBDE27D00AE923F /* OAuth1FlowDialogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OAuth1FlowDialogTests.m; sourceTree = ""; }; + 81665C6F1BBDE27D00AE923F /* OAuthCoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OAuthCoreTests.m; sourceTree = ""; }; + 81665C701BBDE27D00AE923F /* TwitterAuthenticationProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TwitterAuthenticationProviderTests.m; sourceTree = ""; }; + 81665C711BBDE27D00AE923F /* TwitterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TwitterTests.m; sourceTree = ""; }; + 81665C721BBDE27D00AE923F /* TwitterUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TwitterUtilsTests.m; sourceTree = ""; }; + 81665C7A1BBDE2EE00AE923F /* PFTwitterTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterTestCase.h; sourceTree = ""; }; + 81665C7B1BBDE2EE00AE923F /* PFTwitterTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTwitterTestCase.m; sourceTree = ""; }; + 8166FB671B4F1D77003841A2 /* ParseTwitterTestApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ParseTwitterTestApplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8166FB8F1B4F1DC5003841A2 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 8166FB911B4F1DC5003841A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8178BEB21B716EB900051CF4 /* Common.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; + 8178BEB41B716EB900051CF4 /* iOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = iOS.xcconfig; sourceTree = ""; }; + 8178BEB51B716EB900051CF4 /* OSX.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = OSX.xcconfig; sourceTree = ""; }; + 8178BEB71B716EB900051CF4 /* Application.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Application.xcconfig; sourceTree = ""; }; + 8178BEB81B716EB900051CF4 /* Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Framework.xcconfig; sourceTree = ""; }; + 8178BEB91B716EB900051CF4 /* UnitTest.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UnitTest.xcconfig; sourceTree = ""; }; + 8178BEBB1B716EB900051CF4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 8178BEBC1B716EB900051CF4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8178BEBE1B716EB900051CF4 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 817A37C91B4B741A00129AFA /* PF_OAuthCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PF_OAuthCore.h; sourceTree = ""; }; + 817A37CA1B4B741A00129AFA /* PF_OAuthCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PF_OAuthCore.m; sourceTree = ""; }; + 81930A391BBDE76E00A5E4BB /* PFTwitterTestMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFTwitterTestMacros.h; sourceTree = ""; }; + 819DAAD41BB5EC79002BDE2B /* PFTwitterAlertView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTwitterAlertView.h; sourceTree = ""; }; + 819DAAD51BB5EC79002BDE2B /* PFTwitterAlertView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTwitterAlertView.m; sourceTree = ""; }; + 81B3F22A1AC9CA5300A92677 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = Localizable.strings; path = Resources/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 81CB98C61AB7905D00136FA5 /* ParseTwitterUtils-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ParseTwitterUtils-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81CB98D31AB7906D00136FA5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Resources/Info.plist; sourceTree = ""; }; + 81CB98DC1AB7921C00136FA5 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.2.sdk/usr/lib/libsqlite3.dylib; sourceTree = DEVELOPER_DIR; }; + 81CB98DE1AB7922600136FA5 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.2.sdk/System/Library/Frameworks/AudioToolbox.framework; sourceTree = DEVELOPER_DIR; }; + 81CB98E01AB7922C00136FA5 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.2.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; + 81D342A01B4C7DA500B6C124 /* ParseTwitterUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParseTwitterUtils.h; sourceTree = ""; }; + D2AAC07E0554694100DB518D /* ParseTwitterUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseTwitterUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F31BB2625FB261014BCD4FB6 /* libPods-ParseTwitterUtils-Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ParseTwitterUtils-Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F51535531B57453700C49F56 /* ParseTwitterUtils-iOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "ParseTwitterUtils-iOS.xcconfig"; sourceTree = ""; }; + F51535541B57454500C49F56 /* ParseTwitterUtils-Tests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "ParseTwitterUtils-Tests.xcconfig"; sourceTree = ""; }; + F51535551B57455200C49F56 /* ParseTwitterTestApplication.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ParseTwitterTestApplication.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8166FB641B4F1D77003841A2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81CB98C31AB7905D00136FA5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81CB98E11AB7922C00136FA5 /* SystemConfiguration.framework in Frameworks */, + 81CB98DF1AB7922600136FA5 /* AudioToolbox.framework in Frameworks */, + 81CB98DD1AB7921C00136FA5 /* libsqlite3.dylib in Frameworks */, + 81CB98CC1AB7905D00136FA5 /* ParseTwitterUtils.framework in Frameworks */, + 81CB98D81AB791FB00136FA5 /* Bolts.framework in Frameworks */, + 81CB98DB1AB7920E00136FA5 /* Parse.framework in Frameworks */, + 77DE89F60F51105C769BEC0B /* libPods-ParseTwitterUtils-Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2AAC07C0554694100DB518D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 813DFC951AB251F700F25A08 /* Parse.framework in Frameworks */, + 813DFC931AB2515A00F25A08 /* Bolts.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 034768DFFF38A50411DB9C8B /* Products */ = { + isa = PBXGroup; + children = ( + D2AAC07E0554694100DB518D /* ParseTwitterUtils.framework */, + 81CB98C61AB7905D00136FA5 /* ParseTwitterUtils-Tests.xctest */, + 8166FB671B4F1D77003841A2 /* ParseTwitterTestApplication.app */, + ); + name = Products; + sourceTree = ""; + }; + 0867D691FE84028FC02AAC07 /* Breakpad */ = { + isa = PBXGroup; + children = ( + F51535381B57451200C49F56 /* Configurations */, + 8135E48C1B4B6A0E0092F452 /* ParseTwitterUtils */, + 813DFC961AB2524C00F25A08 /* Resources */, + 81CB98D21AB7906D00136FA5 /* Tests */, + 0867D69AFE84028FC02AAC07 /* Frameworks */, + 034768DFFF38A50411DB9C8B /* Products */, + 6ADED1B195979E68C4289C24 /* Pods */, + ); + indentWidth = 4; + name = Breakpad; + sourceTree = ""; + }; + 0867D69AFE84028FC02AAC07 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 813DFC8F1AB2513D00F25A08 /* User Frameworks */, + 813DFC8E1AB2513300F25A08 /* System Frameworks */, + F31BB2625FB261014BCD4FB6 /* libPods-ParseTwitterUtils-Tests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6ADED1B195979E68C4289C24 /* Pods */ = { + isa = PBXGroup; + children = ( + ); + name = Pods; + sourceTree = ""; + }; + 8135E48C1B4B6A0E0092F452 /* ParseTwitterUtils */ = { + isa = PBXGroup; + children = ( + 8135E48D1B4B6A0E0092F452 /* Internal */, + 81D342A01B4C7DA500B6C124 /* ParseTwitterUtils.h */, + 8135E4931B4B6A0E0092F452 /* PFTwitterUtils.h */, + 8135E4941B4B6A0E0092F452 /* PFTwitterUtils.m */, + 8135E4911B4B6A0E0092F452 /* PF_Twitter.h */, + 8135E4921B4B6A0E0092F452 /* PF_Twitter.m */, + ); + path = ParseTwitterUtils; + sourceTree = ""; + }; + 8135E48D1B4B6A0E0092F452 /* Internal */ = { + isa = PBXGroup; + children = ( + 815F183D1B4B730E0066E996 /* Dialog */, + 817A37C81B4B741A00129AFA /* OAuthCore */, + 8135E48E1B4B6A0E0092F452 /* PF_Twitter_Private.h */, + 813E54A31BB5DDEF00C727E8 /* PFTwitterUtils_Private.h */, + 8135E48F1B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.h */, + 8135E4901B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.m */, + 813E54A51BB5E52000C727E8 /* PFTwitterPrivateUtilities.h */, + 813E54A61BB5E52000C727E8 /* PFTwitterPrivateUtilities.m */, + 819DAAD41BB5EC79002BDE2B /* PFTwitterAlertView.h */, + 819DAAD51BB5EC79002BDE2B /* PFTwitterAlertView.m */, + ); + path = Internal; + sourceTree = ""; + }; + 813DFC8E1AB2513300F25A08 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + 81CB98E01AB7922C00136FA5 /* SystemConfiguration.framework */, + 81CB98DE1AB7922600136FA5 /* AudioToolbox.framework */, + 81CB98DC1AB7921C00136FA5 /* libsqlite3.dylib */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; + 813DFC8F1AB2513D00F25A08 /* User Frameworks */ = { + isa = PBXGroup; + children = ( + 813DFC941AB251F700F25A08 /* Parse.framework */, + 813DFC921AB2515A00F25A08 /* Bolts.framework */, + ); + name = "User Frameworks"; + sourceTree = ""; + }; + 813DFC961AB2524C00F25A08 /* Resources */ = { + isa = PBXGroup; + children = ( + 81B3F22A1AC9CA5300A92677 /* Localizable.strings */, + 813DFC971AB2526000F25A08 /* third_party_licenses.txt */, + ); + name = Resources; + path = Classes; + sourceTree = ""; + }; + 815F183D1B4B730E0066E996 /* Dialog */ = { + isa = PBXGroup; + children = ( + 815F183E1B4B730E0066E996 /* PFOAuth1FlowDialog.h */, + 815F183F1B4B730E0066E996 /* PFOAuth1FlowDialog.m */, + ); + path = Dialog; + sourceTree = ""; + }; + 81665C6D1BBDE27D00AE923F /* Unit */ = { + isa = PBXGroup; + children = ( + 81665C6E1BBDE27D00AE923F /* OAuth1FlowDialogTests.m */, + 81665C6F1BBDE27D00AE923F /* OAuthCoreTests.m */, + 81665C701BBDE27D00AE923F /* TwitterAuthenticationProviderTests.m */, + 81665C711BBDE27D00AE923F /* TwitterTests.m */, + 81665C721BBDE27D00AE923F /* TwitterUtilsTests.m */, + ); + path = Unit; + sourceTree = ""; + }; + 81665C781BBDE2C000AE923F /* Other */ = { + isa = PBXGroup; + children = ( + 81665C791BBDE2EE00AE923F /* TestCase */, + 81930A391BBDE76E00A5E4BB /* PFTwitterTestMacros.h */, + ); + path = Other; + sourceTree = ""; + }; + 81665C791BBDE2EE00AE923F /* TestCase */ = { + isa = PBXGroup; + children = ( + 81665C7A1BBDE2EE00AE923F /* PFTwitterTestCase.h */, + 81665C7B1BBDE2EE00AE923F /* PFTwitterTestCase.m */, + ); + path = TestCase; + sourceTree = ""; + }; + 8166FB8D1B4F1DC5003841A2 /* TestApplication */ = { + isa = PBXGroup; + children = ( + 8166FB8E1B4F1DC5003841A2 /* Classes */, + 8166FB901B4F1DC5003841A2 /* Resources */, + ); + path = TestApplication; + sourceTree = ""; + }; + 8166FB8E1B4F1DC5003841A2 /* Classes */ = { + isa = PBXGroup; + children = ( + 8166FB8F1B4F1DC5003841A2 /* main.m */, + ); + path = Classes; + sourceTree = ""; + }; + 8166FB901B4F1DC5003841A2 /* Resources */ = { + isa = PBXGroup; + children = ( + 8166FB911B4F1DC5003841A2 /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 8178BEB11B716EB900051CF4 /* Shared */ = { + isa = PBXGroup; + children = ( + 8178BEB21B716EB900051CF4 /* Common.xcconfig */, + 8178BEB31B716EB900051CF4 /* Platform */, + 8178BEB61B716EB900051CF4 /* Product */, + 8178BEBA1B716EB900051CF4 /* Project */, + 8178BEBE1B716EB900051CF4 /* Warnings.xcconfig */, + ); + path = Shared; + sourceTree = ""; + }; + 8178BEB31B716EB900051CF4 /* Platform */ = { + isa = PBXGroup; + children = ( + 8178BEB41B716EB900051CF4 /* iOS.xcconfig */, + 8178BEB51B716EB900051CF4 /* OSX.xcconfig */, + ); + path = Platform; + sourceTree = ""; + }; + 8178BEB61B716EB900051CF4 /* Product */ = { + isa = PBXGroup; + children = ( + 8178BEB71B716EB900051CF4 /* Application.xcconfig */, + 8178BEB81B716EB900051CF4 /* Framework.xcconfig */, + 8178BEB91B716EB900051CF4 /* UnitTest.xcconfig */, + ); + path = Product; + sourceTree = ""; + }; + 8178BEBA1B716EB900051CF4 /* Project */ = { + isa = PBXGroup; + children = ( + 8178BEBB1B716EB900051CF4 /* Debug.xcconfig */, + 8178BEBC1B716EB900051CF4 /* Release.xcconfig */, + ); + path = Project; + sourceTree = ""; + }; + 817A37C81B4B741A00129AFA /* OAuthCore */ = { + isa = PBXGroup; + children = ( + 817A37C91B4B741A00129AFA /* PF_OAuthCore.h */, + 817A37CA1B4B741A00129AFA /* PF_OAuthCore.m */, + ); + path = OAuthCore; + sourceTree = ""; + }; + 81CB98D21AB7906D00136FA5 /* Tests */ = { + isa = PBXGroup; + children = ( + 81665C6D1BBDE27D00AE923F /* Unit */, + 81665C781BBDE2C000AE923F /* Other */, + 81CB98D71AB7907500136FA5 /* Resources */, + 8166FB8D1B4F1DC5003841A2 /* TestApplication */, + ); + path = Tests; + sourceTree = ""; + }; + 81CB98D71AB7907500136FA5 /* Resources */ = { + isa = PBXGroup; + children = ( + 81CB98D31AB7906D00136FA5 /* Info.plist */, + ); + name = Resources; + sourceTree = ""; + }; + F51535381B57451200C49F56 /* Configurations */ = { + isa = PBXGroup; + children = ( + 8178BEB11B716EB900051CF4 /* Shared */, + F51535531B57453700C49F56 /* ParseTwitterUtils-iOS.xcconfig */, + F51535541B57454500C49F56 /* ParseTwitterUtils-Tests.xcconfig */, + F51535551B57455200C49F56 /* ParseTwitterTestApplication.xcconfig */, + ); + path = Configurations; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D2AAC07A0554694100DB518D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 815F18401B4B730E0066E996 /* PFOAuth1FlowDialog.h in Headers */, + 817A37CB1B4B741A00129AFA /* PF_OAuthCore.h in Headers */, + 813E54A41BB5DDEF00C727E8 /* PFTwitterUtils_Private.h in Headers */, + 81D342A11B4C7DA500B6C124 /* ParseTwitterUtils.h in Headers */, + 813E54AA1BB5E5FF00C727E8 /* PFTwitterPrivateUtilities.h in Headers */, + 8135E49A1B4B6A0E0092F452 /* PFTwitterUtils.h in Headers */, + 8135E4951B4B6A0E0092F452 /* PF_Twitter_Private.h in Headers */, + 8135E4961B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.h in Headers */, + 819DAAD61BB5EC79002BDE2B /* PFTwitterAlertView.h in Headers */, + 8135E4981B4B6A0E0092F452 /* PF_Twitter.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8166FB661B4F1D77003841A2 /* ParseTwitterTestApplication */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8166FB8B1B4F1D77003841A2 /* Build configuration list for PBXNativeTarget "ParseTwitterTestApplication" */; + buildPhases = ( + 8166FB631B4F1D77003841A2 /* Sources */, + 8166FB641B4F1D77003841A2 /* Frameworks */, + 8166FB651B4F1D77003841A2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ParseTwitterTestApplication; + productName = ParseTwitterTestApplication; + productReference = 8166FB671B4F1D77003841A2 /* ParseTwitterTestApplication.app */; + productType = "com.apple.product-type.application"; + }; + 81CB98C51AB7905D00136FA5 /* ParseTwitterUtils-Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 81CB98D11AB7905D00136FA5 /* Build configuration list for PBXNativeTarget "ParseTwitterUtils-Tests" */; + buildPhases = ( + 26730F125F6A472AD0747C6D /* Check Pods Manifest.lock */, + 81CB98C21AB7905D00136FA5 /* Sources */, + 81CB98C31AB7905D00136FA5 /* Frameworks */, + 81CB98C41AB7905D00136FA5 /* Resources */, + 4D5B07C5EAC8A041D08E10C6 /* Embed Pods Frameworks */, + 7A5233E6AC771DC4EC9B4146 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 81CB98CE1AB7905D00136FA5 /* PBXTargetDependency */, + 8166FB951B4F1E9A003841A2 /* PBXTargetDependency */, + ); + name = "ParseTwitterUtils-Tests"; + productName = "ParseFacebookUtilsV4-Tests"; + productReference = 81CB98C61AB7905D00136FA5 /* ParseTwitterUtils-Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2AAC07D0554694100DB518D /* ParseTwitterUtils-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "ParseTwitterUtils-iOS" */; + buildPhases = ( + 81F035F41BC311FA0055BFDE /* Fetch latest Parse.framework */, + 81B3F2291AC9CA2600A92677 /* Generate Localizable Strings */, + D2AAC07A0554694100DB518D /* Headers */, + D2AAC07B0554694100DB518D /* Sources */, + D2AAC07C0554694100DB518D /* Frameworks */, + 8139B1341A7BF6B5002BEF84 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ParseTwitterUtils-iOS"; + productName = Breakpad; + productReference = D2AAC07E0554694100DB518D /* ParseTwitterUtils.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0867D690FE84028FC02AAC07 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = PF; + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Parse, LLC"; + TargetAttributes = { + 8166FB661B4F1D77003841A2 = { + CreatedOnToolsVersion = 6.4; + }; + 81CB98C51AB7905D00136FA5 = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 8166FB661B4F1D77003841A2; + }; + }; + }; + buildConfigurationList = 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "ParseTwitterUtils" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + da, + de, + es, + fr, + it, + ja, + nl, + no, + sl, + sv, + tr, + en, + Base, + ); + mainGroup = 0867D691FE84028FC02AAC07 /* Breakpad */; + productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2AAC07D0554694100DB518D /* ParseTwitterUtils-iOS */, + 81CB98C51AB7905D00136FA5 /* ParseTwitterUtils-Tests */, + 8166FB661B4F1D77003841A2 /* ParseTwitterTestApplication */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8139B1341A7BF6B5002BEF84 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 813DFC981AB2526000F25A08 /* third_party_licenses.txt in Resources */, + 81B3F22B1AC9CA5300A92677 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8166FB651B4F1D77003841A2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81CB98C41AB7905D00136FA5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 26730F125F6A472AD0747C6D /* 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; + }; + 4D5B07C5EAC8A041D08E10C6 /* 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-ParseTwitterUtils-Tests/Pods-ParseTwitterUtils-Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7A5233E6AC771DC4EC9B4146 /* 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-ParseTwitterUtils-Tests/Pods-ParseTwitterUtils-Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 81B3F2291AC9CA2600A92677 /* Generate Localizable Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Generate Localizable Strings"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Generate localizable strings\nfind $PROJECT_DIR -name '*.m' -print0 | xargs -0 genstrings -q -o $PROJECT_DIR/Resources\necho \"Finished converting images\""; + }; + 81F035F41BC311FA0055BFDE /* Fetch latest Parse.framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Fetch latest Parse.framework"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ ! -d $SRCROOT/Vendor ]; then\n mkdir $SRCROOT/Vendor\nfi\n\ncd $SRCROOT/Vendor\n\nif [[ ! -d \"Parse.framework\" || ! -d \"Bolts.framework\" ]]; then\n ARCHIVE_NAME=Parse-iOS.zip\n\n LATEST_TAG=$(bash -l -c \"bundle exec pod spec cat Parse\" | grep version | head -n 1 | cut -d '\"' -f 4)\n ARCHIVE_URL=\"https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/releases/download/${LATEST_TAG}/${ARCHIVE_NAME}\"\n curl -O -L $ARCHIVE_URL\n\n unzip $ARCHIVE_NAME\n rm $ARCHIVE_NAME\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8166FB631B4F1D77003841A2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8166FB921B4F1DC5003841A2 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81CB98C21AB7905D00136FA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81665C751BBDE27D00AE923F /* TwitterAuthenticationProviderTests.m in Sources */, + 81665C741BBDE27D00AE923F /* OAuthCoreTests.m in Sources */, + 81665C731BBDE27D00AE923F /* OAuth1FlowDialogTests.m in Sources */, + 81665C771BBDE27D00AE923F /* TwitterUtilsTests.m in Sources */, + 81665C761BBDE27D00AE923F /* TwitterTests.m in Sources */, + 81665C7C1BBDE2EE00AE923F /* PFTwitterTestCase.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2AAC07B0554694100DB518D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8135E4971B4B6A0E0092F452 /* PFTwitterAuthenticationProvider.m in Sources */, + 8135E49B1B4B6A0E0092F452 /* PFTwitterUtils.m in Sources */, + 8135E4991B4B6A0E0092F452 /* PF_Twitter.m in Sources */, + 815F18411B4B730E0066E996 /* PFOAuth1FlowDialog.m in Sources */, + 813E54A91BB5E5FA00C727E8 /* PFTwitterPrivateUtilities.m in Sources */, + 817A37CC1B4B741A00129AFA /* PF_OAuthCore.m in Sources */, + 819DAAD71BB5EC79002BDE2B /* PFTwitterAlertView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8166FB951B4F1E9A003841A2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8166FB661B4F1D77003841A2 /* ParseTwitterTestApplication */; + targetProxy = 8166FB941B4F1E9A003841A2 /* PBXContainerItemProxy */; + }; + 81CB98CE1AB7905D00136FA5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2AAC07D0554694100DB518D /* ParseTwitterUtils-iOS */; + targetProxy = 81CB98CD1AB7905D00136FA5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 1DEB921F08733DC00010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535531B57453700C49F56 /* ParseTwitterUtils-iOS.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 1DEB922008733DC00010E9CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535531B57453700C49F56 /* ParseTwitterUtils-iOS.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 1DEB922308733DC00010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8178BEBB1B716EB900051CF4 /* Debug.xcconfig */; + buildSettings = { + PARSE_DIR = "$(PROJECT_DIR)/.."; + }; + name = Debug; + }; + 1DEB922408733DC00010E9CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8178BEBC1B716EB900051CF4 /* Release.xcconfig */; + buildSettings = { + PARSE_DIR = "$(PROJECT_DIR)/.."; + }; + name = Release; + }; + 8166FB871B4F1D77003841A2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535551B57455200C49F56 /* ParseTwitterTestApplication.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 8166FB881B4F1D77003841A2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535551B57455200C49F56 /* ParseTwitterTestApplication.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 81CB98CF1AB7905D00136FA5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535541B57454500C49F56 /* ParseTwitterUtils-Tests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + }; + name = Debug; + }; + 81CB98D01AB7905D00136FA5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F51535541B57454500C49F56 /* ParseTwitterUtils-Tests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "ParseTwitterUtils-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB921F08733DC00010E9CD /* Debug */, + 1DEB922008733DC00010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "ParseTwitterUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB922308733DC00010E9CD /* Debug */, + 1DEB922408733DC00010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8166FB8B1B4F1D77003841A2 /* Build configuration list for PBXNativeTarget "ParseTwitterTestApplication" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8166FB871B4F1D77003841A2 /* Debug */, + 8166FB881B4F1D77003841A2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81CB98D11AB7905D00136FA5 /* Build configuration list for PBXNativeTarget "ParseTwitterUtils-Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81CB98CF1AB7905D00136FA5 /* Debug */, + 81CB98D01AB7905D00136FA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0867D690FE84028FC02AAC07 /* Project object */; +} diff --git a/ParseTwitterUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ParseTwitterUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..f1755b4 --- /dev/null +++ b/ParseTwitterUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ParseTwitterUtils.xcodeproj/xcshareddata/xcschemes/ParseTwitterUtils-iOS.xcscheme b/ParseTwitterUtils.xcodeproj/xcshareddata/xcschemes/ParseTwitterUtils-iOS.xcscheme new file mode 100644 index 0000000..3393d2b --- /dev/null +++ b/ParseTwitterUtils.xcodeproj/xcshareddata/xcschemes/ParseTwitterUtils-iOS.xcscheme @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseTwitterUtils.xcworkspace/contents.xcworkspacedata b/ParseTwitterUtils.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..8e37bef --- /dev/null +++ b/ParseTwitterUtils.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.h b/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.h new file mode 100644 index 0000000..e8e6354 --- /dev/null +++ b/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.h @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@class PFOAuth1FlowDialog; + +@protocol PFOAuth1FlowDialogDataSource + +/*! + Asks if a link touched by a user should be opened in an external browser. + + If a user touches a link, the default behavior is to open the link in the Safari browser, + which will cause your app to quit. You may want to prevent this from happening, open the link + in your own internal browser, or perhaps warn the user that they are about to leave your app. + If so, implement this method on your delegate and return NO. If you warn the user, you + should hold onto the URL and once you have received their acknowledgement open the URL yourself + using [[UIApplication sharedApplication] openURL:]. + */ +- (BOOL)dialog:(PFOAuth1FlowDialog *)dialog shouldOpenURLInExternalBrowser:(NSURL *)url; + +@end + +typedef void (^PFOAuth1FlowDialogCompletion)(BOOL succeeded, NSURL *url, NSError *error); + +/*! + To allow for greater mockability, this protocol exposes all of the methods implemented by PFOAuth1FlowDialog. + */ +@protocol PFOAuth1FlowDialogInterface + +@property (nonatomic, weak) id dataSource; +@property (nonatomic, strong) PFOAuth1FlowDialogCompletion completion; + +@property (nonatomic, copy) NSDictionary *queryParameters; +@property (nonatomic, copy) NSString *redirectURLPrefix; + +/*! + The title that is shown in the header atop the view. + */ +@property (nonatomic, copy) NSString *title; + ++ (instancetype)dialogWithURL:(NSURL *)url queryParameters:(NSDictionary *)queryParameters; + +/*! + The view will be added to the top of the current key window. + */ +- (void)showAnimated:(BOOL)animated; + +/*! + Hides the view. + This method does not call the completion block. + */ +- (void)dismissAnimated:(BOOL)animated; + +/*! + Displays a URL in the dialog. + */ +- (void)loadURL:(NSURL *)url queryParameters:(NSDictionary *)parameters; + +@end + +@interface PFOAuth1FlowDialog : UIView { +@public + // Ensures that UI elements behind the dialog are disabled. + UIView *_modalBackgroundView; + + NSURL *_baseURL; + NSURL *_loadingURL; + + UILabel *_titleLabel; + UIButton *_closeButton; + UIWebView *_webView; + UIActivityIndicatorView *_activityIndicator; + + UIInterfaceOrientation _orientation; + BOOL _showingKeyboard; +} + +- (instancetype)initWithURL:(NSURL *)url queryParameters:(NSDictionary *)parameters; + +@end diff --git a/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.m b/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.m new file mode 100644 index 0000000..46fd168 --- /dev/null +++ b/ParseTwitterUtils/Internal/Dialog/PFOAuth1FlowDialog.m @@ -0,0 +1,585 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOAuth1FlowDialog.h" + +#import + +@implementation PFOAuth1FlowDialog + +@synthesize dataSource = _dataSource; +@synthesize completion = _completion; + +@synthesize queryParameters = _queryParameters; +@synthesize redirectURLPrefix = _redirectURLPrefix; + +static NSString *const PFOAuth1FlowDialogDefaultTitle = @"Connect to Service"; + +static const CGFloat PFOAuth1FlowDialogBorderGreyColorComponents[4] = {0.3f, 0.3f, 0.3f, 0.8f}; +static const CGFloat PFOAuth1FlowDialogBorderBlackColorComponents[4] = {0.3f, 0.3f, 0.3f, 1.0f}; + +static const NSTimeInterval PFOAuth1FlowDialogAnimationDuration = 0.3; + +static const UIEdgeInsets PFOAuth1FlowDialogContentInsets = { + .top = 10.0f, + .left = 10.0f, + .bottom = 10.0f, + .right = 10.0f, +}; + +static const UIEdgeInsets PFOAuth1FlowDialogTitleInsets = {.top = 4.0f, .left = 8.0f, .bottom = 4.0f, .right = 8.0f}; + +static const CGFloat PFOAuth1FlowDialogScreenInset = 10.0f; + +static BOOL PFOAuth1FlowDialogScreenHasAutomaticRotation() { + static BOOL automaticRotation; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + automaticRotation = [[UIScreen mainScreen] respondsToSelector:NSSelectorFromString(@"coordinateSpace")]; + }); + return automaticRotation; +} + +static BOOL PFOAuth1FlowDialogIsDevicePad() { + static BOOL isPad; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + isPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); + }); + return isPad; +} + +#pragma mark - +#pragma mark Class + ++ (void)_fillRect:(CGRect)rect withColorComponents:(const CGFloat *)colorComponents radius:(CGFloat)radius { + CGContextRef context = UIGraphicsGetCurrentContext(); + + if (colorComponents) { + CGContextSaveGState(context); + CGContextSetFillColor(context, colorComponents); + if (radius != 0.0f) { + UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius]; + CGContextAddPath(context, [bezierPath CGPath]); + CGContextFillPath(context); + } else { + CGContextFillRect(context, rect); + } + CGContextRestoreGState(context); + } +} + ++ (void)_strokeRect:(CGRect)rect withColorComponents:(const CGFloat *)strokeColor { + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + { + CGContextSetStrokeColor(context, strokeColor); + CGContextSetLineWidth(context, 1.0f); + CGContextStrokeRect(context, rect); + } + CGContextRestoreGState(context); +} + ++ (NSURL *)_urlFromBaseURL:(NSURL *)baseURL queryParameters:(NSDictionary *)params { + if ([params count] > 0) { + NSMutableArray *parameterPairs = [NSMutableArray array]; + [params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFStringRef escapedString = CFURLCreateStringByAddingPercentEscapes( + NULL, /* allocator */ + (CFStringRef)obj, + NULL, /* charactersToLeaveUnescaped */ + (CFStringRef)@"!*'();:@&=+$,/?%#[]", + kCFStringEncodingUTF8); + [parameterPairs addObject:[NSString stringWithFormat:@"%@=%@", key, CFBridgingRelease(escapedString)]]; + }]; + + NSString *query = [parameterPairs componentsJoinedByString:@"&"]; + NSString *url = [NSString stringWithFormat:@"%@?%@", [baseURL absoluteString], query]; + + return [NSURL URLWithString:url]; + } + + return baseURL; +} + ++ (UIImage *)_closeButtonImage { + CGRect imageRect = CGRectZero; + imageRect.size = CGSizeMake(30.0f, 30.0f); + + UIGraphicsBeginImageContextWithOptions(imageRect.size, NO, 0.0f); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGRect outerRingRect = CGRectInset(imageRect, 2.0f, 2.0f); + + [[UIColor whiteColor] set]; + CGContextFillEllipseInRect(context, outerRingRect); + + CGRect innerRingRect = CGRectInset(outerRingRect, 2.0f, 2.0f); + + [[UIColor blackColor] set]; + CGContextFillEllipseInRect(context, innerRingRect); + + CGRect crossRect = CGRectInset(innerRingRect, 6.0f, 6.0f); + + CGContextBeginPath(context); + + [[UIColor whiteColor] setStroke]; + CGContextSetLineWidth(context, 3.0f); + + CGContextMoveToPoint(context, CGRectGetMinX(crossRect), CGRectGetMinY(crossRect)); + CGContextAddLineToPoint(context, CGRectGetMaxX(crossRect), CGRectGetMaxY(crossRect)); + + CGContextMoveToPoint(context, CGRectGetMaxX(crossRect), CGRectGetMinY(crossRect)); + CGContextAddLineToPoint(context, CGRectGetMinX(crossRect), CGRectGetMaxY(crossRect)); + + CGContextStrokePath(context); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +#pragma mark - +#pragma mark Init + +- (instancetype)init { + self = [super initWithFrame:CGRectZero]; + if (self) { + self.backgroundColor = [UIColor clearColor]; + self.autoresizesSubviews = YES; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.contentMode = UIViewContentModeRedraw; + + _orientation = UIInterfaceOrientationPortrait; + + _closeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _closeButton.showsTouchWhenHighlighted = YES; + _closeButton.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin); + [_closeButton setImage:[[self class] _closeButtonImage] forState:UIControlStateNormal]; + [_closeButton addTarget:self + action:@selector(_cancelButtonAction) + forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_closeButton]; + + CGFloat titleLabelFontSize = (PFOAuth1FlowDialogIsDevicePad() ? 18.0f : 14.0f); + _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + _titleLabel.backgroundColor = [UIColor clearColor]; + _titleLabel.text = PFOAuth1FlowDialogDefaultTitle; + _titleLabel.textColor = [UIColor whiteColor]; + _titleLabel.font = [UIFont boldSystemFontOfSize:titleLabelFontSize]; + [self addSubview:_titleLabel]; + + _webView = [[UIWebView alloc] initWithFrame:CGRectZero]; + _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _webView.delegate = self; + [self addSubview:_webView]; + + _activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: + UIActivityIndicatorViewStyleWhiteLarge]; + _activityIndicator.color = [UIColor grayColor]; + _activityIndicator.autoresizingMask = (UIViewAutoresizingFlexibleTopMargin | + UIViewAutoresizingFlexibleBottomMargin | + UIViewAutoresizingFlexibleLeftMargin | + UIViewAutoresizingFlexibleRightMargin); + [self addSubview:_activityIndicator]; + + _modalBackgroundView = [[UIView alloc] init]; + } + return self; +} + +- (instancetype)initWithURL:(NSURL *)url queryParameters:(NSDictionary *)parameters { + self = [self init]; + if (self) { + _baseURL = url; + _queryParameters = [parameters mutableCopy]; + } + return self; +} + ++ (instancetype)dialogWithURL:(NSURL *)url queryParameters:(NSDictionary *)queryParameters { + return [[self alloc] initWithURL:url queryParameters:queryParameters]; +} + +#pragma mark - +#pragma mark Dealloc + +- (void)dealloc { + _webView.delegate = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - +#pragma mark UIView + +- (void)drawRect:(CGRect)rect { + [super drawRect:rect]; + + [[self class] _fillRect:self.bounds withColorComponents:PFOAuth1FlowDialogBorderGreyColorComponents radius:10.0f]; + [[self class] _strokeRect:_webView.frame withColorComponents:PFOAuth1FlowDialogBorderBlackColorComponents]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + const CGRect bounds = self.bounds; + const CGRect contentRect = UIEdgeInsetsInsetRect(bounds, PFOAuth1FlowDialogContentInsets); + + CGRect titleLabelBoundingRect = UIEdgeInsetsInsetRect(contentRect, PFOAuth1FlowDialogTitleInsets); + CGSize titleLabelSize = [_titleLabel sizeThatFits:titleLabelBoundingRect.size]; + titleLabelBoundingRect.size.width = (CGRectGetMaxX(contentRect) - + PFOAuth1FlowDialogTitleInsets.right - + titleLabelSize.height); + titleLabelSize = [_titleLabel sizeThatFits:titleLabelBoundingRect.size]; + + CGRect titleLabelFrame = titleLabelBoundingRect; + titleLabelFrame.size.height = titleLabelSize.height; + titleLabelFrame.size.width = CGRectGetWidth(titleLabelBoundingRect); + _titleLabel.frame = titleLabelFrame; + + CGRect closeButtonFrame = contentRect; + closeButtonFrame.size.height = (CGRectGetHeight(titleLabelFrame) + + PFOAuth1FlowDialogTitleInsets.top + + PFOAuth1FlowDialogTitleInsets.bottom); + closeButtonFrame.size.width = CGRectGetHeight(closeButtonFrame); + closeButtonFrame.origin.x = CGRectGetMaxX(contentRect) - CGRectGetWidth(closeButtonFrame); + _closeButton.frame = closeButtonFrame; + + CGRect webViewFrame = contentRect; + if (!_showingKeyboard || PFOAuth1FlowDialogIsDevicePad() || UIInterfaceOrientationIsPortrait(_orientation)) { + webViewFrame.origin.y = CGRectGetMaxY(titleLabelFrame) + PFOAuth1FlowDialogTitleInsets.bottom; + webViewFrame.size.height = CGRectGetMaxY(contentRect) - CGRectGetMinY(webViewFrame); + } + _webView.frame = webViewFrame; + + [_activityIndicator sizeToFit]; + _activityIndicator.center = _webView.center; +} + +#pragma mark - +#pragma mark Accessors + +- (NSString *)title { + return _titleLabel.text; +} + +- (void)setTitle:(NSString *)title { + _titleLabel.text = title; + + [self setNeedsLayout]; +} + +#pragma mark - +#pragma mark Present / Dismiss + +- (void)showAnimated:(BOOL)animated { + [self load]; + [self _sizeToFitOrientation]; + + [_activityIndicator startAnimating]; + + UIWindow *window = [UIApplication sharedApplication].keyWindow; + _modalBackgroundView.frame = window.bounds; + [_modalBackgroundView addSubview:self]; + [window addSubview:_modalBackgroundView]; + + CGAffineTransform transform = [self _transformForOrientation:_orientation]; + if (animated) { + self.transform = CGAffineTransformScale(transform, 0.001f, 0.001f); + + NSTimeInterval animationStepDuration = PFOAuth1FlowDialogAnimationDuration / 2.0f; + + [UIView animateWithDuration:animationStepDuration + animations:^{ + self.transform = CGAffineTransformScale(transform, 1.1f, 1.1f); + } + completion:^(BOOL finished) { + [UIView animateWithDuration:animationStepDuration + animations:^{ + self.transform = CGAffineTransformScale(transform, 0.9f, 0.9f); + } + completion:^(BOOL finished) { + [UIView animateWithDuration:animationStepDuration + animations:^{ + self.transform = transform; + }]; + }]; + + }]; + } else { + self.transform = transform; + } + + [self _addObservers]; +} + +- (void)dismissAnimated:(BOOL)animated { + _loadingURL = nil; + + __weak typeof(self) wself = self; + dispatch_block_t completionBlock = ^{ + __strong typeof(wself) sself = wself; + [sself _removeObservers]; + [sself removeFromSuperview]; + [_modalBackgroundView removeFromSuperview]; + }; + + if (animated) { + [UIView animateWithDuration:PFOAuth1FlowDialogAnimationDuration + animations:^{ + typeof(wself) sself = wself; + sself.alpha = 0.0f; + } + completion:^(BOOL finished) { + completionBlock(); + }]; + } else { + completionBlock(); + } +} + +- (void)_dismissWithSuccess:(BOOL)success url:(NSURL *)url error:(NSError *)error { + if (!self.completion) { + return; + } + + PFOAuth1FlowDialogCompletion completion = self.completion; + self.completion = nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + completion(success, url, error); + }); + + [self dismissAnimated:YES]; +} + +- (void)_cancelButtonAction { + [self _dismissWithSuccess:NO url:nil error:nil]; +} + +#pragma mark - +#pragma mark Public + +- (void)load { + [self loadURL:_baseURL queryParameters:self.queryParameters]; +} + +- (void)loadURL:(NSURL *)url queryParameters:(NSDictionary *)parameters { + _loadingURL = [[self class] _urlFromBaseURL:url queryParameters:parameters]; + + NSURLRequest *request = [NSURLRequest requestWithURL:_loadingURL]; + [_webView loadRequest:request]; +} + +#pragma mark - +#pragma mark UIWebViewDelegate + +- (BOOL)webView:(UIWebView *)webView +shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + NSURL *url = request.URL; + + if ([url.absoluteString hasPrefix:self.redirectURLPrefix]) { + [self _dismissWithSuccess:YES url:url error:nil]; + return NO; + } else if ([_loadingURL isEqual:url]) { + return YES; + } else if (navigationType == UIWebViewNavigationTypeLinkClicked) { + if ([self.dataSource dialog:self shouldOpenURLInExternalBrowser:url]) { + [[UIApplication sharedApplication] openURL:url]; + } else { + return YES; + } + } + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView *)webView { + [[PFNetworkActivityIndicatorManager sharedManager] incrementActivityCount]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + [[PFNetworkActivityIndicatorManager sharedManager] decrementActivityCount]; + + [_activityIndicator stopAnimating]; + self.title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + [[PFNetworkActivityIndicatorManager sharedManager] decrementActivityCount]; + + // 102 == WebKitErrorFrameLoadInterruptedByPolicyChange + if (!([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102)) { + [self _dismissWithSuccess:NO url:nil error:error]; + } +} + +#pragma mark - +#pragma mark Observers + +- (void)_addObservers { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_deviceOrientationDidChange:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; +} + +- (void)_removeObservers { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIDeviceOrientationDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIKeyboardWillHideNotification + object:nil]; +} + +#pragma mark - +#pragma mark UIDeviceOrientationDidChangeNotification + +- (void)_deviceOrientationDidChange:(NSNotification *)notification { + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if ([self _shouldRotateToOrientation:orientation]) { + CGFloat duration = [UIApplication sharedApplication].statusBarOrientationAnimationDuration; + [UIView animateWithDuration:duration + animations:^{ + [self _sizeToFitOrientation]; + }]; + } +} + +- (BOOL)_shouldRotateToOrientation:(UIInterfaceOrientation)orientation { + if (orientation == _orientation) { + return NO; + } + + return (orientation == UIDeviceOrientationLandscapeLeft || + orientation == UIDeviceOrientationLandscapeRight || + orientation == UIDeviceOrientationPortrait || + orientation == UIDeviceOrientationPortraitUpsideDown); +} + +- (CGAffineTransform)_transformForOrientation:(UIInterfaceOrientation)orientation { + // No manual rotation required on iOS 8 + // There is coordinateSpace method, since iOS 8 + if (PFOAuth1FlowDialogScreenHasAutomaticRotation()) { + return CGAffineTransformIdentity; + } + + switch (orientation) { + case UIInterfaceOrientationLandscapeLeft: + return CGAffineTransformMakeRotation((CGFloat)(-M_PI / 2.0f)); + break; + case UIInterfaceOrientationLandscapeRight: + return CGAffineTransformMakeRotation((CGFloat)(M_PI / 2.0f)); + break; + case UIInterfaceOrientationPortraitUpsideDown: + return CGAffineTransformMakeRotation((CGFloat)-M_PI); + break; + default: + break; + } + + return CGAffineTransformIdentity; +} + +- (void)_sizeToFitOrientation { + _orientation = [UIApplication sharedApplication].statusBarOrientation; + CGAffineTransform transform = [self _transformForOrientation:_orientation]; + + CGRect bounds = [UIScreen mainScreen].applicationFrame; + CGRect transformedBounds = CGRectApplyAffineTransform(bounds, transform); + transformedBounds.origin = CGPointZero; + + CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); + + CGFloat scale = (PFOAuth1FlowDialogIsDevicePad() ? 0.6f : 1.0f); + + CGFloat width = floor(scale * CGRectGetWidth(transformedBounds)) - PFOAuth1FlowDialogScreenInset * 2.0f; + CGFloat height = floor(scale * CGRectGetHeight(transformedBounds)) - PFOAuth1FlowDialogScreenInset * 2.0f; + + self.transform = transform; + self.center = center; + self.bounds = CGRectMake(0.0f, 0.0f, width, height); + + [self setNeedsLayout]; +} + +#pragma mark - +#pragma mark UIKeyboardNotifications + +- (void)_keyboardWillShow:(NSNotification *)notification { + _showingKeyboard = YES; + + if (PFOAuth1FlowDialogIsDevicePad()) { + // On the iPad the screen is large enough that we don't need to + // resize the dialog to accomodate the keyboard popping up + return; + } + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(orientation)) { + NSDictionary *userInfo = [notification userInfo]; + NSTimeInterval animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + UIViewAnimationCurve animationCurve = [[notification userInfo][UIKeyboardAnimationCurveUserInfoKey] intValue]; + + [UIView animateWithDuration:animationDuration + delay:0.0 + options:animationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState + animations:^{ + [self setNeedsLayout]; + [self layoutIfNeeded]; + + [self setNeedsDisplay]; + } + completion:nil]; + } +} + +- (void)_keyboardWillHide:(NSNotification *)notification { + _showingKeyboard = NO; + + if (PFOAuth1FlowDialogIsDevicePad()) { + return; + } + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + if (UIInterfaceOrientationIsLandscape(orientation)) { + NSDictionary *userInfo = [notification userInfo]; + NSTimeInterval animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + UIViewAnimationCurve animationCurve = [[notification userInfo][UIKeyboardAnimationCurveUserInfoKey] intValue]; + + [UIView animateWithDuration:animationDuration + delay:0.0 + options:animationCurve << 16 | UIViewAnimationOptionBeginFromCurrentState + animations:^{ + [self setNeedsLayout]; + [self layoutIfNeeded]; + + [self setNeedsDisplay]; + } + completion:nil]; + } +} + +@end diff --git a/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.h b/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.h new file mode 100644 index 0000000..a44f469 --- /dev/null +++ b/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFOAuthConfiguration : NSObject + +@property (nonatomic, strong) NSURL *url; +@property (nonatomic, copy) NSString *method; +@property (nullable, nonatomic, strong) NSData *body; + +@property (nonatomic, copy) NSString *consumerKey; +@property (nonatomic, copy) NSString *consumerSecret; + +@property (nullable, nonatomic, copy) NSString *token; +@property (nullable, nonatomic, copy) NSString *tokenSecret; + +@property (nullable, nonatomic, copy) NSDictionary *additionalParameters; + +@property (nonatomic, copy) NSString *nonce; +@property (nonatomic, strong) NSDate *timestampDate; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + ++ (instancetype)configurationForURL:(NSURL *)url + method:(NSString *)method + body:(nullable NSData *)body + additionalParameters:(nullable NSDictionary *)additionalParams + consumerKey:(NSString *)consumerKey + consumerSecret:(NSString *)consumerSecret + token:(nullable NSString *)token + tokenSecret:(nullable NSString *)tokenSecret; + +@end + +@interface PFOAuth : NSObject + ++ (NSString *)authorizationHeaderFromConfiguration:(PFOAuthConfiguration *)configuration; + +@end + +@interface NSURL (PF_OAuthAdditions) + ++ (NSDictionary *)PF_ab_parseURLQueryString:(NSString *)query; + +@end + +@interface NSString (PF_OAuthAdditions) + +- (NSString *)PF_ab_RFC3986EncodedString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.m b/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.m new file mode 100644 index 0000000..b56ca95 --- /dev/null +++ b/ParseTwitterUtils/Internal/OAuthCore/PF_OAuthCore.m @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PF_OAuthCore.h" + +#import + +static NSData *PF_HMAC_SHA1(NSString *data, NSString *key) { + unsigned char buf[CC_SHA1_DIGEST_LENGTH]; + CCHmac(kCCHmacAlgSHA1, [key UTF8String], [key length], [data UTF8String], [data length], buf); + return [NSData dataWithBytes:buf length:CC_SHA1_DIGEST_LENGTH]; +} + +@implementation PFOAuthConfiguration + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _nonce = [[NSUUID UUID] UUIDString]; + _timestampDate = [NSDate date]; + + return self; +} + ++ (instancetype)configurationForURL:(NSURL *)url + method:(NSString *)method + body:(nullable NSData *)body + additionalParameters:(nullable NSDictionary *)additionalParams + consumerKey:(NSString *)consumerKey + consumerSecret:(NSString *)consumerSecret + token:(nullable NSString *)token + tokenSecret:(nullable NSString *)tokenSecret { + PFOAuthConfiguration *configuration = [[self alloc] init]; + configuration.url = url; + configuration.method = method; + configuration.body = body; + configuration.additionalParameters = additionalParams; + configuration.consumerKey = consumerKey; + configuration.consumerSecret = consumerSecret; + configuration.token = token; + configuration.tokenSecret = tokenSecret; + return configuration; +} + +@end + +@implementation PFOAuth + ++ (NSString *)authorizationHeaderFromConfiguration:(PFOAuthConfiguration *)configuration { + NSString *authTimeStamp = [NSString stringWithFormat:@"%llu", + (unsigned long long)floor([configuration.timestampDate timeIntervalSince1970])]; + NSString *authSignatureMethod = @"HMAC-SHA1"; + NSString *authVersion = @"1.0"; + NSURL *url = configuration.url; + + // Don't use -mutableCopy here, as that will return nil if `additionalParams` is nil. + NSMutableDictionary *oAuthAuthorizationParameters = [NSMutableDictionary dictionaryWithDictionary:configuration.additionalParameters]; + + oAuthAuthorizationParameters[@"oauth_nonce"] = configuration.nonce; + oAuthAuthorizationParameters[@"oauth_timestamp"] = authTimeStamp; + oAuthAuthorizationParameters[@"oauth_signature_method"] = authSignatureMethod; + oAuthAuthorizationParameters[@"oauth_version"] = authVersion; + oAuthAuthorizationParameters[@"oauth_consumer_key"] = configuration.consumerKey; + + if (configuration.token) { + oAuthAuthorizationParameters[@"oauth_token"] = configuration.token; + } + + // get query and body parameters + NSDictionary *additionalQueryParameters = [NSURL PF_ab_parseURLQueryString:[url query]]; + NSDictionary *additionalBodyParameters = nil; + if (configuration.body) { + NSString *string = [[NSString alloc] initWithData:configuration.body encoding:NSUTF8StringEncoding]; + additionalBodyParameters = [NSURL PF_ab_parseURLQueryString:string]; + } + + // combine all parameters + NSMutableDictionary *parameters = [oAuthAuthorizationParameters mutableCopy]; + [parameters addEntriesFromDictionary:additionalQueryParameters]; + [parameters addEntriesFromDictionary:additionalBodyParameters]; + + NSArray *sortedKeys = [[parameters allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2] ?: [parameters[obj1] compare:parameters[obj2]]; + }]; + + NSMutableArray *parameterArray = [NSMutableArray array]; + for (NSString *key in sortedKeys) { + [parameterArray addObject:[NSString stringWithFormat:@"%@=%@", key, [parameters[key] PF_ab_RFC3986EncodedString]]]; + } + + NSString *normalizedParameterString = [parameterArray componentsJoinedByString:@"&"]; + NSString *normalizedURLString = [NSString stringWithFormat:@"%@://%@%@", [url scheme], [url host], [url path]]; + + NSString *signatureBaseString = [NSString stringWithFormat:@"%@&%@&%@", + [configuration.method PF_ab_RFC3986EncodedString], + [normalizedURLString PF_ab_RFC3986EncodedString], + [normalizedParameterString PF_ab_RFC3986EncodedString]]; + + NSString *key = [NSString stringWithFormat:@"%@&%@", + [configuration.consumerSecret PF_ab_RFC3986EncodedString], + (configuration.tokenSecret ? [configuration.tokenSecret PF_ab_RFC3986EncodedString] : @"")]; + + NSData *signature = PF_HMAC_SHA1(signatureBaseString, key); + NSString *base64Signature = [signature base64EncodedStringWithOptions:0]; + + oAuthAuthorizationParameters[@"oauth_signature"] = base64Signature; + + NSMutableArray *authorizationHeaderItems = [NSMutableArray array]; + [oAuthAuthorizationParameters enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + [authorizationHeaderItems addObject:[NSString stringWithFormat:@"%@=\"%@\"", + [key PF_ab_RFC3986EncodedString], + [value PF_ab_RFC3986EncodedString]]]; + }]; + + NSString *authorizationHeaderString = [authorizationHeaderItems componentsJoinedByString:@", "]; + authorizationHeaderString = [NSString stringWithFormat:@"OAuth %@", authorizationHeaderString]; + + return authorizationHeaderString; +} + +@end + +@implementation NSURL (OAuthAdditions) + ++ (NSDictionary *)PF_ab_parseURLQueryString:(NSString *)query { + // Use NSURLComponents if available. + if ([NSURLComponents class] != nil && [NSURLComponents instancesRespondToSelector:@selector(queryItems)]) { + NSURLComponents *components = [[NSURLComponents alloc] init]; + [components setQuery:query]; + + NSArray *queryItems = components.queryItems; + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:components.queryItems.count]; + for (NSURLQueryItem *item in queryItems) { + dictionary[item.name] = [item.value stringByRemovingPercentEncoding]; + } + return dictionary; + } + + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + NSArray *pairs = [query componentsSeparatedByString:@"&"]; + for (NSString *pair in pairs) { + NSArray *keyValue = [pair componentsSeparatedByString:@"="]; + if ([keyValue count] == 2) { + NSString *key = [keyValue objectAtIndex:0]; + NSString *value = [keyValue objectAtIndex:1]; + value = [value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + if (key && value) + [dict setObject:value forKey:key]; + } + } + return [NSDictionary dictionaryWithDictionary:dict]; +} + +@end + +@implementation NSString (OAuthAdditions) + +- (NSString *)PF_ab_RFC3986EncodedString // UTF-8 encodes prior to URL encoding +{ + NSMutableString *result = [NSMutableString string]; + const char *p = [self UTF8String]; + unsigned char c; + + for (; (c = *p); p++) { + switch (c) { + case '0' ... '9': + case 'A' ... 'Z': + case 'a' ... 'z': + case '.': + case '-': + case '~': + case '_': + [result appendFormat:@"%c", c]; + break; + default: + [result appendFormat:@"%%%02X", c]; + } + } + return result; +} + +@end diff --git a/ParseTwitterUtils/Internal/PFTwitterAlertView.h b/ParseTwitterUtils/Internal/PFTwitterAlertView.h new file mode 100644 index 0000000..3b8c815 --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterAlertView.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef void (^PFTwitterAlertViewCompletion)(NSUInteger selectedOtherButtonIndex); + +@interface PFTwitterAlertView : NSObject + ++ (void)showAlertWithTitle:(NSString *)title + message:(NSString *)message + cancelButtonTitle:(NSString *)cancelButtonTitle + otherButtonTitles:(NSArray *)otherButtonTitles + completion:(PFTwitterAlertViewCompletion)completion NS_EXTENSION_UNAVAILABLE_IOS(""); + +@end diff --git a/ParseTwitterUtils/Internal/PFTwitterAlertView.m b/ParseTwitterUtils/Internal/PFTwitterAlertView.m new file mode 100644 index 0000000..e42371b --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterAlertView.m @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterAlertView.h" + +@interface PFTwitterAlertView () + +@property (nonatomic, copy) PFTwitterAlertViewCompletion completion; + +@end + +@implementation PFTwitterAlertView + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (void)showAlertWithTitle:(NSString *)title + message:(NSString *)message + cancelButtonTitle:(NSString *)cancelButtonTitle + otherButtonTitles:(NSArray *)otherButtonTitles + completion:(PFTwitterAlertViewCompletion)completion { + if ([UIAlertController class] != nil) { + __block UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + void (^alertActionHandler)(UIAlertAction *) = [^(UIAlertAction *action) { + if (completion) { + // This block intentionally retains alertController, and releases it afterwards. + if (action.style == UIAlertActionStyleCancel) { + completion(NSNotFound); + } else { + NSUInteger index = [alertController.actions indexOfObject:action]; + completion(index - 1); + } + } + alertController = nil; + } copy]; + + [alertController addAction:[UIAlertAction actionWithTitle:cancelButtonTitle + style:UIAlertActionStyleCancel + handler:alertActionHandler]]; + + for (NSString *buttonTitle in otherButtonTitles) { + [alertController addAction:[UIAlertAction actionWithTitle:buttonTitle + style:UIAlertActionStyleDefault + handler:alertActionHandler]]; + } + + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + UIViewController *viewController = keyWindow.rootViewController; + while (viewController.presentedViewController) { + viewController = viewController.presentedViewController; + } + + [viewController presentViewController:alertController animated:YES completion:nil]; + } else { +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + __block PFTwitterAlertView *pfAlertView = [[self alloc] init]; + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:nil + cancelButtonTitle:cancelButtonTitle + otherButtonTitles:nil]; + + for (NSString *buttonTitle in otherButtonTitles) { + [alertView addButtonWithTitle:buttonTitle]; + } + + pfAlertView.completion = ^(NSUInteger index) { + if (completion) { + completion(index); + } + + pfAlertView = nil; + }; + + alertView.delegate = pfAlertView; + [alertView show]; +#endif + } +} + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + +///-------------------------------------- +#pragma mark - UIAlertViewDelegate +///-------------------------------------- + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + if (self.completion) { + if (buttonIndex == alertView.cancelButtonIndex) { + self.completion(NSNotFound); + } else { + self.completion(buttonIndex - 1); + } + } +} + +#endif + +@end diff --git a/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.h b/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.h new file mode 100644 index 0000000..f33b923 --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class BFTask PF_GENERIC(__covariant BFGenericType); +@class PF_Twitter; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const PFTwitterUserAuthenticationType; + +@interface PFTwitterAuthenticationProvider : NSObject + +@property (nonatomic, strong, readonly) PF_Twitter *twitter; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithTwitter:(PF_Twitter *)twitter NS_DESIGNATED_INITIALIZER; ++ (instancetype)providerWithTwitter:(PF_Twitter *)twitter; + +- (BFTask *)authenticateAsync; + +- (NSDictionary *)authDataWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + secret:(NSString *)authTokenSecret; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.m b/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.m new file mode 100644 index 0000000..1c4a2bc --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterAuthenticationProvider.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterAuthenticationProvider.h" + +#import + +#import + +#import "PFTwitterPrivateUtilities.h" +#import "PF_Twitter.h" + +NSString *const PFTwitterUserAuthenticationType = @"twitter"; + +static NSString *const _PFTwitterAuthDataIdKey = @"id"; +static NSString *const _PFTwitterAuthDataScreenNameKey = @"screen_name"; +static NSString *const _PFTwitterAuthDataAuthTokenKey = @"auth_token"; +static NSString *const _PFTwitterAuthDataAuthTokenSecretKey = @"auth_token_secret"; +static NSString *const _PFTwitterAuthDataConsumerKeyKey = @"consumer_key"; +static NSString *const _PFTwitterAuthDataConsumerSecretKey = @"consumer_secret"; + +@implementation PFTwitterAuthenticationProvider + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFTWConsistencyAssert(NO, @"%@ is not a designated initializer for instances of %@.", + NSStringFromSelector(_cmd), NSStringFromClass([self class])); + return nil; +} + +- (instancetype)initWithTwitter:(PF_Twitter *)twitter { + self = [super init]; + if (!self) return nil; + + _twitter = twitter; + + return self; +} + ++ (instancetype)providerWithTwitter:(PF_Twitter *)twitter { + return [[self alloc] initWithTwitter:twitter]; +} + +///-------------------------------------- +#pragma mark - Auth Data +///-------------------------------------- + +- (NSDictionary *)authDataWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + secret:(NSString *)authTokenSecret { + NSDictionary *authData = @{_PFTwitterAuthDataIdKey : twitterId, + _PFTwitterAuthDataScreenNameKey : screenName, + _PFTwitterAuthDataAuthTokenKey : authToken, + _PFTwitterAuthDataAuthTokenSecretKey : authTokenSecret, + _PFTwitterAuthDataConsumerKeyKey : self.twitter.consumerKey, + _PFTwitterAuthDataConsumerSecretKey : self.twitter.consumerSecret}; + return authData; +} + +///-------------------------------------- +#pragma mark - Authentication +///-------------------------------------- + +- (BFTask *)authenticateAsync { + return [[self.twitter authorizeInBackground] continueWithSuccessBlock:^id(BFTask *task) { + NSDictionary *authData = [self authDataWithTwitterId:self.twitter.userId + screenName:self.twitter.screenName + authToken:self.twitter.authToken + secret:self.twitter.authTokenSecret]; + return [BFTask taskWithResult:authData]; + }]; +} + +///-------------------------------------- +#pragma mark - PFUserAuthenticationDelegate +///-------------------------------------- + +- (BOOL)restoreAuthenticationWithAuthData:(NSDictionary PF_GENERIC(NSString *, NSString *)*)authData { + // If authData is nil, this is an unlink operation, which should succeed. + if (!authData) { + self.twitter.userId = nil; + self.twitter.authToken = nil; + self.twitter.authTokenSecret = nil; + self.twitter.screenName = nil; + return YES; + } + + // Check that the authData contains the required fields, and if so, synchronize. + NSString *userId = authData[_PFTwitterAuthDataIdKey]; + NSString *screenName = authData[_PFTwitterAuthDataScreenNameKey]; + NSString *authToken = authData[_PFTwitterAuthDataAuthTokenKey]; + NSString *authTokenSecret = authData[_PFTwitterAuthDataAuthTokenSecretKey]; + if (userId && screenName && authToken && authTokenSecret) { + self.twitter.userId = userId; + self.twitter.screenName = screenName; + self.twitter.authToken = authToken; + self.twitter.authTokenSecret = authTokenSecret; + return YES; + } + return NO; +} + +@end diff --git a/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.h b/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.h new file mode 100644 index 0000000..62c02ae --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import + +@interface PFTwitterPrivateUtilities : NSObject + ++ (void)safePerformSelector:(SEL)selector onTarget:(id)target withObject:(id)object object:(id)anotherObject; + +@end + +@interface BFTask (ParseTwitterUtils) + +- (id)pftw_waitForResult:(NSError **)error; + +//TODO: (nlutsenko) Look into killing this and replacing with generic continueWithBlock: +- (instancetype)pftw_continueAsyncWithBlock:(BFContinuationBlock)block; + +- (instancetype)pftw_continueWithMainThreadUserBlock:(PFUserResultBlock)block; +- (instancetype)pftw_continueWithMainThreadBooleanBlock:(PFBooleanResultBlock)block; +- (instancetype)pftw_continueWithMainThreadBlock:(BFContinuationBlock)block; + +@end + +/*! + Raises an `NSInternalInconsistencyException` if the `condition` does not pass. + Use `description` to supply the way to fix the exception. + */ +#define PFTWConsistencyAssert(condition, description, ...) \ +do { \ + if (!(condition)) { \ + [NSException raise:NSInternalInconsistencyException \ + format:description, ##__VA_ARGS__]; \ + } \ +} while(0) diff --git a/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.m b/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.m new file mode 100644 index 0000000..14c8874 --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterPrivateUtilities.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterPrivateUtilities.h" + +#import + +@implementation PFTwitterPrivateUtilities + ++ (void)safePerformSelector:(SEL)selector + onTarget:(id)target + withObject:(id)object + object:(id)anotherObject { + if (target == nil || selector == nil || ![target respondsToSelector:selector]) { + return; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:selector withObject:object withObject:anotherObject]; +#pragma clang diagnostic pop +} + +@end + +@implementation BFTask (ParseTwitterUtils) + +- (id)pftw_waitForResult:(NSError **)error { + [self waitUntilFinished]; + + if (self.cancelled) { + return nil; + } else if (self.exception) { + @throw self.exception; + } + if (self.error && error) { + *error = self.error; + } + return self.result; +} + +- (instancetype)pftw_continueAsyncWithBlock:(BFContinuationBlock)block { + BFExecutor *executor = [BFExecutor executorWithDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + return [self continueWithExecutor:executor withBlock:block]; +} + +- (instancetype)pftw_continueWithMainThreadUserBlock:(PFUserResultBlock)block { + return [self pftw_continueWithMainThreadBlock:^id(BFTask *task) { + if (block) { + block(task.result, task.error); + } + return nil; + }]; +} + +- (instancetype)pftw_continueWithMainThreadBooleanBlock:(PFBooleanResultBlock)block { + return [self pftw_continueWithMainThreadBlock:^id(BFTask *task) { + if (block) { + block([task.result boolValue], task.error); + } + return nil; + }]; +} + +- (instancetype)pftw_continueWithMainThreadBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:block]; +} + +@end diff --git a/ParseTwitterUtils/Internal/PFTwitterUtils_Private.h b/ParseTwitterUtils/Internal/PFTwitterUtils_Private.h new file mode 100644 index 0000000..9113a6b --- /dev/null +++ b/ParseTwitterUtils/Internal/PFTwitterUtils_Private.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFTwitterAuthenticationProvider; + +@interface PFTwitterUtils () + ++ (PFTwitterAuthenticationProvider *)_authenticationProvider; ++ (void)_setAuthenticationProvider:(PFTwitterAuthenticationProvider *)provider; + +@end diff --git a/ParseTwitterUtils/Internal/PF_Twitter_Private.h b/ParseTwitterUtils/Internal/PF_Twitter_Private.h new file mode 100644 index 0000000..f714c05 --- /dev/null +++ b/ParseTwitterUtils/Internal/PF_Twitter_Private.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class ACAccount; +@class ACAccountStore; +@protocol PFOAuth1FlowDialogInterface; + +NS_ASSUME_NONNULL_BEGIN + +@interface PF_Twitter () + +@property (nonatomic, strong, readonly) ACAccountStore *accountStore; +@property (nonatomic, strong, readonly) NSURLSession *urlSession; +@property (nonatomic, strong, readonly) Class oauthDialogClass; + +- (instancetype)initWithAccountStore:(ACAccountStore *)accountStore + urlSession:(NSURLSession *)urlSession + dialogClass:(Class)dialogClass; + +/*! + Obtain access to the local twitter account. + Returns the twitter account if access is obtained. Otherwise, returns null. + */ +- (BFTask PF_GENERIC(ACAccount *)*)_getLocalTwitterAccountAsync; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ParseTwitterUtils/PFTwitterUtils.h b/ParseTwitterUtils/PFTwitterUtils.h new file mode 100644 index 0000000..e7c3841 --- /dev/null +++ b/ParseTwitterUtils/PFTwitterUtils.h @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class BFTask PF_GENERIC(__covariant BFGenericType); +@class PF_Twitter; + +/*! + The `PFTwitterUtils` class provides utility functions for working with Twitter in a Parse application. + + This class is currently for iOS only. + */ +@interface PFTwitterUtils : NSObject + +///-------------------------------------- +/// @name Interacting With Twitter +///-------------------------------------- + +/*! + @abstract Gets the instance of the object that Parse uses. + + @returns An instance of object. + */ ++ (nullable PF_Twitter *)twitter; + +/*! + @abstract Initializes the Twitter singleton. + + @warning You must invoke this in order to use the Twitter functionality in Parse. + + @param consumerKey Your Twitter application's consumer key. + @param consumerSecret Your Twitter application's consumer secret. + */ ++ (void)initializeWithConsumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret; + +/*! + @abstract Whether the user has their account linked to Twitter. + + @param user User to check for a Twitter link. The user must be logged in on this device. + + @returns `YES` if the user has their account linked to Twitter, otherwise `NO`. + */ ++ (BOOL)isLinkedWithUser:(nullable PFUser *)user; + +///-------------------------------------- +/// @name Logging In & Creating Twitter-Linked Users +///-------------------------------------- + +/*! + @abstract *Asynchronously* logs in a user using Twitter. + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically logs in (or creates, in the case where it is a new user) a . + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask PF_GENERIC(PFUser *)*)logInInBackground; + +/*! + @abstract *Asynchronously* logs in a user using Twitter. + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically logs in (or creates, in the case where it is a new user) . + + @param block The block to execute. + It should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)logInWithBlock:(nullable PFUserResultBlock)block; + +/* + @abstract *Asynchronously* Logs in a user using Twitter. + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically logs in (or creates, in the case where it is a new user) a . + + @param target Target object for the selector + @param selector The selector that will be called when the asynchrounous request is complete. + It should have the following signature: `(void)callbackWithUser:(PFUser *)user error:(NSError **)error`. + */ ++ (void)logInWithTarget:(nullable id)target selector:(nullable SEL)selector; + +/*! + @abstract *Asynchronously* logs in a user using Twitter. + + @discussion Allows you to handle user login to Twitter, then provide authentication + data to log in (or create, in the case where it is a new user) the . + + @param twitterId The id of the Twitter user being linked. + @param screenName The screen name of the Twitter user being linked. + @param authToken The auth token for the user's session. + @param authTokenSecret The auth token secret for the user's session. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask PF_GENERIC(PFUser *)*)logInWithTwitterIdInBackground:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret; + +/*! + @abstract Logs in a user using Twitter. + + @discussion Allows you to handle user login to Twitter, then provide authentication data + to log in (or create, in the case where it is a new user) the . + + @param twitterId The id of the Twitter user being linked + @param screenName The screen name of the Twitter user being linked + @param authToken The auth token for the user's session + @param authTokenSecret The auth token secret for the user's session + @param block The block to execute. + It should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)logInWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + block:(nullable PFUserResultBlock)block; + +/* + @abstract Logs in a user using Twitter. + + @discussion Allows you to handle user login to Twitter, then provide authentication data + to log in (or create, in the case where it is a new user) the . + + @param twitterId The id of the Twitter user being linked. + @param screenName The screen name of the Twitter user being linked. + @param authToken The auth token for the user's session. + @param authTokenSecret The auth token secret for the user's session. + @param target Target object for the selector. + @param selector The selector that will be called when the asynchronous request is complete. + It should have the following signature: `(void)callbackWithUser:(PFUser *)user error:(NSError *)error`. + */ ++ (void)logInWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + target:(nullable id)target + selector:(nullable SEL)selector; + +///-------------------------------------- +/// @name Linking Users with Twitter +///-------------------------------------- + +/*! + @abstract *Asynchronously* links Twitter to an existing PFUser. + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically links the account to the . + + @param user User to link to Twitter. + + @deprecated Please use `[PFTwitterUtils linkUserInBackground:]` instead. + */ ++ (void)linkUser:(PFUser *)user PARSE_DEPRECATED("Please use +linkUserInBackground: instead."); + +/*! + @abstract *Asynchronously* links Twitter to an existing . + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically links the account to the . + + @param user User to link to Twitter. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask PF_GENERIC(NSNumber *)*)linkUserInBackground:(PFUser *)user; + +/*! + @abstract *Asynchronously* links Twitter to an existing . + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically links the account to the . + + @param user User to link to Twitter. + @param block The block to execute. + It should have the following argument signature: `^(BOOL success, NSError *error)`. + */ ++ (void)linkUser:(PFUser *)user block:(nullable PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* links Twitter to an existing . + + @discussion This method delegates to Twitter to authenticate the user, + and then automatically links the account to the . + + @param user User to link to Twitter. + @param target Target object for the selector + @param selector The selector that will be called when the asynchrounous request is complete. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + */ ++ (void)linkUser:(PFUser *)user target:(nullable id)target selector:(nullable SEL)selector; + +/*! + @abstract *Asynchronously* links Twitter to an existing PFUser asynchronously. + + @discussion Allows you to handle user login to Twitter, + then provide authentication data to link the account to the . + + @param user User to link to Twitter. + @param twitterId The id of the Twitter user being linked. + @param screenName The screen name of the Twitter user being linked. + @param authToken The auth token for the user's session. + @param authTokenSecret The auth token secret for the user's session. + @returns The task, that encapsulates the work being done. + */ ++ (BFTask PF_GENERIC(NSNumber *)*)linkUserInBackground:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret; + +/*! + @abstract *Asynchronously* links Twitter to an existing . + + @discussionAllows you to handle user login to Twitter, + then provide authentication data to link the account to the . + + @param user User to link to Twitter. + @param twitterId The id of the Twitter user being linked. + @param screenName The screen name of the Twitter user being linked. + @param authToken The auth token for the user's session. + @param authTokenSecret The auth token secret for the user's session. + @param block The block to execute. + It should have the following argument signature: `^(BOOL success, NSError *error)`. + */ ++ (void)linkUser:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + block:(nullable PFBooleanResultBlock)block; + +/* + @abstract Links Twitter to an existing . + + @discussion This method allows you to handle user login to Twitter, + then provide authentication data to link the account to the . + + @param user User to link to Twitter. + @param twitterId The id of the Twitter user being linked. + @param screenName The screen name of the Twitter user being linked. + @param authToken The auth token for the user's session. + @param authTokenSecret The auth token secret for the user's session. + @param target Target object for the selector. + @param selector The selector that will be called when the asynchronous request is complete. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + */ ++ (void)linkUser:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + target:(nullable id)target + selector:(nullable SEL)selector; + +///-------------------------------------- +/// @name Unlinking Users from Twitter +///-------------------------------------- + +/*! + @abstract *Synchronously* unlinks the from a Twitter account. + + @param user User to unlink from Twitter. + + @returns Returns true if the unlink was successful. + */ ++ (BOOL)unlinkUser:(PFUser *)user; + +/*! + @abstract *Synchronously* unlinks the PFUser from a Twitter account. + + @param user User to unlink from Twitter. + @param error Error object to set on error. + + @returns Returns `YES` if the unlink was successful, otherwise `NO`. + */ ++ (BOOL)unlinkUser:(PFUser *)user error:(NSError **)error; + +/*! + @abstract Makes an *asynchronous* request to unlink a user from a Twitter account. + + @param user User to unlink from Twitter. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask PF_GENERIC(NSNumber *)*)unlinkUserInBackground:(PFUser *)user; + +/*! + @abstract Makes an *asynchronous* request to unlink a user from a Twitter account. + + @param user User to unlink from Twitter. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)unlinkUserInBackground:(PFUser *)user block:(nullable PFBooleanResultBlock)block; + +/* + @abstract Makes an *asynchronous* request to unlink a user from a Twitter account. + + @param user User to unlink from Twitter + @param target Target object for the selector + @param selector The selector that will be called when the asynchrounous request is complete. + */ ++ (void)unlinkUserInBackground:(PFUser *)user target:(nullable id)target selector:(nullable SEL)selector; + +@end + +PF_ASSUME_NONNULL_END diff --git a/ParseTwitterUtils/PFTwitterUtils.m b/ParseTwitterUtils/PFTwitterUtils.m new file mode 100644 index 0000000..a745674 --- /dev/null +++ b/ParseTwitterUtils/PFTwitterUtils.m @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterUtils.h" + +#import +#import + +#import "PFTwitterAuthenticationProvider.h" +#import "PFTwitterPrivateUtilities.h" +#import "PF_Twitter.h" + +@implementation PFTwitterUtils + +///-------------------------------------- +#pragma mark - Authentication Provider +///-------------------------------------- + +static PFTwitterAuthenticationProvider *authenticationProvider_; + ++ (PFTwitterAuthenticationProvider *)_authenticationProvider { + return authenticationProvider_; +} + ++ (void)_setAuthenticationProvider:(PFTwitterAuthenticationProvider *)provider { + authenticationProvider_ = provider; +} + +///-------------------------------------- +#pragma mark - Initialize +///-------------------------------------- + ++ (void)_assertTwitterInitialized { + PFTWConsistencyAssert([self _authenticationProvider], + @"You must call PFTwitterUtils initializeWithConsumerKey:consumerSecret: to use PFTwitterUtils."); +} + ++ (void)initializeWithConsumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret { + if (![self _authenticationProvider]) { + PF_Twitter *twitter = [[PF_Twitter alloc] init]; + twitter.consumerKey = consumerKey; + twitter.consumerSecret = consumerSecret; + + PFTwitterAuthenticationProvider *provider = [[PFTwitterAuthenticationProvider alloc] initWithTwitter:twitter]; + [PFUser registerAuthenticationDelegate:provider forAuthType:PFTwitterUserAuthenticationType]; + + [self _setAuthenticationProvider:provider]; + } +} + ++ (PF_Twitter *)twitter { + return [self _authenticationProvider].twitter; +} + ++ (BOOL)isLinkedWithUser:(PFUser *)user { + return [user isLinkedWithAuthType:PFTwitterUserAuthenticationType]; +} + ++ (BOOL)unlinkUser:(PFUser *)user { + return [self unlinkUser:user error:nil]; +} + ++ (BOOL)unlinkUser:(PFUser *)user error:(NSError **)error { + return [[[self unlinkUserInBackground:user] pftw_waitForResult:error] boolValue]; +} + ++ (BFTask PF_GENERIC(NSNumber *)*)unlinkUserInBackground:(PFUser *)user { + [self _assertTwitterInitialized]; + return [user unlinkWithAuthTypeInBackground:PFTwitterUserAuthenticationType]; +} + ++ (void)unlinkUserInBackground:(PFUser *)user block:(PFBooleanResultBlock)block { + [[self unlinkUserInBackground:user] pftw_continueWithMainThreadBooleanBlock:block]; +} + ++ (void)unlinkUserInBackground:(PFUser *)user target:(id)target selector:(SEL)selector { + [PFTwitterUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError *error) { + [PFTwitterPrivateUtilities safePerformSelector:selector onTarget:target withObject:@(succeeded) object:error]; + }]; +} + ++ (void)linkUser:(PFUser *)user { + // This is misnamed `*InBackground` method. Left as is for backward compatability. + [self linkUserInBackground:user]; +} + ++ (BFTask PF_GENERIC(NSNumber *)*)linkUserInBackground:(PFUser *)user { + [self _assertTwitterInitialized]; + + PFTwitterAuthenticationProvider *provider = [self _authenticationProvider]; + return [[provider authenticateAsync] continueWithSuccessBlock:^id(BFTask *task) { + return [user linkWithAuthTypeInBackground:PFTwitterUserAuthenticationType authData:task.result]; + }]; +} + ++ (void)linkUser:(PFUser *)user block:(PFBooleanResultBlock)block { + [[self linkUserInBackground:user] pftw_continueWithMainThreadBooleanBlock:block]; +} + ++ (void)linkUser:(PFUser *)user target:(id)target selector:(SEL)selector { + [PFTwitterUtils linkUser:user block:^(BOOL succeeded, NSError *error) { + [PFTwitterPrivateUtilities safePerformSelector:selector onTarget:target withObject:@(succeeded) object:error]; + }]; +} + ++ (BFTask PF_GENERIC(NSNumber *)*)linkUserInBackground:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret { + [self _assertTwitterInitialized]; + + PFTwitterAuthenticationProvider *provider = [self _authenticationProvider]; + NSDictionary *authData = [provider authDataWithTwitterId:twitterId + screenName:screenName + authToken:authToken + secret:authTokenSecret]; + return [user linkWithAuthTypeInBackground:PFTwitterUserAuthenticationType authData:authData]; +} + ++ (void)linkUser:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + block:(PFBooleanResultBlock)block { + [[self linkUserInBackground:user + twitterId:twitterId + screenName:screenName + authToken:authToken + authTokenSecret:authTokenSecret] pftw_continueWithMainThreadBooleanBlock:block]; +} + ++ (void)linkUser:(PFUser *)user + twitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + target:(id)target + selector:(SEL)selector { + [PFTwitterUtils linkUser:user + twitterId:twitterId + screenName:screenName + authToken:authToken + authTokenSecret:authTokenSecret + block:^(BOOL succeeded, NSError *error) { + [PFTwitterPrivateUtilities safePerformSelector:selector + onTarget:target + withObject:@(succeeded) + object:error]; + }]; +} + ++ (BFTask PF_GENERIC(PFUser *)*)logInInBackground { + [self _assertTwitterInitialized]; + + PFTwitterAuthenticationProvider *provider = [self _authenticationProvider]; + return [[provider authenticateAsync] continueWithSuccessBlock:^id(BFTask *task) { + return [PFUser logInWithAuthTypeInBackground:PFTwitterUserAuthenticationType authData:task.result]; + }]; +} + ++ (void)logInWithBlock:(PFUserResultBlock)block { + [[self logInInBackground] pftw_continueWithMainThreadUserBlock:block]; +} + ++ (void)logInWithTarget:(id)target selector:(SEL)selector { + [self logInWithBlock:^(PFUser *user, NSError *error) { + [PFTwitterPrivateUtilities safePerformSelector:selector onTarget:target withObject:user object:error]; + }]; +} + ++ (BFTask PF_GENERIC(PFUser *)*)logInWithTwitterIdInBackground:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret { + [self _assertTwitterInitialized]; + + PFTwitterAuthenticationProvider *provider = [self _authenticationProvider]; + NSDictionary *authData = [provider authDataWithTwitterId:twitterId + screenName:screenName + authToken:authToken + secret:authTokenSecret]; + return [PFUser logInWithAuthTypeInBackground:PFTwitterUserAuthenticationType authData:authData]; +} + ++ (void)logInWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + block:(PFUserResultBlock)block { + [[self logInWithTwitterIdInBackground:twitterId + screenName:screenName + authToken:authToken + authTokenSecret:authTokenSecret] pftw_continueWithMainThreadUserBlock:block]; +} + ++ (void)logInWithTwitterId:(NSString *)twitterId + screenName:(NSString *)screenName + authToken:(NSString *)authToken + authTokenSecret:(NSString *)authTokenSecret + target:(id)target + selector:(SEL)selector { + [self logInWithTwitterId:twitterId + screenName:screenName + authToken:authToken + authTokenSecret:authTokenSecret + block:^(PFUser *user, NSError *error) { + [PFTwitterPrivateUtilities safePerformSelector:selector + onTarget:target + withObject:user + object:error]; + }]; +} + +@end diff --git a/ParseTwitterUtils/PF_Twitter.h b/ParseTwitterUtils/PF_Twitter.h new file mode 100644 index 0000000..a2f0a10 --- /dev/null +++ b/ParseTwitterUtils/PF_Twitter.h @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class BFTask PF_GENERIC(__covariant BFGenericType); + +/*! + The `PF_Twitter` class is a simple interface for interacting with the Twitter REST API, + automating sign-in and OAuth signing of requests against the API. + */ +@interface PF_Twitter : NSObject + +/*! + @abstract Consumer key of the application that is used to authorize with Twitter. + */ +@property (nullable, nonatomic, copy) NSString *consumerKey; + +/*! + @abstract Consumer secret of the application that is used to authorize with Twitter. + */ +@property (nullable, nonatomic, copy) NSString *consumerSecret; + +/*! + @abstract Auth token for the current user. + */ +@property (nullable, nonatomic, copy) NSString *authToken; + +/*! + @abstract Auth token secret for the current user. + */ +@property (nullable, nonatomic, copy) NSString *authTokenSecret; + +/*! + @abstract Twitter user id of the currently signed in user. + */ +@property (nullable, nonatomic, copy) NSString *userId; + +/*! + @abstract Twitter screen name of the currently signed in user. + */ +@property (nullable, nonatomic, copy) NSString *screenName; + +/*! + @abstract Displays an auth dialog and populates the authToken, authTokenSecret, userId, and screenName properties + if the Twitter user grants permission to the application. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)authorizeInBackground; + +/*! + @abstract Displays an auth dialog and populates the authToken, authTokenSecret, userId, and screenName properties + if the Twitter user grants permission to the application. + + @param success Invoked upon successful authorization. + @param failure Invoked upon an error occurring in the authorization process. + @param cancel Invoked when the user cancels authorization. + */ +- (void)authorizeWithSuccess:(nullable void (^)(void))success + failure:(nullable void (^)(NSError *__nullable error))failure + cancel:(nullable void (^)(void))cancel; + +/*! + @abstract Adds a 3-legged OAuth signature to an `NSMutableURLRequest` based + upon the properties set for the Twitter object. + + @discussion Use this function to sign requests being made to the Twitter API. + + @param request Request to sign. + */ +- (void)signRequest:(nullable NSMutableURLRequest *)request; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ParseTwitterUtils/PF_Twitter.m b/ParseTwitterUtils/PF_Twitter.m new file mode 100644 index 0000000..29e04da --- /dev/null +++ b/ParseTwitterUtils/PF_Twitter.m @@ -0,0 +1,447 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PF_Twitter.h" +#import "PF_Twitter_Private.h" + +#import +#import +#import + +#import +#import + +#import + +#import "PFOAuth1FlowDialog.h" +#import "PFTwitterAlertView.h" +#import "PFTwitterPrivateUtilities.h" +#import "PF_OAuthCore.h" + +@implementation PF_Twitter + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + return [self initWithAccountStore:[[ACAccountStore alloc] init] + urlSession:[NSURLSession sharedSession] + dialogClass:[PFOAuth1FlowDialog class]]; +} + +- (instancetype)initWithAccountStore:(ACAccountStore *)accountStore + urlSession:(NSURLSession *)urlSession + dialogClass:(Class)dialogClass { + self = [super init]; + if (!self) return nil; + + _accountStore = accountStore; + _urlSession = urlSession; + _oauthDialogClass = dialogClass; + + PFTWConsistencyAssert(_oauthDialogClass == nil || + [(id)_oauthDialogClass conformsToProtocol:@protocol(PFOAuth1FlowDialogInterface)], + @"OAuth Dialog class must conform to the Dialog Interface protocol!"); + + return self; +} + +///-------------------------------------- +#pragma mark - Authorize +///-------------------------------------- + +- (BFTask *)authorizeInBackground { + if (self.consumerKey.length == 0 || self.consumerSecret.length == 0) { + //TODO: (nlutsenko) This doesn't look right, maybe we should add additional error code? + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain code:1 userInfo:nil]]; + } + + return [[self _performReverseAuthAsync] pftw_continueAsyncWithBlock:^id(BFTask *task) { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + dispatch_async(dispatch_get_main_queue(), ^{ + // if reverse auth was successful then return + if (task.cancelled) { + [source cancel]; + return; + } else if (!task.error && !task.result) { + source.result = nil; + return; + } + + // fallback to the webview auth + [[self _performWebViewAuthAsync] pftw_continueAsyncWithBlock:^id(BFTask *task) { + NSError *error = task.error; + if (task.cancelled) { + [source cancel]; + } else if (!error) { + [source setResult:task.result]; + } else { + [source setError:error]; + } + return nil; + }]; + }); + return source.task; + }]; +} + +- (void)authorizeWithSuccess:(void (^)(void))success + failure:(void (^)(NSError *error))failure + cancel:(void (^)(void))cancel { + [[self authorizeInBackground] continueWithExecutor:[BFExecutor mainThreadExecutor] + withBlock:^id(BFTask *task) { + if (task.error) { + failure(task.error); + } else if (task.cancelled) { + cancel(); + } else { + success(); + } + return nil; + }]; +} + +// Displays the web view dialog +- (BFTask *)_showWebViewDialogAsync:(NSString *)requestToken requestSecret:(NSString *)requestSecret { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + + static NSString *twitterAuthURLString = @"https://api.twitter.com/oauth/authenticate"; + + PFOAuth1FlowDialog *dialog = [_oauthDialogClass dialogWithURL:[NSURL URLWithString:twitterAuthURLString] + queryParameters:@{ @"oauth_token" : requestToken }]; + dialog.redirectURLPrefix = @"http://twitter-oauth.callback"; + dialog.completion = ^(BOOL succeeded, NSURL *url, NSError *error) { + // In case of error + if (error) { + source.error = error; + return; + } + // In case the dialog was cancelled + if (!succeeded) { + [source cancel]; + return; + } + + // Handle URL received. + NSDictionary *authQueryParams = [NSURL PF_ab_parseURLQueryString:[url query]]; + NSString *verifier = [authQueryParams objectForKey:@"oauth_verifier"]; + NSString *token = [authQueryParams objectForKey:@"oauth_token"]; + + [[self _getAccessTokenForWebAuthAsync:verifier requestSecret:requestSecret token:token] + pftw_continueAsyncWithBlock:^id (BFTask *task) { + NSError *error = task.error; + if (!error) { + NSDictionary *accessResult = (NSDictionary*) task.result; + [self setLoginResultValues:accessResult]; + source.result = nil; + } else { + source.error = error; + } + return nil; + }]; + }; + [dialog showAnimated:YES]; + + return source.task; +} + +/*! + Get the request token for the authentication. This is the first step in auth. + if isReverseAuth is YES then get the request token for reverse auth mode. Otherwise, get the request token for web auth mode. + */ +- (BFTask *)_getRequestTokenAsync:(BOOL)isReverseAuth { + NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/oauth/request_token"]; + NSMutableDictionary *params = nil; + NSData *body = nil; + + if (isReverseAuth) { + body = [[NSString stringWithFormat:@"x_auth_mode=%@", @"reverse_auth"] dataUsingEncoding:NSUTF8StringEncoding]; + } else { + params = [NSMutableDictionary dictionary]; + [params setObject:@"http://twitter-oauth.callback" forKey:@"oauth_callback"]; + } + + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:url + method:@"POST" + body:body + additionalParameters:params + consumerKey:_consumerKey + consumerSecret:_consumerSecret + token:nil + tokenSecret:nil]; + NSString *header = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setHTTPMethod:@"POST"]; + [request addValue:header forHTTPHeaderField:@"Authorization"]; + [request setHTTPBody:body]; + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + [[self.urlSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + [taskCompletionSource trySetError:error]; + } else { + [taskCompletionSource trySetResult:data]; + } + }] resume]; + return taskCompletionSource.task; +} + +// Get the access token for web authentication +- (BFTask *)_getAccessTokenForWebAuthAsync:(NSString *)verifier + requestSecret:(NSString *)requestSecret + token:(NSString *)token { + NSURL *accessTokenURL = [NSURL URLWithString:@"https://api.twitter.com/oauth/access_token"]; + NSData *body = [[NSString stringWithFormat:@"oauth_verifier=%@", verifier] dataUsingEncoding:NSUTF8StringEncoding]; + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:accessTokenURL + method:@"POST" + body:body + additionalParameters:nil + consumerKey:_consumerKey + consumerSecret:_consumerSecret + token:token + tokenSecret:requestSecret]; + NSString *accessTokenHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + NSMutableURLRequest *accessRequest = [NSMutableURLRequest requestWithURL:accessTokenURL]; + [accessRequest setHTTPMethod:@"POST"]; + [accessRequest addValue:accessTokenHeader forHTTPHeaderField:@"Authorization"]; + [accessRequest setHTTPBody:body]; + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + [[self.urlSession dataTaskWithRequest:accessRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + [taskCompletionSource trySetError:error]; + } else { + NSString *accessResponseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSDictionary *accessResponseValues = [NSURL PF_ab_parseURLQueryString:accessResponseString]; + [taskCompletionSource trySetResult:accessResponseValues]; + } + }] resume]; + return taskCompletionSource.task; +} + +/*! + Get the access token for reverse authentication. + If the Task is successful then, Task result is dictionary containing logged in user's Auth token, Screenname and other attributes. + */ +- (BFTask *)_getAccessTokenForReverseAuthAsync:(NSString *)signedReverseAuthSignature + localTwitterAccount:(ACAccount *)localTwitterAccount { + + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + if (!signedReverseAuthSignature || + [signedReverseAuthSignature length] == 0 || + !localTwitterAccount) { + + source.error = [NSError errorWithDomain:PFParseErrorDomain code:1 userInfo:nil]; + return source.task; + } + + NSDictionary *params = @{ @"x_reverse_auth_parameters" : signedReverseAuthSignature, + @"x_reverse_auth_target" : _consumerKey }; + + NSURL *accessTokenUrl = [NSURL URLWithString:@"https://api.twitter.com/oauth/access_token"]; + SLRequest *accessRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter + requestMethod:SLRequestMethodPOST + URL:accessTokenUrl + parameters:params]; + + [accessRequest setAccount:localTwitterAccount]; + [accessRequest performRequestWithHandler:^(NSData *data, NSHTTPURLResponse *urlResponse, NSError *error) { + if (error) { + [source setError:error]; + } else { + NSString *accessResponseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSDictionary *accessResponseValues = [NSURL PF_ab_parseURLQueryString:accessResponseString]; + [source setResult:accessResponseValues]; + } + }]; + + return source.task; +} + +// Set the result parameters from the data returned om succesful login +- (void)setLoginResultValues:(NSDictionary *)resultData { + self.authToken = [resultData objectForKey:@"oauth_token"]; + self.authTokenSecret = [resultData objectForKey:@"oauth_token_secret"]; + self.userId = [resultData objectForKey:@"user_id"]; + self.screenName = [resultData objectForKey:@"screen_name"]; +} + +// Performs the Reverse auth for the the twitter account setup on the device. +- (BFTask *)_performReverseAuthAsync { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + + // get permission to access the account if its setup and available. + [[self _getLocalTwitterAccountAsync] pftw_continueAsyncWithBlock:^id(BFTask *task) { + + if (task.error) { + source.error = task.error; + return source.task; + } + + if (task.cancelled) { + [source cancel]; + return source.task; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + ACAccount *localTwitterAccount = (ACAccount*) task.result; + + if(!localTwitterAccount) { + source.error = [NSError errorWithDomain:PFParseErrorDomain code:2 userInfo:nil]; + return; + } + + // continue with reverse auth since its permitted + [[self _getRequestTokenAsync:YES] pftw_continueAsyncWithBlock:^id(BFTask *task) { + if (task.error) { + source.error = task.error; + return source.task; + } + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *requestTokenResponse = [[NSString alloc] initWithData:task.result encoding:NSUTF8StringEncoding]; + + [[self _getAccessTokenForReverseAuthAsync:requestTokenResponse + localTwitterAccount:localTwitterAccount] pftw_continueAsyncWithBlock:^id(BFTask *task) { + NSError *error = task.error; + if (!error) { + NSDictionary *accessResult = (NSDictionary*) task.result; + [self setLoginResultValues:accessResult]; + source.result = nil; + } else { + source.error = task.error; + } + return nil; + }]; + }); + return nil; + }]; + + }); + return nil; + }]; + + return source.task; +} + +- (BFTask *)_performWebViewAuthAsync { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + + [[self _getRequestTokenAsync:NO] pftw_continueAsyncWithBlock:^id(BFTask *task) { + if (task.error || task.isCancelled) { + if (task.error) { + [source setError:task.error]; + } else { + [source cancel]; + } + return task; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *requestTokenResponse = [[NSString alloc] initWithData:task.result encoding:NSUTF8StringEncoding]; + + NSDictionary *requestTokenParsed = [NSURL PF_ab_parseURLQueryString:requestTokenResponse]; + NSString *requestToken = [requestTokenParsed objectForKey:@"oauth_token"]; + NSString *requestSecret = [requestTokenParsed objectForKey:@"oauth_token_secret"]; + + // show the webview dialog for auth token + [[self _showWebViewDialogAsync:requestToken + requestSecret:requestSecret] pftw_continueAsyncWithBlock:^id(BFTask *task) { + NSError *error = task.error; + if (task.isCancelled) { + [source cancel]; + } else if (!error) { + [source setResult:task.result]; + } else { + [source setError:error]; + } + return nil; + }]; + }); + return nil; + }]; + + return source.task; +} + +- (BFTask PF_GENERIC(ACAccount *)*)_getLocalTwitterAccountAsync { + BFTaskCompletionSource PF_GENERIC(ACAccount *)*source = [BFTaskCompletionSource taskCompletionSource]; + + // If no twitter accounts present in the system, then no need to ask for permission to the user + if (![SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) { + [source setResult:nil]; + return source.task; + } + + ACAccountType *twitterType = [_accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]; + [_accountStore requestAccessToAccountsWithType:twitterType options:nil completion:^(BOOL granted, NSError *error) { + if (error) { + [source setError:error]; + return; + } + + if (!granted) { + [source setResult:nil]; + return; + } + + NSArray *accounts = [_accountStore accountsWithAccountType:twitterType]; + + // No accounts - provide an empty result + if ([accounts count] == 0) { + [source setResult:nil]; + return; + } + + // Finish if there is only 1 account + if ([accounts count] == 1) { + [source setResult:accounts[0]]; + return; + } + + NSArray *usernames = [accounts valueForKey:@"accountDescription"]; + + // Call async on the main thread, as the completion isn't executed on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + [PFTwitterAlertView showAlertWithTitle:NSLocalizedString(@"Select a Twitter Account", @"Select a Twitter Account") + message:nil + cancelButtonTitle:NSLocalizedString(@"Cancel", @"Cancel") + otherButtonTitles:usernames + completion:^(NSUInteger buttonIndex) { + if (buttonIndex == NSNotFound) { + [source cancel]; + } else { + ACAccount *account = accounts[buttonIndex]; + [source setResult:account]; + } + }]; + }); + }]; + + return source.task; +} + +///-------------------------------------- +#pragma mark - Sign Request +///-------------------------------------- + +- (void)signRequest:(NSMutableURLRequest *)request { + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:request.URL + method:request.HTTPMethod ?: @"GET" + body:request.HTTPBody + additionalParameters:nil + consumerKey:_consumerKey + consumerSecret:_consumerSecret + token:_authToken + tokenSecret:_authTokenSecret]; + NSString *header = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + [request addValue:header forHTTPHeaderField:@"Authorization"]; +} + +@end diff --git a/ParseTwitterUtils/ParseTwitterUtils.h b/ParseTwitterUtils/ParseTwitterUtils.h new file mode 100644 index 0000000..2c49673 --- /dev/null +++ b/ParseTwitterUtils/ParseTwitterUtils.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..9e54539 --- /dev/null +++ b/Podfile @@ -0,0 +1,6 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '7.0' + +target 'ParseTwitterUtils-Tests', :exclusive => true do + pod 'OCMock' +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..844ef74 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,10 @@ +PODS: + - OCMock (3.2) + +DEPENDENCIES: + - OCMock + +SPEC CHECKSUMS: + OCMock: 28def049ef47f996b515a8eeea958be7ccab2dbb + +COCOAPODS: 0.39.0.beta.4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..97ccc28 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# Parse Twitter Utils for iOS + +[![Build Status][build-status-svg]][build-status-link] +[![Coverage Status][coverage-status-svg]][coverage-status-link] +[![Podspec][podspec-svg]][podspec-link] +[![License][license-svg]][license-link] +![Platforms][platforms-svg] + +A utility library to authenticate Parse `PFUser`s with Twitter. For more information see our [guide][guide]. + +## Getting Started + +To use parse, head on over to the [releases][releases] page, and download the latest build. +And you're off! Take a look at the public [documentation][docs] and start building. + +**Other Installation Options** + + 1. **CocoaPods** + + Add the following line to your podfile: + + pod 'ParseTwitterUtils' + + Run pod install, and you should now have the latest parse release. + + 2. **Using ParseTwitterUtils as a sub-project** + + You can also include parse as a subproject inside of your application if you'd prefer, although we do not recommend this, as it will increase your indexing time significantly. To do so, just drag and drop the `ParseTwitterUtils.xcodeproj` file into your workspace. + +## How Do I Contribute? + +We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines][contributing]. + +## Other Parse Projects + + - [Parse for iOS/OS X][parse-iosx-link] + - [ParseUI for iOS][parseui-ios-link] + - [ParseFacebookUtils for iOS][parsefacebookutils-ios-link] + - [Parse SDK for Android][android-sdk-link] + +## Dependencies + +We use the following libraries as dependencies inside of ParseTwitterUtils: + + - [Parse SDK][parse-iosx-link] + - [Bolts][bolts-framework], for task management. + - [OCMock][ocmock-framework], for unit testing. + +## License + +``` +Copyright (c) 2015-present, Parse, LLC. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +``` + + [parse.com]: https://www.parse.com/products/ios + [docs]: https://www.parse.com/docs/ios/guide + [guide]: https://parse.com/docs/ios/guide#users-twitter-users + [blog]: https://blog.parse.com/ + + [parse-iosx-link]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX + [parseui-ios-link]: https://github.com/ParsePlatform/ParseUI-iOS + [parsefacebookutils-ios-link]: https://github.com/ParsePlatform/ParseFacebookUtils-iOS + [android-sdk-link]: https://github.com/ParsePlatform/Parse-SDK-Android + + [releases]: https://github.com/ParsePlatform/ParseTwitterUtils-iOS/releases + [contributing]: https://github.com/ParsePlatform/ParseTwitterUtils-iOS/blob/master/CONTRIBUTING.md + + [bolts-framework]: https://github.com/BoltsFramework/Bolts-iOS + [ocmock-framework]: http://ocmock.org + + [build-status-svg]: https://travis-ci.org/ParsePlatform/ParseTwitterUtils-iOS.svg + [build-status-link]: https://travis-ci.org/ParsePlatform/ParseTwitterUtils-iOS/branches + + [coverage-status-svg]: https://codecov.io/github/ParsePlatform/ParseTwitterUtils-iOS/coverage.svg?branch=master + [coverage-status-link]: https://codecov.io/github/ParsePlatform/ParseTwitterUtils-iOS?branch=master + + [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg + [license-link]: https://github.com/ParsePlatform/ParseTwitterUtils-iOS/blob/master/LICENSE + + [podspec-svg]: https://img.shields.io/cocoapods/v/ParseTwitterUtils.svg + [podspec-link]: https://cocoapods.org/pods/ParseTwitterUtils + + [platforms-svg]: https://img.shields.io/badge/platform-ios-lightgrey.svg diff --git a/Resources/Info.plist b/Resources/Info.plist new file mode 100644 index 0000000..ee88f8c --- /dev/null +++ b/Resources/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ParseTwitterUtils + CFBundleIdentifier + com.parse.twitterutils + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.9.0 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + iPhoneSimulator + iPhoneOS + + CFBundleVersion + 1.9.0 + MinimumOSVersion + 6.0 + + diff --git a/Resources/Localizable.strings b/Resources/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..2ebc81b9e3663a359d3e1d69aab3edb56ace3fcf GIT binary patch literal 250 zcmezWPoF`HL4m=UA(0`EA( + +@import XCTest; + +@interface PFTwitterTestCase : XCTestCase + +///-------------------------------------- +/// @name XCTestCase +///-------------------------------------- + +- (void)setUp NS_REQUIRES_SUPER; +- (void)tearDown NS_REQUIRES_SUPER; + +///-------------------------------------- +/// @name Expectations +///-------------------------------------- + +- (XCTestExpectation *)currentSelectorTestExpectation; +- (void)waitForTestExpectations; + +///-------------------------------------- +/// @name Mocks +///-------------------------------------- + +- (void)registerMockObject:(id)mockObject; + +@end + +#define _PFRegisterMock(mockObject) [self registerMockObject:mockObject] +#define _PFMockShim(method, args...) ({ id mock = method(args); _PFRegisterMock(mock); mock; }) +#define _PFOCMockWarning _Pragma("GCC warning \"Please use PF mocking methods instead of OCMock ones.\"") + +#define _PFStrictClassMock(kls) [OCMockObject mockForClass:kls] +#define _PFClassMock(kls) [OCMockObject niceMockForClass:kls] +#define _PFStrictProtocolMock(proto) [OCMockObject mockForProtocol:proto] +#define _PFProtocolMock(proto) [OCMockObject niceMockForProtocol:proto] +#define _PFPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define PFStrictClassMock(...) _PFMockShim(_PFStrictClassMock, __VA_ARGS__) +#define PFClassMock(...) _PFMockShim(_PFClassMock, __VA_ARGS__) +#define PFStrictProtocolMock(...) _PFMockShim(_PFStrictProtocolMock, __VA_ARGS__) +#define PFProtocolMock(...) _PFMockShim(_PFProtocolMock, __VA_ARGS__) +#define PFPartialMock(...) _PFMockShim(_PFPartialMock, __VA_ARGS__) + +#undef OCMStrictClassMock +#undef OCMClassMock +#undef OCMStrictProtocolMock +#undef OCMProtocolMock +#undef OCMPartialMock + +#define OCMStrictClassMock _PFOCMockWarning _PFStrictClassMock +#define OCMClassMock _PFOCMockWarning _PFClassMock +#define OCMStrictProtocolMock _PFOCMockWarning _PFStrictProtocolMock +#define OCMProtocolMock _PFOCMockWarning _PFProtocolMock +#define OCMPartialMock _PFOCMockWarning _PFPartialMock + +#define PFAssertIsKindOfClass(a1, a2, description...) \ +XCTAssertTrue([a1 isKindOfClass:[a2 class]], ## description) + +#define PFAssertNotKindOfClass(a1, a2, description...) \ +XCTAssertFalse([a1 isKindOfClass:[a2 class]], ## description) + +#define PFAssertThrowsInconsistencyException(expression, ...) \ +XCTAssertThrowsSpecificNamed(expression, NSException, NSInternalInconsistencyException, __VA_ARGS__) + +#define PFAssertThrowsInvalidArgumentException(expression, ...) \ +XCTAssertThrowsSpecificNamed(expression, NSException, NSInvalidArgumentException, __VA_ARGS__) + +#define PFAssertStringContains(a, b) XCTAssertTrue([(a) rangeOfString:(b)].location != NSNotFound) diff --git a/Tests/Other/TestCase/PFTwitterTestCase.m b/Tests/Other/TestCase/PFTwitterTestCase.m new file mode 100644 index 0000000..9afe39c --- /dev/null +++ b/Tests/Other/TestCase/PFTwitterTestCase.m @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterTestCase.h" + +@implementation PFTwitterTestCase { + NSMutableArray *_mocks; + dispatch_queue_t _mockQueue; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + _mocks = [[NSMutableArray alloc] init]; + _mockQueue = dispatch_queue_create("com.parse.tests.mock.queue", DISPATCH_QUEUE_SERIAL); +} + +- (void)tearDown { + dispatch_sync(_mockQueue, ^{ + [_mocks makeObjectsPerformSelector:@selector(stopMocking)]; + }); + + _mocks = nil; + _mockQueue = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (XCTestExpectation *)currentSelectorTestExpectation { + NSInvocation *invocation = self.invocation; + NSString *selectorName = invocation ? NSStringFromSelector(invocation.selector) : @"testExpectation"; + return [self expectationWithDescription:selectorName]; +} + +- (void)waitForTestExpectations { + [self waitForExpectationsWithTimeout:10.0 handler:nil]; +} + +///-------------------------------------- +#pragma mark - Mock Registration +///-------------------------------------- + +- (void)registerMockObject:(id)mockObject { + dispatch_sync(_mockQueue, ^{ + [_mocks addObject:mockObject]; + }); +} + +@end diff --git a/Tests/Resources/Info.plist b/Tests/Resources/Info.plist new file mode 100644 index 0000000..53d9932 --- /dev/null +++ b/Tests/Resources/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.parse.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Tests/TestApplication/Classes/main.m b/Tests/TestApplication/Classes/main.m new file mode 100644 index 0000000..453fc12 --- /dev/null +++ b/Tests/TestApplication/Classes/main.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface AppDelegate : NSObject + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].applicationFrame]; + window.rootViewController = [[UIViewController alloc] init]; + [window makeKeyAndVisible]; + return YES; +} + +@end + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Tests/TestApplication/Resources/Info.plist b/Tests/TestApplication/Resources/Info.plist new file mode 100644 index 0000000..0fd90b8 --- /dev/null +++ b/Tests/TestApplication/Resources/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.parse.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Tests/Unit/OAuth1FlowDialogTests.m b/Tests/Unit/OAuth1FlowDialogTests.m new file mode 100644 index 0000000..d7c7094 --- /dev/null +++ b/Tests/Unit/OAuth1FlowDialogTests.m @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOAuth1FlowDialog.h" +#import "PFTwitterTestCase.h" + +@interface UIActivityIndicatorView (Private) + +- (void)_generateImages; + +@end + +@interface OAuth1FlowDialogTests : PFTwitterTestCase +@end + +@interface UIDevice (Yolo) + +- (void)setOrientation:(UIDeviceOrientation)orientation animated:(BOOL)animated; + +@end + +@implementation OAuth1FlowDialogTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + NSURL *exampleURL = [NSURL URLWithString:@"http://foo.bar"]; + NSDictionary *parameters = @{ @"a" : @"b" }; + + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:exampleURL + queryParameters:parameters]; + XCTAssertNotNil(flowDialog); + XCTAssertEqualObjects(flowDialog.queryParameters, parameters); + XCTAssertEqualObjects(flowDialog->_baseURL, exampleURL); + + flowDialog = [PFOAuth1FlowDialog dialogWithURL:exampleURL queryParameters:parameters]; + XCTAssertNotNil(flowDialog); + XCTAssertEqualObjects(flowDialog.queryParameters, parameters); + XCTAssertEqualObjects(flowDialog->_baseURL, exampleURL); +} + +- (void)testTitle { + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:nil queryParameters:nil]; + XCTAssertEqualObjects(flowDialog.title, @"Connect to Service"); + flowDialog.title = @"Bleh"; + XCTAssertEqualObjects(flowDialog.title, @"Bleh"); +} + +- (void)testShow { + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:nil queryParameters:nil]; + + [flowDialog showAnimated:NO]; + [flowDialog layoutSubviews]; + [flowDialog dismissAnimated:NO]; +} + +- (void)testKeyboard { + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:nil queryParameters:nil]; + [flowDialog showAnimated:NO]; + + NSDictionary *notificationuserInfo = @{ UIKeyboardAnimationDurationUserInfoKey : @0, + UIKeyboardAnimationCurveUserInfoKey : @(UIViewAnimationCurveLinear) }; + [[NSNotificationCenter defaultCenter] postNotificationName:UIKeyboardWillShowNotification + object:nil + userInfo:notificationuserInfo]; + + [[NSNotificationCenter defaultCenter] postNotificationName:UIKeyboardWillHideNotification + object:nil + userInfo:notificationuserInfo]; + + [flowDialog dismissAnimated:NO]; +} + +- (void)testRotation { + [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait]; + [[UIDevice currentDevice] setOrientation:UIDeviceOrientationPortrait animated:NO]; + + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:nil queryParameters:nil]; + + [flowDialog showAnimated:NO]; + CGRect oldBounds = flowDialog.bounds; + + [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft]; + [[UIDevice currentDevice] setOrientation:UIDeviceOrientationLandscapeLeft animated:NO]; + [[NSNotificationCenter defaultCenter] postNotificationName:UIDeviceOrientationDidChangeNotification object:nil]; + + CGRect newBounds = flowDialog.bounds; + XCTAssertFalse(CGRectEqualToRect(oldBounds, newBounds)); + + [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait]; + [[UIDevice currentDevice] setOrientation:UIDeviceOrientationPortrait animated:NO]; + [[NSNotificationCenter defaultCenter] postNotificationName:UIDeviceOrientationDidChangeNotification object:nil]; + + newBounds = flowDialog.bounds; + XCTAssertTrue(CGRectEqualToRect(oldBounds, newBounds)); + + [flowDialog dismissAnimated:NO]; +} + +- (void)testWebViewDelegate { + NSURL *sampleURL = [NSURL URLWithString:@"http://foo.bar"]; + NSURL *successURL = [NSURL URLWithString:@"foo://success"]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + PFOAuth1FlowDialog *flowDialog = [[PFOAuth1FlowDialog alloc] initWithURL:sampleURL queryParameters:nil]; + flowDialog.redirectURLPrefix = @"foo://"; + flowDialog.completion = ^(BOOL succeeded, NSURL *url, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + XCTAssertEqualObjects(url, successURL); + + [expectation fulfill]; + }; + + [flowDialog showAnimated:NO]; + + id webView = PFStrictClassMock([UIWebView class]); + + NSURLRequest *request = [NSURLRequest requestWithURL:sampleURL]; + XCTAssertTrue([flowDialog webView:webView + shouldStartLoadWithRequest:request + navigationType:UIWebViewNavigationTypeOther]); + + [flowDialog webViewDidStartLoad:webView]; + [flowDialog webViewDidFinishLoad:webView]; + + NSURLRequest *successRequest = [NSURLRequest requestWithURL:successURL]; + [flowDialog webView:webView shouldStartLoadWithRequest:successRequest navigationType:UIWebViewNavigationTypeOther]; + + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/OAuthCoreTests.m b/Tests/Unit/OAuthCoreTests.m new file mode 100644 index 0000000..c4e20cb --- /dev/null +++ b/Tests/Unit/OAuthCoreTests.m @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +@import ObjectiveC.runtime; + +#import "PFTwitterTestCase.h" +#import "PF_OAuthCore.h" + +@implementation NSURLComponents (OAuthCoreTests) + ++ (Class)class { + return [super class]; +} + ++ (Class)_nilClass { + return nil; +} + +@end + +@interface OAuthCoreTests : PFTwitterTestCase + +@property (nonatomic, strong) NSDate *authDate; +@property (nonatomic, copy) NSString *authNonce; + +@end + +@implementation OAuthCoreTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSURL *)sampleURL { + return [NSURL URLWithString:@"https://localhost/foo/bar"]; +} + +- (NSData *)sampleData { + return [@"sampe=@!value" dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (void)assertAuthHeader:(NSString *)authHeader matchesExpectedSignature:(NSString *)signature { + XCTAssertTrue([authHeader hasPrefix:@"OAuth "]); + + PFAssertStringContains(authHeader, + ([NSString stringWithFormat:@"oauth_timestamp=\"%llu\"", + (unsigned long long)[_authDate timeIntervalSince1970]])); + + PFAssertStringContains(authHeader, ([NSString stringWithFormat:@"oauth_nonce=\"%@\"", _authNonce])); + PFAssertStringContains(authHeader, @"oauth_version=\"1.0\""); + PFAssertStringContains(authHeader, @"oauth_consumer_key=\"consumer_key\""); + PFAssertStringContains(authHeader, @"oauth_signature_method=\"HMAC-SHA1\""); + PFAssertStringContains(authHeader, ([NSString stringWithFormat:@"oauth_signature=\"%@\"", signature])); +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + _authDate = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; + _authNonce = @"UUID-STRING"; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testBasic { + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:[self sampleURL] + method:@"POST" + body:[self sampleData] + additionalParameters:nil + consumerKey:@"consumer_key" + consumerSecret:@"consumer_secret" + token:nil + tokenSecret:nil]; + configuration.nonce = self.authNonce; + configuration.timestampDate = self.authDate; + NSString *authHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + + [self assertAuthHeader:authHeader matchesExpectedSignature:@"3Nvy4O1Ok3qkeKcjvtv4wtyjc%2FY%3D"]; +} + +- (void)testNoBody { + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:[self sampleURL] + method:@"POST" + body:nil + additionalParameters:nil + consumerKey:@"consumer_key" + consumerSecret:@"consumer_secret" + token:nil + tokenSecret:nil]; + configuration.nonce = self.authNonce; + configuration.timestampDate = self.authDate; + NSString *authHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + + [self assertAuthHeader:authHeader matchesExpectedSignature:@"rXtmqPIUmMbl4e1%2Bz4JgJUuVIz0%3D"]; +} + +- (void)testWithToken { + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:[self sampleURL] + method:@"POST" + body:nil + additionalParameters:nil + consumerKey:@"consumer_key" + consumerSecret:@"consumer_secret" + token:@"token" + tokenSecret:nil]; + configuration.nonce = self.authNonce; + configuration.timestampDate = self.authDate; + NSString *authHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + + XCTAssertTrue([authHeader rangeOfString:@"oauth_token=\"token\""].location != NSNotFound); + [self assertAuthHeader:authHeader matchesExpectedSignature:@"iRsvN%2FUCXyzhf3o9tIL0DAX%2F4HY%3D"]; +} + +- (void)testNoNSURLComponents { + // Disable NSURLComponents for a single test. + Method originalMethod = class_getClassMethod([NSURLComponents class], @selector(class)); + Method replacementMethod = class_getClassMethod([NSURLComponents class], @selector(_nilClass)); + method_exchangeImplementations(originalMethod, replacementMethod); + + @try { + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:[self sampleURL] + method:@"POST" + body:[self sampleData] + additionalParameters:nil + consumerKey:@"consumer_key" + consumerSecret:@"consumer_secret" + token:nil + tokenSecret:nil]; + configuration.nonce = self.authNonce; + configuration.timestampDate = self.authDate; + NSString *authHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + + [self assertAuthHeader:authHeader matchesExpectedSignature:@"3Nvy4O1Ok3qkeKcjvtv4wtyjc%2FY%3D"]; + } @finally { + method_exchangeImplementations(originalMethod, replacementMethod); + } +} + +- (void)testWithQuery { + NSURL *url = [NSURL URLWithString:[[[self sampleURL] absoluteString] stringByAppendingString:@"?key=value"]]; + + PFOAuthConfiguration *configuration = [PFOAuthConfiguration configurationForURL:url + method:@"GET" + body:nil + additionalParameters:nil + consumerKey:@"consumer_key" + consumerSecret:@"consumer_secret" + token:nil + tokenSecret:nil]; + configuration.nonce = self.authNonce; + configuration.timestampDate = self.authDate; + NSString *authHeader = [PFOAuth authorizationHeaderFromConfiguration:configuration]; + [self assertAuthHeader:authHeader matchesExpectedSignature:@"LecftA2NX%2FvSD4KakdTFjPZlmc0%3D"]; +} + +@end diff --git a/Tests/Unit/TwitterAuthenticationProviderTests.m b/Tests/Unit/TwitterAuthenticationProviderTests.m new file mode 100644 index 0000000..63d020d --- /dev/null +++ b/Tests/Unit/TwitterAuthenticationProviderTests.m @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +@import Bolts.BFTask; + +#import "PFTwitterAuthenticationProvider.h" +#import "PFTwitterTestCase.h" +#import "PF_Twitter.h" + +@interface TwitterAuthenticationProviderTests : PFTwitterTestCase + +@end + +@implementation TwitterAuthenticationProviderTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PF_Twitter *)mockedTwitter { + PF_Twitter *twitter = PFStrictClassMock([PF_Twitter class]); + + OCMStub(twitter.consumerKey).andReturn(@"yarr"); + OCMStub(twitter.consumerSecret).andReturn(@"yolo"); + + return twitter; +} + +- (void)assertValidAuthenticationData:(NSDictionary *)authData forTwitter:(PF_Twitter *)twitter { + XCTAssertEqualObjects(authData[@"id"], @"a"); + XCTAssertEqualObjects(authData[@"screen_name"], @"b"); + XCTAssertEqualObjects(authData[@"auth_token"], @"c"); + XCTAssertEqualObjects(authData[@"auth_token_secret"], @"d"); + XCTAssertEqualObjects(authData[@"consumer_key"], twitter.consumerKey); + XCTAssertEqualObjects(authData[@"consumer_secret"], twitter.consumerSecret); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PF_Twitter *twitter = [self mockedTwitter]; + PFTwitterAuthenticationProvider *provider = [[PFTwitterAuthenticationProvider alloc] initWithTwitter:twitter]; + XCTAssertNotNil(provider); + XCTAssertEqual(provider.twitter, twitter); + + provider = [PFTwitterAuthenticationProvider providerWithTwitter:twitter]; + XCTAssertNotNil(provider); + XCTAssertEqual(provider.twitter, twitter); + + PFAssertThrowsInconsistencyException([PFTwitterAuthenticationProvider new]); +} + +- (void)testAuthData { + PF_Twitter *twitter = [self mockedTwitter]; + PFTwitterAuthenticationProvider *provider = [[PFTwitterAuthenticationProvider alloc] initWithTwitter:twitter]; + + NSDictionary *authData = [provider authDataWithTwitterId:@"a" + screenName:@"b" + authToken:@"c" + secret:@"d"]; + [self assertValidAuthenticationData:authData forTwitter:twitter]; +} + +- (void)testAuthType { + XCTAssertEqualObjects(PFTwitterUserAuthenticationType, @"twitter"); +} + +- (void)testAuthenticateAsync { + PF_Twitter *twitter = [self mockedTwitter]; + + OCMStub(twitter.userId).andReturn(@"a"); + OCMStub(twitter.screenName).andReturn(@"b"); + OCMStub(twitter.authToken).andReturn(@"c"); + OCMStub(twitter.authTokenSecret).andReturn(@"d"); + + BFTask *task = [BFTask taskWithResult:nil]; + OCMStub([twitter authorizeInBackground]).andReturn(task); + + PFTwitterAuthenticationProvider *provider = [PFTwitterAuthenticationProvider providerWithTwitter:twitter]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[provider authenticateAsync] continueWithBlock:^id(BFTask *task) { + NSDictionary *authData = task.result; + XCTAssertNotNil(authData); + [self assertValidAuthenticationData:authData forTwitter:twitter]; + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testRestoreAuthentication { + PF_Twitter *twitter = [self mockedTwitter]; + OCMExpect(twitter.userId = @"a"); + OCMExpect(twitter.screenName = @"b"); + OCMExpect(twitter.authToken = @"c"); + OCMExpect(twitter.authTokenSecret = @"d"); + + PFTwitterAuthenticationProvider *provider = [PFTwitterAuthenticationProvider providerWithTwitter:twitter]; + + NSDictionary *authData = @{ @"id" : @"a", + @"screen_name" : @"b", + @"auth_token" : @"c", + @"auth_token_secret" : @"d" }; + XCTAssertTrue([provider restoreAuthenticationWithAuthData:authData]); + + OCMVerifyAll((id)twitter); +} + +- (void)testRestoreAuthenticationBadData { + PF_Twitter *twitter = [self mockedTwitter]; + PFTwitterAuthenticationProvider *provider = [PFTwitterAuthenticationProvider providerWithTwitter:twitter]; + + NSDictionary *authData = @{ @"bad" : @"data" }; + XCTAssertFalse([provider restoreAuthenticationWithAuthData:authData]); +} + +- (void)testRestoreAuthenticationWithNoData { + PF_Twitter *twitter = [self mockedTwitter]; + OCMExpect(twitter.userId = nil); + OCMExpect(twitter.authToken = nil); + OCMExpect(twitter.authTokenSecret = nil); + OCMExpect(twitter.screenName = nil); + + PFTwitterAuthenticationProvider *provider = [PFTwitterAuthenticationProvider providerWithTwitter:twitter]; + XCTAssertTrue([provider restoreAuthenticationWithAuthData:nil]); + + OCMVerifyAll((id)twitter); +} + +@end diff --git a/Tests/Unit/TwitterTests.m b/Tests/Unit/TwitterTests.m new file mode 100644 index 0000000..6100b06 --- /dev/null +++ b/Tests/Unit/TwitterTests.m @@ -0,0 +1,625 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +@import Accounts; +@import Bolts.BFTask; +@import Parse.PFConstants; +@import Social; + +#import "PFOAuth1FlowDialog.h" +#import "PFTwitterAlertView.h" +#import "PFTwitterTestCase.h" +#import "PFTwitterTestMacros.h" +#import "PF_Twitter_Private.h" + +typedef void (^NSURLSessionDataTaskCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + +@interface TwitterTests : PFTwitterTestCase +@end + +@implementation TwitterTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PF_Twitter *twitter = [[PF_Twitter alloc] init]; + XCTAssertNotNil(twitter); + XCTAssertNotNil(twitter.accountStore); + + ACAccountStore *store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + id dialogClass = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:dialogClass]; + XCTAssertNotNil(twitter); + XCTAssertEqual(twitter.accountStore, store); + XCTAssertEqual(twitter.urlSession, session); + XCTAssertEqual(twitter.oauthDialogClass, dialogClass); +} + +- (void)testProperties { + PF_Twitter *twitter = [[PF_Twitter alloc] init]; + + XCTAssertNil(twitter.consumerKey); + XCTAssertNil(twitter.consumerKey); + XCTAssertNil(twitter.consumerSecret); + XCTAssertNil(twitter.authToken); + XCTAssertNil(twitter.authTokenSecret); + XCTAssertNil(twitter.userId); + XCTAssertNil(twitter.screenName); + + twitter.consumerKey = @"a"; + XCTAssertEqualObjects(twitter.consumerKey, @"a"); + twitter.consumerSecret = @"b"; + XCTAssertEqualObjects(twitter.consumerSecret, @"b"); + twitter.authToken = @"c"; + XCTAssertEqualObjects(twitter.authToken, @"c"); + twitter.authTokenSecret = @"d"; + XCTAssertEqualObjects(twitter.authTokenSecret, @"d"); + twitter.userId = @"e"; + XCTAssertEqualObjects(twitter.userId, @"e"); + twitter.screenName = @"f"; + XCTAssertEqualObjects(twitter.screenName, @"f"); +} + +- (void)testAuthorizeWithoutRequiredKeys { + id store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + //TODO: (nlutsenko) Add code verification when we have proper code reported. + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testSignRequest { + id store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [twitter signRequest:request]; + + XCTAssertNotNil([request valueForHTTPHeaderField:@"Authorization"]); +} + +- (void)testAuthorizeWithCallbackBlocks { + id store = PFStrictClassMock([ACAccountStore class]); + NSURLSession *session = PFStrictClassMock([NSURLSession class]); + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:store urlSession:session dialogClass:mockedDialog]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [twitter authorizeWithSuccess:^{ + XCTFail(@"Did not expect success!"); + } failure:^(NSError *error) { + [expectation fulfill]; + } cancel:^{ + XCTFail(@"Did not expect cancellation!"); + }]; + + [self waitForTestExpectations]; +} + +- (void)testAuthorizeWithLocalAccountErrorAndNetworkError { + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedOperationQueue = PFStrictClassMock([NSOperationQueue class]); + id mockedComposeViewController = PFStrictClassMock([SLComposeViewController class]); + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + + OCMStub(ClassMethod([mockedComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])).andReturn(YES); + OCMStub([mockedStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]).andReturn(nil); + + OCMStub([mockedStore requestAccessToAccountsWithType:nil + options:nil + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained ACAccountStoreRequestAccessCompletionHandler handler = nil; + [invocation getArgument:&handler atIndex:4]; + + handler(NO, expectedError); + }); + + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + completionHandler(nil, nil, expectedError); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.error, expectedError); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + OCMVerifyAll(mockedOperationQueue); +} + +- (void)testAuthorizeWithoutLocalAccountAndNetworkError { + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedOperationQueue = PFStrictClassMock([NSOperationQueue class]); + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + completionHandler(nil, nil, expectedError); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.error, expectedError); + XCTAssertNil(task.result); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + OCMVerifyAll(mockedOperationQueue); +} + +- (void)testAuthorizeWithLocalAccountAndNetworkError { + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedComposeViewController = PFStrictClassMock([SLComposeViewController class]); + + OCMStub(ClassMethod([mockedComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])).andReturn(YES); + OCMStub([mockedStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]).andReturn(nil); + + OCMStub([mockedStore requestAccessToAccountsWithType:nil + options:nil + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained ACAccountStoreRequestAccessCompletionHandler handler = nil; + [invocation getArgument:&handler atIndex:4]; + + handler(YES, nil); + }); + + id mockedAccount = PFStrictClassMock([ACAccount class]); + + NSArray *twitterAccounts = @[ mockedAccount ]; + OCMStub([mockedStore accountsWithAccountType:nil]).andReturn(twitterAccounts); + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + + __block NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [OCMStub([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + completionHandler = obj; + return (obj != nil); + }]]).andDo(^(NSInvocation *invocation) { + completionHandler(nil, nil, expectedError); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.error, expectedError); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testAuthorizeWithSingleLocalAccountAndNetworkSuccess { + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedComposeViewController = PFStrictClassMock([SLComposeViewController class]); + id mockedSLRequest = PFStrictClassMock([SLRequest class]); + + OCMStub(ClassMethod([mockedComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])).andReturn(YES); + OCMStub([mockedStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]).andReturn(nil); + + OCMStub([mockedStore requestAccessToAccountsWithType:nil + options:nil + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained ACAccountStoreRequestAccessCompletionHandler handler = nil; + [invocation getArgument:&handler atIndex:4]; + + handler(YES, nil); + }); + + id mockedAccount = PFStrictClassMock([ACAccount class]); + OCMStub([mockedAccount accountType]).andReturn(nil); + + NSArray *twitterAccounts = @[ mockedAccount ]; + OCMStub([mockedStore accountsWithAccountType:nil]).andReturn(twitterAccounts); + + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + NSString *successString = @"oauth_token=request_token&oauth_token_secret=request_secret"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + __weak typeof(mockedSLRequest) weakSLRequest = mockedSLRequest; + OCMStub(ClassMethod([[mockedSLRequest ignoringNonObjectArgs] requestForServiceType:SLServiceTypeTwitter + requestMethod:0 + URL:OCMOCK_ANY + parameters:OCMOCK_ANY])) + .andDo(^(NSInvocation *invocation) { + __strong typeof(mockedSLRequest) slRequest = weakSLRequest; + [invocation setReturnValue:&slRequest]; + }); + + OCMStub([mockedSLRequest setAccount:OCMOCK_ANY]); + OCMStub([mockedSLRequest performRequestWithHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained SLRequestHandler requestHandler = nil; + [invocation getArgument:&requestHandler atIndex:2]; + + NSString *successString = @"oauth_token=access_token&oauth_token_secret=access_secret&user_id=test_user&" + @"screen_name=test_name"; + requestHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }); + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + XCTAssertNil(task.result); + + XCTAssertEqualObjects(@"access_token", twitter.authToken); + XCTAssertEqualObjects(@"access_secret", twitter.authTokenSecret); + XCTAssertEqualObjects(@"test_user", twitter.userId); + XCTAssertEqualObjects(@"test_name", twitter.screenName); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testAuthorizeWithMultipleLocalAccountsAndNetworkSuccess { + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedComposeViewController = PFStrictClassMock([SLComposeViewController class]); + id mockedSLRequest = PFStrictClassMock([SLRequest class]); + id mockedAlertView = PFStrictClassMock([PFTwitterAlertView class]); + + OCMStub(ClassMethod([mockedComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])).andReturn(YES); + OCMStub([mockedStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]).andReturn(nil); + + OCMStub([mockedStore requestAccessToAccountsWithType:nil + options:nil + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained ACAccountStoreRequestAccessCompletionHandler handler = nil; + [invocation getArgument:&handler atIndex:4]; + + handler(YES, nil); + }); + + id mockedAccount = PFStrictClassMock([ACAccount class]); + OCMStub([mockedAccount accountType]).andReturn(nil); + OCMStub([mockedAccount valueForKey:@"accountDescription"]).andReturn(@"An Account"); + + NSArray *twitterAccounts = @[ mockedAccount, mockedAccount ]; + OCMStub([mockedStore accountsWithAccountType:nil]).andReturn(twitterAccounts); + + [OCMExpect([mockedSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + + NSString *successString = @"oauth_token=request_token&oauth_token_secret=request_secret"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + __weak typeof(mockedSLRequest) weakSLRequest = mockedSLRequest; + OCMStub(ClassMethod([[mockedSLRequest ignoringNonObjectArgs] requestForServiceType:SLServiceTypeTwitter + requestMethod:0 + URL:OCMOCK_ANY + parameters:OCMOCK_ANY])) + .andDo(^(NSInvocation *invocation) { + __strong typeof(mockedSLRequest) slRequest = weakSLRequest; + [invocation setReturnValue:&slRequest]; + }); + + OCMStub([mockedSLRequest setAccount:OCMOCK_ANY]); + OCMStub([mockedSLRequest performRequestWithHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained SLRequestHandler requestHandler = nil; + [invocation getArgument:&requestHandler atIndex:2]; + + NSString *successString = @"oauth_token=access_token&oauth_token_secret=access_secret&user_id=test_user&" + @"screen_name=test_name"; + requestHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }); + + OCMStub(ClassMethod([mockedAlertView showAlertWithTitle:OCMOCK_ANY + message:nil + cancelButtonTitle:@"Cancel" + otherButtonTitles:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj count] == 2; + }] + completion:OCMOCK_ANY])).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFTwitterAlertViewCompletion completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:6]; + + completionHandler(0); + }); + + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + XCTAssertNil(task.result); + + XCTAssertEqualObjects(@"access_token", twitter.authToken); + XCTAssertEqualObjects(@"access_secret", twitter.authTokenSecret); + XCTAssertEqualObjects(@"test_user", twitter.userId); + XCTAssertEqualObjects(@"test_name", twitter.screenName); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testAuthorizeWithZeroLocalAccountsAndNetworkSuccess { + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + id mockedComposeViewController = PFStrictClassMock([SLComposeViewController class]); + id mockedSLRequest = PFStrictClassMock([SLRequest class]); + + OCMStub(ClassMethod([mockedComposeViewController isAvailableForServiceType:SLServiceTypeTwitter])).andReturn(YES); + OCMStub([mockedStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]).andReturn(nil); + + OCMStub([mockedStore requestAccessToAccountsWithType:nil + options:nil + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained ACAccountStoreRequestAccessCompletionHandler handler = nil; + [invocation getArgument:&handler atIndex:4]; + + handler(YES, nil); + }); + + id mockedAccount = PFStrictClassMock([ACAccount class]); + OCMStub([mockedAccount accountType]).andReturn(nil); + + NSArray *twitterAccounts = @[]; + OCMStub([mockedStore accountsWithAccountType:nil]).andReturn(twitterAccounts); + + __weak typeof(mockedSLRequest) weakSLRequest = mockedSLRequest; + OCMStub(ClassMethod([[mockedSLRequest ignoringNonObjectArgs] requestForServiceType:SLServiceTypeTwitter + requestMethod:0 + URL:OCMOCK_ANY + parameters:OCMOCK_ANY])) + .andDo(^(NSInvocation *invocation) { + __strong typeof(mockedSLRequest) slRequest = weakSLRequest; + [invocation setReturnValue:&slRequest]; + }); + + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + + NSString *successString = @"oauth_token=request_token&oauth_token_secret=request_secret"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"access_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + + NSString *successString = @"oauth_token=access_token&oauth_token_secret=access_secret&user_id=test_user&" + @"screen_name=test_name"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + OCMExpect([mockedDialog dialogWithURL:OCMOCK_ANY queryParameters:OCMOCK_ANY]).andReturnWeak(mockedDialog); + OCMExpect([mockedDialog setRedirectURLPrefix:@"http://twitter-oauth.callback"]); + + __block PFOAuth1FlowDialogCompletion completionHandler = nil; + OCMExpect([mockedDialog setCompletion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFOAuth1FlowDialogCompletion newHandler = nil; + [invocation getArgument:&newHandler atIndex:2]; + + completionHandler = [newHandler copy]; + }); + + OCMExpect([[mockedDialog ignoringNonObjectArgs] showAnimated:NO]).andDo(^(NSInvocation *invocation) { + completionHandler( + YES, + [NSURL URLWithString:@"http://twitter-oauth.callback/?oauth_token=sucess_token&oauth_token_secret=success_secret"], + nil + ); + }); + + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + XCTAssertNil(task.result); + + XCTAssertEqualObjects(@"access_token", twitter.authToken); + XCTAssertEqualObjects(@"access_secret", twitter.authTokenSecret); + XCTAssertEqualObjects(@"test_user", twitter.userId); + XCTAssertEqualObjects(@"test_name", twitter.screenName); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedDialog); + OCMVerifyAll(mockedURLSession); +} + +- (void)testAuthorizeWithoutLocalAccountAndNetworkSuccess { + id mockedDialog = PFStrictProtocolMock(@protocol(PFOAuth1FlowDialogInterface)); + id mockedStore = PFStrictClassMock([ACAccountStore class]); + id mockedURLSession = PFStrictClassMock([NSURLSession class]); + + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"request_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + + NSString *successString = @"oauth_token=request_token&oauth_token_secret=request_secret"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + [OCMExpect([mockedURLSession dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + NSURLRequest *request = obj; + return [request.URL.lastPathComponent isEqualToString:@"access_token"]; + }] completionHandler:[OCMArg isNotNil]]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSURLSessionDataTaskCompletionHandler completionHandler = nil; + [invocation getArgument:&completionHandler atIndex:3]; + + + NSString *successString = @"oauth_token=access_token&oauth_token_secret=access_secret&user_id=test_user&" + @"screen_name=test_name"; + completionHandler([successString dataUsingEncoding:NSUTF8StringEncoding], nil, nil); + }) andReturn:[OCMockObject niceMockForClass:[NSURLSessionDataTask class]]]; + + OCMExpect([mockedDialog dialogWithURL:OCMOCK_ANY queryParameters:OCMOCK_ANY]).andReturnWeak(mockedDialog); + OCMExpect([mockedDialog setRedirectURLPrefix:@"http://twitter-oauth.callback"]); + + __block PFOAuth1FlowDialogCompletion completionHandler = nil; + OCMExpect([mockedDialog setCompletion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFOAuth1FlowDialogCompletion newHandler = nil; + [invocation getArgument:&newHandler atIndex:2]; + + completionHandler = [newHandler copy]; + }); + + OCMExpect([[mockedDialog ignoringNonObjectArgs] showAnimated:NO]).andDo(^(NSInvocation *invocation) { + completionHandler( + YES, + [NSURL URLWithString:@"http://twitter-oauth.callback/?oauth_token=sucess_token&oauth_token_secret=success_secret"], + nil + ); + }); + + PF_Twitter *twitter = [[PF_Twitter alloc] initWithAccountStore:mockedStore + urlSession:mockedURLSession + dialogClass:mockedDialog]; + + twitter.consumerKey = @"consumer_key"; + twitter.consumerSecret = @"consumer_secret"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[twitter authorizeInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + XCTAssertNil(task.result); + + XCTAssertEqualObjects(@"access_token", twitter.authToken); + XCTAssertEqualObjects(@"access_secret", twitter.authTokenSecret); + XCTAssertEqualObjects(@"test_user", twitter.userId); + XCTAssertEqualObjects(@"test_name", twitter.screenName); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedDialog); + OCMVerifyAll(mockedURLSession); +} + +@end diff --git a/Tests/Unit/TwitterUtilsTests.m b/Tests/Unit/TwitterUtilsTests.m new file mode 100644 index 0000000..f0fc212 --- /dev/null +++ b/Tests/Unit/TwitterUtilsTests.m @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTwitterTestCase.h" +#import "PFTwitterUtils_Private.h" +#import "PF_Twitter.h" + +@import Parse.PFUser; +@import Parse.PFUserAuthenticationDelegate; + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +@interface TwitterUtilsTests : PFTwitterTestCase + +@end + +@implementation TwitterUtilsTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)tearDown { + [PFTwitterUtils _setAuthenticationProvider:nil]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInitialize { + id userMock = PFStrictClassMock([PFUser class]); + OCMExpect(ClassMethod([userMock registerAuthenticationDelegate:[OCMArg checkWithBlock:^BOOL(id obj) { + return (obj != nil); + }] forAuthType:@"twitter"])); + + [PFTwitterUtils initializeWithConsumerKey:@"a" consumerSecret:@"b"]; + XCTAssertNotNil([PFTwitterUtils twitter]); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerKey, @"a"); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerSecret, @"b"); + + OCMVerifyAll(userMock); +} + +- (void)testInitializeTwice { + id userMock = PFStrictClassMock([PFUser class]); + + [PFTwitterUtils initializeWithConsumerKey:@"a" consumerSecret:@"b"]; + XCTAssertNotNil([PFTwitterUtils twitter]); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerKey, @"a"); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerSecret, @"b"); + + [PFTwitterUtils initializeWithConsumerKey:@"b" consumerSecret:@"c"]; + XCTAssertNotNil([PFTwitterUtils twitter]); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerKey, @"a"); + XCTAssertEqualObjects([PFTwitterUtils twitter].consumerSecret, @"b"); + + OCMVerifyAll(userMock); +} + +@end diff --git a/third_party_licenses.txt b/third_party_licenses.txt new file mode 100644 index 0000000..492c443 --- /dev/null +++ b/third_party_licenses.txt @@ -0,0 +1,35 @@ +THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THE PARSE PRODUCT. + +----- + +The following software may be included in this product: OAuthCore. This software contains the following license and notice below: + +Copyright (C) 2012 Loren Brichter + +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. + +-------------------------------------------------------------------- + +Copyright 2001-2004 Unicode, Inc. + +Disclaimer + +This source code is provided as is by Unicode, Inc. No claims are +made as to fitness for any particular purpose. No warranties of any +kind are expressed or implied. The recipient agrees to determine +applicability of information provided. If this file has been +purchased on magnetic or optical media from Unicode, Inc., the +sole remedy for any claim will be exchange of defective media +within 90 days of receipt. + +Limitations on Rights to Redistribute This Code + +Unicode, Inc. hereby grants the right to freely use the information +supplied in this file in the creation of products supporting the +Unicode Standard, and to make copies of this file in any form +for internal or external distribution as long as this notice +remains attached.