Move code from braintree-ios

This commit is contained in:
lkorth and brluk
2016-09-09 15:41:36 -05:00
committed by bluk
commit 249331f4bd
296 changed files with 22292 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.strings text

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# OS X
.DS_Store
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
profile
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcscmblueprint
# CocoaPods
Pods
appledocs
# This file is needed for signing the ipa with our enterprise cert;
# you can get it from the iOS Developer Center.
EverybodyVenmo.mobileprovision
# This file should contain your HockeyApp auth
# token from https://rink.hockeyapp.net/manage/auth_tokens
.hockeyapp
*.pid

1
.ruby-gemset Normal file
View File

@@ -0,0 +1 @@
braintree-ios-drop-in

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
2.1.5

26
.travis.yml Normal file
View File

@@ -0,0 +1,26 @@
language: objective-c
cache:
- bundler
- cocoapods
osx_image: xcode7.3
before_install:
- brew update || brew update
- brew outdated xctool || brew upgrade xctool
- SIMULATOR_ID=$(xcrun instruments -s | grep -o "iPhone 6 (9.3) \[.*\]" | grep -o "\[.*\]" | sed "s/^\[\(.*\)\]$/\1/")
install:
- bundle install
- bundle exec pod install
script:
- echo $SIMULATOR_ID
# Launching the simulator before building and testing is necessary to prevent spurious timeouts.
# Specifying the simulator UDID is also necessary.
- open -a "Simulator" --args -CurrentDeviceUDID $SIMULATOR_ID
- travis_wait bundle exec rake spec:unit
- bundle exec rake spec:api:integration
notifications:
email:
- team-ios@getbraintree.com

313
ACKNOWLEDGEMENTS.md Normal file
View File

@@ -0,0 +1,313 @@
# Acknowledgements
This application makes use of the following third party libraries:
## NSURL+QueryDictionary
The MIT License (MIT)
Copyright (c) 2013 Jonathan Crooke
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.
## AFNetworking
Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
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.
## Braintree
Copyright (c) 2015 Braintree, a division of PayPal, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## CardIO
All header files are released under the MIT License:
The MIT License (MIT)
Copyright (c) 2013-2014 eBay Software Foundation
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.
## FLEX
Copyright (c) 2014, Flipboard
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 of Flipboard 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.
## HockeySDK
## Licenses
The Hockey SDK is provided under the following license:
The MIT License
Copyright (c) 2012-2015 HockeyApp, Bit Stadium GmbH.
All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
Except as noted below, PLCrashReporter
is provided under the following license:
Copyright (c) 2008 - 2015 Plausible Labs Cooperative, Inc.
Copyright (c) 2012 - 2015 HockeyApp, Bit Stadium GmbH.
All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
The protobuf-c library, as well as the PLCrashLogWriterEncoding.c
file are licensed as follows:
Copyright 2008, Dave Benson.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with
the License. You may obtain a copy of the License
at http://www.apache.org/licenses/LICENSE-2.0 Unless
required by applicable law or agreed to in writing,
software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
TTTAttributedLabel is licensed as follows:
Copyright (c) 2011 Mattt Thompson (http://mattt.me/)
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.
SFHFKeychainUtils is licensed as follows:
Created by Buzz Andersen on 10/20/08.
Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
## InAppSettingsKit
Copyright (c) 2009-2014:
Luc Vandal, Edovia Inc., http://www.edovia.com
Ortwin Gentz, FutureTap GmbH, http://www.futuretap.com
All rights reserved.
It is appreciated but not required that you give credit to Luc Vandal and Ortwin Gentz,
as the original authors of this code. You can give credit in a blog post, a tweet or on
a info page of your app. Also, the original authors appreciate letting them know if you
use this code.
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
This code is licensed under the BSD license that is available at: <http://www.opensource.org/licenses/bsd-license.php>
## PureLayout
This code is distributed under the terms and conditions of the MIT license.
Copyright (c) 2014-2015 Tyler Fox
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.
## iOS-Slide-Menu
Created by Aryan Gh on 5/4/13.
Copyright (c) 2013 Aryan Ghassemi. All rights reserved.
https://github.com/aryaxt/iOS-Slide-Menu
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.
Generated by CocoaPods - http://cocoapods.org

44
BraintreeDropIn.podspec Normal file
View File

@@ -0,0 +1,44 @@
Pod::Spec.new do |s|
s.name = "BraintreeDropIn"
s.version = "4.6.1"
s.summary = "Braintree v.zero: A modern foundation for accepting payments"
s.description = <<-DESC
Braintree is a full-stack payments platform for developers
This CocoaPod will help you accept payments in your iOS app.
Check out our development portal at https://developers.braintreepayments.com.
DESC
s.homepage = "https://www.braintreepayments.com/how-braintree-works"
s.documentation_url = "https://developers.braintreepayments.com/ios/start/hello-client"
s.screenshots = "https://raw.githubusercontent.com/braintree/braintree_ios/master/screenshot.png"
s.license = "MIT"
s.author = { "Braintree" => "code@getbraintree.com" }
s.source = { :git => "https://github.com/braintree/braintree-ios-drop-in.git", :tag => s.version.to_s }
s.social_media_url = "https://twitter.com/braintree"
s.platform = :ios, "9.0"
s.requires_arc = true
s.compiler_flags = "-Wall -Werror -Wextra"
s.default_subspecs = %w[DropIn]
s.subspec "DropIn" do |s|
s.source_files = "BraintreeDropIn/**/*.{h,m}"
s.public_header_files = "BraintreeDropIn/Public/*.h"
s.frameworks = "UIKit"
s.dependency "Braintree/Card", "~> 4.0"
s.dependency "Braintree/Core", "~> 4.0"
s.dependency "Braintree/UnionPay", "~> 4.0"
s.dependency "BraintreeDropIn/UIKit"
end
s.subspec "UIKit" do |s|
s.source_files = "BraintreeUIKit/**/*.{h,m}"
s.public_header_files = "BraintreeUIKit/Public/*.h"
s.frameworks = "UIKit"
s.resource_bundles = {
"Braintree-UIKit-Localization" => ["BraintreeUIKit/Localization/*.lproj"] }
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:BraintreeDropIn.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A56C41691D833348000DFFAB"
BuildableName = "BraintreeDropIn.framework"
BlueprintName = "BraintreeDropIn"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A56C41731D833348000DFFAB"
BuildableName = "BraintreeDropInTests.xctest"
BlueprintName = "BraintreeDropInTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A56C41691D833348000DFFAB"
BuildableName = "BraintreeDropIn.framework"
BlueprintName = "BraintreeDropIn"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A56C41691D833348000DFFAB"
BuildableName = "BraintreeDropIn.framework"
BlueprintName = "BraintreeDropIn"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A56C41691D833348000DFFAB"
BuildableName = "BraintreeDropIn.framework"
BlueprintName = "BraintreeDropIn"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A519F9F41D88618200819A39"
BuildableName = "libBraintreeDropIn-StaticLibrary.a"
BlueprintName = "BraintreeDropIn-StaticLibrary"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A519F9F41D88618200819A39"
BuildableName = "libBraintreeDropIn-StaticLibrary.a"
BlueprintName = "BraintreeDropIn-StaticLibrary"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A519F9F41D88618200819A39"
BuildableName = "libBraintreeDropIn-StaticLibrary.a"
BlueprintName = "BraintreeDropIn-StaticLibrary"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A52900351D88FB5100032220"
BuildableName = "BraintreeUIKit.framework"
BlueprintName = "BraintreeUIKit"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A52900351D88FB5100032220"
BuildableName = "BraintreeUIKit.framework"
BlueprintName = "BraintreeUIKit"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A52900351D88FB5100032220"
BuildableName = "BraintreeUIKit.framework"
BlueprintName = "BraintreeUIKit"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D131D835832009BE61A"
BuildableName = "DropInDemo.app"
BlueprintName = "DropInDemo"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D2B1D835832009BE61A"
BuildableName = "DropInDemoTests.xctest"
BlueprintName = "DropInDemoTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D361D835832009BE61A"
BuildableName = "DropInDemoUITests.xctest"
BlueprintName = "DropInDemoUITests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A529003E1D88FB5200032220"
BuildableName = "BraintreeUIKitTests.xctest"
BlueprintName = "BraintreeUIKitTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A1B1D8A1F090041BE92"
BuildableName = "UnitTests.xctest"
BlueprintName = "UnitTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A291D8A20D00041BE92"
BuildableName = "UITests.xctest"
BlueprintName = "UITests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D131D835832009BE61A"
BuildableName = "DropInDemo.app"
BlueprintName = "DropInDemo"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D131D835832009BE61A"
BuildableName = "DropInDemo.app"
BlueprintName = "DropInDemo"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5CE1D131D835832009BE61A"
BuildableName = "DropInDemo.app"
BlueprintName = "DropInDemo"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A291D8A20D00041BE92"
BuildableName = "UITests.xctest"
BlueprintName = "UITests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A1B1D8A1F090041BE92"
BuildableName = "UnitTests.xctest"
BlueprintName = "UnitTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A1B1D8A1F090041BE92"
BuildableName = "UnitTests.xctest"
BlueprintName = "UnitTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5411A1B1D8A1F090041BE92"
BuildableName = "UnitTests.xctest"
BlueprintName = "UnitTests"
ReferencedContainer = "container:BraintreeDropIn.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:BraintreeDropIn.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,30 @@
{
"DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "8507F29F2A160B72DBF0B7E1A26572B0035F8386",
"DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
},
"DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
"8507F29F2A160B72DBF0B7E1A26572B0035F8386" : 0,
"C651443B7B39808FD2DCA93E3E7C4F2141FF7B0D" : 0
},
"DVTSourceControlWorkspaceBlueprintIdentifierKey" : "90EDD10A-3848-47DD-9307-53E7EB32CAC5",
"DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
"8507F29F2A160B72DBF0B7E1A26572B0035F8386" : "braintree-ios-drop-in\/",
"C651443B7B39808FD2DCA93E3E7C4F2141FF7B0D" : "braintree-ios\/"
},
"DVTSourceControlWorkspaceBlueprintNameKey" : "BraintreeDropIn",
"DVTSourceControlWorkspaceBlueprintVersion" : 204,
"DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "BraintreeDropIn.xcworkspace",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
{
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:braintree\/braintree-ios-drop-in.git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8507F29F2A160B72DBF0B7E1A26572B0035F8386"
},
{
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.paypal.com:card-io\/braintree-vzero-ios.git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
"DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C651443B7B39808FD2DCA93E3E7C4F2141FF7B0D"
}
]
}

View File

@@ -0,0 +1,34 @@
#if __has_include("BraintreeCore.h")
#import "BraintreeCore.h"
#else
#import <BraintreeCore/BraintreeCore.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class BTPaymentMethodNonce;
@class BTHTTP;
@class BTAnalyticsService;
@interface BTAPIClient (Internal)
@property (nonatomic, copy, nullable) NSString *tokenizationKey;
@property (nonatomic, strong, nullable) BTClientToken *clientToken;
/// Client metadata that is used for tracking the client session
@property (nonatomic, readonly, strong) BTClientMetadata *metadata;
/// Exposed for testing analytics
@property (nonatomic, strong) BTAnalyticsService *analyticsService;
/// Analytics should only be posted by internal clients.
- (void)sendAnalyticsEvent:(NSString *)eventName;
/// An internal initializer to toggle whether to send an analytics event during initialization.
/// This prevents copyWithSource:integration: from sending a duplicate event. It can also be used
/// to suppress excessive network chatter during testing.
- (nullable instancetype)initWithAuthorization:(NSString *)authorization sendAnalyticsEvent:(BOOL)sendAnalyticsEvent;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,679 @@
#import "BTCardFormViewController.h"
#import "BTDropInController.h"
#import "BTPaymentSelectionViewController.h"
#import "BTEnrollmentVerificationViewController.h"
#if __has_include("BraintreeCore.h")
#import "BraintreeCore.h"
#else
#import <BraintreeCore/BraintreeCore.h>
#endif
#import "BTAPIClient_Internal_Category.h"
#import "BTUIKBarButtonItem_Internal_Declaration.h"
#import "BTEnrollmentVerificationViewController.h"
#import "BTDropInUIUtilities.h"
#if __has_include("BraintreeCard.h")
#import "BraintreeCard.h"
#else
#import <BraintreeCard/BraintreeCard.h>
#endif
#if __has_include("BraintreeUnionPay.h")
#import "BraintreeUnionPay.h"
#else
#import <BraintreeUnionPay/BraintreeUnionPay.h>
#endif
@interface BTCardFormViewController ()
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *scrollViewContentWrapper;
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong, readwrite) BTUIKCardNumberFormField *cardNumberField;
@property (nonatomic, strong, readwrite) BTUIKExpiryFormField *expirationDateField;
@property (nonatomic, strong, readwrite) BTUIKSecurityCodeFormField *securityCodeField;
@property (nonatomic, strong, readwrite) BTUIKPostalCodeFormField *postalCodeField;
@property (nonatomic, strong, readwrite) BTUIKMobileCountryCodeFormField *mobileCountryCodeField;
@property (nonatomic, strong, readwrite) BTUIKMobileNumberFormField *mobilePhoneField;
@property (nonatomic, strong) UIStackView *cardNumberErrorView;
@property (nonatomic, strong) UIStackView *cardNumberHeader;
@property (nonatomic, strong) UIStackView *enrollmentFooter;
@property (nonatomic, strong) UIButton *nextButton;
@property (nonatomic, strong) NSArray <BTUIKFormField *> *formFields;
@property (nonatomic, strong) NSMutableArray <BTUIKFormField *> *requiredFields;
@property (nonatomic, strong) NSMutableArray <BTUIKFormField *> *optionalFields;
@property (nonatomic, strong) UIStackView *cardNumberFooter;
@property (nonatomic, strong) BTUIKCardListLabel *cardList;
@property (nonatomic, getter=isCollapsed) BOOL collapsed;
@property (nonatomic, strong) BTUIKFormField *firstResponderFormField;
@property (nonatomic, strong, nullable, readwrite) BTCardCapabilities *cardCapabilities;
@property (nonatomic) BOOL unionPayEnabledMerchant;
@property (nonatomic, assign) BOOL cardEntryDidBegin;
@property (nonatomic, assign) BOOL cardEntryDidFocus;
@end
@implementation BTCardFormViewController
#pragma mark - Lifecycle
- (instancetype)initWithAPIClient:(BTAPIClient *)apiClient request:(nonnull BTDropInRequest *)request {
if (self = [super initWithAPIClient:apiClient request:request]) {
_requiredFields = [NSMutableArray new];
_optionalFields = [NSMutableArray new];
_supportedCardTypes = [NSArray new];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Using ivar so that setter is not called
_collapsed = YES;
self.unionPayEnabledMerchant = NO;
self.formFields = @[];
self.view.backgroundColor = [BTUIKAppearance sharedInstance].formBackgroundColor;
self.navigationController.navigationBar.barTintColor = [BTUIKAppearance sharedInstance].barBackgroundColor;
self.navigationController.navigationBar.translucent = NO;
[self.navigationController.navigationBar setTitleTextAttributes:@{
NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].primaryTextColor
}];
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView setAlwaysBounceVertical:NO];
self.scrollView.scrollEnabled = YES;
[self.view addSubview:self.scrollView];
self.scrollViewContentWrapper = [[UIView alloc] init];
self.scrollViewContentWrapper.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.scrollViewContentWrapper];
self.stackView = [BTDropInUIUtilities newStackView];
[self.scrollViewContentWrapper addSubview:self.stackView];
NSDictionary *viewBindings = @{@"stackView":self.stackView,
@"scrollView":self.scrollView,
@"scrollViewContentWrapper": self.scrollViewContentWrapper};
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor].active = YES;
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollViewContentWrapper]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollViewContentWrapper(scrollView)]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[stackView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[stackView]-|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)];
[self.view addGestureRecognizer:tapGesture];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[self setupForm];
[self resetForm];
[self showLoadingScreen:YES animated:NO];
[self loadConfiguration];
self.firstResponderFormField = self.cardNumberField;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.title = @"Card Details";
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear: animated];
if (self.firstResponderFormField) {
[self.firstResponderFormField becomeFirstResponder];
self.firstResponderFormField = nil;
}
}
#pragma mark - Setup
- (void)setupForm {
self.nextButton = [[UIButton alloc] init];
[self.nextButton setTitle:@"Next" forState:UIControlStateNormal];
self.nextButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.nextButton setTitleColor:self.view.tintColor forState:UIControlStateNormal];
self.cardNumberField = [[BTUIKCardNumberFormField alloc] init];
self.cardNumberField.delegate = self;
self.cardNumberField.cardNumberDelegate = self;
self.expirationDateField = [[BTUIKExpiryFormField alloc] init];
self.expirationDateField.delegate = self;
self.securityCodeField = [[BTUIKSecurityCodeFormField alloc] init];
self.securityCodeField.delegate = self;
self.postalCodeField = [[BTUIKPostalCodeFormField alloc] init];
self.postalCodeField.delegate = self;
self.mobileCountryCodeField = [[BTUIKMobileCountryCodeFormField alloc] init];
self.mobileCountryCodeField.delegate = self;
self.mobilePhoneField = [[BTUIKMobileNumberFormField alloc] init];
self.mobilePhoneField.delegate = self;
self.cardNumberHeader = [BTDropInUIUtilities newStackView];
self.cardNumberHeader.layoutMargins = UIEdgeInsetsMake(0, [BTUIKAppearance verticalFormSpace], 0, [BTUIKAppearance verticalFormSpace]);
self.cardNumberHeader.layoutMarginsRelativeArrangement = true;
UILabel *cardNumberHeaderLabel = [[UILabel alloc] init];
cardNumberHeaderLabel.numberOfLines = 0;
cardNumberHeaderLabel.textAlignment = NSTextAlignmentCenter;
cardNumberHeaderLabel.text = @"Enter your card details starting with the card number.";
[BTUIKAppearance styleLargeLabelSecondary:cardNumberHeaderLabel];
[self.cardNumberHeader addArrangedSubview:cardNumberHeaderLabel];
[BTDropInUIUtilities addSpacerToStackView:self.cardNumberHeader beforeView:cardNumberHeaderLabel size: [BTUIKAppearance verticalFormSpace]];
[self.stackView addArrangedSubview:self.cardNumberHeader];
self.formFields = @[self.cardNumberField, self.expirationDateField, self.securityCodeField, self.postalCodeField, self.mobileCountryCodeField, self.mobilePhoneField];
for (NSUInteger i = 0; i < self.formFields.count; i++) {
BTUIKFormField *formField = self.formFields[i];
[self.stackView addArrangedSubview:formField];
NSLayoutConstraint* heightConstraint = [formField.heightAnchor constraintEqualToConstant:[BTUIKAppearance formCellHeight]];
// Setting the prioprity is necessary to avoid autolayout errors when UIStackView rotates
heightConstraint.priority = UILayoutPriorityDefaultHigh;
heightConstraint.active = YES;
[formField updateConstraints];
}
self.cardNumberField.formLabel.text = @"";
[self.cardNumberField updateConstraints];
self.expirationDateField.hidden = YES;
self.securityCodeField.hidden = YES;
self.postalCodeField.hidden = YES;
self.mobileCountryCodeField.hidden = YES;
self.mobilePhoneField.hidden = YES;
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.cardNumberField size: [BTUIKAppearance verticalFormSpace]];
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.expirationDateField size: [BTUIKAppearance verticalFormSpace]];
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.mobileCountryCodeField size: [BTUIKAppearance verticalFormSpace]];
self.cardNumberFooter = [BTDropInUIUtilities newStackView];
self.cardNumberFooter.layoutMargins = UIEdgeInsetsMake(0, [BTUIKAppearance verticalFormSpace], 0, [BTUIKAppearance verticalFormSpace]);
self.cardNumberFooter.layoutMarginsRelativeArrangement = true;
[self.stackView addArrangedSubview:self.cardNumberFooter];
self.cardList = [BTUIKCardListLabel new];
self.cardList.translatesAutoresizingMaskIntoConstraints = NO;
self.cardList.availablePaymentOptions = self.supportedCardTypes;
[self.cardNumberFooter addArrangedSubview:self.cardList];
[BTDropInUIUtilities addSpacerToStackView:self.cardNumberFooter beforeView:self.cardList size: [BTUIKAppearance horizontalFormContentPadding]];
NSUInteger indexOfCardNumberField = [self.stackView.arrangedSubviews indexOfObject:self.cardNumberField];
[self.stackView insertArrangedSubview:self.cardNumberFooter atIndex:(indexOfCardNumberField + 1)];
[self updateFormBorders];
//Error labels
self.cardNumberErrorView = [BTDropInUIUtilities newStackViewForError:@"You must provide a valid card number"];
[self cardNumberErrorHidden:YES];
//Enrollment footer
self.enrollmentFooter = [BTDropInUIUtilities newStackView];
self.enrollmentFooter.layoutMargins = UIEdgeInsetsMake(0, [BTUIKAppearance horizontalFormContentPadding], 0, [BTUIKAppearance horizontalFormContentPadding]);
self.enrollmentFooter.layoutMarginsRelativeArrangement = true;
UILabel *enrollmentFooterLabel = [[UILabel alloc] init];
enrollmentFooterLabel.numberOfLines = 0;
enrollmentFooterLabel.textAlignment = [BTUIKViewUtil naturalTextAlignment];
enrollmentFooterLabel.text = @"Enrollment is required for this card. An enrollment number will be sent by SMS.";
[BTUIKAppearance styleLabelSecondary:enrollmentFooterLabel];
[self.enrollmentFooter addArrangedSubview:enrollmentFooterLabel];
[BTDropInUIUtilities addSpacerToStackView:self.enrollmentFooter beforeView:enrollmentFooterLabel size: [BTUIKAppearance verticalFormSpaceTight]];
self.enrollmentFooter.hidden = YES;
[self.stackView addArrangedSubview:self.enrollmentFooter];
}
- (void)configurationLoaded:(__unused BTConfiguration *)configuration error:(NSError *)error {
[self showLoadingScreen:NO animated:YES];
if (!error) {
self.collapsed = YES;
self.unionPayEnabledMerchant = NO;
BTJSON *unionPayJSON = self.configuration.json[@"unionPay"];
if (![unionPayJSON isError] && [unionPayJSON[@"enabled"] isTrue] && !self.apiClient.tokenizationKey) {
self.unionPayEnabledMerchant = YES;
[self.cardNumberField setAccessoryViewHidden:NO animated:NO];
}
self.cardNumberField.state = BTUIKCardNumberFormFieldStateValidate;
[self updateRequiredFields];
}
}
- (void)updateRequiredFields {
NSArray <NSString *> *challenges = [self.configuration.json[@"challenges"] asStringArray];
self.requiredFields = [NSMutableArray arrayWithArray:@[self.cardNumberField, self.expirationDateField]];
if ([challenges containsObject:@"cvv"]) {
[self.requiredFields addObject:self.securityCodeField];
}
if ([challenges containsObject:@"postal_code"]) {
[self.requiredFields addObject:self.postalCodeField];
}
}
#pragma mark - Custom accessors
- (BTCardRequest *)cardRequest {
if (![self isFormValid]) {
return nil;
}
BTCard *card = [[BTCard alloc] initWithNumber:self.cardNumberField.number
expirationMonth:self.expirationDateField.expirationMonth
expirationYear:self.expirationDateField.expirationYear
cvv:self.securityCodeField.securityCode];
if ([self.requiredFields containsObject:self.postalCodeField]) {
card.postalCode = self.postalCodeField.postalCode;
}
card.shouldValidate = self.apiClient.tokenizationKey ? NO : YES;
BTCardRequest *cardRequest = [[BTCardRequest alloc] initWithCard:card];
if (self.cardCapabilities != nil && self.cardCapabilities.isUnionPay && self.cardCapabilities.isSupported) {
cardRequest.mobileCountryCode = self.mobileCountryCodeField.countryCode;
cardRequest.mobilePhoneNumber = self.mobilePhoneField.mobileNumber;
}
return cardRequest;
}
- (void)setCollapsed:(BOOL)collapsed {
if (collapsed == self.collapsed) {
return;
}
// Using ivar so that setter is not called
_collapsed = collapsed;
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionBeginFromCurrentState animations:^{
self.cardNumberFooter.hidden = !collapsed;
self.cardNumberHeader.hidden = !collapsed;
self.expirationDateField.hidden = collapsed;
self.securityCodeField.hidden = ![self.requiredFields containsObject:self.securityCodeField] || collapsed;
self.postalCodeField.hidden = ![self.requiredFields containsObject:self.postalCodeField] || collapsed;
self.mobileCountryCodeField.hidden = ![self.requiredFields containsObject:self.mobileCountryCodeField] || collapsed;
self.mobilePhoneField.hidden = ![self.requiredFields containsObject:self.mobilePhoneField] || collapsed;
self.enrollmentFooter.hidden = self.mobilePhoneField.hidden;
[self updateFormBorders];
} completion:^(__unused BOOL finished) {
self.cardNumberFooter.hidden = !collapsed;
self.cardNumberHeader.hidden = !collapsed;
self.expirationDateField.hidden = collapsed;
self.securityCodeField.hidden = ![self.requiredFields containsObject:self.securityCodeField] || collapsed;
self.postalCodeField.hidden = ![self.requiredFields containsObject:self.postalCodeField] || collapsed;
self.mobileCountryCodeField.hidden = ![self.requiredFields containsObject:self.mobileCountryCodeField] || collapsed;
self.mobilePhoneField.hidden = ![self.requiredFields containsObject:self.mobilePhoneField] || collapsed;
self.enrollmentFooter.hidden = self.mobilePhoneField.hidden;
[self updateFormBorders];
[self updateSubmitButton];
}];
});
}
#pragma mark - Public methods
- (void)resetForm {
self.navigationItem.leftBarButtonItem = [[BTUIKBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStylePlain target:self action:@selector(cancelTapped)];
BTUIKBarButtonItem *addButton = [[BTUIKBarButtonItem alloc] initWithTitle:@"Add Card" style:UIBarButtonItemStylePlain target:self action:@selector(tokenizeCard)];
addButton.bold = true;
self.navigationItem.rightBarButtonItem = addButton;
self.navigationItem.rightBarButtonItem.enabled = NO;
for (BTUIKFormField *formField in self.formFields) {
formField.text = @"";
formField.hidden = YES;
}
// Using ivar so that setter is not called
_collapsed = YES;
self.unionPayEnabledMerchant = NO;
self.cardNumberField.hidden = NO;
[self.cardNumberField resetFormField];
self.cardNumberFooter.hidden = NO;
self.cardNumberHeader.hidden = NO;
[self.cardList emphasizePaymentOption:BTUIKPaymentOptionTypeUnknown];
[self updateFormBorders];
}
#pragma mark - Keyboard management
-(void)hideKeyboard {
[self.view endEditing:YES];
}
- (void)keyboardWillShow:(NSNotification *)notification
{
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
- (void)keyboardWillHide:(__unused NSNotification *)notification
{
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = 0.0;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
#pragma mark - Helper methods
- (void)cancelTapped {
[self hideKeyboard];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)updateFormBorders {
self.cardNumberField.bottomBorder = YES;
self.cardNumberField.topBorder = YES;
self.mobileCountryCodeField.topBorder = YES;
self.mobileCountryCodeField.interFieldBorder = YES;
self.mobilePhoneField.bottomBorder = YES;
NSArray *groupedFormFields = @[self.expirationDateField, self.securityCodeField, self.postalCodeField];
BOOL topBorderAdded = NO;
BTUIKFormField* lastVisibleFormField;
for (NSUInteger i = 0; i < groupedFormFields.count; i++) {
BTUIKFormField *formField = groupedFormFields[i];
if (!formField.hidden) {
if (!topBorderAdded) {
formField.topBorder = YES;
topBorderAdded = YES;
} else {
formField.topBorder = NO;
}
formField.bottomBorder = NO;
formField.interFieldBorder = YES;
lastVisibleFormField = formField;
}
}
if (lastVisibleFormField) {
lastVisibleFormField.bottomBorder = YES;
}
}
- (BOOL)isFormValid {
__block BOOL isFormValid = YES;
[self.requiredFields enumerateObjectsUsingBlock:^(BTUIKFormField * _Nonnull formField, __unused NSUInteger idx, BOOL * _Nonnull stop) {
if (![self.optionalFields containsObject:formField] && !formField.valid) {
*stop = YES;
isFormValid = NO;
}
}];
return isFormValid;
}
- (void)updateSubmitButton {
if (!self.collapsed && [self isFormValid]) {
self.navigationItem.rightBarButtonItem.enabled = YES;
} else {
self.navigationItem.rightBarButtonItem.enabled = NO;
}
}
- (void)advanceFocusFromField:(BTUIKFormField *)currentField {
NSUInteger currentIdx = [self.requiredFields indexOfObject:currentField];
if (currentIdx < [self.requiredFields count] - 1) {
[[self.requiredFields objectAtIndex:currentIdx + 1] becomeFirstResponder];
}
}
- (void)fetchCardCapabilities {
[self cardNumberErrorHidden:YES];
self.cardNumberField.state = BTUIKCardNumberFormFieldStateLoading;
BTCardClient *unionPayClient = [[BTCardClient alloc] initWithAPIClient:self.apiClient];
[unionPayClient fetchCapabilities:self.cardNumberField.number completion:^(BTCardCapabilities * _Nullable cardCapabilities, NSError * _Nullable error) {
if (error || (!cardCapabilities.isUnionPay && !self.cardNumberField.valid)) {
[self cardNumberErrorHidden:NO];
self.cardNumberField.state = BTUIKCardNumberFormFieldStateValidate;
return;
} else if (cardCapabilities.isUnionPay && !cardCapabilities.isSupported) {
[self cardNumberErrorHidden:NO];
self.cardNumberField.state = BTUIKCardNumberFormFieldStateValidate;
return;
}
if (cardCapabilities.isUnionPay) {
self.requiredFields = [NSMutableArray arrayWithArray:@[self.cardNumberField, self.expirationDateField]];
} else {
[self updateRequiredFields];
}
self.optionalFields = [NSMutableArray new];
self.cardCapabilities = cardCapabilities;
if (cardCapabilities.isUnionPay) {
if (cardCapabilities.isDebit) {
[self.requiredFields addObject:self.securityCodeField];
[self.optionalFields addObject:self.securityCodeField];
[self.optionalFields addObject:self.expirationDateField];
} else {
[self.requiredFields addObject:self.securityCodeField];
}
[self.requiredFields addObject:self.mobileCountryCodeField];
[self.requiredFields addObject:self.mobilePhoneField];
}
self.securityCodeField.textField.placeholder = self.cardNumberField.cardType.securityCodeName;
self.cardNumberField.state = BTUIKCardNumberFormFieldStateDefault;
self.collapsed = NO;
[self advanceFocusFromField:self.cardNumberField];
[self formFieldDidChange:nil];
}];
}
- (void)cardNumberErrorHidden:(BOOL)hidden {
NSInteger indexOfCardNumberFormField = [self.stackView.arrangedSubviews indexOfObject:self.cardNumberField];
if (indexOfCardNumberFormField != NSNotFound && !hidden) {
[self.stackView insertArrangedSubview:self.cardNumberErrorView atIndex:indexOfCardNumberFormField + 1];
} else if (self.cardNumberErrorView.superview != nil && hidden) {
[self.cardNumberErrorView removeFromSuperview];
}
}
- (void)tokenizeCard {
[self.view endEditing:YES];
__block BTCardRequest *cardRequest = self.cardRequest;
__block BTCardClient *cardClient = [[BTCardClient alloc] initWithAPIClient:self.apiClient];
void (^basicTokenizeBlock)() = ^void() {
UIActivityIndicatorView *spinner = [UIActivityIndicatorView new];
spinner.activityIndicatorViewStyle = [BTUIKAppearance sharedInstance].activityIndicatorViewStyle;
[spinner startAnimating];
UIBarButtonItem *addCardButton = self.navigationItem.rightBarButtonItem;
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:spinner];
self.view.userInteractionEnabled = NO;
[cardClient tokenizeCard:cardRequest options:nil completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.view.userInteractionEnabled = YES;
self.navigationItem.rightBarButtonItem = addCardButton;
if (self.dropInRequest.threeDSecureVerification && self.dropInRequest.amount != nil
&& [self.configuration.json[@"threeDSecureEnabled"] isTrue] && [[BTTokenizationService sharedService] isTypeAvailable:@"ThreeDSecure"]) {
NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
options[BTTokenizationServiceViewPresentingDelegateOption] = self;
options[BTTokenizationServiceAmountOption] = [[NSDecimalNumber alloc] initWithString:self.dropInRequest.amount];
options[BTTokenizationServiceNonceOption] = tokenizedCard.nonce;
[[BTTokenizationService sharedService] tokenizeType:@"ThreeDSecure" options:options withAPIClient:self.apiClient completion:^(BTPaymentMethodNonce * _Nullable tokenizedCard, NSError * _Nullable error) {
[self.delegate cardTokenizationCompleted:tokenizedCard error:error sender:self];
}];
} else {
[self.delegate cardTokenizationCompleted:tokenizedCard error:error sender:self];
}
});
}];
};
if (self.cardCapabilities != nil && self.cardCapabilities.isUnionPay && self.cardCapabilities.isSupported) {
[cardClient enrollCard:cardRequest completion:^(NSString * _Nullable enrollmentID, BOOL smsCodeRequired, NSError * _Nullable error) {
if (error) {
[self.delegate cardTokenizationCompleted:nil error:error sender:self];
return;
}
cardRequest.enrollmentID = enrollmentID;
if (!smsCodeRequired) {
basicTokenizeBlock();
return;
}
__block UINavigationController *navController = self.navigationController;
__block BTEnrollmentVerificationViewController *enrollmentController;
enrollmentController = [[BTEnrollmentVerificationViewController alloc] initWithPhone:self.mobilePhoneField.mobileNumber mobileCountryCode:self.mobileCountryCodeField.countryCode handler:^(NSString* authCode, BOOL resend) {
if (resend) {
self.firstResponderFormField = self.mobilePhoneField;
[self.navigationController popViewControllerAnimated:YES];
return;
}
__block UIBarButtonItem *originalRightBarButtonItem = enrollmentController.navigationItem.rightBarButtonItem;
dispatch_async(dispatch_get_main_queue(), ^{
UIActivityIndicatorView *spinner = [UIActivityIndicatorView new];
spinner.activityIndicatorViewStyle = [BTUIKAppearance sharedInstance].activityIndicatorViewStyle;
[spinner startAnimating];
enrollmentController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:spinner];
self.view.userInteractionEnabled = NO;
});
cardRequest.smsCode = authCode;
[cardClient tokenizeCard:cardRequest options:nil completion:^(BTCardNonce * _Nullable tokenizedCard, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.view.userInteractionEnabled = YES;
enrollmentController.navigationItem.rightBarButtonItem = originalRightBarButtonItem;
if (error) {
[enrollmentController smsErrorHidden:NO];
return;
}
[self.delegate cardTokenizationCompleted:tokenizedCard error:error sender:self];
});
}];
}];
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"";
[self.navigationController pushViewController:enrollmentController animated:YES];
BTJSON *environment = self.configuration.json[@"environment"];
if(![environment isError] && [[environment asString] isEqualToString:@"sandbox"]) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Sandbox Sample SMS Code" message:@"Any code passes, example: 12345 \n\nIncorrect code is: 999999" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *alertAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction: alertAction];
[navController presentViewController:alertController animated:YES completion:nil];
}
});
}];
} else {
basicTokenizeBlock();
}
}
#pragma mark - Protocol conformance
#pragma mark FormField Delegate Methods
- (void)validateButtonPressed:(__unused BTUIKFormField *)formField {
if (!self.unionPayEnabledMerchant) {
[self cardNumberErrorHidden:formField.valid];
if (formField.valid) {
self.cardNumberField.state = BTUIKCardNumberFormFieldStateDefault;
self.collapsed = NO;
[self advanceFocusFromField:formField];
}
} else {
[self fetchCardCapabilities];
}
}
- (void)formFieldDidBeginEditing:(__unused BTUIKFormField *)formField {
if (!self.cardEntryDidFocus) {
[self.apiClient sendAnalyticsEvent:@"ios.dropin2.card.focus"];
self.cardEntryDidFocus = YES;
}
if (!self.collapsed && formField == self.cardNumberField) {
self.cardNumberField.state = BTUIKCardNumberFormFieldStateValidate;
self.collapsed = YES;
if (self.unionPayEnabledMerchant) {
self.cardCapabilities = nil;
}
}
}
- (void)formFieldDidChange:(BTUIKFormField *)formField {
[self updateSubmitButton];
// When focus moves from card number field, display the error state if the value in the field is invalid
if (formField == self.cardNumberField && self.cardNumberField.state == BTUIKCardNumberFormFieldStateDefault) {
[self cardNumberErrorHidden:self.cardNumberField.displayAsValid];
}
// Analytics event - fires when a customer begins enterinf card information
if (!self.cardEntryDidBegin && formField.text.length > 0) {
[self.apiClient sendAnalyticsEvent:@"ios.dropin2.add-card.start"];
self.cardEntryDidBegin = YES;
}
// Highlight card brand in card hint view according to BIN number
if (self.collapsed && formField == self.cardNumberField && !self.unionPayEnabledMerchant) {
BTUIKPaymentOptionType paymentMethodType = [BTUIKViewUtil paymentMethodTypeForCardType:self.cardNumberField.cardType];
[self.cardList emphasizePaymentOption:paymentMethodType];
}
// Auto-advance fields when complete
if (self.collapsed && formField == self.cardNumberField && formField.text.length > 0) {
BTUIKCardType *cardType = self.cardNumberField.cardType;
if (cardType != nil && formField.text.length >= cardType.maxNumberLength) {
[self validateButtonPressed:formField];
}
} else if (formField == self.expirationDateField && formField.text.length > 0) {
if (formField.text.length >= 5) {
[self advanceFocusFromField:formField];
}
} else if (formField == self.securityCodeField && formField.text.length > 0) {
BTUIKCardType *cardType = self.cardNumberField.cardType;
if (cardType != nil && formField.text.length >= cardType.validCvvLength) {
[self advanceFocusFromField:formField];
}
}
}
#pragma mark UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(__unused UITextField *)textField {
return YES;
}
@end

View File

@@ -0,0 +1,74 @@
#import "BTDropInBaseViewController.h"
#import "BTAPIClient_Internal_Category.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@interface BTDropInBaseViewController ()
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView;
@property (nonatomic, strong) UIView *activityIndicatorWrapperView;
@end
@implementation BTDropInBaseViewController
- (instancetype)initWithAPIClient:(BTAPIClient *)apiClient request:(BTDropInRequest *)request
{
if (self = [super init]) {
self.apiClient = [apiClient copyWithSource:apiClient.metadata.source integration:BTClientMetadataIntegrationDropIn2];
_dropInRequest = request;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.activityIndicatorWrapperView = [[UIView alloc] init];
self.activityIndicatorWrapperView.backgroundColor = [UIColor clearColor];
self.activityIndicatorWrapperView.hidden = YES;
[self.view addSubview:self.activityIndicatorWrapperView];
self.activityIndicatorWrapperView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[activityWrapper]|" options:0 metrics:nil views:@{@"activityWrapper": self.activityIndicatorWrapperView}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[activityWrapper]|" options:0 metrics:nil views:@{@"activityWrapper": self.activityIndicatorWrapperView}]];
self.activityIndicatorView = [UIActivityIndicatorView new];
self.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
self.activityIndicatorView.activityIndicatorViewStyle = [BTUIKAppearance sharedInstance].activityIndicatorViewStyle;
[self.activityIndicatorView startAnimating];
[self.activityIndicatorWrapperView addSubview:self.activityIndicatorView];
[self.activityIndicatorView.centerXAnchor constraintEqualToAnchor:self.activityIndicatorWrapperView.centerXAnchor].active = YES;
[self.activityIndicatorView.centerYAnchor constraintEqualToAnchor:self.activityIndicatorWrapperView.centerYAnchor].active = YES;
}
- (void)loadConfiguration {
[self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration *configuration, __unused NSError *error) {
self.configuration = configuration;
[self configurationLoaded:configuration error:error];
}];
}
- (void)showLoadingScreen:(BOOL)show animated:(BOOL)animated {
if (show) {
[self.view bringSubviewToFront:self.activityIndicatorWrapperView];
}
if (animated) {
[UIView transitionWithView:self.activityIndicatorWrapperView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
self.activityIndicatorWrapperView.hidden = !show;
} completion:nil];
} else {
self.activityIndicatorWrapperView.hidden = !show;
}
}
- (void)configurationLoaded:(__unused BTConfiguration *)configuration error:(__unused NSError *)error {
//Subclasses should override this method
}
@end

View File

@@ -0,0 +1,488 @@
#import "BTDropInController.h"
#import "BTVaultManagementViewController.h"
#import "BTCardFormViewController.h"
#import "BTEnrollmentVerificationViewController.h"
#import "BTAPIClient_Internal_Category.h"
#if __has_include("BraintreeCard.h")
#import "BraintreeCard.h"
#import "BraintreeUnionPay.h"
#else
#import <BraintreeCard/BraintreeCard.h>
#import <BraintreeUnionPay/BraintreeUnionPay.h>
#endif
#define BT_ANIMATION_SLIDE_SPEED 0.35
#define BT_ANIMATION_TRANSITION_SPEED 0.1
#define BT_HALF_SHEET_HEIGHT 470
#define BT_HALF_SHEET_MARGIN 5
#define BT_HALF_SHEET_CORNER_RADIUS 12
@interface BTDropInController ()
@property (nonatomic, strong) BTConfiguration *configuration;
@property (nonatomic, strong, readwrite) BTAPIClient *apiClient;
@property (nonatomic, strong) UIToolbar *btToolbar;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UIView *contentClippingView;
@property (nonatomic, strong) BTPaymentSelectionViewController *paymentSelectionViewController;
@property (nonatomic, strong) NSLayoutConstraint *contentHeightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *contentHeightConstraintBottom;
@property (nonatomic) BOOL useBlur;
@property (nonatomic, copy) NSArray *displayCardTypes;
@property (nonatomic, strong) UIVisualEffectView *blurredContentBackgroundView;
@property (nonatomic, copy, nullable) BTDropInControllerHandler handler;
@end
@implementation BTDropInController
#pragma mark - Prefetch BTDropInResult
+ (void)fetchDropInResultForAuthorization:(NSString *)authorization handler:(BTDropInControllerFetchHandler)handler {
BTUIKPaymentOptionType lastSelectedPaymentOptionType = [[NSUserDefaults standardUserDefaults] integerForKey:@"BT_dropInLastSelectedPaymentMethodType"];
__block BTAPIClient *apiClient = [[BTAPIClient alloc] initWithAuthorization:authorization];
apiClient = [apiClient copyWithSource:apiClient.metadata.source integration:BTClientMetadataIntegrationDropIn2];
[apiClient fetchPaymentMethodNonces:NO completion:^(NSArray<BTPaymentMethodNonce *> *paymentMethodNonces, NSError *error) {
if (error != nil) {
handler(nil, error);
} else {
BTDropInResult *result = [BTDropInResult new];
if (lastSelectedPaymentOptionType == BTUIKPaymentOptionTypeApplePay) {
result.paymentOptionType = lastSelectedPaymentOptionType;
} else if (paymentMethodNonces != nil && paymentMethodNonces.count > 0) {
BTPaymentMethodNonce *paymentMethod = paymentMethodNonces.firstObject;
result.paymentOptionType = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:paymentMethod.type];
result.paymentMethod = paymentMethod;
}
handler(result, error);
}
apiClient = nil;
}];
}
#pragma mark - Lifecycle
- (nullable instancetype)initWithAuthorization:(NSString *)authorization
request:(BTDropInRequest *)request
handler:(BTDropInControllerHandler) handler {
if (self = [super init]) {
BTAPIClient *client = [[BTAPIClient alloc] initWithAuthorization:authorization];
self.apiClient = [client copyWithSource:client.metadata.source integration:BTClientMetadataIntegrationDropIn2];
_dropInRequest = [request copy];
if (!_apiClient || !_dropInRequest) {
return nil;
}
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
self.modalPresentationStyle = UIModalPresentationFormSheet;
// Customize the iPad size...
// self.preferredContentSize = CGSizeMake(600, 400);
} else {
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
}
self.useBlur = !UIAccessibilityIsReduceTransparencyEnabled();
if (![BTUIKAppearance sharedInstance].useBlurs) {
self.useBlur = NO;
}
self.handler = handler;
self.displayCardTypes = @[];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpViews];
[self setUpChildViewControllers];
[self setUpConstraints];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.isBeingPresented) {
[self.paymentSelectionViewController loadConfiguration];
[self resetDropInState];
[self loadConfiguration];
if ([self isFormSheet]) {
// Position the views in screen before appearing
[self flexViewAnimated:NO];
} else {
// Move content off screen so it can be animated in when it appears
CGFloat sh = CGRectGetHeight([[UIScreen mainScreen] bounds]) + [UIApplication sharedApplication].statusBarFrame.size.height;
self.contentHeightConstraintBottom.constant = sh;
self.contentHeightConstraint.constant = sh;
[self.view setNeedsUpdateConstraints];
[self.view layoutIfNeeded];
[self flexViewAnimated:YES];
}
} else {
[self flexViewAnimated:NO];
[self.view setNeedsDisplay];
}
[self.apiClient sendAnalyticsEvent:@"ios.dropin2.appear"];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.apiClient sendAnalyticsEvent:@"ios.dropin2.disappear"];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// before rotating
[coordinator animateAlongsideTransition:^(__unused id<UIViewControllerTransitionCoordinatorContext> context) {
// while rotating
if (self.view.window != nil && !self.isBeingDismissed) {
[self flexViewAnimated:NO];
[self.view setNeedsDisplay];
[self.view setNeedsLayout];
}
} completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> context) {
// after rotating
}];
}
#pragma mark - Setup
- (void)setUpViews {
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[BTDropInController.self]] setTitleTextAttributes:@{NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].tintColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]]} forState:UIControlStateNormal];
if ([BTUIKAppearance sharedInstance].tintColor != nil) {
self.view.tintColor = [BTUIKAppearance sharedInstance].tintColor;
}
self.view.opaque = NO;
self.view.backgroundColor = [BTUIKAppearance sharedInstance].overlayColor;
self.view.userInteractionEnabled = YES;
self.contentView = [[UIView alloc] init];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.backgroundColor = self.useBlur ? [UIColor clearColor] : [BTUIKAppearance sharedInstance].formBackgroundColor;
self.contentView.layer.cornerRadius = BT_HALF_SHEET_CORNER_RADIUS;
self.contentView.clipsToBounds = true;
[self.view addSubview: self.contentView];
self.contentClippingView = [[UIView alloc] init];
self.contentClippingView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview: self.contentClippingView];
self.contentClippingView.backgroundColor = [UIColor clearColor];
self.contentClippingView.clipsToBounds = true;
self.btToolbar = [[UIToolbar alloc] init];
self.btToolbar.delegate = self;
self.btToolbar.userInteractionEnabled = YES;
self.btToolbar.barStyle = UIBarStyleDefault;
self.btToolbar.translucent = YES;
self.btToolbar.backgroundColor = [UIColor clearColor];
[self.btToolbar setBackgroundImage:[UIImage new] forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
self.btToolbar.barTintColor = [UIColor clearColor];
self.btToolbar.translatesAutoresizingMaskIntoConstraints = false;
[self.contentView addSubview:self.btToolbar];
UIBlurEffect *contentEffect = [UIBlurEffect effectWithStyle:[BTUIKAppearance sharedInstance].blurStyle];
self.blurredContentBackgroundView = [[UIVisualEffectView alloc] initWithEffect:contentEffect];
self.blurredContentBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
self.blurredContentBackgroundView.hidden = !self.useBlur;
[self.contentView addSubview:self.blurredContentBackgroundView];
[self.contentView sendSubviewToBack:self.blurredContentBackgroundView];
}
- (void)setUpChildViewControllers {
self.paymentSelectionViewController = [[BTPaymentSelectionViewController alloc] initWithAPIClient:self.apiClient request:self.dropInRequest];
self.paymentSelectionViewController.delegate = self;
[self.contentClippingView addSubview:self.paymentSelectionViewController.view];
self.paymentSelectionViewController.view.hidden = YES;
self.paymentSelectionViewController.navigationItem.leftBarButtonItem.target = self;
self.paymentSelectionViewController.navigationItem.leftBarButtonItem.action = @selector(cancelHit:);
}
- (void)setUpConstraints {
NSDictionary *viewBindings = @{
@"view": self,
@"toolbar": self.btToolbar,
@"contentView": self.contentView,
@"contentClippingView":self.contentClippingView,
@"paymentSelectionViewController":self.paymentSelectionViewController.view
};
NSDictionary *metrics = @{@"BT_HALF_SHEET_MARGIN":@([self sheetInset])};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(BT_HALF_SHEET_MARGIN)-[contentView]-(BT_HALF_SHEET_MARGIN)-|"
options:0
metrics:metrics
views:viewBindings]];
self.contentHeightConstraint = [NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0];
[self.view addConstraint:self.contentHeightConstraint];
self.contentHeightConstraintBottom = [NSLayoutConstraint constraintWithItem:self.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
[self.view addConstraint:self.contentHeightConstraintBottom];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[paymentSelectionViewController]|"
options:0
metrics:metrics
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[paymentSelectionViewController]|"
options:0
metrics:metrics
views:viewBindings]];
[self.blurredContentBackgroundView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor].active = YES;
[self.blurredContentBackgroundView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor].active = YES;
[self.blurredContentBackgroundView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor].active = YES;
[self.blurredContentBackgroundView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor].active = YES;
[self applyContentViewConstraints];
}
- (void)loadConfiguration {
[self.apiClient fetchOrReturnRemoteConfiguration:^(BTConfiguration * _Nullable configuration, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.configuration = configuration;
if (!error) {
self.paymentSelectionViewController.view.hidden = NO;
self.paymentSelectionViewController.view.alpha = 1.0;
[self updateToolbarForViewController:self.paymentSelectionViewController];
NSArray *supportedCardTypes = [configuration.json[@"creditCards"][@"supportedCardTypes"] asArray];
NSMutableArray *paymentOptionTypes = [NSMutableArray new];
for (NSString *supportedCardType in supportedCardTypes) {
BTUIKPaymentOptionType paymentOptionType = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:supportedCardType];
if (paymentOptionType != BTUIKPaymentOptionTypeUnknown) {
[paymentOptionTypes addObject: @(paymentOptionType)];
}
}
self.displayCardTypes = paymentOptionTypes;
} else {
if (self.handler) {
self.handler(self, nil, error);
}
}
});
}];
}
#pragma mark - View management and actions
- (void)cancelHit:(__unused id)sender {
BTDropInResult *result = [[BTDropInResult alloc] init];
result.cancelled = YES;
if (self.handler) {
self.handler(self, result, nil);
}
}
- (void)cardTokenizationCompleted:(BTPaymentMethodNonce *)tokenizedCard error:(NSError *)error sender:(BTCardFormViewController *)sender {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.handler) {
BTDropInResult *result = [[BTDropInResult alloc] init];
if (tokenizedCard != nil) {
result.paymentOptionType = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:tokenizedCard.type];
result.paymentMethod = tokenizedCard;
}
[sender dismissViewControllerAnimated:YES completion:^{
self.handler(self, result, error);
}];
}
});
}
- (void)updateToolbarForViewController:(UIViewController*)viewController {
UILabel *titleLabel = [[UILabel alloc] init];
[BTUIKAppearance styleLabelBoldPrimary:titleLabel];
titleLabel.text = viewController.title ? viewController.title : @"";
titleLabel.textAlignment = NSTextAlignmentCenter;
[titleLabel sizeToFit];
UIBarButtonItem *barTitle = [[UIBarButtonItem alloc] initWithCustomView:titleLabel];
UIBarButtonItem *flex = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *fixed = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
UIBarButtonItem *leftItem = viewController.navigationItem.leftBarButtonItem ? viewController.navigationItem.leftBarButtonItem : fixed;
UIBarButtonItem *rightItem = viewController.navigationItem.rightBarButtonItem ? viewController.navigationItem.rightBarButtonItem : fixed;
[self.btToolbar setItems:@[leftItem, flex, barTitle, flex, rightItem] animated:YES];
}
- (void)showCardForm:(__unused id)sender {
BTCardFormViewController* vd = [[BTCardFormViewController alloc] initWithAPIClient:self.apiClient request:self.dropInRequest];
vd.supportedCardTypes = self.displayCardTypes;
vd.delegate = self;
UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:vd];
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
navController.modalPresentationStyle = UIModalPresentationPageSheet;
}
[self presentViewController:navController animated:YES completion:nil];
}
#pragma mark - UI Helpers
- (float)sheetInset {
return [self isFormSheet] ? 0 : BT_HALF_SHEET_MARGIN;
}
- (BOOL)isFullScreen {
return ![self supportsHalfSheet] || [self isFormSheet] ;
}
- (void)flexViewAnimated:(BOOL)animated{
[self.btToolbar removeFromSuperview];
[self.contentView addSubview:self.btToolbar];
if ([self isFormSheet]) {
// iPad formSheet
self.contentHeightConstraint.constant = 0;
} else {
// Flexible views
int statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
int sh = [[UIScreen mainScreen] bounds].size.height;
int sheetHeight = BT_HALF_SHEET_HEIGHT;
self.contentHeightConstraint.constant = self.isFullScreen ? statusBarHeight + [self sheetInset] : (sh - sheetHeight - [self sheetInset]);
}
[self applyContentViewConstraints];
[self.view setNeedsUpdateConstraints];
self.contentHeightConstraintBottom.constant = -[self sheetInset];
if (animated) {
[UIView animateWithDuration:BT_ANIMATION_SLIDE_SPEED delay:0.0 usingSpringWithDamping:0.8 initialSpringVelocity:4 options:0 animations:^{
[self.view layoutIfNeeded];
} completion:nil];
} else {
[self.view updateConstraints];
[self.view layoutIfNeeded];
}
}
- (void)applyContentViewConstraints {
NSDictionary *viewBindings = @{@"toolbar": self.btToolbar,
@"contentClippingView": self.contentClippingView};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[toolbar]|"
options:0
metrics:nil
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentClippingView]|"
options:0
metrics:nil
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[toolbar][contentClippingView]|"
options:0
metrics:nil
views:viewBindings]];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
// No iPad specific dismissal animation
} else {
CGFloat sh = CGRectGetHeight([[UIScreen mainScreen] bounds]);
self.contentHeightConstraintBottom.constant = sh;
self.contentHeightConstraint.constant = sh;
[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:BT_ANIMATION_SLIDE_SPEED animations:^{
[self.view layoutIfNeeded];
}];
}
[super dismissViewControllerAnimated:flag completion:completion];
}
- (void)resetDropInState {
self.configuration = nil;
self.paymentSelectionViewController.view.hidden = NO;
self.paymentSelectionViewController.view.alpha = 1.0;
}
// No fullscreen when in landscape or FormSheet modes.
- (BOOL)supportsHalfSheet {
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight || [self isFormSheet]) {
return false;
}
return true;
}
- (BOOL)isFormSheet {
return self.modalPresentationStyle == UIModalPresentationFormSheet;
}
#pragma mark - UI Preferences
- (BOOL)prefersStatusBarHidden {
if (self.presentingViewController != nil) {
return [self.presentingViewController prefersStatusBarHidden];
}
return NO;
}
- (UIStatusBarStyle)preferredStatusBarStyle {
if (self.presentingViewController != nil) {
return [self.presentingViewController preferredStatusBarStyle];
}
return UIStatusBarStyleDefault;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation {
return UIStatusBarAnimationSlide;
}
- (UIBarPosition)positionForBar:(id<UIBarPositioning>)bar {
if (bar == self.btToolbar && self.isFullScreen && ![self isFormSheet]) {
return UIBarPositionTopAttached;
}
return UIBarPositionTop;
}
#pragma mark BTAppSwitchDelegate
- (void)appSwitcherWillPerformAppSwitch:(__unused id)appSwitcher {
// No action
}
- (void)appSwitcherWillProcessPaymentInfo:(__unused id)appSwitcher {
// No action
}
- (void)appSwitcher:(__unused id)appSwitcher didPerformSwitchToTarget:(__unused BTAppSwitchTarget)target {
// No action
}
- (void)paymentDriver:(__unused id)driver requestsPresentationOfViewController:(UIViewController *)viewController {
// Needed for iPad
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[self presentViewController:viewController animated:YES completion:nil];
}
- (void)paymentDriver:(__unused id)driver requestsDismissalOfViewController:(UIViewController *)viewController {
[viewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)selectionCompletedWithPaymentMethodType:(BTUIKPaymentOptionType)type nonce:(BTPaymentMethodNonce *)nonce error:(NSError *)error {
if (error == nil) {
[[NSUserDefaults standardUserDefaults] setInteger:type forKey:@"BT_dropInLastSelectedPaymentMethodType"];
if (self.handler != nil) {
BTDropInResult *result = [BTDropInResult new];
result.paymentOptionType = type;
result.paymentMethod = nonce;
self.handler(self, result, error);
}
}
}
@end

View File

@@ -0,0 +1,14 @@
#import <UIKit/UIKit.h>
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@interface BTDropInUIUtilities : NSObject
+ (UIView *)addSpacerToStackView:(UIStackView*)stackView beforeView:(UIView*)view size:(float)size;
+ (UIStackView *)newStackView;
+ (UIStackView *)newStackViewForError:(NSString*)errorText;
@end

View File

@@ -0,0 +1,42 @@
#import "BTDropInUIUtilities.h"
@implementation BTDropInUIUtilities
+ (UIView *)addSpacerToStackView:(UIStackView*)stackView beforeView:(UIView*)view size:(float)size {
NSInteger indexOfView = [stackView.arrangedSubviews indexOfObject:view];
if (indexOfView != NSNotFound) {
UIView *spacer = [[UIView alloc] init];
spacer.translatesAutoresizingMaskIntoConstraints = NO;
[stackView insertArrangedSubview:spacer atIndex:indexOfView];
NSLayoutConstraint *heightConstraint = [spacer.heightAnchor constraintEqualToConstant:size];
heightConstraint.priority = UILayoutPriorityDefaultHigh;
heightConstraint.active = true;
return spacer;
}
return nil;
}
+ (UIStackView *)newStackView {
UIStackView *stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisVertical;
stackView.distribution = UIStackViewDistributionFill;
stackView.alignment = UIStackViewAlignmentFill;
stackView.spacing = 0;
return stackView;
}
+ (UIStackView *)newStackViewForError:(NSString*)errorText {
UIStackView *newStackView = [self newStackView];
UILabel *errorLabel = [UILabel new];
errorLabel.translatesAutoresizingMaskIntoConstraints = NO;
[BTUIKAppearance styleSmallLabelPrimary:errorLabel];
errorLabel.textColor = [BTUIKAppearance sharedInstance].errorForegroundColor;
errorLabel.text = errorText;
newStackView.layoutMargins = UIEdgeInsetsMake([BTUIKAppearance verticalFormSpaceTight], [BTUIKAppearance horizontalFormContentPadding], [BTUIKAppearance verticalFormSpaceTight], [BTUIKAppearance horizontalFormContentPadding]);
newStackView.layoutMarginsRelativeArrangement = true;
[newStackView addArrangedSubview:errorLabel];
return newStackView;
}
@end

View File

@@ -0,0 +1,414 @@
#import "BTPaymentSelectionViewController.h"
#import "BTUIPaymentMethodCollectionViewCell.h"
#import "BTDropInController.h"
#import "BTDropInPaymentSeletionCell.h"
#import "BTAPIClient_Internal_Category.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
#if __has_include("BraintreeCard.h")
#import "BraintreeCard.h"
#else
#import <BraintreeCard/BraintreeCard.h>
#endif
#define SAVED_PAYMENT_METHODS_COLLECTION_SPACING 6
#define SAVED_PAYMENT_METHODS_COLLECTION_WIDTH 105
#define SAVED_PAYMENT_METHODS_COLLECTION_HEIGHT 165
@interface BTPaymentSelectionViewController ()
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIView *scrollViewContentWrapper;
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong) UIStackView *paymentOptionsLabelContainerStackView;
@property (nonatomic, strong) UIStackView *vaultedPaymentsLabelContainerStackView;
@property (nonatomic, strong) NSArray *paymentOptionsData;
@property (nonatomic, strong) UITableView *paymentOptionsTableView;
@property (nonatomic, strong) NSLayoutConstraint *savedPaymentMethodsCollectionViewConstraint;
@property (nonatomic, strong) UILabel *paymentOptionsHeader;
@property (nonatomic, strong) UILabel *vaultedPaymentsHeader;
@property (nonatomic, strong) UICollectionView *savedPaymentMethodsCollectionView;
@end
@implementation BTPaymentSelectionViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.translatesAutoresizingMaskIntoConstraints = NO;
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStylePlain target:nil action:nil];
self.title = @"Select Payment Method";
self.paymentMethodNonces = @[];
self.paymentOptionsData = @[@(BTUIKPaymentOptionTypePayPal), @(BTUIKPaymentOptionTypeUnknown)];
self.view.translatesAutoresizingMaskIntoConstraints = false;
self.view.backgroundColor = [UIColor clearColor];
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView setAlwaysBounceVertical:NO];
self.scrollView.scrollEnabled = YES;
[self.view addSubview:self.scrollView];
self.scrollViewContentWrapper = [[UIView alloc] init];
self.scrollViewContentWrapper.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:self.scrollViewContentWrapper];
self.stackView = [self newStackView];
[self.scrollViewContentWrapper addSubview:self.stackView];
self.view.translatesAutoresizingMaskIntoConstraints = false;
self.view.backgroundColor = [UIColor clearColor];
NSDictionary *viewBindings = @{@"stackView": self.stackView,
@"scrollView": self.scrollView,
@"scrollViewContentWrapper": self.scrollViewContentWrapper};
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor].active = YES;
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollViewContentWrapper]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollViewContentWrapper(scrollView)]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(0)-[stackView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(VERTICAL_SECTION_SPACE)-[stackView]-(VERTICAL_FORM_SPACE_TIGHT)-|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
NSLayoutConstraint *heightConstraint;
self.vaultedPaymentsHeader = [self sectionHeaderLabelWithString:@"Recent"];
self.vaultedPaymentsHeader.translatesAutoresizingMaskIntoConstraints = NO;
self.vaultedPaymentsLabelContainerStackView = [self newStackView];
self.vaultedPaymentsLabelContainerStackView.layoutMargins = UIEdgeInsetsMake(0, [BTUIKAppearance horizontalFormContentPadding], 0, [BTUIKAppearance horizontalFormContentPadding]);
self.vaultedPaymentsLabelContainerStackView.layoutMarginsRelativeArrangement = true;
[self.vaultedPaymentsLabelContainerStackView addArrangedSubview:self.vaultedPaymentsHeader];
[self.stackView addArrangedSubview:self.vaultedPaymentsLabelContainerStackView];
//[self addSpacerToStackView:self.stackView beforeView:self.vaultedPaymentsLabelContainerStackView];
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setScrollDirection: UICollectionViewScrollDirectionHorizontal];
self.savedPaymentMethodsCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
self.savedPaymentMethodsCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
self.savedPaymentMethodsCollectionView.delegate = self;
self.savedPaymentMethodsCollectionView.dataSource = self;
[self.savedPaymentMethodsCollectionView registerClass:[BTUIPaymentMethodCollectionViewCell class] forCellWithReuseIdentifier:@"BTUIPaymentMethodCollectionViewCellIdentifier"];
self.savedPaymentMethodsCollectionView.backgroundColor = [UIColor clearColor];
self.savedPaymentMethodsCollectionView.showsHorizontalScrollIndicator = NO;
heightConstraint = [self.savedPaymentMethodsCollectionView.heightAnchor constraintEqualToConstant:SAVED_PAYMENT_METHODS_COLLECTION_HEIGHT + [BTUIKAppearance verticalFormSpace]];
// Setting the prioprity is necessary to avoid autolayout errors when UIStackView rotates
heightConstraint.priority = UILayoutPriorityDefaultHigh;
heightConstraint.active = YES;
[self.stackView addArrangedSubview:self.savedPaymentMethodsCollectionView];
self.paymentOptionsHeader = [self sectionHeaderLabelWithString:@"Other"];
self.paymentOptionsHeader.translatesAutoresizingMaskIntoConstraints = NO;
self.paymentOptionsLabelContainerStackView = [self newStackView];
self.paymentOptionsLabelContainerStackView.layoutMargins = UIEdgeInsetsMake(0, [BTUIKAppearance horizontalFormContentPadding], [BTUIKAppearance verticalFormSpaceTight], [BTUIKAppearance horizontalFormContentPadding]);
self.paymentOptionsLabelContainerStackView.layoutMarginsRelativeArrangement = true;
[self.paymentOptionsLabelContainerStackView addArrangedSubview:self.paymentOptionsHeader];
[self.stackView addArrangedSubview:self.paymentOptionsLabelContainerStackView];
self.paymentOptionsTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
[self.paymentOptionsTableView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
self.paymentOptionsTableView.backgroundColor = [UIColor clearColor];
[self.paymentOptionsTableView registerClass:[BTDropInPaymentSeletionCell class] forCellReuseIdentifier:@"BTDropInPaymentSeletionCell"];
self.paymentOptionsTableView.translatesAutoresizingMaskIntoConstraints = NO;
self.paymentOptionsTableView.delegate = self;
self.paymentOptionsTableView.dataSource = self;
self.paymentOptionsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.paymentOptionsTableView setAlwaysBounceVertical:NO];
[self.stackView addArrangedSubview:self.paymentOptionsTableView];
[self loadConfiguration];
}
- (void)loadConfiguration {
[self showLoadingScreen:YES animated:NO];
self.stackView.hidden = YES;
[super loadConfiguration];
}
- (void)dealloc {
[self.paymentOptionsTableView removeObserver:self forKeyPath:@"contentSize"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary <NSString *, id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentSize"]) {
[self.paymentOptionsTableView removeConstraints:self.paymentOptionsTableView.constraints];
NSLayoutConstraint *heightConstraint = [self.paymentOptionsTableView.heightAnchor constraintEqualToConstant:self.paymentOptionsTableView.contentSize.height];
// Setting the prioprity is necessary to avoid autolayout errors when UIStackView rotates
heightConstraint.priority = UILayoutPriorityDefaultHigh;
heightConstraint.active = YES;
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)configurationLoaded:(__unused BTConfiguration *)configuration error:(NSError *)error {
NSMutableArray *activePaymentOptions = [@[] mutableCopy];
if (!error) {
[self fetchPaymentMethodsOnCompletion:^{
if ([[BTTokenizationService sharedService] isTypeAvailable:@"PayPal"] && [self.configuration.json[@"paypalEnabled"] isTrue]) {
[activePaymentOptions addObject:@(BTUIKPaymentOptionTypePayPal)];
}
BTJSON *venmoAccessToken = self.configuration.json[@"payWithVenmo"][@"accessToken"];
if ([[BTTokenizationService sharedService] isTypeAvailable:@"Venmo"] && venmoAccessToken.isString) {
NSURLComponents *components = [NSURLComponents componentsWithString:@"com.venmo.touch.v2://x-callback-url/vzero/auth"];
BOOL isVenmoAppInstalled = [[UIApplication sharedApplication] canOpenURL:components.URL];
if (isVenmoAppInstalled) {
[activePaymentOptions addObject:@(BTUIKPaymentOptionTypeVenmo)];
}
}
// Always add Cards
[activePaymentOptions addObject:@(BTUIKPaymentOptionTypeUnknown)];
#ifdef __BT_APPLE_PAY
BTJSON *applePayConfiguration = self.configuration.json[@"applePay"];
if ([applePayConfiguration[@"status"] isString] && ![[applePayConfiguration[@"status"] asString] isEqualToString:@"off"] && !self.dropInRequest.applePayDisabled) {
// Short-circuits if BraintreeApplePay is not available at runtime
if (__BT_AVAILABLE(@"BTApplePayClient") && [configuration canMakeApplePayPayments]) {
[activePaymentOptions addObject:@(BTUIKPaymentOptionTypeApplePay)];
}
}
#endif
self.paymentOptionsData = [activePaymentOptions copy];
[self.savedPaymentMethodsCollectionView reloadData];
[self.paymentOptionsTableView reloadData];
if (self.paymentMethodNonces.count == 0) {
self.savedPaymentMethodsCollectionView.hidden = YES;
self.vaultedPaymentsHeader.hidden = YES;
self.paymentOptionsLabelContainerStackView.hidden = YES;
self.vaultedPaymentsLabelContainerStackView.hidden = YES;
} else {
self.savedPaymentMethodsCollectionView.hidden = NO;
self.vaultedPaymentsHeader.hidden = NO;
self.paymentOptionsLabelContainerStackView.hidden = NO;
self.vaultedPaymentsLabelContainerStackView.hidden = NO;
[self.savedPaymentMethodsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}
[self showLoadingScreen:NO animated:YES];
self.stackView.hidden = NO;
}];
}
}
#pragma mark - Helpers
- (void)fetchPaymentMethodsOnCompletion:(void(^)())completionBlock {
if (!self.apiClient.clientToken) {
self.paymentMethodNonces = @[];
if (completionBlock) {
completionBlock();
}
return;
}
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
[self.apiClient fetchPaymentMethodNonces:NO completion:^(NSArray<BTPaymentMethodNonce *> *paymentMethodNonces, NSError *error) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
if (error) {
// no action
} else {
self.paymentMethodNonces = [paymentMethodNonces copy];
if (completionBlock) {
completionBlock();
}
}
}];
}
- (BOOL)prefersStatusBarHidden {
if (self.presentingViewController != nil) {
return [self.presentingViewController prefersStatusBarHidden];
}
return NO;
}
- (UIStackView *)newStackView {
UIStackView *stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisVertical;
stackView.distribution = UIStackViewDistributionFill;
stackView.alignment = UIStackViewAlignmentFill;
stackView.spacing = 0;
return stackView;
}
- (UILabel *)sectionHeaderLabelWithString:(NSString*)string {
UILabel *sectionLabel = [UILabel new];
sectionLabel.text = [string uppercaseString];
sectionLabel.textAlignment = NSTextAlignmentNatural;
[BTUIKAppearance styleSystemLabelSecondary:sectionLabel];
return sectionLabel;
}
- (UIView *)addSpacerToStackView:(UIStackView *)stackView beforeView:(UIView *)view {
NSInteger indexOfView = [stackView.arrangedSubviews indexOfObject:view];
if (indexOfView != NSNotFound) {
UIView* spacer = [[UIView alloc] init];
spacer.translatesAutoresizingMaskIntoConstraints = NO;
[stackView insertArrangedSubview:spacer atIndex:indexOfView];
NSLayoutConstraint* heightConstraint = [spacer.heightAnchor constraintEqualToConstant:22];
heightConstraint.priority = UILayoutPriorityDefaultHigh;
heightConstraint.active = true;
return spacer;
}
return nil;
}
#pragma mark - Protocol conformance
#pragma mark UICollectionViewDelegate
-(NSInteger)numberOfSectionsInCollectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView {
return 1;
}
- (NSInteger)collectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView numberOfItemsInSection:(__unused NSInteger)section {
return [self.paymentMethodNonces count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)savedPaymentMethodsCollectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
BTUIPaymentMethodCollectionViewCell* cell = [savedPaymentMethodsCollectionView dequeueReusableCellWithReuseIdentifier:@"BTUIPaymentMethodCollectionViewCellIdentifier" forIndexPath:indexPath];
BTPaymentMethodNonce *paymentInfo = self.paymentMethodNonces[indexPath.row];
cell.paymentMethodNonce = paymentInfo;
NSString *typeString = paymentInfo.type;
NSMutableAttributedString *typeWithDescription = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", paymentInfo.localizedDescription ?: @""]];
if ([paymentInfo isKindOfClass:[BTCardNonce class]]) {
typeWithDescription = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"••• ••%@", ((BTCardNonce*)paymentInfo).lastTwo ?: @""]];
}
cell.highlighted = NO;
cell.descriptionLabel.attributedText = typeWithDescription;
cell.titleLabel.text = [BTUIKViewUtil nameForPaymentMethodType:[BTUIKViewUtil paymentOptionTypeForPaymentInfoType:typeString]];
cell.paymentOptionCardView.paymentOptionType = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:typeString];
return cell;
}
- (CGSize)collectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView layout:(__unused UICollectionViewLayout *)savedPaymentMethodsCollectionViewLayout sizeForItemAtIndexPath:(__unused NSIndexPath *)indexPath {
return CGSizeMake(SAVED_PAYMENT_METHODS_COLLECTION_WIDTH, SAVED_PAYMENT_METHODS_COLLECTION_HEIGHT);
}
#pragma mark collection view cell paddings
- (UIEdgeInsets)collectionView:(__unused UICollectionView*)savedPaymentMethodsCollectionView layout:(__unused UICollectionViewLayout *)savedPaymentMethodsCollectionViewLayout insetForSectionAtIndex:(__unused NSInteger)section {
return UIEdgeInsetsMake(0, [BTUIKAppearance horizontalFormContentPadding], 0, [BTUIKAppearance horizontalFormContentPadding]);
}
- (CGFloat)collectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView layout:(__unused UICollectionViewLayout*)savedPaymentMethodsCollectionViewLayout minimumInteritemSpacingForSectionAtIndex:(__unused NSInteger)section {
return SAVED_PAYMENT_METHODS_COLLECTION_SPACING;
}
- (CGFloat)collectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView layout:(__unused UICollectionViewLayout*)savedPaymentMethodsCollectionViewLayout minimumLineSpacingForSectionAtIndex:(__unused NSInteger)section {
return SAVED_PAYMENT_METHODS_COLLECTION_SPACING;
}
- (void)collectionView:(__unused UICollectionView *)savedPaymentMethodsCollectionView didSelectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
BTUIPaymentMethodCollectionViewCell *cell = (BTUIPaymentMethodCollectionViewCell*)[savedPaymentMethodsCollectionView cellForItemAtIndexPath:indexPath];
if (self.delegate) {
[self.delegate selectionCompletedWithPaymentMethodType:[BTUIKViewUtil paymentOptionTypeForPaymentInfoType:cell.paymentMethodNonce.type] nonce:cell.paymentMethodNonce error:nil];
}
}
#pragma mark UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = @"BTDropInPaymentSeletionCell";
BTDropInPaymentSeletionCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
BTUIKPaymentOptionType option = ((NSNumber*)self.paymentOptionsData[indexPath.row]).intValue;
cell.label.text = [BTUIKViewUtil nameForPaymentMethodType:option];
if (option == BTUIKPaymentOptionTypeUnknown) {
cell.label.text = @"Credit or Debit Card";
}
cell.iconView.paymentOptionType = option;
cell.type = option;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
BTDropInPaymentSeletionCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.type == BTUIKPaymentOptionTypeUnknown) {
if ([self.delegate respondsToSelector:@selector(showCardForm:)]){
[self.delegate performSelector:@selector(showCardForm:) withObject:self];
}
} else if (cell.type == BTUIKPaymentOptionTypePayPal) {
NSMutableDictionary *options = [NSMutableDictionary dictionary];
if (self.delegate != nil) {
options[BTTokenizationServiceViewPresentingDelegateOption] = self.delegate;
}
if (self.dropInRequest.additionalPayPalScopes != nil) {
options[BTTokenizationServicePayPalScopesOption] = self.dropInRequest.additionalPayPalScopes;
}
[[BTTokenizationService sharedService] tokenizeType:@"PayPal" options:options withAPIClient:self.apiClient completion:^(BTPaymentMethodNonce * _Nullable paymentMethodNonce, NSError * _Nullable error) {
if (self.delegate && paymentMethodNonce != nil) {
BTUIKPaymentOptionType type = [BTUIKViewUtil paymentOptionTypeForPaymentInfoType:paymentMethodNonce.type];
[self.delegate selectionCompletedWithPaymentMethodType:type nonce:paymentMethodNonce error:error];
}
}];
} else if (cell.type == BTUIKPaymentOptionTypeVenmo) {
NSMutableDictionary *options = [NSMutableDictionary dictionary];
if (self.delegate != nil) {
options[BTTokenizationServiceViewPresentingDelegateOption] = self.delegate;
}
[[BTTokenizationService sharedService] tokenizeType:@"Venmo" options:options withAPIClient:self.apiClient completion:^(BTPaymentMethodNonce * _Nullable paymentMethodNonce, NSError * _Nullable error) {
if (self.delegate && paymentMethodNonce != nil) {
[self.delegate selectionCompletedWithPaymentMethodType:BTUIKPaymentOptionTypeVenmo nonce:paymentMethodNonce error:error];
}
}];
} else if(cell.type == BTUIKPaymentOptionTypeApplePay) {
if (self.delegate) {
[self.delegate selectionCompletedWithPaymentMethodType:BTUIKPaymentOptionTypeApplePay nonce:nil error:nil];
}
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(__unused NSInteger)section {
return [self.paymentOptionsData count];
}
@end

View File

@@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface BTUIKBarButtonItem : UIBarButtonItem
@property (nonatomic) BOOL bold;
@end

View File

@@ -0,0 +1,16 @@
#import "BTVaultManagementViewController.h"
#import "BTDropInController.h"
@interface BTVaultManagementViewController ()
@end
@implementation BTVaultManagementViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.translatesAutoresizingMaskIntoConstraints = NO;
}
@end

View File

@@ -0,0 +1,15 @@
#import <UIKit/UIKit.h>
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@interface BTDropInPaymentSeletionCell : UITableViewCell
@property (nonatomic, strong) UILabel* label;
@property (nonatomic, strong) BTUIKPaymentOptionCardView* iconView;
@property (nonatomic, strong) UIView *bottomBorder;
@property (nonatomic) BTUIKPaymentOptionType type;
@end

View File

@@ -0,0 +1,110 @@
#import "BTDropInPaymentSeletionCell.h"
#if __has_include("UIColor+BTUIK.h")
#import "UIColor+BTUIK.h"
#else
#import <BraintreeUIKit/UIColor+BTUIK.h>
#endif
@interface BTDropInPaymentSeletionCell()
@end
@implementation BTDropInPaymentSeletionCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.textLabel.hidden = YES;
self.detailTextLabel.hidden = YES;
self.separatorInset = UIEdgeInsetsZero;
self.layoutMargins = UIEdgeInsetsZero;
self.preservesSuperviewLayoutMargins = NO;
[self.contentView removeConstraints:self.contentView.constraints];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.label = [[UILabel alloc] init];
[BTUIKAppearance styleLabelPrimary:self.label];
self.label.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.label];
self.iconView = [BTUIKPaymentOptionCardView new];
self.iconView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.iconView];
self.backgroundColor = [UIColor clearColor];
self.bottomBorder = [UIView new];
self.bottomBorder.translatesAutoresizingMaskIntoConstraints = NO;
self.bottomBorder.backgroundColor = [BTUIKAppearance sharedInstance].lineColor;
[self.contentView addSubview:self.bottomBorder];
UIView *backgroundView = [UIView new];
backgroundView.backgroundColor = [[BTUIKAppearance sharedInstance].formBackgroundColor btuik_adjustedBrightness:0.8];
self.selectedBackgroundView = backgroundView;
self.backgroundView = nil;
[self applyConstraints];
}
return self;
}
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
UIColor *backgroundColor = self.iconView.backgroundColor;
[super setHighlighted:highlighted animated:animated];
self.iconView.backgroundColor = backgroundColor;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
UIColor *backgroundColor = self.iconView.backgroundColor;
[super setSelected:selected animated:animated];
self.iconView.backgroundColor = backgroundColor;
}
- (void)applyConstraints {
[self removeConstraints:self.constraints];
[self.contentView removeConstraints:self.contentView.constraints];
[self.label removeConstraints:self.label.constraints];
NSDictionary* viewBindings = @{@"contentView":self.contentView, @"label":self.label, @"iconView":self.iconView, @"bottomBorder":self.bottomBorder};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label][bottomBorder(0.5)]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[bottomBorder(label)]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:self.iconView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.contentView
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(HORIZONTAL_FORM_PADDING)-[iconView(ICON_WIDTH)]-(HORIZONTAL_FORM_PADDING)-[label]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[iconView(ICON_HEIGHT)]"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
}
@end

View File

@@ -0,0 +1,17 @@
#import <UIKit/UIKit.h>
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@interface BTEnrollmentVerificationViewController : UIViewController <UITextFieldDelegate, BTUIKFormFieldDelegate>
typedef void (^BTEnrollmentHandler)(NSString* authCode, BOOL resendSms);
- (instancetype)initWithPhone:(NSString *)mobilePhoneNumber
mobileCountryCode:(NSString *)mobileCountryCode
handler:(BTEnrollmentHandler)handler;
- (void)smsErrorHidden:(BOOL)hidden;
@end

View File

@@ -0,0 +1,136 @@
#import "BTEnrollmentVerificationViewController.h"
#import "BTUIKBarButtonItem_Internal_Declaration.h"
#import "BTDropInUIUtilities.h"
@interface BTEnrollmentVerificationViewController ()
@property (nonatomic, strong) NSString *mobilePhoneNumber;
@property (nonatomic, strong) NSString *mobileCountryCode;
@property (nonatomic, strong) BTEnrollmentHandler handler;
@property (nonatomic, strong) BTUIKFormField *smsTextField;
@property (nonatomic, strong) UILabel *smsSentLabel;
@property (nonatomic, strong) UIButton *resendSmsButton;
@property (nonatomic, strong) UIStackView *stackView;
@property (nonatomic, strong) UIStackView *smsErrorView;
@end
@implementation BTEnrollmentVerificationViewController
- (instancetype)initWithPhone:(NSString *)mobilePhoneNumber
mobileCountryCode:(NSString *)mobileCountryCode
handler:(BTEnrollmentHandler)handler {
if (self = [super init]) {
_mobilePhoneNumber = mobilePhoneNumber;
_mobileCountryCode = mobileCountryCode;
_handler = handler;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Confirm Enrollment";
self.view.backgroundColor = [BTUIKAppearance sharedInstance].formBackgroundColor;
self.navigationController.navigationBar.barTintColor = [BTUIKAppearance sharedInstance].barBackgroundColor;
self.navigationController.navigationBar.translucent = NO;
[self.navigationController.navigationBar setTitleTextAttributes:@{
NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].primaryTextColor
}];
//self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel)];
BTUIKBarButtonItem *confirmButton = [[BTUIKBarButtonItem alloc] initWithTitle:@"Confirm" style:UIBarButtonItemStyleDone target:self action:@selector(confirm)];
confirmButton.bold = YES;
self.navigationItem.rightBarButtonItem = confirmButton;
self.navigationItem.rightBarButtonItem.enabled = NO;
self.edgesForExtendedLayout = UIRectEdgeNone;
self.view.backgroundColor = [BTUIKAppearance sharedInstance].formBackgroundColor;
self.smsSentLabel = [UILabel new];
self.smsSentLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.smsSentLabel.textAlignment = NSTextAlignmentCenter;
self.smsSentLabel.text = [NSString stringWithFormat:@"Enter the SMS code sent to\n+%@ %@", self.mobileCountryCode, self.mobilePhoneNumber];
self.smsSentLabel.numberOfLines = 0;
[self.view addSubview:self.smsSentLabel];
[BTUIKAppearance styleLargeLabelSecondary:self.smsSentLabel];
self.smsTextField = [BTUIKFormField new];
self.smsTextField.translatesAutoresizingMaskIntoConstraints = NO;
self.smsTextField.textField.keyboardType = UIKeyboardTypeNumberPad;
self.smsTextField.textField.placeholder = @"SMS Code";
self.smsTextField.delegate = self;
[self.view addSubview:self.smsTextField];
NSString *smsButtonText = @"Use a Different Phone Number";
self.resendSmsButton = [UIButton new];
self.resendSmsButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.resendSmsButton setTitle:smsButtonText forState:UIControlStateNormal];
NSAttributedString *normalValidateButtonString = [[NSAttributedString alloc] initWithString:smsButtonText attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].tintColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]]}];
[self.resendSmsButton setAttributedTitle:normalValidateButtonString forState:UIControlStateNormal];
NSAttributedString *disabledValidateButtonString = [[NSAttributedString alloc] initWithString:smsButtonText attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].disabledColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]]}];
[self.resendSmsButton setAttributedTitle:disabledValidateButtonString forState:UIControlStateDisabled];
[self.resendSmsButton sizeToFit];
[self.resendSmsButton layoutIfNeeded];
[self.resendSmsButton addTarget:self action:@selector(resendTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.resendSmsButton];
self.stackView = [BTDropInUIUtilities newStackView];
[self.stackView addArrangedSubview:self.smsSentLabel];
[self.stackView addArrangedSubview:self.smsTextField];
[self.stackView addArrangedSubview:self.resendSmsButton];
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.smsSentLabel size:[BTUIKAppearance verticalFormSpace]];
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.smsTextField size:[BTUIKAppearance verticalFormSpace]];
[BTDropInUIUtilities addSpacerToStackView:self.stackView beforeView:self.resendSmsButton size:[BTUIKAppearance verticalFormSpaceTight]];
[self.smsTextField.heightAnchor constraintEqualToConstant:[BTUIKAppearance formCellHeight]].active = YES;
[self.view addSubview:self.stackView];
self.smsErrorView = [BTDropInUIUtilities newStackViewForError:@"Invalid SMS Code"];
[self smsErrorHidden:YES];
NSDictionary* viewBindings = @{
@"smsSentLabel": self.smsSentLabel,
@"smsTextField": self.smsTextField,
@"resendSmsButton": self.resendSmsButton,
@"stackView": self.stackView,
};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[stackView]" options:0 metrics:[BTUIKAppearance metrics] views:viewBindings]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[stackView]|" options:0 metrics:[BTUIKAppearance metrics] views:viewBindings]];
}
- (void)smsErrorHidden:(BOOL)hidden {
NSInteger indexOfSMSCodeFormField = [self.stackView.arrangedSubviews indexOfObject:self.smsTextField];
if (indexOfSMSCodeFormField != NSNotFound && !hidden) {
[self.stackView insertArrangedSubview:self.smsErrorView atIndex:indexOfSMSCodeFormField + 1];
} else if (self.smsErrorView.superview != nil && hidden) {
[self.smsErrorView removeFromSuperview];
}
}
- (void)formFieldDidChange:(BTUIKFormField *)formField {
if (formField.text.length > 0) {
self.navigationItem.rightBarButtonItem.enabled = YES;
} else {
self.navigationItem.rightBarButtonItem.enabled = NO;
}
}
- (void)cancel {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)resendTapped {
self.handler(@"", YES);
}
- (void)confirm {
self.handler(self.smsTextField.text, NO);
}
@end

View File

@@ -0,0 +1,17 @@
#import <UIKit/UIKit.h>
#if __has_include("BTPaymentMethodNonce.h")
#import "BTPaymentMethodNonce.h"
#else
#import <BraintreeCore/BTPaymentMethodNonce.h>
#endif
@class BTUIKPaymentOptionCardView;
@interface BTUIPaymentMethodCollectionViewCell : UICollectionViewCell
@property (nonatomic, strong) BTUIKPaymentOptionCardView* paymentOptionCardView;
@property (nonatomic, strong) UILabel* titleLabel;
@property (nonatomic, strong) UILabel* descriptionLabel;
@property (nonatomic, strong) BTPaymentMethodNonce* paymentMethodNonce;
@end

View File

@@ -0,0 +1,92 @@
#import "BTUIPaymentMethodCollectionViewCell.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
#define LARGE_ICON_INNER_PADDING 10.0
#define LARGE_ICON_CORNER_RADIUS 20.0
@implementation BTUIPaymentMethodCollectionViewCell
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self removeConstraints:self.constraints];
[self.contentView removeConstraints:self.contentView.constraints];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
self.paymentOptionCardView = [[BTUIKPaymentOptionCardView alloc] init];
self.paymentOptionCardView.translatesAutoresizingMaskIntoConstraints = NO;
self.paymentOptionCardView.innerPadding = LARGE_ICON_INNER_PADDING;
self.paymentOptionCardView.vectorArtSize = BTUIKVectorArtSizeLarge;
self.paymentOptionCardView.cornerRadius = LARGE_ICON_CORNER_RADIUS;
self.paymentOptionCardView.borderColor = [UIColor whiteColor];
self.paymentOptionCardView.layer.masksToBounds = NO;
self.paymentOptionCardView.layer.shadowColor = [UIColor blackColor].CGColor;
self.paymentOptionCardView.layer.shadowOffset = CGSizeMake(0.0f, 1.0f);
self.paymentOptionCardView.layer.shadowOpacity = 0.12f;
self.paymentOptionCardView.layer.shadowRadius = 5.0f;
[self.contentView addSubview:self.paymentOptionCardView];
self.titleLabel = [[UILabel alloc] init];
[BTUIKAppearance styleSmallLabelBoldPrimary:self.titleLabel];
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.titleLabel.text = @"";
self.titleLabel.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:self.titleLabel];
self.descriptionLabel = [[UILabel alloc] init];
[BTUIKAppearance styleLabelSecondary:self.descriptionLabel];
self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.descriptionLabel.text = @"";
self.descriptionLabel.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:self.descriptionLabel];
NSDictionary* viewBindings = @{@"contentView":self.contentView, @"paymentOptionCardView":self.paymentOptionCardView, @"titleLabel":self.titleLabel,
@"descriptionLabel":self.descriptionLabel};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[paymentOptionCardView(LARGE_ICON_WIDTH)]"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[titleLabel]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[descriptionLabel]|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[paymentOptionCardView(LARGE_ICON_HEIGHT)]-(HORIZONTAL_FORM_PADDING)-[titleLabel][descriptionLabel]-(>=1)-|"
options:0
metrics:[BTUIKAppearance metrics]
views:viewBindings]];
[self.paymentOptionCardView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor].active = YES;
}
return self;
}
-(void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
self.alpha = highlighted ? 0.5 : 1.0;
}
@end

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>4.6.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>4.6.1</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
#import "BTDropInRequest.h"
@implementation BTDropInRequest
- (id)copyWithZone:(__unused NSZone *)zone {
BTDropInRequest *request = [BTDropInRequest new];
request.amount = self.amount;
request.currencyCode = self.currencyCode;
request.noShipping = self.noShipping;
request.shippingAddress = self.shippingAddress;
request.applePayDisabled = self.applePayDisabled;
request.threeDSecureVerification = self.threeDSecureVerification;
return request;
}
@end

View File

@@ -0,0 +1,24 @@
#import "BTDropInResult.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@implementation BTDropInResult
- (UIView *)paymentIcon {
return [BTUIKViewUtil vectorArtViewForPaymentOptionType:self.paymentOptionType];
}
- (NSString *)paymentDescription {
if (self.paymentMethod != nil) {
return self.paymentMethod.localizedDescription;
} else if (self.paymentOptionType == BTUIKPaymentOptionTypeApplePay) {
return [BTUIKLocalizedString PAYMENT_METHOD_TYPE_APPLE_PAY];
}
return @"";
}
@end

View File

@@ -0,0 +1,57 @@
#import "BTDropInBaseViewController.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
@class BTCardRequest, BTCardCapabilities, BTPaymentMethodNonce;
NS_ASSUME_NONNULL_BEGIN
@protocol BTCardFormViewControllerDelegate;
/// Contains form elements for entering card information.
@interface BTCardFormViewController : BTDropInBaseViewController <UITextFieldDelegate, BTUIKFormFieldDelegate, BTUIKCardNumberFormFieldDelegate>
@property (nonatomic, weak) id<BTCardFormViewControllerDelegate> delegate;
/// The card number form field.
@property (nonatomic, strong, readonly) BTUIKCardNumberFormField *cardNumberField;
/// The expiration date form field.
@property (nonatomic, strong, readonly) BTUIKExpiryFormField *expirationDateField;
/// The security code (ccv) form field.
@property (nonatomic, strong, readonly) BTUIKSecurityCodeFormField *securityCodeField;
/// The postal code form field.
@property (nonatomic, strong, readonly) BTUIKPostalCodeFormField *postalCodeField;
/// The mobile country code form field.
@property (nonatomic, strong, readonly) BTUIKMobileCountryCodeFormField *mobileCountryCodeField;
/// The mobile phone number field.
@property (nonatomic, strong, readonly) BTUIKMobileNumberFormField *mobilePhoneField;
/// If the form is valid, returns a BTCardRequest using the values of the form fields. Otherwise `nil`.
@property (nonatomic, strong, nullable, readonly) BTCardRequest *cardRequest;
/// The BTCardCapabilities used to update the form after checking the card number. Applicable when UnionPay is enabled.
@property (nonatomic, strong, nullable, readonly) BTCardCapabilities *cardCapabilities;
/// The card network types supported by this merchant
@property (nonatomic, copy) NSArray *supportedCardTypes;
/// Resets the state of the form fields
- (void)resetForm;
@end
@protocol BTCardFormViewControllerDelegate <NSObject>
- (void)cardTokenizationCompleted:(BTPaymentMethodNonce * _Nullable )tokenizedCard error:(NSError * _Nullable )error sender:(BTCardFormViewController *) sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,42 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class BTAPIClient, BTDropInRequest, BTConfiguration;
/// The base UIViewController for the sub UIViewControllers using in Drop-In
@interface BTDropInBaseViewController : UIViewController
/// Initialize a new Drop-in view controller.
///
/// @param apiClient A BTAPIClient used for communicating with Braintree servers. Required.
///
/// @return A new Drop-in view controller that is ready to be presented.
- (instancetype)initWithAPIClient:(BTAPIClient *)apiClient request:(BTDropInRequest *)request;
/// The API Client used for communication with Braintree servers.
@property (nonatomic, strong) BTAPIClient *apiClient;
/// The BTConfiguration, set during loadConfiguration.
@property (nonatomic, strong, nullable) BTConfiguration *configuration;
/// Subclasses should override this method to be notified when the configuration is loaded
- (void)configurationLoaded:(__unused BTConfiguration *)configuration error:(__unused NSError *)error;
/// Load the configuration and then call `configurationLoaded:error:`
- (void)loadConfiguration;
/// The BTDropInRequest that defines the Drop-in experience.
///
/// The properties of this payment request are used to customize Drop-in.
@property (nonatomic, strong, nullable) BTDropInRequest *dropInRequest;
/// Displays an overlay loading screen
///
/// @param show Modifies the hidden property of the overlay
/// @param animated Will animate the overlay changing the hidden property if set to `true`.
- (void)showLoadingScreen:(BOOL)show animated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,51 @@
#pragma message "⚠️ BraintreeDropIn is currently in beta and may change."
#import <UIKit/UIKit.h>
#import "BTDropInBaseViewController.h"
#import "BTDropInResult.h"
#import "BTDropInRequest.h"
#import "BTPaymentSelectionViewController.h"
#import "BTCardFormViewController.h"
@class BTPaymentMethodNonce;
NS_ASSUME_NONNULL_BEGIN
/// The primary UIViewController for Drop-In. BTDropInController will manage the other UIViewControllers and return a BTDropInResult.
@interface BTDropInController : UIViewController <UIToolbarDelegate, UIViewControllerTransitioningDelegate,BTAppSwitchDelegate, BTViewControllerPresentingDelegate, BTPaymentSelectionViewControllerDelegate, BTCardFormViewControllerDelegate>
typedef void (^BTDropInControllerFetchHandler)(BTDropInResult * _Nullable result, NSError * _Nullable error);
typedef void (^BTDropInControllerHandler)(BTDropInController * _Nonnull controller, BTDropInResult * _Nullable result, NSError * _Nullable error);
/// Initialize a new Drop-in view controller.
///
/// @param authorization A Braintree tokenization key, or a client token generated by your server.
/// Passing an invalid value may return `nil`.
/// @param handler A callback block that is invoked when tokenization has succeeded or failed.
///
/// @return A Drop-in controller that is ready to be presented, or `nil` if `authorization` is invalid.
- (nullable instancetype)initWithAuthorization:(NSString *)authorization
request:(BTDropInRequest *)request
handler:(nullable BTDropInControllerHandler)handler;
/// Fetch a BTDropInResult without displaying or initializing a BTDropInController. Works with client tokens that
/// were created with a `customer_id`.
///
/// @param authorization Your tokenization key or client token.
/// @param handler The handler for callbacks.
+ (void)fetchDropInResultForAuthorization:(NSString *)authorization handler:(BTDropInControllerFetchHandler)handler;
/// The API client used for communication with Braintree servers.
@property (nonatomic, strong, readonly) BTAPIClient *apiClient;
/// The BTDropInRequest used to customize Drop-in
@property (nonatomic, strong, readonly) BTDropInRequest *dropInRequest;
/// Show the BTCardFormViewController
///
/// @param sender The sender requesting the view be changed.
- (void)showCardForm:(id)sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,53 @@
#import <Foundation/Foundation.h>
#if __has_include("BraintreeCore.h")
#import "BTPostalAddress.h"
#else
#import <BraintreeCore/BTPostalAddress.h>
#endif
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@interface BTDropInRequest : NSObject <NSCopying>
/// Optional: Amount of the transaction.
///
/// Amount must be a non-negative number, may optionally contain exactly 2 decimal places
/// separated by '.', optional thousands separator ',', limited to 7 digits before the decimal point.
///
/// Used by PayPal single payments and ThreeDSecure.
@property (nonatomic, copy, nullable) NSString *amount;
/// Optional: A valid ISO currency code to use for the transaction. Defaults to merchant currency code if not set.
///
/// Used by PayPal.
@property (nonatomic, copy, nullable) NSString *currencyCode;
/// Defaults to false. When set to true, the shipping address selector will not be displayed.
///
/// Used by PayPal.
@property (nonatomic, assign) BOOL noShipping;
/// Optional: A valid shipping address to be displayed in the transaction flow.
/// An error will occur if this address is not valid.
///
/// Used by PayPal.
@property (nonatomic, strong, nullable) BTPostalAddress *shippingAddress;
/// Optional: A set of PayPal scopes to use when requesting payment via PayPal. Used by Drop-in and payment button.
@property (nonatomic, strong, nullable) NSSet<NSString *> *additionalPayPalScopes;
/// Optional: Use this parameter to disable Apple Pay. Otherwise if Apple Pay is correctly configured, Apple Pay will appear as a selection in the Payment Method options.
@property (nonatomic, assign) BOOL applePayDisabled;
/// Optional: If true and an amount is set, ThreeDSecure will be used to verify the card. ThreeDSecure must be enabled in the control panel.
/// Defaults to false.
@property (nonatomic, assign) BOOL threeDSecureVerification;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,33 @@
#if __has_include("BraintreeCore.h")
#import "BraintreeCore.h"
#else
#import <BraintreeCore/BraintreeCore.h>
#endif
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@interface BTDropInResult : NSObject
/// True if the modal was dismissed without selecting a payment method
@property (nonatomic, assign, getter=isCancelled) BOOL cancelled;
/// The type of the payment option
@property (nonatomic, assign) BTUIKPaymentOptionType paymentOptionType;
/// A UIView (BTUIKPaymentOptionCardView) that represents the payment option
@property (nonatomic, readonly) UIView *paymentIcon;
/// A description of the payment option (e.g `ending in 1234`)
@property (nonatomic, readonly) NSString *paymentDescription;
/// The payment method nonce
@property (nonatomic, strong, nullable) BTPaymentMethodNonce *paymentMethod;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,43 @@
#import <UIKit/UIKit.h>
#import "BTDropInBaseViewController.h"
#if __has_include("BraintreeUIKit.h")
#import "BraintreeUIKit.h"
#else
#import <BraintreeUIKit/BraintreeUIKit.h>
#endif
#if __has_include("BraintreeApplePay.h")
#define __BT_APPLE_PAY
#import "BraintreeApplePay.h"
#elif __has_include(<BraintreeApplePay/BraintreeApplePay.h>)
#define __BT_APPLE_PAY
#import <BraintreeApplePay/BraintreeApplePay.h>
#endif
@class BTPaymentMethodNonce;
@protocol BTPaymentSelectionViewControllerDelegate;
/// @class A UIViewController that displays vaulted payment methods for a customer and available payment options
@interface BTPaymentSelectionViewController : BTDropInBaseViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UITableViewDataSource, UITableViewDelegate>
/// The array of `BTPaymentMethodNonce` payment method nonces on file. The payment method nonces may be in the Vault.
/// Most payment methods are automatically Vaulted if the client token was generated with a customer ID.
@property (nonatomic, strong) NSArray *paymentMethodNonces;
/// The delegate
@property (nonatomic, weak) id<BTPaymentSelectionViewControllerDelegate> delegate;
@end
@protocol BTPaymentSelectionViewControllerDelegate <NSObject>
/// Called on the delegate when a payment method is selected
///
/// @param type The BTUIKPaymentOptionType of the selected payment method
/// @param nonce The BTPaymentMethodNonce of the selected payment method. @note This can be `nil` in the case of Apple Pay.
/// @param error The error that occured during tokenization of a new payment method.
- (void) selectionCompletedWithPaymentMethodType:(BTUIKPaymentOptionType) type nonce:(BTPaymentMethodNonce *)nonce error:(NSError *)error;
@end

View File

@@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
#import "BTDropInBaseViewController.h"
/// @class Pending implementation
@interface BTVaultManagementViewController : BTDropInBaseViewController
@end

View File

@@ -0,0 +1,19 @@
#import <UIKit/UIKit.h>
//! Project version number for BraintreeUI.
FOUNDATION_EXPORT double BraintreeDropInVersionNumber;
//! Project version string for BraintreeUI.
FOUNDATION_EXPORT const unsigned char BraintreeDropInVersionString[];
#if __has_include("BraintreeCore.h")
#import "BraintreeCore.h"
#else
#import <BraintreeCore/BraintreeCore.h>
#endif
#import "BTCardFormViewController.h"
#import "BTDropInController.h"
#import "BTDropInResult.h"
#import "BTPaymentSelectionViewController.h"
#import "BTVaultManagementViewController.h"
#import "BTDropInRequest.h"

View File

@@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface BTUIKBarButtonItem : UIBarButtonItem
@property (nonatomic) BOOL bold;
@end

View File

@@ -0,0 +1,17 @@
#import "BTUIKBarButtonItem.h"
#import "BTUIKAppearance.h"
@implementation BTUIKBarButtonItem
- (void)setEnabled:(BOOL)enabled {
[super setEnabled:enabled];
NSString* fontName = self.bold ? [BTUIKAppearance sharedInstance].boldFontFamily : [BTUIKAppearance sharedInstance].fontFamily;
if (enabled) {
[self setTitleTextAttributes:@{NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].tintColor, NSFontAttributeName:[UIFont fontWithName:fontName size:[UIFont labelFontSize]]} forState:UIControlStateNormal];
} else {
[self setTitleTextAttributes:@{NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].disabledColor, NSFontAttributeName:[UIFont fontWithName:fontName size:[UIFont labelFontSize]]} forState:UIControlStateNormal];
}
}
@end

View File

@@ -0,0 +1,95 @@
#import "BTUIKCardListLabel.h"
#import "BTUIKPaymentOptionCardView.h"
#import "BTUIKViewUtil.h"
#import "BTUIKAppearance.h"
#import <QuartzCore/QuartzCore.h>
@interface BTUIKCardListLabel ()
@property (nonatomic, strong) NSArray *availablePaymentOptionAttachments;
@property (nonatomic) BTUIKPaymentOptionType emphasisedPaymentOption;
@end
@implementation BTUIKCardListLabel
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.numberOfLines = 0;
self.textAlignment = NSTextAlignmentCenter;
self.emphasisedPaymentOption = BTUIKPaymentOptionTypeUnknown;
self.availablePaymentOptionAttachments = @[];
self.availablePaymentOptions = @[];
}
return self;
}
- (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
- (void)setAvailablePaymentOptions:(NSArray *)availablePaymentOptions {
_availablePaymentOptions = availablePaymentOptions;
if ([BTUIKViewUtil isLanguageLayoutDirectionRightToLeft]) {
_availablePaymentOptions = [[_availablePaymentOptions reverseObjectEnumerator] allObjects];
}
[self updateAppearance];
[self emphasizePaymentOption:self.emphasisedPaymentOption];
}
- (void)updateAppearance {
NSMutableAttributedString *at = [[NSMutableAttributedString alloc] initWithString:@""];
NSMutableArray *attachments = [NSMutableArray new];
BTUIKPaymentOptionCardView* hint = [BTUIKPaymentOptionCardView new];
hint.frame = CGRectMake(0, 0, [BTUIKAppearance smallIconWidth], [BTUIKAppearance smallIconHeight]);
for(NSNumber *paymentType in self.availablePaymentOptions) {
NSTextAttachment *composeAttachment = [NSTextAttachment new];
BTUIKPaymentOptionType paymentOption = ((NSNumber*)paymentType).intValue;
hint.paymentOptionType = paymentOption;
[hint setNeedsLayout];
[hint layoutIfNeeded];
UIImage* composeImage = [self imageWithView:hint];
[attachments addObject:composeAttachment];
composeAttachment.image = composeImage;
[at appendAttributedString:[NSAttributedString attributedStringWithAttachment:composeAttachment]];
[at appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@" "]];
}
self.attributedText = at;
self.availablePaymentOptionAttachments = attachments;
}
- (void)emphasizePaymentOption:(BTUIKPaymentOptionType)paymentOption
{
if (paymentOption == self.emphasisedPaymentOption) {
return;
}
[self updateAppearance];
for (NSUInteger i = 0; i < self.availablePaymentOptions.count; i++) {
BTUIKPaymentOptionType option = ((NSNumber*)self.availablePaymentOptions[i]).intValue;
float newAlpha = (paymentOption == option || paymentOption == BTUIKPaymentOptionTypeUnknown) ? 1.0 : 0.25;
NSTextAttachment *attachment = self.availablePaymentOptionAttachments[i];
UIGraphicsBeginImageContextWithOptions(attachment.image.size, NO, attachment.image.scale);
[attachment.image drawAtPoint:CGPointZero blendMode:kCGBlendModeNormal alpha:newAlpha];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
attachment.image = image;
}
self.emphasisedPaymentOption = paymentOption;
[self setNeedsDisplay];
}
@end

View File

@@ -0,0 +1,227 @@
#import "BTUIKCardNumberFormField.h"
#import "BTUIKPaymentOptionCardView.h"
#import "BTUIKLocalizedString.h"
#import "BTUIKUtil.h"
#import "BTUIKTextField.h"
#import "BTUIKViewUtil.h"
#import "BTUIKInputAccessoryToolbar.h"
#import "BTUIKAppearance.h"
#define TEMP_KERNING 8.0
@interface BTUIKCardNumberFormField ()
@property (nonatomic, strong) BTUIKPaymentOptionCardView *hint;
@property (nonatomic, strong) UIButton *validateButton;
@property (nonatomic, strong) UIActivityIndicatorView *loadingView;
@end
@implementation BTUIKCardNumberFormField
@synthesize number = _number;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.state = BTUIKCardNumberFormFieldStateDefault;
self.textField.accessibilityLabel = BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
self.textField.placeholder = BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
self.formLabel.text = @"";
self.textField.keyboardType = UIKeyboardTypeNumberPad;
self.hint = [BTUIKPaymentOptionCardView new];
self.hint.paymentOptionType = BTUIKPaymentOptionTypeUnknown;
self.hint.translatesAutoresizingMaskIntoConstraints = NO;
[self.hint addConstraint:[NSLayoutConstraint constraintWithItem:self.hint attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:[BTUIKAppearance smallIconHeight]]];
[self.hint addConstraint:[NSLayoutConstraint constraintWithItem:self.hint attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:[BTUIKAppearance smallIconWidth]]];
self.accessoryView = self.hint;
[self setAccessoryViewHidden:YES animated:NO];
self.validateButton = [UIButton new];
[self.validateButton setTitle:@"Next" forState:UIControlStateNormal];
NSAttributedString *normalValidateButtonString = [[NSAttributedString alloc] initWithString:@"Next" attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].tintColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].boldFontFamily size:[UIFont labelFontSize]]}];
[self.validateButton setAttributedTitle:normalValidateButtonString forState:UIControlStateNormal];
NSAttributedString *disabledValidateButtonString = [[NSAttributedString alloc] initWithString:@"Next" attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].disabledColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].boldFontFamily size:[UIFont labelFontSize]]}];
[self.validateButton setAttributedTitle:disabledValidateButtonString forState:UIControlStateDisabled];
[self.validateButton sizeToFit];
[self.validateButton layoutIfNeeded];
[self.validateButton addTarget:self action:@selector(validateButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[self updateValidationButton];
self.loadingView = [UIActivityIndicatorView new];
self.loadingView.activityIndicatorViewStyle = [BTUIKAppearance sharedInstance].activityIndicatorViewStyle;
[self.loadingView sizeToFit];
}
return self;
}
- (void)validateButtonPressed {
if (self.cardNumberDelegate != nil) {
[self.cardNumberDelegate validateButtonPressed:self];
}
}
- (void)updateValidationButton {
self.validateButton.enabled = _number.length > 13;
}
- (BOOL)valid {
return [self.cardType validNumber:self.number];
}
- (BOOL)entryComplete {
return [super entryComplete] && [self.cardType validAndNecessarilyCompleteNumber:self.number];
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSUInteger newLength = textField.text.length - range.length + string.length;
NSUInteger maxLength = self.cardType == nil ? [BTUIKCardType maxNumberLength] : self.cardType.maxNumberLength;
if ([self isShowingValidateButton]) {
return YES;
} else {
return newLength <= maxLength;
}
}
- (void)setText:(NSString *)text {
[super setText:text];
[self fieldContentDidChange];
}
- (void)fieldContentDidChange {
_number = [BTUIKUtil stripNonDigits:self.textField.text];
BTUIKCardType *oldCardType = _cardType;
_cardType = [BTUIKCardType cardTypeForNumber:_number];
[self formatCardNumber];
if (self.cardType != oldCardType) {
[self updateCardHint];
}
self.displayAsValid = self.valid || (!self.isValidLength && self.isPotentiallyValid) || self.state == BTUIKCardNumberFormFieldStateValidate;
[self updateValidationButton];
[self updateAppearance];
[self setNeedsDisplay];
[self.delegate formFieldDidChange:self];
}
- (void)formatCardNumber {
if (self.cardType != nil) {
UITextRange *r = self.textField.selectedTextRange;
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:[self.cardType formatNumber:_number kerning:TEMP_KERNING]];
self.textField.attributedText = text;
self.textField.selectedTextRange = r;
}
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
self.textField.text = _number;
[super textFieldDidBeginEditing:textField];
self.displayAsValid = self.valid || (!self.isValidLength && self.isPotentiallyValid);
self.formLabel.text = @"";
[UIView transitionWithView:self
duration:0.2
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
if ([self isShowingValidateButton]) {
[self setAccessoryViewHidden:NO animated:NO];
} else {
[self setAccessoryViewHidden:YES animated:YES];
}
[self updateConstraints];
[self updateAppearance];
if (self.isPotentiallyValid) {
[self formatCardNumber];
}
} completion:nil];
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
[super textFieldDidEndEditing:textField];
self.displayAsValid = _number.length == 0 || (![self isValidLength] && self.state == BTUIKCardNumberFormFieldStateValidate) || (_cardType != nil && [_cardType validNumber:_number]);
self.formLabel.text = _number.length == 0 || (![self isValidLength] && self.state == BTUIKCardNumberFormFieldStateValidate) ? @"" : BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
[UIView animateWithDuration:0.2 animations:^{
if ([self isShowingValidateButton]) {
[self setAccessoryViewHidden:NO animated:NO];
} else {
if (_number.length == 0) {
[self setAccessoryViewHidden:YES animated:YES];
} else {
[self showCardHintAccessory];
}
}
if (_number.length > 7 && ([self isValidLength] || self.state != BTUIKCardNumberFormFieldStateValidate)) {
NSString *lastFour = [_number substringFromIndex: [_number length] - 4];
self.textField.text = [NSString stringWithFormat:@"•••• %@", lastFour];
}
[self updateConstraints];
[self updateAppearance];
}];
}
- (void)resetFormField {
self.formLabel.text = @"";
self.textField.text = @"";
[self setAccessoryViewHidden:YES animated:NO];
[self updateConstraints];
[self updateAppearance];
}
#pragma mark - Public Methods
- (void)setState:(BTUIKCardNumberFormFieldState)state {
if (state == self.state) {
return;
}
_state = state;
if (self.state == BTUIKCardNumberFormFieldStateDefault) {
self.accessoryView = self.hint;
[self setAccessoryViewHidden:(self.formLabel.text.length <= 0) animated:YES];
} else if (self.state == BTUIKCardNumberFormFieldStateLoading) {
self.accessoryView = self.loadingView;
[self setAccessoryViewHidden:NO animated:YES];
[self.loadingView startAnimating];
} else {
self.accessoryView = self.validateButton;
[self setAccessoryViewHidden:NO animated:YES];
}
}
- (void)setNumber:(NSString *)number {
self.text = number;
_number = self.textField.text;
}
- (void)showCardHintAccessory {
[self setAccessoryViewHidden:NO animated:YES];
}
#pragma mark - Private Helpers
- (BOOL)isShowingValidateButton {
return self.state == BTUIKCardNumberFormFieldStateValidate;
}
- (BOOL)isValidCardType {
return self.cardType != nil || _number.length == 0;
}
- (BOOL)isPotentiallyValid {
return [BTUIKCardType possibleCardTypesForNumber:self.number].count > 0;
}
- (BOOL)isValidLength {
return self.cardType != nil && [self.cardType completeNumber:_number];
}
- (void)updateCardHint {
BTUIKPaymentOptionType paymentMethodType = [BTUIKViewUtil paymentMethodTypeForCardType:self.cardType];
self.hint.paymentOptionType = paymentMethodType;
}
@end

View File

@@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface BTUIKCollectionReusableView : UICollectionReusableView
@property (nonatomic, strong) UILabel* label;
@end

View File

@@ -0,0 +1,31 @@
#import "BTUIKCollectionReusableView.h"
#import "UIColor+BTUIK.h"
@implementation BTUIKCollectionReusableView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.label = [[UILabel alloc] init];
self.label.translatesAutoresizingMaskIntoConstraints = NO;
self.label.textAlignment = NSTextAlignmentCenter;
self.label.font = [UIFont systemFontOfSize:12];
self.label.textColor = [UIColor btuik_colorFromHex:@"666666" alpha:1.0];
[self addSubview:self.label];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|"
options:0
metrics:nil
views:@{@"label":self.label}]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|"
options:0
metrics:nil
views:@{@"label":self.label}]];
}
return self;
}
@end

View File

@@ -0,0 +1,210 @@
#import "BTUIKCardExpiryFormat.h"
#import "BTUIKCardExpirationValidator.h"
#import "BTUIKExpiryFormField.h"
#import "BTUIKInputAccessoryToolbar.h"
#import "BTUIKLocalizedString.h"
#import "BTUIKTextField.h"
#import "BTUIKUtil.h"
#define BTUIKCardExpiryFieldYYYYPrefix @"20"
#define BTUIKCardExpiryFieldComponentSeparator @"/"
#define BTUIKCardExpiryPlaceholderFourDigitYear BTUIKLocalizedString(EXPIRY_PLACEHOLDER_FOUR_DIGIT_YEAR)
#define BTUIKCardExpiryPlaceholderTwoDigitYear BTUIKLocalizedString(EXPIRY_PLACEHOLDER_TWO_DIGIT_YEAR)
@interface BTUIKExpiryFormField ()
@property (nonatomic, strong) BTUIKExpiryInputView *expiryInputView;
@end
@implementation BTUIKExpiryFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.textField.accessibilityLabel = @"Expiration Date";
self.formLabel.text = @"Expiration Date";
[self updatePlaceholder];
self.expiryInputView = [BTUIKExpiryInputView new];
self.expiryInputView.delegate = self;
// Use custom date picker, but fall back to number pad keyboard if inputView is set to nil
self.textField.keyboardType = UIKeyboardTypeNumberPad;
self.textField.inputView = self.expiryInputView;
}
return self;
}
#pragma mark - Custom accessors
- (void)setExpirationDate:(NSString *)expirationDate {
[self setText:expirationDate];
}
- (NSString *)expirationDate {
if (!self.expirationMonth || !self.expirationYear) return nil;
return [NSString stringWithFormat:@"%@%@", self.expirationMonth, self.expirationYear];
}
- (BOOL)valid {
if (!self.expirationYear || !self.expirationMonth) {
return NO;
}
return [BTUIKCardExpirationValidator month:self.expirationMonth.intValue year:self.expirationYear.intValue validForDate:[NSDate date]];
}
#pragma mark - Private methods
- (void)updatePlaceholder {
NSString *placeholder = BTUIKLocalizedString(EXPIRY_PLACEHOLDER_FOUR_DIGIT_YEAR);
[self setThemedPlaceholder:placeholder];
self.textField.accessibilityLabel = placeholder;
}
- (void)kernExpiration:(NSMutableAttributedString *)input {
CGFloat kerningValue = 4;
[input removeAttribute:NSKernAttributeName range:NSMakeRange(0, input.length)];
[input beginEditing];
if (input.length > 2) {
[input addAttribute:NSKernAttributeName value:@(kerningValue) range:NSMakeRange(1, 1)];
if (input.length > 3) {
[input addAttribute:NSKernAttributeName value:@(kerningValue) range:NSMakeRange(2, 1)];
}
}
[input endEditing];
}
- (void)setThemedPlaceholder:(NSString *)placeholder {
NSMutableAttributedString *attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder ?: @""
attributes:@{}];
[self kernExpiration:attributedPlaceholder];
self.textField.placeholder = placeholder;
}
#pragma mark - Helpers
- (BOOL)dateCouldEndWithFourDigitYear:(NSString *)expirationDate {
NSArray *expirationComponents = [expirationDate componentsSeparatedByString:BTUIKCardExpiryFieldComponentSeparator];
NSString *yearComponent = [expirationComponents count] >= 2 ? expirationComponents[1] : nil;
return (yearComponent && yearComponent.length >= 2 && [[yearComponent substringToIndex:2] isEqualToString:BTUIKCardExpiryFieldYYYYPrefix]);
}
// Returns YES if date is either a valid date or can have digits appended to make one. It does not contain any expiration
// date validation.
- (BOOL)dateIsValid:(NSString *)date {
NSArray *dateComponents = [date componentsSeparatedByString:BTUIKCardExpiryFieldComponentSeparator];
NSString *yearComponent;
if (dateComponents.count >= 2) {
yearComponent = dateComponents[1];
} else {
yearComponent = date.length >= 4 ? [date substringWithRange:NSMakeRange(2, date.length - 2)] : nil;
}
BOOL couldEndWithFourDigitYear = yearComponent && yearComponent.length >= 2 && [[yearComponent substringToIndex:2] isEqualToString:BTUIKCardExpiryFieldYYYYPrefix];
if (couldEndWithFourDigitYear ? date.length > 7 : date.length > 5) {
return NO;
}
NSString *updatedNumberText = [BTUIKUtil stripNonDigits:date];
NSString *monthStr = [updatedNumberText substringToIndex:MIN((NSUInteger)2, updatedNumberText.length)];
if (monthStr.length > 0) {
NSInteger month = [monthStr integerValue];
if(month < 0 || 12 < month) {
return NO;
}
if(monthStr.length >= 2 && month == 0) {
return NO;
}
}
return YES;
}
#pragma mark - Protocol conformance
#pragma mark UITextFieldDelegate
- (void)fieldContentDidChange {
_expirationMonth = nil;
_expirationYear = nil;
NSString *formattedValue;
NSUInteger formattedCursorLocation;
BTUIKCardExpiryFormat *format = [[BTUIKCardExpiryFormat alloc] init];
format.value = self.textField.text;
format.cursorLocation = [self.textField offsetFromPosition:self.textField.beginningOfDocument toPosition:self.textField.selectedTextRange.start];
format.backspace = self.backspace;
[format formattedValue:&formattedValue cursorLocation:&formattedCursorLocation];
// Important: Reset the state of self.backspace.
// Otherwise, the user won't be able to do the following:
// Enter "11/16", then backspace to
// "1", and then type e.g. "2". Instead of showing:
// "12/" (as it should), the form would instead remain stuck at
// "1".
self.backspace = NO;
// This is because UIControlEventEditingChanged is *not* sent after the "/" is removed.
// We can't trigger UIControlEventEditingChanged here (after removing a "/") because that would cause an infinite loop.
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:formattedValue];
[self kernExpiration:result];
self.textField.attributedText = result;
UITextPosition *newPosition = [self.textField positionFromPosition:self.textField.beginningOfDocument offset:formattedCursorLocation];
UITextRange *newRange = [self.textField textRangeFromPosition:newPosition toPosition:newPosition];
self.textField.selectedTextRange = newRange;
NSArray *expirationComponents = [self.textField.text componentsSeparatedByString:BTUIKCardExpiryFieldComponentSeparator];
if(expirationComponents.count == 2 && (self.textField.text.length == 3 || self.textField.text.length == 5 || self.textField.text.length == 7)) {
_expirationMonth = expirationComponents[0];
_expirationYear = expirationComponents[1];
}
[self updatePlaceholder];
self.displayAsValid = ((self.textField.text.length != 5 && self.textField.text.length != 7) || self.valid);
[self.delegate formFieldDidChange:self];
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
self.expiryInputView.selectedYear = self.expirationYear.intValue;
self.expiryInputView.selectedMonth = self.expirationMonth.intValue;
[super textFieldDidBeginEditing:textField];
self.displayAsValid = YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
[super textFieldDidEndEditing:textField];
self.displayAsValid = self.textField.text.length == 0 || self.valid;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)newText {
NSString *numericNewText = [BTUIKUtil stripNonDigits:newText];
if (![numericNewText isEqualToString:newText]) {
return NO;
}
NSString *updatedText = [textField.text stringByReplacingCharactersInRange:range withString:numericNewText];
return [self dateIsValid:updatedText];
}
- (BOOL)entryComplete {
return [super entryComplete] && ![self.expirationYear isEqualToString:BTUIKCardExpiryFieldYYYYPrefix];
}
#pragma mark BTUIKExpiryInputViewDelegate
- (void)expiryInputViewDidChange:(BTUIKExpiryInputView *)expiryInputView {
if (expiryInputView.selectedYear > 0) {
self.expirationDate = [NSString stringWithFormat:@"%02li%04li", (long)expiryInputView.selectedMonth, (long)expiryInputView.selectedYear];
} else {
self.expirationDate = [NSString stringWithFormat:@"%02li", (long)expiryInputView.selectedMonth];
}
}
@end

View File

@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
@interface BTUIKExpiryInputCollectionViewCell : UICollectionViewCell
@property (nonatomic, strong) UILabel* label;
- (NSInteger)getInteger;
@end

View File

@@ -0,0 +1,40 @@
#import "BTUIKExpiryInputCollectionViewCell.h"
#import "UIColor+BTUIK.h"
@implementation BTUIKExpiryInputCollectionViewCell
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.label = [[UILabel alloc] init];
self.backgroundColor = [UIColor whiteColor];
self.label.font = [UIFont systemFontOfSize:24];
self.label.translatesAutoresizingMaskIntoConstraints = NO;
self.label.textAlignment = NSTextAlignmentCenter;
[self.contentView addSubview:self.label];
UIView* bgView = [[UIView alloc] initWithFrame:self.frame];
bgView.layer.cornerRadius = 4;
self.selectedBackgroundView = bgView;
self.selectedBackgroundView.backgroundColor = [UIColor btuik_colorFromHex:@"D1D4D9" alpha:1.0];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|"
options:0
metrics:nil
views:@{@"label":self.label}]];
[self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|"
options:0
metrics:nil
views:@{@"label":self.label}]];
}
return self;
}
- (NSInteger)getInteger {
return [self.label.text integerValue];
}
@end

View File

@@ -0,0 +1,351 @@
#import "BTUIKExpiryInputView.h"
#import "BTUIKExpiryInputCollectionViewCell.h"
#import "BTUIKCollectionReusableView.h"
#import "UIColor+BTUIK.h"
#define BT_EXPIRY_FULL_PADDING 10
#define BT_EXPIRY_SECTION_HEADER_HEIGHT 12
@interface BTUIKExpiryInputView ()
@property (nonatomic, strong) NSArray* months;
@property (nonatomic, strong) NSArray* years;
@property (nonatomic, strong) UICollectionView* monthCollectionView;
@property (nonatomic, strong) UICollectionView* yearCollectionView;
@property (nonatomic, strong) UIView* verticalLine;
@property (nonatomic) NSInteger currentYear;
@property (nonatomic) NSInteger currentMonth;
@property (nonatomic, strong) UIPageControl* pageControl;
@property (nonatomic) BOOL needsOrientationChange;
@end
@implementation BTUIKExpiryInputView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.needsOrientationChange = NO;
self.backgroundColor = [UIColor whiteColor];
self.months = @[@"01", @"02", @"03", @"04", @"05", @"06", @"07", @"08", @"09", @"10", @"11", @"12"];
NSDate *currentDate = [NSDate date];
NSCalendar* calendar = [NSCalendar currentCalendar];
NSDateComponents* components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth fromDate:currentDate];
self.currentYear = [components year];
self.currentMonth = [components month];
NSMutableArray* mutableYears = [@[] mutableCopy];
NSInteger yearCounter = self.currentYear;
while (yearCounter < self.currentYear + 20) {
[mutableYears addObject:[NSString stringWithFormat:@"%li", (long)yearCounter]];
yearCounter++;
}
self.years = [NSArray arrayWithArray:mutableYears];
UICollectionViewFlowLayout *layout=[[UICollectionViewFlowLayout alloc] init];
self.monthCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
[self.monthCollectionView registerClass:[BTUIKExpiryInputCollectionViewCell class] forCellWithReuseIdentifier:@"BTMonthCell"];
[self.monthCollectionView registerClass:[BTUIKCollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView"];
self.monthCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
self.monthCollectionView.delegate = self;
self.monthCollectionView.dataSource = self;
self.monthCollectionView.backgroundColor = [UIColor clearColor];
[self addSubview:self.monthCollectionView];
layout=[[UICollectionViewFlowLayout alloc] init];
self.yearCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
[self.yearCollectionView registerClass:[BTUIKExpiryInputCollectionViewCell class] forCellWithReuseIdentifier:@"BTYearCell"];
[self.yearCollectionView registerClass:[BTUIKCollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView"];
self.yearCollectionView.translatesAutoresizingMaskIntoConstraints = NO;
self.yearCollectionView.delegate = self;
self.yearCollectionView.dataSource = self;
self.yearCollectionView.backgroundColor = [UIColor clearColor];
self.yearCollectionView.showsVerticalScrollIndicator = YES;
[self addSubview:self.yearCollectionView];
self.verticalLine = [[UIView alloc] init];
self.verticalLine.translatesAutoresizingMaskIntoConstraints = NO;
self.verticalLine.backgroundColor = [UIColor btuik_colorFromHex:@"8D8D8D" alpha:1.0];
[self addSubview:self.verticalLine];
[self.yearCollectionView reloadData];
self.pageControl = [[UIPageControl alloc] init];
self.pageControl.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.pageControl];
self.pageControl.transform = CGAffineTransformMakeRotation(M_PI_2);
self.pageControl.numberOfPages = 6;
self.pageControl.currentPage = 1;
self.pageControl.pageIndicatorTintColor = [UIColor lightGrayColor];
self.pageControl.currentPageIndicatorTintColor = self.tintColor;
self.pageControl.hidden = true;
NSDictionary* viewBindings = @{@"view":self, @"monthCollectionView":self.monthCollectionView, @"yearCollectionView": self.yearCollectionView, @"verticalLine":self.verticalLine, @"pageControl": self.pageControl};
NSDictionary* metrics = @{@"BT_EXPIRY_FULL_PADDING":@BT_EXPIRY_FULL_PADDING, @"BT_EXPIRY_FULL_PADDING_HALF": @(BT_EXPIRY_FULL_PADDING/2.0)};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(BT_EXPIRY_FULL_PADDING_HALF)-[monthCollectionView][verticalLine(0.5)][yearCollectionView]-(BT_EXPIRY_FULL_PADDING_HALF)-|"
options:0
metrics:metrics
views:viewBindings]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(BT_EXPIRY_FULL_PADDING)-[monthCollectionView]|"
options:0
metrics:metrics
views:viewBindings]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(BT_EXPIRY_FULL_PADDING)-[yearCollectionView]|"
options:0
metrics:metrics
views:viewBindings]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[verticalLine]|"
options:0
metrics:nil
views:viewBindings]];
CGSize sizeOfPageControl = [self.pageControl sizeForNumberOfPages:self.pageControl.numberOfPages];
[self addConstraint:
[NSLayoutConstraint constraintWithItem:self.pageControl attribute:NSLayoutAttributeRight relatedBy:0 toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:sizeOfPageControl.width/2 - sizeOfPageControl.height/2 + 10]];
[self addConstraint:
[NSLayoutConstraint constraintWithItem:self.pageControl attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
[self addConstraint:
[NSLayoutConstraint constraintWithItem:self.yearCollectionView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:self attribute:NSLayoutAttributeWidth multiplier:0.33 constant:0]];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(orientationChange)
name:UIDeviceOrientationDidChangeNotification
object:nil];
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
}
return self;
}
#pragma mark - Accessors
- (void)setSelectedYear:(NSInteger)selectedYear {
NSString *stringToSearch = [NSString stringWithFormat:@"%li", (long)selectedYear];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains[c] %@",stringToSearch];
NSString *results = [[self.years filteredArrayUsingPredicate:predicate] firstObject];
NSInteger yearIndex = [self.years indexOfObject:results];
if (([self isValidYear:selectedYear forMonth:self.selectedMonth] && yearIndex != NSNotFound) || selectedYear == 0) {
_selectedYear = selectedYear;
if (selectedYear > 0) {
[self.yearCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:
yearIndex inSection:0] animated:NO scrollPosition:0];
} else {
NSIndexPath *selectedIndexPath = [[self.yearCollectionView indexPathsForSelectedItems] firstObject];
[self.yearCollectionView deselectItemAtIndexPath:selectedIndexPath animated:NO];
}
[self updateVisibleCells];
}
}
- (void)setSelectedMonth:(NSInteger)selectedMonth {
if ([self isValidMonth:selectedMonth forYear:self.selectedYear] || selectedMonth == 0) {
_selectedMonth = selectedMonth;
if (selectedMonth > 0) {
[self.monthCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:selectedMonth - 1 inSection:0] animated:NO scrollPosition:0];
} else {
NSIndexPath *selectedIndexPath = [[self.monthCollectionView indexPathsForSelectedItems] firstObject];
[self.monthCollectionView deselectItemAtIndexPath:selectedIndexPath animated:NO];
}
[self updateVisibleCells];
}
}
#pragma mark - UICollectionView Datasource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(__unused NSInteger)section {
if (collectionView == self.monthCollectionView) {
return [self.months count];
}
return [self.years count];
}
- (NSInteger)numberOfSectionsInCollectionView: (__unused UICollectionView *)collectionView {
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.monthCollectionView) {
BTUIKExpiryInputCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"BTMonthCell" forIndexPath:indexPath];
cell.userInteractionEnabled = true;
NSString* date = self.months[indexPath.row];
cell.label.text = date;
cell.backgroundColor = [UIColor whiteColor];
cell.label.textColor = cell.selected ? [UIColor blackColor] : [UIColor blackColor];
if (self.selectedYear && self.selectedYear == self.currentYear) {
if ([cell getInteger] < self.currentMonth) {
cell.userInteractionEnabled = false;
cell.label.textColor = [UIColor lightGrayColor];
}
}
return cell;
}
BTUIKExpiryInputCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"BTYearCell" forIndexPath:indexPath];
NSString* date = self.years[indexPath.row];
cell.userInteractionEnabled = true;
cell.label.text = date;
cell.backgroundColor = [UIColor whiteColor];
cell.label.textColor = cell.selected ? [UIColor blackColor] : [UIColor blackColor];
if (self.selectedMonth && self.selectedMonth < self.currentMonth && [cell getInteger] == self.currentYear) {
cell.userInteractionEnabled = false;
cell.label.textColor = [UIColor lightGrayColor];
}
return cell;
}
- (UICollectionReusableView *)collectionView:
(__unused UICollectionView *)collectionView viewForSupplementaryElementOfKind:(__unused NSString *)kind atIndexPath:(__unused NSIndexPath *)indexPath
{
BTUIKCollectionReusableView* view = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"HeaderView" forIndexPath:indexPath];
view.label.text = collectionView == self.yearCollectionView ? @"YEAR" : @"MONTH";
return view;
}
- (CGSize)collectionView:(__unused UICollectionView *)collectionView layout:(__unused UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(__unused NSInteger)section {
return CGSizeMake(100, BT_EXPIRY_SECTION_HEADER_HEIGHT);
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(__unused UICollectionView *)collectionView didSelectItemAtIndexPath:(__unused NSIndexPath *)indexPath
{
BTUIKExpiryInputCollectionViewCell *cell = (BTUIKExpiryInputCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
if (collectionView == self.yearCollectionView) {
_selectedYear = [cell getInteger];
// If a year is selected first, select a month that is valid for that year
if (self.selectedMonth == 0) {
_selectedMonth = 12;
}
} else {
_selectedMonth = [cell getInteger];
}
cell.label.textColor = [UIColor blackColor];
if (self.delegate != nil) {
[self.delegate expiryInputViewDidChange:self];
}
[self updateVisibleCells];
}
- (void)collectionView:(__unused UICollectionView *)collectionView didDeselectItemAtIndexPath:(__unused NSIndexPath *)indexPath {
BTUIKExpiryInputCollectionViewCell *cell = (BTUIKExpiryInputCollectionViewCell*)[collectionView cellForItemAtIndexPath:indexPath];
cell.label.textColor = [UIColor blackColor];
}
- (BOOL)isValidYear:(NSInteger)year forMonth:(NSInteger)month {
if (month == 0) {
return YES;
} else if (month < self.currentMonth && year <= self.currentYear) {
return NO;
} else if (month > 12 || year < self.currentYear) {
return NO;
}
return YES;
}
- (BOOL)isValidMonth:(NSInteger)month forYear:(NSInteger)year {
if (year == 0) {
return YES;
} else if (month < self.currentMonth && year <= self.currentYear) {
return NO;
} else if (month > 12) {
return NO;
}
return YES;
}
- (void) updateVisibleCells {
for (BTUIKExpiryInputCollectionViewCell* cell in [self.yearCollectionView visibleCells]) {
cell.userInteractionEnabled = true;
cell.label.textColor = cell.selected ? [UIColor blackColor] : [UIColor blackColor];
if (self.selectedMonth && self.selectedMonth < self.currentMonth && [cell getInteger] == self.currentYear) {
cell.userInteractionEnabled = false;
cell.label.textColor = [UIColor lightGrayColor];
}
}
for (BTUIKExpiryInputCollectionViewCell* cell in [self.monthCollectionView visibleCells]) {
cell.userInteractionEnabled = true;
cell.label.textColor = cell.selected ? [UIColor blackColor] : [UIColor blackColor];
if (self.selectedYear && self.selectedYear == self.currentYear) {
if ([cell getInteger] < self.currentMonth) {
cell.userInteractionEnabled = false;
cell.label.textColor = [UIColor lightGrayColor];
}
}
}
}
#pragma mark UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(__unused UICollectionView *)collectionView layout:(__unused UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(__unused NSIndexPath *)indexPath {
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
BOOL isLandscape = NO;
if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
isLandscape = YES;
}
int monthCols = isLandscape ? 4.0 : 3.0;
int monthRows = isLandscape ? 3.0 : 4.0;
CGSize monthCellSize = CGSizeMake((self.monthCollectionView.frame.size.width - ((BT_EXPIRY_FULL_PADDING * monthCols) + BT_EXPIRY_FULL_PADDING + (BT_EXPIRY_FULL_PADDING))) / monthCols , (self.monthCollectionView.frame.size.height - BT_EXPIRY_SECTION_HEADER_HEIGHT - ((BT_EXPIRY_FULL_PADDING * monthRows) + BT_EXPIRY_FULL_PADDING)) / monthRows);
if (self.monthCollectionView == collectionView) {
return monthCellSize;
}
int yearCols = isLandscape ? 2.0 : 1.0;
return CGSizeMake((self.yearCollectionView.frame.size.width - ((BT_EXPIRY_FULL_PADDING * yearCols) + BT_EXPIRY_FULL_PADDING + (BT_EXPIRY_FULL_PADDING))) / yearCols , monthCellSize.height);
}
- (UIEdgeInsets)collectionView:(__unused UICollectionView *)collectionView layout:(__unused UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(__unused NSInteger)section {
return UIEdgeInsetsMake(BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING, BT_EXPIRY_FULL_PADDING);
}
#pragma mark - Orientation
- (void)orientationChange {
if (self.window != nil) {
[self.monthCollectionView.collectionViewLayout invalidateLayout];
[self.yearCollectionView.collectionViewLayout invalidateLayout];
[self.monthCollectionView performBatchUpdates:nil completion:nil];
[self.yearCollectionView performBatchUpdates:nil completion:nil];
[self.yearCollectionView flashScrollIndicators];
[self setNeedsDisplay];
} else {
self.needsOrientationChange = YES;
}
}
- (void)layoutSubviews {
[super layoutSubviews];
if (self.needsOrientationChange) {
self.needsOrientationChange = NO;
[self orientationChange];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

View File

@@ -0,0 +1,367 @@
#import "BTUIKFormField.h"
#import "BTUIKVectorArtView.h"
#import "BTUIKViewUtil.h"
#import "BTUIKAppearance.h"
@interface BTUIKFormField ()<BTUIKTextFieldEditDelegate>
@property (nonatomic, copy) NSString *previousTextFieldText;
@property (nonatomic, strong) NSMutableArray *layoutConstraints;
@end
@implementation BTUIKFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [BTUIKAppearance sharedInstance].formFieldBackgroundColor;
self.translatesAutoresizingMaskIntoConstraints = NO;
self.displayAsValid = YES;
BTUIKTextField *textField = [BTUIKTextField new];
textField.editDelegate = self;
_textField = textField;
self.textField.translatesAutoresizingMaskIntoConstraints = NO;
self.textField.borderStyle = UITextBorderStyleNone;
self.textField.backgroundColor = [UIColor clearColor];
self.textField.opaque = NO;
self.textField.adjustsFontSizeToFitWidth = YES;
self.textField.returnKeyType = UIReturnKeyNext;
[self.textField addTarget:self action:@selector(fieldContentDidChange) forControlEvents:UIControlEventEditingChanged];
[self.textField addTarget:self action:@selector(editingDidBegin) forControlEvents:UIControlEventEditingDidBegin];
[self.textField addTarget:self action:@selector(editingDidEnd) forControlEvents:UIControlEventEditingDidEnd];
self.textField.delegate = self;
[self addSubview:self.textField];
self.formLabel = [[UILabel alloc] init];
[BTUIKAppearance styleLabelBoldPrimary:self.formLabel];
self.formLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.formLabel.text = @"";
[self addSubview:self.formLabel];
[self.formLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self.formLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self.textField setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedField)]];
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
self.opaque = NO;
[self updateConstraints];
}
return self;
}
- (void)updateConstraints {
if (self.layoutConstraints != nil) {
[self removeConstraints:self.layoutConstraints];
}
self.layoutConstraints = [NSMutableArray array];
NSMutableDictionary* viewBindings = [@{@"view":self, @"textField":self.textField, @"formLabel": self.formLabel} mutableCopy];
if (self.accessoryView) {
viewBindings[@"accessoryView"] = self.accessoryView;
}
NSDictionary* metrics = @{@"PADDING":@15};
BOOL hasFormLabel = (self.formLabel.text.length > 0);
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textField]|"
options:0
metrics:metrics
views:viewBindings]];
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[formLabel]|"
options:0
metrics:metrics
views:viewBindings]];
if (hasFormLabel) {
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(PADDING)-[formLabel(<=0@1)]-[textField]"
options:0
metrics:metrics
views:viewBindings]];
} else {
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(PADDING)-[textField]"
options:0
metrics:metrics
views:viewBindings]];
}
if (self.accessoryView && !self.accessoryView.hidden) {
[self.layoutConstraints addObjectsFromArray:@[[NSLayoutConstraint constraintWithItem:self.accessoryView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]]];
;
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[textField]-[accessoryView]-(PADDING)-|"
options:0
metrics:metrics
views:viewBindings]];
} else {
[self.layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[textField]-(PADDING)-|"
options:0
metrics:metrics
views:viewBindings]];
}
NSArray *contraintsToAdd = [self.layoutConstraints copy];
[self addConstraints:contraintsToAdd];
NSTextAlignment newAlignment = hasFormLabel ? [BTUIKViewUtil naturalTextAlignmentInverse] : [BTUIKViewUtil naturalTextAlignment];
if (newAlignment != self.textField.textAlignment) {
self.textField.textAlignment = newAlignment;
}
[super updateConstraints];
}
- (void)textFieldDidBeginEditing:(__unused UITextField *)textField {
if ([self.delegate respondsToSelector:@selector(formFieldDidBeginEditing:)]) {
[self.delegate formFieldDidBeginEditing:self];
}
}
- (void)textFieldDidEndEditing:(__unused UITextField *)textField {
if ([self.delegate respondsToSelector:@selector(formFieldDidEndEditing:)]) {
[self.delegate formFieldDidEndEditing:self];
}
}
#pragma mark - Drawing
- (void)drawRect:(CGRect)rect {
// Draw borders
[[BTUIKAppearance sharedInstance].lineColor setFill];
CGContextRef context = UIGraphicsGetCurrentContext();
if (!self.displayAsValid) {
CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x, CGRectGetMaxY(rect) - 0.5f, rect.size.width, 0.5f), NULL);
CGContextAddPath(context, path);
CGPathRelease(path);
path = CGPathCreateWithRect(CGRectMake(rect.origin.x, 0, rect.size.width, 0.5f), NULL);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathFill);
CGPathRelease(path);
} else {
if (self.interFieldBorder || self.bottomBorder) {
CGFloat horizontalMargin = self.bottomBorder ? 0 : 17.0f;
CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x + horizontalMargin, CGRectGetMaxY(rect) - 0.5f, rect.size.width - horizontalMargin, 0.5f), NULL);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathFill);
CGPathRelease(path);
}
if (self.topBorder) {
CGPathRef path = CGPathCreateWithRect(CGRectMake(rect.origin.x, 0, rect.size.width, 0.5f), NULL);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathFill);
CGPathRelease(path);
}
}
}
- (void)setBottomBorder:(BOOL)bottomBorder {
_bottomBorder = bottomBorder;
[self setNeedsDisplay];
}
- (void)setInterFieldBorder:(BOOL)interFieldBorder {
_interFieldBorder = interFieldBorder;
[self setNeedsDisplay];
}
- (void)setTopBorder:(BOOL)topBorder {
_topBorder = topBorder;
[self setNeedsDisplay];
}
- (void)updateAppearance {
UIColor *textColor;
NSString *currentAccessibilityLabel = self.textField.accessibilityLabel;
if (!self.displayAsValid){
textColor = [BTUIKAppearance sharedInstance].errorForegroundColor;
if (currentAccessibilityLabel != nil) {
self.textField.accessibilityLabel = [self addInvalidAccessibilityToString:currentAccessibilityLabel];
}
} else {
textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
if (currentAccessibilityLabel != nil) {
self.textField.accessibilityLabel = [self stripInvalidAccessibilityFromString:currentAccessibilityLabel];
}
}
NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:self.textField.attributedText];
[mutableText addAttributes:@{NSForegroundColorAttributeName: textColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]]} range:NSMakeRange(0, mutableText.length)];
UITextRange *currentRange = self.textField.selectedTextRange;
self.textField.attributedText = mutableText;
// Reassign current selection range, since it gets cleared after attributedText assignment
self.textField.selectedTextRange = currentRange;
}
#pragma mark - BTUITextFieldEditDelegate methods
- (void)textFieldWillDeleteBackward:(__unused BTUIKFormField *)textField {
// _backspace indicates that the backspace key was typed.
_backspace = YES;
}
- (void)textFieldDidDeleteBackward:(__unused BTUIKFormField *)textField originalText:(__unused NSString *)originalText {
// To be implemented by subclasses
}
- (void)textField:(__unused BTUIKFormField *)textField willInsertText:(__unused NSString *)text {
_backspace = NO;
}
- (void)textField:(__unused BTUIKFormField *)textField didInsertText:(__unused NSString *)text {
// To be implemented by subclasses
}
#pragma mark - Custom accessors
- (void)setText:( __unused NSString *)text {
BOOL shouldChange = [self.textField.delegate textField:self.textField
shouldChangeCharactersInRange:NSMakeRange(0, self.textField.text.length)
replacementString:text];
if (shouldChange) {
[self.textField.editDelegate textField:self.textField willInsertText:text];
self.textField.text = text;
[self fieldContentDidChange];
[self.textField.editDelegate textField:self.textField didInsertText:text];
}
[self updateAppearance];
}
- (NSString *)text {
return self.textField.text;
}
#pragma mark - Delegate methods and handlers
- (void)resetFormField {
// To be implemented by subclass
}
- (BOOL)becomeFirstResponder {
return [self.textField becomeFirstResponder];
}
- (void)fieldContentDidChange {
// To be implemented by subclass
if (self.delegate) {
[self.delegate formFieldDidChange:self];
}
[self updateAppearance];
}
- (void)editingDidBegin {
[self setAccessoryHighlighted:YES];
}
- (void)editingDidEnd {
[self setAccessoryHighlighted:NO];
}
- (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(__unused NSRange)range replacementString:(__unused NSString *)newText {
// To be implemented by subclass
return YES;
}
- (BOOL)textFieldShouldReturn:(__unused UITextField *)textField {
if ([self.delegate respondsToSelector:@selector(formFieldShouldReturn:)]) {
return [self.delegate formFieldShouldReturn:self];
} else {
return YES;
}
}
- (void)tappedField {
[self.textField becomeFirstResponder];
}
#pragma mark UIKeyInput
- (void)insertText:(__unused NSString *)text {
[self.textField insertText:text];
}
- (void)deleteBackward {
[self.textField deleteBackward];
}
- (BOOL)hasText {
return [self.textField hasText];
}
#pragma mark Accessibility Helpers
- (NSString *)stripInvalidAccessibilityFromString:(NSString *)str {
return [str stringByReplacingOccurrencesOfString:@"Invalid: " withString:@""];
}
- (NSString *)addInvalidAccessibilityToString:(NSString *)str {
return [NSString stringWithFormat:@"Invalid: %@", [self stripInvalidAccessibilityFromString:str]];
}
#pragma mark Accessory View Helpers
- (void)setAccessoryView:(UIView *)accessoryView {
if (self.accessoryView && self.accessoryView.superview) {
[self.accessoryView removeFromSuperview];
_accessoryView = nil;
}
_accessoryView = accessoryView;
self.accessoryView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.accessoryView];
[self.accessoryView setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self.accessoryView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self updateConstraints];
}
- (void)setAccessoryViewHidden:(BOOL)hidden animated:(__unused BOOL)animated {
if (self.accessoryView == nil) {
[self updateConstraints];
return;
}
if (animated) {
[UIView animateWithDuration:0.1 animations:^{
self.accessoryView.hidden = hidden;
[self updateConstraints];
}];
} else {
self.accessoryView.hidden = hidden;
[self updateConstraints];
}
}
- (void)setAccessoryHighlighted:(BOOL)highlight {
if (self.accessoryView) {
if ([self.accessoryView respondsToSelector:@selector(setHighlighted:animated:)]) {
SEL selector = @selector(setHighlighted:animated:);
BOOL animated = YES;
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.accessoryView methodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:self.accessoryView];
[invocation setArgument:&highlight atIndex:2];
[invocation setArgument:&animated atIndex:3];
[invocation invoke];
}
}
}
@end

View File

@@ -0,0 +1,29 @@
#import "BTUIKInputAccessoryToolbar.h"
#import <UIKit/UIKit.h>
#import "UIColor+BTUIK.h"
@implementation BTUIKInputAccessoryToolbar
- (instancetype)init
{
self = [super init];
if (self) {
self.barStyle = UIBarStyleDefault;
self.translucent = YES;
self.barTintColor = [UIColor btuik_colorFromHex:@"FFFFFF" alpha:0.88];
self.tintColor = [UIColor btuik_colorFromHex:@"858E99" alpha:1.0];
self.translatesAutoresizingMaskIntoConstraints = NO;
}
return self;
}
- (instancetype)initWithDoneButtonForInput:(id <UITextInput>)input {
if (self = [self init]) {
UIBarButtonItem *flexSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:input action:@selector(endEditing:)];
self.items = @[flexSpace, doneButton];
}
return self;
}
@end

View File

@@ -0,0 +1,45 @@
#import "BTUIKMobileCountryCodeFormField.h"
#import "BTUIKTextField.h"
#import "BTUIKInputAccessoryToolbar.h"
@implementation BTUIKMobileCountryCodeFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.textField.accessibilityLabel = @"Mobile Country Code";
self.formLabel.text = @"Mobile Country Code";
self.textField.placeholder = @"+65";
self.textField.keyboardType = UIKeyboardTypeNumberPad;
}
return self;
}
- (void)fieldContentDidChange {
NSMutableString *s = [NSMutableString stringWithString:self.textField.text];
NSUInteger slashLocation = [s rangeOfString:@"+"].location;
if (slashLocation == NSNotFound && s.length > 0) {
[s insertString:@"+" atIndex:0];
} else if (s.length == 1) {
s = [NSMutableString stringWithString:@""];
}
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:s];
self.textField.attributedText = result;
[self updateAppearance];
[self.delegate formFieldDidChange:self];
}
#pragma mark - Custom accessors
- (BOOL)valid {
return self.countryCode.length >= 1;
}
- (NSString *)countryCode {
return [self.textField.text stringByReplacingOccurrencesOfString:@"+" withString:@""];
}
@end

View File

@@ -0,0 +1,33 @@
#import "BTUIKMobileNumberFormField.h"
#import "BTUIKTextField.h"
#import "BTUIKInputAccessoryToolbar.h"
@implementation BTUIKMobileNumberFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.textField.accessibilityLabel = @"Mobile Number";
self.formLabel.text = @"Mobile Number";
self.textField.placeholder = @"00 0000 0000";
self.textField.keyboardType = UIKeyboardTypeNumberPad;
}
return self;
}
- (void)fieldContentDidChange {
[self.delegate formFieldDidChange:self];
[self updateAppearance];
}
#pragma mark - Custom accessors
- (BOOL)valid {
return self.mobileNumber.length >= 8;
}
- (NSString *)mobileNumber {
return self.textField.text;
}
@end

View File

@@ -0,0 +1,99 @@
#import "BTUIKPaymentOptionCardView.h"
#import "BTUIKVectorArtView.h"
#import "BTUIKAppearance.h"
@interface BTUIKPaymentOptionCardView()
@property (nonatomic, strong) BTUIKVectorArtView* imageView;
@end
@implementation BTUIKPaymentOptionCardView
- (instancetype)init
{
self = [super init];
if (self) {
self.vectorArtSize = BTUIKVectorArtSizeRegular;
self.cornerRadius = 4.0;
self.innerPadding = 0.0;
self.borderWidth = 0.5;
self.borderColor = [BTUIKAppearance sharedInstance].lineColor;
self.clipsToBounds = YES;
self.backgroundColor = [UIColor whiteColor];
}
return self;
}
- (void)setImageView:(BTUIKVectorArtView *)imageView {
if (self.imageView) {
[self.imageView removeFromSuperview];
}
_imageView = imageView;
self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.imageView];
[self updateAppearance];
}
- (void)updateAppearance {
NSDictionary* viewBindings = @{@"imageView":self.imageView};
NSDictionary* metrics = @{@"PADDING": @(self.innerPadding)};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(PADDING)-[imageView]-(PADDING)-|"
options:0
metrics:metrics
views:viewBindings]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self.imageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self.imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.imageView
attribute:NSLayoutAttributeWidth
multiplier:self.imageView.artDimensions.height/self.imageView.artDimensions.width
constant:0]];
}
- (void)setPaymentOptionType:(BTUIKPaymentOptionType)paymentOptionType {
_paymentOptionType = paymentOptionType;
self.borderWidth = self.paymentOptionType == BTUIKPaymentOptionTypeApplePay ? 0.0 : self.borderWidth;
self.imageView = [BTUIKViewUtil vectorArtViewForPaymentOptionType:self.paymentOptionType size:self.vectorArtSize];
}
- (void)setHighlighted:(BOOL)highlighted {
if (highlighted) {
self.layer.borderColor = self.tintColor.CGColor;
} else {
self.layer.borderColor = self.borderColor.CGColor;
}
}
- (CGSize)getArtDimensions {
return self.imageView.artDimensions;
}
- (void)setCornerRadius:(float)cornerRadius {
_cornerRadius = cornerRadius;
self.layer.cornerRadius = self.cornerRadius;
}
- (void)setBorderWidth:(float)borderWidth {
_borderWidth = borderWidth;
self.layer.borderWidth = _borderWidth;
}
- (void)setBorderColor:(UIColor *)borderColor {
_borderColor = borderColor;
self.layer.borderColor = _borderColor.CGColor;
}
- (void)setInnerPadding:(float)innerPadding {
_innerPadding = innerPadding;
if (self.imageView != nil) {
[self updateAppearance];
}
}
@end

View File

@@ -0,0 +1,54 @@
#import "BTUIKPostalCodeFormField.h"
#import "BTUIKUtil.h"
#import "BTUIKTextField.h"
#import "BTUIKLocalizedString.h"
#import "BTUIKInputAccessoryToolbar.h"
#import "BTUIKAppearance.h"
@implementation BTUIKPostalCodeFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.textField.accessibilityLabel = BTUIKLocalizedString(POSTAL_CODE_PLACEHOLDER);
self.formLabel.text = BTUIKLocalizedString(POSTAL_CODE_PLACEHOLDER);
self.textField.placeholder = @"12345";
self.textField.keyboardType = [BTUIKAppearance sharedInstance].postalCodeFormFieldKeyboardType;
self.textField.autocorrectionType = UITextAutocorrectionTypeNo;
self.textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.textField.returnKeyType = UIReturnKeyDone;
}
return self;
}
- (NSString *)postalCode {
return self.textField.text;
}
- (BOOL)entryComplete {
// Never allow auto-advancing out of postal code field since there is no way to know that the
// input value constitutes a complete postal code.
return NO;
}
- (BOOL)valid {
return self.postalCode.length > 0;
}
- (void)fieldContentDidChange {
[self.delegate formFieldDidChange:self];
[self updateAppearance];
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
self.displayAsValid = YES;
[super textFieldDidBeginEditing:textField];
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
self.displayAsValid = YES;
[super textFieldDidEndEditing:textField];
}
@end

View File

@@ -0,0 +1,53 @@
#import "BTUIKSecurityCodeFormField.h"
#import "BTUIKTextField.h"
#import "BTUIKInputAccessoryToolbar.h"
@interface BTUIKSecurityCodeFormField ()
@end
@implementation BTUIKSecurityCodeFormField
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.textField.accessibilityLabel = @"Security Code";
self.formLabel.text = @"Security Code";
self.textField.placeholder = @"CVV";
self.textField.keyboardType = UIKeyboardTypeNumberPad;
}
return self;
}
#pragma mark - Custom accessors
- (BOOL)valid {
return self.securityCode.length >= 3;
}
- (NSString *)securityCode {
return self.textField.text;
}
#pragma mark UITextFieldDelegate
- (void)fieldContentDidChange {
[self.delegate formFieldDidChange:self];
[self updateAppearance];
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[super textFieldDidBeginEditing:textField];
[self updateAppearance];
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
[super textFieldDidEndEditing:textField];
[self updateAppearance];
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
return textField.text.length - range.length + string.length <= 4;
}
@end

View File

@@ -0,0 +1,122 @@
#import "BTUIKTextField.h"
#import "BTUIKInputAccessoryToolbar.h"
#import "BTUIKAppearance.h"
#import "BTUIKViewUtil.h"
@interface BTUIKTextField () <UITextFieldDelegate>
@property (nonatomic, copy) NSString *previousText;
@end
@implementation BTUIKTextField
- (instancetype)init {
if (self = [super init]) {
self.hideCaret = NO;
if ([UIDevice currentDevice].systemVersion.intValue == 9) {
[self addTarget:self action:@selector(iOS9_changed) forControlEvents:UIControlEventEditingChanged];
self.delegate = self;
}
}
return self;
}
- (void)setPlaceholder:(NSString *)placeholder {
NSMutableAttributedString *mutablePlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder];
[mutablePlaceholder beginEditing];
[mutablePlaceholder addAttributes:@{NSForegroundColorAttributeName: [BTUIKAppearance sharedInstance].placeholderTextColor, NSFontAttributeName:[UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]]} range:NSMakeRange(0, [mutablePlaceholder length])];
[mutablePlaceholder endEditing];
self.attributedPlaceholder = mutablePlaceholder;
}
- (void)iOS9_changed {
// We only want to notify when this text field's text length has increased
if (self.previousText.length >= self.text.length) {
self.previousText = self.text;
return;
}
self.previousText = self.text;
NSString *insertedText = [self.text substringWithRange:NSMakeRange(self.previousText.length, self.text.length - self.previousText.length)];
if ([self.editDelegate respondsToSelector:@selector(textField:willInsertText:)]) {
// Sets _backspace = NO; in the BTUIKFormField or BTUIKFormField subclass
[self.editDelegate textField:self willInsertText:insertedText];
}
self.previousText = self.text;
if ([self.editDelegate respondsToSelector:@selector(textField:didInsertText:)]) {
[self.editDelegate textField:self didInsertText:insertedText];
}
}
- (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField {
if ([self.editDelegate respondsToSelector:@selector(textFieldWillDeleteBackward:)]) {
[self.editDelegate textFieldWillDeleteBackward:self];
}
BOOL shouldDelete = YES;
if ([UITextField instancesRespondToSelector:_cmd]) {
BOOL (*keyboardInputShouldDelete)(id, SEL, UITextField *) = (BOOL (*)(id, SEL, UITextField *))[UITextField instanceMethodForSelector:_cmd];
if (keyboardInputShouldDelete) {
shouldDelete = keyboardInputShouldDelete(self, _cmd, textField);
}
}
BOOL isIos8 = ([[[UIDevice currentDevice] systemVersion] intValue] == 8);
BOOL isLessThanIos8_3 = ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.3f);
// iOS 8.0-8.2 has a bug where deleteBackward is not called even when this method returns YES and the character is deleted
// As a result, we do so manually but return NO in order to prevent UITextField from double-calling the delegate method
// (textFieldDidDeleteBackwards:originalText:)
if (isIos8 && isLessThanIos8_3) {
[self deleteBackward];
shouldDelete = NO;
}
return shouldDelete;
}
- (void)deleteBackward
{
BOOL shouldDismiss = [self.text length] == 0;
NSString *originalText = self.text;
[super deleteBackward];
if (shouldDismiss) {
if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
[self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""];
}
}
if ([self.editDelegate respondsToSelector:@selector(textFieldDidDeleteBackward:originalText:)]) {
[self.editDelegate textFieldDidDeleteBackward:self originalText:originalText];
}
}
- (void)insertText:(NSString *)text {
if ([self.editDelegate respondsToSelector:@selector(textField:willInsertText:)]) {
[self.editDelegate textField:self willInsertText:text];
}
[super insertText:text];
if ([self.editDelegate respondsToSelector:@selector(textField:didInsertText:)]) {
[self.editDelegate textField:self didInsertText:text];
}
}
- (CGRect)caretRectForPosition:(UITextPosition *)position
{
if (self.hideCaret) {
return CGRectZero;
}
return [super caretRectForPosition:position];
}
@end

View File

@@ -0,0 +1,147 @@
#import "BTUIKAppearance.h"
#import "UIColor+BTUIK.h"
@implementation BTUIKAppearance
static BTUIKAppearance *sharedTheme;
+ (instancetype) sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedTheme = [BTUIKAppearance new];
[BTUIKAppearance lightTheme];
});
return sharedTheme;
}
+ (void) lightTheme {
sharedTheme.overlayColor = [UIColor btuik_colorFromHex:@"000000" alpha:0.5];
sharedTheme.tintColor = [UIColor btuik_colorFromHex:@"2489F6" alpha:1.0];
sharedTheme.barBackgroundColor = [UIColor whiteColor];
sharedTheme.fontFamily = [UIFont systemFontOfSize:10].fontName;
sharedTheme.boldFontFamily = [UIFont boldSystemFontOfSize:10].fontName;
sharedTheme.formBackgroundColor = [UIColor groupTableViewBackgroundColor];
sharedTheme.formFieldBackgroundColor = [UIColor whiteColor];
sharedTheme.primaryTextColor = [UIColor blackColor];
sharedTheme.secondaryTextColor = [UIColor btuik_colorFromHex:@"666666" alpha:1.0];
sharedTheme.disabledColor = [UIColor lightGrayColor];
sharedTheme.placeholderTextColor = [UIColor lightGrayColor];
sharedTheme.lineColor = [UIColor btuik_colorFromHex:@"BFBFBF" alpha:1.0];
sharedTheme.errorForegroundColor = [UIColor btuik_colorFromHex:@"ff3b30" alpha:1.0];
sharedTheme.blurStyle = UIBlurEffectStyleExtraLight;
sharedTheme.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
sharedTheme.useBlurs = YES;
sharedTheme.postalCodeFormFieldKeyboardType = UIKeyboardTypeNumberPad;
}
+ (void) darkTheme {
sharedTheme.overlayColor = [UIColor btuik_colorFromHex:@"000000" alpha:0.5];
sharedTheme.tintColor = [UIColor btuik_colorFromHex:@"2489F6" alpha:1.0];
sharedTheme.barBackgroundColor = [UIColor btuik_colorFromHex:@"222222" alpha:1.0];
sharedTheme.fontFamily = [UIFont systemFontOfSize:10].fontName;
sharedTheme.boldFontFamily = [UIFont boldSystemFontOfSize:10].fontName;
sharedTheme.formBackgroundColor = [UIColor btuik_colorFromHex:@"222222" alpha:1.0];
sharedTheme.formFieldBackgroundColor = [UIColor btuik_colorFromHex:@"333333" alpha:1.0];
sharedTheme.primaryTextColor = [UIColor whiteColor];
sharedTheme.secondaryTextColor = [UIColor btuik_colorFromHex:@"999999" alpha:1.0];
sharedTheme.disabledColor = [UIColor lightGrayColor];
sharedTheme.placeholderTextColor = [UIColor btuik_colorFromHex:@"8E8E8E" alpha:1.0];
sharedTheme.lineColor = [UIColor btuik_colorFromHex:@"666666" alpha:1.0];
sharedTheme.errorForegroundColor = [UIColor btuik_colorFromHex:@"ff3b30" alpha:1.0];
sharedTheme.blurStyle = UIBlurEffectStyleDark;
sharedTheme.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
sharedTheme.useBlurs = YES;
sharedTheme.postalCodeFormFieldKeyboardType = UIKeyboardTypeNumberPad;
}
+ (void) styleLabelPrimary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
}
+ (void) styleLabelBoldPrimary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].boldFontFamily size:[UIFont labelFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
}
+ (void) styleSmallLabelBoldPrimary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].boldFontFamily size:[UIFont smallSystemFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
}
+ (void) styleSmallLabelPrimary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont smallSystemFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].primaryTextColor;
}
+ (void) styleLabelSecondary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont smallSystemFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].secondaryTextColor;
}
+ (void) styleLargeLabelSecondary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont labelFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].secondaryTextColor;
}
+ (void) styleSystemLabelSecondary:(UILabel *)label {
label.font = [UIFont fontWithName:[BTUIKAppearance sharedInstance].fontFamily size:[UIFont systemFontSize]];
label.textColor = [BTUIKAppearance sharedInstance].secondaryTextColor;
}
+ (float)horizontalFormContentPadding {
return 15.0f;
}
+ (float)formCellHeight {
return 44.0f;
}
+ (float)verticalFormSpace {
return 35.0f;
}
+ (float)verticalFormSpaceTight {
return 10.0f;
}
+ (float)verticalSectionSpace {
return 30.0f;
}
+ (float)smallIconWidth {
return 45.0;
}
+ (float)smallIconHeight {
return 29.0;
}
+ (float)largeIconWidth {
return 100.0;
}
+ (float)largeIconHeight {
return 100.0;
}
+ (NSDictionary*)metrics {
static NSDictionary *sharedMetrics;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMetrics = @{@"HORIZONTAL_FORM_PADDING":@([BTUIKAppearance horizontalFormContentPadding]),
@"FORM_CELL_HEIGHT":@([BTUIKAppearance formCellHeight]),
@"VERTICAL_FORM_SPACE":@([BTUIKAppearance verticalFormSpace]),
@"VERTICAL_FORM_SPACE_TIGHT":@([BTUIKAppearance verticalFormSpaceTight]),
@"VERTICAL_SECTION_SPACE":@([BTUIKAppearance verticalSectionSpace]),
@"ICON_WIDTH":@([BTUIKAppearance smallIconWidth]),
@"ICON_HEIGHT":@([BTUIKAppearance smallIconHeight]),
@"LARGE_ICON_WIDTH":@([BTUIKAppearance largeIconWidth]),
@"LARGE_ICON_HEIGHT":@([BTUIKAppearance largeIconHeight])};
});
return sharedMetrics;
}
@end

View File

@@ -0,0 +1,204 @@
#import "BTUIKViewUtil.h"
#import "BTUIKMasterCardVectorArtView.h"
#import "BTUIKJCBVectorArtView.h"
#import "BTUIKMaestroVectorArtView.h"
#import "BTUIKVisaVectorArtView.h"
#import "BTUIKDiscoverVectorArtView.h"
#import "BTUIKUnknownCardVectorArtView.h"
#import "BTUIKDinersClubVectorArtView.h"
#import "BTUIKAmExVectorArtView.h"
#import "BTUIKPayPalMonogramCardView.h"
#import "BTUIKCoinbaseMonogramCardView.h"
#import "BTUIKVenmoMonogramCardView.h"
#import "BTUIKUnionPayVectorArtView.h"
#import "BTUIKApplePayMarkVectorArtView.h"
#import "BTUIKLargeMasterCardVectorArtView.h"
#import "BTUIKLargeJCBVectorArtView.h"
#import "BTUIKLargeMaestroVectorArtView.h"
#import "BTUIKLargeVisaVectorArtView.h"
#import "BTUIKLargeDiscoverVectorArtView.h"
#import "BTUIKLargeUnknownCardVectorArtView.h"
#import "BTUIKLargeDinersClubVectorArtView.h"
#import "BTUIKLargeAmExVectorArtView.h"
#import "BTUIKLargePayPalMonogramCardView.h"
#import "BTUIKLargeCoinbaseMonogramCardView.h"
#import "BTUIKLargeVenmoMonogramCardView.h"
#import "BTUIKLargeUnionPayVectorArtView.h"
#import "BTUIKLargeApplePayMarkVectorArtView.h"
@import AudioToolbox;
@implementation BTUIKViewUtil
+ (BTUIKPaymentOptionType)paymentMethodTypeForCardType:(BTUIKCardType *)cardType {
if (cardType == nil) {
return BTUIKPaymentOptionTypeUnknown;
}
if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_AMERICAN_EXPRESS)]) {
return BTUIKPaymentOptionTypeAMEX;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_VISA)]) {
return BTUIKPaymentOptionTypeVisa;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_MASTER_CARD)]) {
return BTUIKPaymentOptionTypeMasterCard;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_DISCOVER)]) {
return BTUIKPaymentOptionTypeDiscover;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_JCB)]) {
return BTUIKPaymentOptionTypeJCB;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_MAESTRO)]) {
return BTUIKPaymentOptionTypeMaestro;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_DINERS_CLUB)]) {
return BTUIKPaymentOptionTypeDinersClub;
} else if ([cardType.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_UNION_PAY)]) {
return BTUIKPaymentOptionTypeUnionPay;
} else {
return BTUIKPaymentOptionTypeUnknown;
}
}
+ (NSString *)nameForPaymentMethodType:(BTUIKPaymentOptionType)paymentMethodType {
switch (paymentMethodType) {
case BTUIKPaymentOptionTypeUnknown:
return @"Card";
case BTUIKPaymentOptionTypeAMEX:
return BTUIKLocalizedString(CARD_TYPE_AMERICAN_EXPRESS);
case BTUIKPaymentOptionTypeDinersClub:
return BTUIKLocalizedString(CARD_TYPE_DINERS_CLUB);
case BTUIKPaymentOptionTypeDiscover:
return BTUIKLocalizedString(CARD_TYPE_DISCOVER);
case BTUIKPaymentOptionTypeMasterCard:
return BTUIKLocalizedString(CARD_TYPE_MASTER_CARD);
case BTUIKPaymentOptionTypeVisa:
return BTUIKLocalizedString(CARD_TYPE_VISA);
case BTUIKPaymentOptionTypeJCB:
return BTUIKLocalizedString(CARD_TYPE_JCB);
case BTUIKPaymentOptionTypeLaser:
return BTUIKLocalizedString(CARD_TYPE_LASER);
case BTUIKPaymentOptionTypeMaestro:
return BTUIKLocalizedString(CARD_TYPE_MAESTRO);
case BTUIKPaymentOptionTypeUnionPay:
return BTUIKLocalizedString(CARD_TYPE_UNION_PAY);
case BTUIKPaymentOptionTypeSolo:
return BTUIKLocalizedString(CARD_TYPE_SOLO);
case BTUIKPaymentOptionTypeSwitch:
return BTUIKLocalizedString(CARD_TYPE_SWITCH);
case BTUIKPaymentOptionTypeUKMaestro:
return BTUIKLocalizedString(CARD_TYPE_MAESTRO);
case BTUIKPaymentOptionTypePayPal:
return BTUIKLocalizedString(PAYPAL_CARD_BRAND);
case BTUIKPaymentOptionTypeCoinbase:
return BTUIKLocalizedString(PAYMENT_METHOD_TYPE_COINBASE);
case BTUIKPaymentOptionTypeVenmo:
return BTUIKLocalizedString(PAYMENT_METHOD_TYPE_VENMO);
case BTUIKPaymentOptionTypeApplePay:
return BTUIKLocalizedString(PAYMENT_METHOD_TYPE_APPLE_PAY);
}
}
+ (void)vibrate {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
}
#pragma mark Icons
+ (BTUIKPaymentOptionType)paymentOptionTypeForPaymentInfoType:(NSString *)typeString {
if ([typeString isEqualToString:@"Visa"]) {
return BTUIKPaymentOptionTypeVisa;
} else if ([typeString isEqualToString:@"MasterCard"]) {
return BTUIKPaymentOptionTypeMasterCard;
} else if ([typeString isEqualToString:@"Coinbase"]) {
return BTUIKPaymentOptionTypeCoinbase;
} else if ([typeString isEqualToString:@"PayPal"]) {
return BTUIKPaymentOptionTypePayPal;
} else if ([typeString isEqualToString:@"DinersClub"]) {
return BTUIKPaymentOptionTypeDinersClub;
} else if ([typeString isEqualToString:@"JCB"]) {
return BTUIKPaymentOptionTypeJCB;
} else if ([typeString isEqualToString:@"Maestro"]) {
return BTUIKPaymentOptionTypeMaestro;
} else if ([typeString isEqualToString:@"Discover"]) {
return BTUIKPaymentOptionTypeDiscover;
} else if ([typeString isEqualToString:@"UKMaestro"]) {
return BTUIKPaymentOptionTypeUKMaestro;
} else if ([typeString isEqualToString:@"AMEX"] || [typeString isEqualToString:@"American Express"]) {
return BTUIKPaymentOptionTypeAMEX;
} else if ([typeString isEqualToString:@"Solo"]) {
return BTUIKPaymentOptionTypeSolo;
} else if ([typeString isEqualToString:@"Laser"]) {
return BTUIKPaymentOptionTypeLaser;
} else if ([typeString isEqualToString:@"Switch"]) {
return BTUIKPaymentOptionTypeSwitch;
} else if ([typeString isEqualToString:@"UnionPay"]) {
return BTUIKPaymentOptionTypeUnionPay;
} else if ([typeString isEqualToString:@"Venmo"]) {
return BTUIKPaymentOptionTypeVenmo;
} else if ([typeString isEqualToString:@"ApplePay"]) {
return BTUIKPaymentOptionTypeApplePay;
} else {
return BTUIKPaymentOptionTypeUnknown;
}
}
+ (BTUIKVectorArtView *)vectorArtViewForPaymentInfoType:(NSString *)typeString {
return [self vectorArtViewForPaymentOptionType:[self.class paymentOptionTypeForPaymentInfoType:typeString]];
}
+ (BTUIKVectorArtView *)vectorArtViewForPaymentOptionType:(BTUIKPaymentOptionType)type {
return [self vectorArtViewForPaymentOptionType:type size:BTUIKVectorArtSizeRegular];
}
+ (BTUIKVectorArtView *)vectorArtViewForPaymentOptionType:(BTUIKPaymentOptionType)type size:(BTUIKVectorArtSize)size {
switch (type) {
case BTUIKPaymentOptionTypeVisa:
return size == BTUIKVectorArtSizeRegular ? [BTUIKVisaVectorArtView new] : [BTUIKLargeVisaVectorArtView new];
case BTUIKPaymentOptionTypeMasterCard:
return size == BTUIKVectorArtSizeRegular ? [BTUIKMasterCardVectorArtView new] : [BTUIKLargeMasterCardVectorArtView new];
case BTUIKPaymentOptionTypeCoinbase:
return size == BTUIKVectorArtSizeRegular ? [BTUIKCoinbaseMonogramCardView new] : [BTUIKLargeCoinbaseMonogramCardView new];
case BTUIKPaymentOptionTypePayPal:
return size == BTUIKVectorArtSizeRegular ? [BTUIKPayPalMonogramCardView new] : [BTUIKLargePayPalMonogramCardView new];
case BTUIKPaymentOptionTypeDinersClub:
return size == BTUIKVectorArtSizeRegular ? [BTUIKDinersClubVectorArtView new] : [BTUIKLargeDinersClubVectorArtView new];
case BTUIKPaymentOptionTypeJCB:
return size == BTUIKVectorArtSizeRegular ? [BTUIKJCBVectorArtView new] : [BTUIKLargeJCBVectorArtView new];
case BTUIKPaymentOptionTypeMaestro:
return size == BTUIKVectorArtSizeRegular ? [BTUIKMaestroVectorArtView new] : [BTUIKLargeMaestroVectorArtView new];
case BTUIKPaymentOptionTypeDiscover:
return size == BTUIKVectorArtSizeRegular ? [BTUIKDiscoverVectorArtView new] : [BTUIKLargeDiscoverVectorArtView new];
case BTUIKPaymentOptionTypeUKMaestro:
return size == BTUIKVectorArtSizeRegular ? [BTUIKMaestroVectorArtView new] : [BTUIKLargeMaestroVectorArtView new];
case BTUIKPaymentOptionTypeAMEX:
return size == BTUIKVectorArtSizeRegular ? [BTUIKAmExVectorArtView new] : [BTUIKLargeAmExVectorArtView new];
case BTUIKPaymentOptionTypeVenmo:
return size == BTUIKVectorArtSizeRegular ? [BTUIKVenmoMonogramCardView new] : [BTUIKLargeVenmoMonogramCardView new];
case BTUIKPaymentOptionTypeUnionPay:
return size == BTUIKVectorArtSizeRegular ? [BTUIKUnionPayVectorArtView new] : [BTUIKLargeUnionPayVectorArtView new];
case BTUIKPaymentOptionTypeApplePay:
// No large apple pay
return [BTUIKApplePayMarkVectorArtView new];
case BTUIKPaymentOptionTypeSolo:
case BTUIKPaymentOptionTypeLaser:
case BTUIKPaymentOptionTypeSwitch:
case BTUIKPaymentOptionTypeUnknown:
return size == BTUIKVectorArtSizeRegular ? [BTUIKUnknownCardVectorArtView new] : [BTUIKLargeUnknownCardVectorArtView new];
}
}
+ (BOOL)isLanguageLayoutDirectionRightToLeft
{
return [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
}
+ (NSTextAlignment)naturalTextAlignment
{
return [self isLanguageLayoutDirectionRightToLeft] ? NSTextAlignmentRight : NSTextAlignmentLeft;
}
+ (NSTextAlignment)naturalTextAlignmentInverse
{
return [self isLanguageLayoutDirectionRightToLeft] ? NSTextAlignmentLeft : NSTextAlignmentRight;
}
@end

View File

@@ -0,0 +1,34 @@
#import "UIColor+BTUIK.h"
@implementation UIColor (BTUIK)
+ (instancetype)btuik_colorWithBytesR:(NSInteger)r G:(NSInteger)g B:(NSInteger)b A:(NSInteger)a {
return [[self class] colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a/255.0f];
}
+ (instancetype)btuik_colorWithBytesR:(NSInteger)r G:(NSInteger)g B:(NSInteger)b {
return [[self class] btuik_colorWithBytesR:r G:g B:b A:255.0f];
}
- (instancetype)btuik_adjustedBrightness:(CGFloat)adjustment {
CGFloat h, s, b, a;
if ([self getHue:&h saturation:&s brightness:&b alpha:&a]) {
CGFloat newB = MAX(0.0f, MIN(1.0f, adjustment * b));
return [[self class] colorWithHue:h saturation:s brightness:newB alpha:a];
} else {
return nil;
}
}
+ (instancetype)btuik_colorFromHex:(NSString *)hex alpha:(CGFloat)alpha {
uint value = 0;
NSScanner *scanner = [NSScanner scannerWithString:hex];
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"#"]];
[scanner scanHexInt:&value];
return [UIColor colorWithRed:((value >> 16) & 255) / 255.0f
green:((value >> 8) & 255) / 255.0f
blue:(value & 255) / 255.0f
alpha:alpha];
}
@end

26
BraintreeUIKit/Info.plist Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>4.6.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>4.6.1</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@@ -0,0 +1,114 @@
#import "BTUIKLocalizedString.h"
@implementation BTUIKLocalizedString
+ (NSBundle *)localizationBundle {
static NSString * bundleName = @"Braintree-UIKit-Localization";
NSString *localizationBundlePath = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
if (!localizationBundlePath) {
localizationBundlePath = [[NSBundle bundleForClass:[self class]] pathForResource:bundleName ofType:@"bundle"];
}
return localizationBundlePath ? [NSBundle bundleWithPath:localizationBundlePath] : [NSBundle mainBundle];
}
+ (NSString *)localizationTable {
return @"BTUI";
}
+ (NSString *)CARD_NUMBER_PLACEHOLDER {
return NSLocalizedStringWithDefaultValue(@"CARD_NUMBER_PLACEHOLDER", [self localizationTable], [self localizationBundle], @"Card Number", @"Credit card number field placeholder");
}
+ (NSString *)CVV_FIELD_PLACEHOLDER {
return NSLocalizedStringWithDefaultValue(@"CVV_FIELD_PLACEHOLDER", [self localizationTable], [self localizationBundle], @"CVV", @"CVV (credit card security code) field placeholder");
}
+ (NSString *)EXPIRY_PLACEHOLDER_FOUR_DIGIT_YEAR {
return NSLocalizedStringWithDefaultValue(@"EXPIRY_PLACEHOLDER_FOUR_DIGIT_YEAR", [self localizationTable], [self localizationBundle], @"MM/YYYY", @"Credit card expiration date field placeholder (MM/YYYY format)");
}
+ (NSString *)EXPIRY_PLACEHOLDER_TWO_DIGIT_YEAR {
return NSLocalizedStringWithDefaultValue(@"EXPIRY_PLACEHOLDER_TWO_DIGIT_YEAR", [self localizationTable], [self localizationBundle], @"MM/YY", @"Credit card expiration date field placeholder (MM/YY format)");
}
+ (NSString *)POSTAL_CODE_PLACEHOLDER {
return NSLocalizedStringWithDefaultValue(@"POSTAL_CODE_PLACEHOLDER", [self localizationTable], [self localizationBundle], @"Postal Code", @"Credit card billing postal code field placeholder");
}
+ (NSString *)TOP_LEVEL_ERROR_ALERT_VIEW_OK_BUTTON_TEXT {
return NSLocalizedStringWithDefaultValue(@"TOP_LEVEL_ERROR_ALERT_VIEW_OK_BUTTON_TEXT", [self localizationTable], [self localizationBundle], @"OK", @"OK Button on card form alert view for top level errors");
}
+ (NSString *)PHONE_NUMBER_PLACEHOLDER {
return NSLocalizedStringWithDefaultValue(@"PHONE_NUMBER_PLACEHOLDER", [self localizationTable], [self localizationBundle], @"Phone Number", @"Phone number field placeholder");
}
#pragma mark Card Brands
+ (NSString *)PAYPAL_CARD_BRAND {
return NSLocalizedStringWithDefaultValue(@"PAYPAL_CARD_BRAND", [self localizationTable], [self localizationBundle], @"PayPal", @"PayPal payment method name");
}
+ (NSString *)CARD_TYPE_AMERICAN_EXPRESS {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_AMERICAN_EXPRESS", [self localizationTable], [self localizationBundle], @"American Express", @"American Express card brand");
}
+ (NSString *)CARD_TYPE_DISCOVER {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_DISCOVER", [self localizationTable], [self localizationBundle], @"Discover", @"Discover card brand");
}
+ (NSString *)CARD_TYPE_DINERS_CLUB {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_DINERS_CLUB", [self localizationTable], [self localizationBundle], @"Diners Club", @"Diners Club card brand");
}
+ (NSString *)CARD_TYPE_MASTER_CARD {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_MASTER_CARD", [self localizationTable], [self localizationBundle], @"MasterCard", @"MasterCard card brand");
}
+ (NSString *)CARD_TYPE_VISA {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_VISA", [self localizationTable], [self localizationBundle], @"Visa", @"Visa card brand");
}
+ (NSString *)CARD_TYPE_JCB {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_JCB", [self localizationTable], [self localizationBundle], @"JCB", @"JCB card brand");
}
+ (NSString *)CARD_TYPE_MAESTRO {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_MAESTRO", [self localizationTable], [self localizationBundle], @"Maestro", @"Maestro card brand");
}
+ (NSString *)CARD_TYPE_UNION_PAY {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_UNION_PAY", [self localizationTable], [self localizationBundle], @"UnionPay", @"UnionPay card brand");
}
+ (NSString *)CARD_TYPE_SWITCH {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_SWITCH", [self localizationTable], [self localizationBundle], @"Switch", @"Switch card brand");
}
+ (NSString *)CARD_TYPE_SOLO {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_SOLO", [self localizationTable], [self localizationBundle], @"Solo", @"Solo card brand");
}
+ (NSString *)CARD_TYPE_LASER {
return NSLocalizedStringWithDefaultValue(@"CARD_TYPE_LASER", [self localizationTable], [self localizationBundle], @"Laser", @"Laser card brand");
}
+ (NSString *)PAYMENT_METHOD_TYPE_PAYPAL {
return NSLocalizedStringWithDefaultValue(@"PAYPAL", [self localizationTable], [self localizationBundle], @"PayPal", @"PayPal (as a standalone term, referring to the payment method type, analogous to Visa or Discover)");
}
+ (NSString *)PAYMENT_METHOD_TYPE_COINBASE {
return NSLocalizedStringWithDefaultValue(@"COINBASE", [self localizationTable], [self localizationBundle], @"Coinbase", @"Coinbase (as a standalone term, referring to the bitcoin wallet company)");
}
+ (NSString *)PAYMENT_METHOD_TYPE_VENMO {
return NSLocalizedStringWithDefaultValue(@"VENMO", [self localizationTable], [self localizationBundle], @"Venmo", @"Venmo (as a standalone term, referring to Venmo the company)");
}
+ (NSString *)PAYMENT_METHOD_TYPE_APPLE_PAY {
return NSLocalizedStringWithDefaultValue(@"APPLE PAY", [self localizationTable], [self localizationBundle], @"Apple Pay", @"Apple Pay (as a standalone term, referring to Apple Pay the product offered by Apple.)");
}
@end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,43 @@
#import "BTUIKCardExpirationValidator.h"
#ifdef __IPHONE_8_0
#define kBTNSGregorianCalendarIdentifier NSCalendarIdentifierGregorian
#else
#define kBTNSGregorianCalendarIdentifier NSGregorianCalendar
#endif
@implementation BTUIKCardExpirationValidator
+ (BOOL)month:(NSUInteger)month year:(NSUInteger)year validForDate:(NSDate *)date {
// Creating NSCalendar is expensive, so cache it!
static NSCalendar *gregorianCalendar;
if (!gregorianCalendar) {
gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:kBTNSGregorianCalendarIdentifier];
}
NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
dateComponents.calendar = gregorianCalendar;
dateComponents.year = ((year % 2000) + 2000) ;
dateComponents.month = month;
NSInteger newMonth = (dateComponents.month + 1);
if (newMonth > 12) {
dateComponents.month = newMonth % 12;
dateComponents.year += 1;
} else {
dateComponents.month = newMonth;
}
BOOL expired = [date compare:dateComponents.date] != NSOrderedAscending;
if (expired) {
return NO;
}
NSDate *farFuture = [date dateByAddingTimeInterval:3600 * 24 * 365.25 * kBTUIKCardExpirationValidatorFarFutureYears]; // roughly years in the future
BOOL tooFarInTheFuture = [farFuture compare:dateComponents.date] != NSOrderedDescending;
return !tooFarInTheFuture;
}
@end

View File

@@ -0,0 +1,51 @@
#import "BTUIKCardExpiryFormat.h"
#import "BTUIKUtil.h"
@implementation BTUIKCardExpiryFormat
- (void)formattedValue:(NSString *__autoreleasing *)value cursorLocation:(NSUInteger *)cursorLocation {
if (value == NULL || cursorLocation == NULL) {
return;
}
NSMutableString *s = [NSMutableString stringWithString:self.value];
*cursorLocation = self.cursorLocation;
if (s.length == 0) {
*value = s;
return;
}
if (*cursorLocation == 1 && s.length == 1 && [s characterAtIndex:0] > '1' && [s characterAtIndex:0] <= '9') {
[s insertString:@"0" atIndex:0];
*cursorLocation += 1;
}
if (self.backspace) {
if (*cursorLocation == 2 && s.length == 2) {
[s deleteCharactersInRange:NSMakeRange(1, 1)];
*cursorLocation -= 1;
}
} else {
NSUInteger slashLocation = [s rangeOfString:@"/"].location;
if (slashLocation != NSNotFound) {
if (slashLocation > 2) {
s = [NSMutableString stringWithString:[BTUIKUtil stripNonDigits:s]];
[s insertString:@"/" atIndex:2];
*cursorLocation += 1;
}
} else {
if (s.length >= 2) {
[s insertString:@"/" atIndex:2];
if (*cursorLocation >= 2) {
*cursorLocation += 1;
}
}
}
}
*value = s;
}
@end

View File

@@ -0,0 +1,236 @@
#import <UIKit/UIKit.h>
#import "BTUIKCardType.h"
#import "BTUIKUtil.h"
#define kDefaultFormatSpaceIndices @[@4, @8, @12, @16]
#define kDefaultCvvLength 3
#define kDefaultValidNumberLengths [NSIndexSet indexSetWithIndex:16]
#define kInvalidCvvCharacterSet [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]
@implementation BTUIKCardType
#pragma mark - Private initializers
- (instancetype)initWithBrand:(NSString *)brand
securityCodeName:(NSString *)securityCodeName
prefixes:(NSArray *)prefixes
{
return [self initWithBrand:brand
securityCodeName:securityCodeName
prefixes:prefixes
validNumberLengths:kDefaultValidNumberLengths
validCvvLength:kDefaultCvvLength
formatSpaces:kDefaultFormatSpaceIndices];
}
- (instancetype)initWithBrand:(NSString *)brand
securityCodeName:(NSString *)securityCodeName
prefixes:(NSArray *)prefixes
validNumberLengths:(NSIndexSet *)validLengths
validCvvLength:(NSUInteger)cvvLength
formatSpaces:(NSArray *)formatSpaces
{
self = [super init];
if (self != nil) {
_brand = brand;
NSError *error;
_validNumberPrefixes = prefixes;
if (error != nil) {
NSLog(@"Braintree-Payments-UI: %@", error);
}
_validNumberLengths = validLengths;
_validCvvLength = cvvLength;
NSArray *sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"self" ascending:YES]];
_formatSpaces = [formatSpaces sortedArrayUsingDescriptors:sortDescriptors] ?: kDefaultFormatSpaceIndices;
_maxNumberLength = [validLengths lastIndex];
_securityCodeName = securityCodeName;
}
return self;
}
#pragma mark - Finders
+ (instancetype)cardTypeForBrand:(NSString *)rename {
return [[self class] cardsByBrand][rename];
}
+ (instancetype)cardTypeForNumber:(NSString *)number {
if (number.length == 0) {
return nil;
}
for (BTUIKCardType *cardType in [[self class] allCards]) {
for (NSString *prefix in cardType.validNumberPrefixes) {
if (number.length >= prefix.length) {
NSUInteger compareLength = MIN(prefix.length, number.length);
NSString *sizedNumber = [number substringToIndex:compareLength];
if ([sizedNumber isEqualToString:prefix]) {
return cardType;
}
}
}
}
return nil;
}
// Since each card type has a list of acceptable card prefixes, we
// can determine which card types may match a given number
+ (NSArray *)possibleCardTypesForNumber:(NSString *)number {
number = [BTUIKUtil stripNonDigits:number];
if (number.length == 0) {
return [[self class] allCards];
}
NSMutableSet *possibleCardTypes = [NSMutableSet set];
for (BTUIKCardType *cardType in [[self class] allCards]) {
for (NSString *prefix in cardType.validNumberPrefixes) {
NSUInteger compareLength = MIN(prefix.length, number.length);
NSString *sizedPrefix = [prefix substringToIndex:compareLength];
NSString *sizedNumber = [number substringToIndex:compareLength];
if ([sizedNumber isEqualToString:sizedPrefix]) {
[possibleCardTypes addObject:cardType];
break;
}
}
}
return [possibleCardTypes allObjects];
}
#pragma mark - Instance methods
- (BOOL)validCvv:(NSString *)cvv {
if (cvv.length != self.validCvvLength) {
return NO;
}
return ([cvv rangeOfCharacterFromSet:kInvalidCvvCharacterSet].location == NSNotFound);
}
- (NSString *)description {
return [NSString stringWithFormat:@"BTUIKCardType %@", self.brand];
}
#pragma mark - Immutable singletons
+ (NSUInteger)maxNumberLength {
static dispatch_once_t p = 0;
static NSUInteger _maxNumberLength = 0;
dispatch_once(&p, ^{
for (BTUIKCardType *t in [self allCards]) {
_maxNumberLength = MAX(_maxNumberLength, t.maxNumberLength);
}
});
return _maxNumberLength;
}
+ (NSArray *)allCards
{
static dispatch_once_t p = 0;
static NSArray *_allCards = nil;
dispatch_once(&p, ^{
BTUIKCardType *visa = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_VISA)
securityCodeName:@"CVV"
prefixes:@[@"4"]];
BTUIKCardType *mastercard = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_MASTER_CARD)
securityCodeName:@"CVC"
prefixes:@[@"51", @"52", @"53", @"54", @"55"]];
BTUIKCardType *discover = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_DISCOVER)
securityCodeName:@"CID"
prefixes:@[@"6011", @"65", @"644", @"645", @"646", @"647", @"648", @"649"]];
BTUIKCardType *jcb = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_JCB)
securityCodeName:@"CVV"
prefixes:@[@"35"]];
BTUIKCardType *amex = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_AMERICAN_EXPRESS)
securityCodeName:@"CID"
prefixes:@[@"34", @"37"]
validNumberLengths:[NSIndexSet indexSetWithIndex:15]
validCvvLength:4
formatSpaces:@[@4, @10]];
BTUIKCardType *dinersClub = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_DINERS_CLUB)
securityCodeName:@"CVV"
prefixes:@[@"36", @"38", @"300", @"301", @"302", @"303", @"304", @"305"]
validNumberLengths:[NSIndexSet indexSetWithIndex:14]
validCvvLength:3
formatSpaces:nil];
BTUIKCardType *maestro = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_MAESTRO)
securityCodeName:@"CVC"
prefixes:@[@"5018", @"5020", @"5038", @"6304", @"6759", @"6761", @"6762", @"6763"]
validNumberLengths:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(12, 8)]
validCvvLength:3
formatSpaces:nil];
BTUIKCardType *unionPay = [[BTUIKCardType alloc] initWithBrand:BTUIKLocalizedString(CARD_TYPE_UNION_PAY)
securityCodeName:@"CVN"
prefixes:@[@"62"]
validNumberLengths:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(16, 4)]
validCvvLength:3
formatSpaces:nil];
_allCards = @[visa, mastercard, discover, amex, dinersClub, jcb, mastercard, maestro, unionPay];
});
// returns the same object each time
return _allCards;
}
+ (NSDictionary *)cardsByBrand {
static dispatch_once_t p = 0;
static NSDictionary *_cardsByBrand = nil;
dispatch_once(&p, ^{
NSMutableDictionary *d = [NSMutableDictionary dictionary];
for (BTUIKCardType *cardType in [self allCards]) {
d[cardType.brand] = cardType;
}
_cardsByBrand = d;
});
return _cardsByBrand;
}
#pragma mark - Formatting
- (NSAttributedString *)formatNumber:(NSString *)input kerning:(CGFloat)kerning{
input = [BTUIKUtil stripNonDigits:input];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:input];
if (input.length > 20) {
return result;
}
for (NSNumber *indexNumber in self.formatSpaces) {
NSUInteger index = [indexNumber unsignedIntegerValue];
if (index >= result.length) {
break;
}
[result setAttributes:@{NSKernAttributeName: @(kerning)} range:NSMakeRange(index-1, 1)];
}
return result;
}
- (NSAttributedString *)formatNumber:(NSString *)input {
return [self formatNumber:input kerning:8.0f];
}
#pragma mark - Validation
- (BOOL)validAndNecessarilyCompleteNumber:(NSString *)number {
return (number.length == self.validNumberLengths.lastIndex &&
([BTUIKUtil luhnValid:number] || [self.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_UNION_PAY)]));
}
- (BOOL)validNumber:(NSString *)number {
return ([self completeNumber:number] &&
([BTUIKUtil luhnValid:number] || [self.brand isEqualToString:BTUIKLocalizedString(CARD_TYPE_UNION_PAY)]));
}
- (BOOL)completeNumber:(NSString *)number {
return [self.validNumberLengths containsIndex:number.length];
}
@end

View File

@@ -0,0 +1,48 @@
#import "BTUIKUtil.h"
@implementation BTUIKUtil
#pragma mark - Class Method Utils
+ (BOOL)luhnValid:(NSString *)cardNumber {
// http://rosettacode.org/wiki/Luhn_test_of_credit_card_numbers#Objective-C
const char *digitChars = [cardNumber UTF8String];
BOOL isOdd = YES;
NSInteger oddSum = 0;
NSInteger evenSum = 0;
for (NSInteger i = [cardNumber length] - 1; i >= 0; i--) {
NSInteger digit = digitChars[i] - '0';
if (isOdd) {
oddSum += digit;
} else {
evenSum += digit/5 + (2*digit) % 10;
}
isOdd = !isOdd;
}
return ((oddSum + evenSum) % 10 == 0);
}
+ (NSString *)stripNonDigits:(NSString *)input {
return [self stripPattern:@"[^0-9]" input:input];
}
+ (NSString *)stripNonExpiry:(NSString *)input {
return [self stripPattern:@"[^0-9/]" input:input];
}
+ (NSString *)stripPattern:(NSString *)pattern input:(NSString *)input {
if (!input) return nil;
NSError *error;
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:&error];
return [re stringByReplacingMatchesInString:input
options:0
range:NSMakeRange(0, input.length)
withTemplate:@""];
}
@end

View File

@@ -0,0 +1,67 @@
#import <UIKit/UIKit.h>
@interface BTUIKAppearance : NSObject
/// Shared instance used by Form elements
+ (instancetype) sharedInstance;
+ (void) darkTheme;
+ (void) lightTheme;
/// Fallback color for the overlay if blur is disabled
@property (nonatomic, strong) UIColor *overlayColor;
/// Tint color, defaults to 007aff
@property (nonatomic, strong) UIColor *tintColor;
/// Bar color
@property (nonatomic, strong) UIColor *barBackgroundColor;
/// Font family
@property (nonatomic, strong) NSString *fontFamily;
/// Bold font family
@property (nonatomic, strong) NSString *boldFontFamily;
/// Sheet background color
@property (nonatomic, strong) UIColor *formBackgroundColor;
/// Form field background color
@property (nonatomic, strong) UIColor *formFieldBackgroundColor;
/// Primary text color
@property (nonatomic, strong) UIColor *primaryTextColor;
/// Secondary text color
@property (nonatomic, strong) UIColor *secondaryTextColor;
/// Color of disabled buttons
@property (nonatomic, strong) UIColor *disabledColor;
/// Placeholder text color for form fields
@property (nonatomic, strong) UIColor *placeholderTextColor;
/// Line and border color
@property (nonatomic, strong) UIColor *lineColor;
/// Error foreground color
@property (nonatomic, strong) UIColor *errorForegroundColor;
/// Blur style
@property (nonatomic) UIBlurEffectStyle blurStyle;
/// Activity indicator style
@property (nonatomic) UIActivityIndicatorViewStyle activityIndicatorViewStyle;
/// Toggle blur effects
@property (nonatomic) BOOL useBlurs;
/// The keyboard the postal code field should use
@property (nonatomic) UIKeyboardType postalCodeFormFieldKeyboardType;
/// Sets the color (primary or secondary) and font with family and size (large or small)
/// These properties are on the [BTUIKAppearance sharedInstance]
+ (void) styleLabelPrimary:(UILabel *) label;
+ (void) styleLabelBoldPrimary:(UILabel *) label;
+ (void) styleSmallLabelBoldPrimary:(UILabel *)label;
+ (void) styleSmallLabelPrimary:(UILabel *)label;
+ (void) styleLabelSecondary:(UILabel *)label;
+ (void) styleLargeLabelSecondary:(UILabel *)label;
+ (void) styleSystemLabelSecondary:(UILabel *)label;
+ (float) horizontalFormContentPadding;
+ (float) formCellHeight;
+ (float) verticalFormSpace;
+ (float) verticalFormSpaceTight;
+ (float) verticalSectionSpace;
+ (float) smallIconWidth;
+ (float) smallIconHeight;
+ (float) largeIconWidth;
+ (float) largeIconHeight;
+ (NSDictionary*)metrics;
@end

View File

@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
#define kBTUIKCardExpirationValidatorFarFutureYears 20
@interface BTUIKCardExpirationValidator : NSObject
+ (BOOL)month:(NSUInteger)month year:(NSUInteger)year validForDate:(NSDate *)date;
@end

View File

@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
@interface BTUIKCardExpiryFormat : NSObject
@property (nonatomic, copy) NSString *value;
@property (nonatomic, assign) NSUInteger cursorLocation;
@property (nonatomic, assign) BOOL backspace;
- (void)formattedValue:(NSString * __autoreleasing *)value cursorLocation:(NSUInteger *)cursorLocation;
@end

View File

@@ -0,0 +1,13 @@
#import <UIKit/UIKit.h>
#import "BTUIKPaymentOptionType.h"
/// @class A UILabel that contains images representing multiple BTUIKPaymentOptionType's
@interface BTUIKCardListLabel : UILabel
/// The array of BTUIKPaymentOptionType's to display
@property (nonatomic, copy) NSArray* availablePaymentOptions;
/// The BTUIKPaymentOptionType to emphasize by fading all other payment methods included in availablePaymentOptions
- (void)emphasizePaymentOption:(BTUIKPaymentOptionType)paymentOption;
@end

View File

@@ -0,0 +1,39 @@
#import "BTUIKFormField.h"
#import "BTUIKCardType.h"
@protocol BTUIKCardNumberFormFieldDelegate;
/// @class Form field to collect a card number.
@interface BTUIKCardNumberFormField : BTUIKFormField
/// BTUIKCardNumberFormFieldState modifies the form field
/// Default: Allows the input of a number upto 16 digits and does Luhn checks for validity while editing.
/// Validate: Displays a `Next` button accessory view rather than validating while edting. Set the cardNumberDelegate to receive button press. Card numbers of any length can be entered.
/// Loading: Displays a loading indicator accessory view
typedef NS_ENUM(NSInteger, BTUIKCardNumberFormFieldState) {
BTUIKCardNumberFormFieldStateDefault = 0,
BTUIKCardNumberFormFieldStateValidate,
BTUIKCardNumberFormFieldStateLoading,
};
/// The card type associated with the number currently being entered
@property (nonatomic, strong, readonly) BTUIKCardType *cardType;
/// The card number
@property (nonatomic, strong) NSString *number;
/// The state of the form
@property (nonatomic) BTUIKCardNumberFormFieldState state;
/// The delegate, primary used for validateButtonPressed calls
/// Not necessary unless using BTUIKCardNumberFormFieldStateValidate
@property (nonatomic, weak) id <BTUIKCardNumberFormFieldDelegate> cardNumberDelegate;
/// Whether to show the card hint accessory
- (void)showCardHintAccessory;
@end
/// @protocol This protocol is required by the delegate to receive the validateButtonPressed calls when using BTUIKCardNumberFormFieldStateValidate
@protocol BTUIKCardNumberFormFieldDelegate <NSObject>
- (void)validateButtonPressed:(BTUIKFormField *)formField;
@end

View File

@@ -0,0 +1,60 @@
#import <UIKit/UIKit.h>
#import "BTUIKLocalizedString.h"
/// Immutable card type
@interface BTUIKCardType : NSObject
/// Obtain the `BTCardType` for the given brand, or nil if none is found
+ (instancetype)cardTypeForBrand:(NSString *)brand;
/// Obtain the `BTCardType` for the given number, or nil if none is found
+ (instancetype)cardTypeForNumber:(NSString *)number;
/// Return all possible card types for a number
+ (NSArray *)possibleCardTypesForNumber:(NSString *)number;
/// Check if a number is valid
- (BOOL)validNumber:(NSString *)number;
/// Check if a number is complete
- (BOOL)completeNumber:(NSString *)number;
/// Check is a number is valid and necessarily complete
/// (i.e. it can't get any longer)
- (BOOL)validAndNecessarilyCompleteNumber:(NSString *)number;
/// Check if the CVV is valid for a `BTCardType`
- (BOOL)validCvv:(NSString *)cvv;
/// Format a number based on type
/// Does NOT validate
- (NSAttributedString *)formatNumber:(NSString *)input;
- (NSAttributedString *)formatNumber:(NSString *)input kerning:(CGFloat)kerning;
/// Max number of characters allowed for a card number
+ (NSUInteger)maxNumberLength;
/// The card's brand
@property (nonatomic, copy, readonly) NSString *brand;
/// An array of valid number prefixes
@property (nonatomic, strong, readonly) NSArray *validNumberPrefixes;
/// The valid card number lengths
@property (nonatomic, strong, readonly) NSIndexSet *validNumberLengths;
/// The valid CVV length
@property (nonatomic, assign, readonly) NSUInteger validCvvLength;
/// An array representing the spacing in the card number
/// Ex: @[@4, @8, @12, @16]
@property (nonatomic, strong, readonly) NSArray *formatSpaces;
/// Max length of the card number
@property (nonatomic, assign, readonly) NSUInteger maxNumberLength;
/// Brand-specific name for card security code
@property (nonatomic, assign, readonly) NSString *securityCodeName;
@end

View File

@@ -0,0 +1,16 @@
#import "BTUIKFormField.h"
#import "BTUIKExpiryInputView.h"
/// @class Form field to collect an expiration date.
@interface BTUIKExpiryFormField : BTUIKFormField <BTUIKExpiryInputViewDelegate>
/// The expiration month
@property (nonatomic, strong, nullable, readonly) NSString *expirationMonth;
/// The expiration year
@property (nonatomic, strong, nullable, readonly) NSString *expirationYear;
/// The expiration date in MMYYYY format.
@property (nonatomic, copy, nullable) NSString *expirationDate;
@end

View File

@@ -0,0 +1,23 @@
#import <UIKit/UIKit.h>
@protocol BTUIKExpiryInputViewDelegate;
/// @class A UIView designed to be used as an `inputView` on a text field.
/// This input view makes it possible to enter a valid expiration date with 2 taps by showing buttons for months and years.
@interface BTUIKExpiryInputView : UIView <UITextFieldDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
/// The selected year
@property (nonatomic) NSInteger selectedYear;
/// The selected month
@property (nonatomic) NSInteger selectedMonth;
/// The delegate that should receive expiryInputViewDidChange calls
@property (nonatomic, weak) id<BTUIKExpiryInputViewDelegate> delegate;
@end
/// @protocol This protocol is required by the delegate to receive the expiryInputViewDidChange calls
@protocol BTUIKExpiryInputViewDelegate <NSObject>
- (void)expiryInputViewDidChange:(BTUIKExpiryInputView *)expiryInputView;
@end

View File

@@ -0,0 +1,64 @@
#import <UIKit/UIKit.h>
#import "BTUIKTextField.h"
@protocol BTUIKFormFieldDelegate;
/// @class A UIView containing a BTUIKTextField and other elements to be displayed as a form field. This class is meant to be extended but can be used as is for other generic form fields.
@interface BTUIKFormField : UIView <UITextFieldDelegate, UIKeyInput>
/// The delegate for this form field
@property (nonatomic, weak) id<BTUIKFormFieldDelegate> delegate;
/// Whether to vibrate on invalid input
@property (nonatomic, assign) BOOL vibrateOnInvalidInput;
/// Is the form field currently valid, this does not imply it is completed
@property (nonatomic, assign, readonly) BOOL valid;
/// Is the entry completed
@property (nonatomic, assign, readonly) BOOL entryComplete;
/// Whether to display as valid
@property (nonatomic, assign) BOOL displayAsValid;
/// Should show a bottom border
@property (nonatomic, assign) BOOL bottomBorder;
/// Should show a top border
@property (nonatomic, assign) BOOL topBorder;
/// Should show an inter bottom border
@property (nonatomic, assign) BOOL interFieldBorder;
/// Whether to allow backspace
@property (nonatomic, assign, readwrite) BOOL backspace;
/// The text displayed by the field
@property (nonatomic, copy) NSString *text;
/// The text field
@property (nonatomic, strong) BTUIKTextField* textField;
/// The label
@property (nonatomic, strong) UILabel* formLabel;
/// The accessory view shown opposite the label
@property (nonatomic, strong) UIView *accessoryView;
/// Updates the appearance of the form field (e.g if it is invalid it will appear with error colors)
- (void)updateAppearance;
/// Update constraints
- (void)updateConstraints;
/// Set the accessory view visibility
/// @param hidden The desired hidden state
/// @param animated Whether to animate when updating the visibility
- (void)setAccessoryViewHidden:(BOOL)hidden animated:(BOOL)animated;
/// To be implemented by subclasses. Otherwise does nothing.
- (void)resetFormField;
@end
/// @protocol Required by the delegate
@protocol BTUIKFormFieldDelegate <NSObject>
/// Called when the content changes
- (void)formFieldDidChange:(BTUIKFormField *)formField;
@optional
/// Use to override the default behavior or returning `YES` for textFieldShouldReturn.
- (BOOL)formFieldShouldReturn:(BTUIKFormField *)formField;
/// Did begin editing
- (void)formFieldDidBeginEditing:(BTUIKFormField *)formField;
/// Did end editing
- (void)formFieldDidEndEditing:(BTUIKFormField *)formField;
@end

Some files were not shown because too many files have changed in this diff Show More