Skip to content

Commit

Permalink
feat: adds support for x-www-form-urlencoded (#58)
Browse files Browse the repository at this point in the history
* feat: feat: adds support for x-www-form-urlencoded content-type

* fix: set text in http client example

* feat: adds default content-type if not present in config

* refactor: simplify example
  • Loading branch information
kevinperaza authored Jul 5, 2023
1 parent 6147678 commit 9d6dfc4
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,70 @@ public struct Config {
}
}

private func mapObjToRequestBody(contentType: String, obj: Any) -> Data? {
switch contentType {
case "application/json":
return try? JSONSerialization.data(withJSONObject: obj, options: [])
case "application/x-www-form-urlencoded":
if let formParams = convertObjectToFormUrlEncoded(obj) {
let encodedParams = encodeParamsToFormUrlEncoded(formParams)
return encodedParams.data(using: .utf8)
}
default:
fatalError("Content-Type not supported")
}
return nil
}

private func convertObjectToFormUrlEncoded(_ obj: Any?, prefix: String = "") -> [String: Any]? {
guard let obj = obj else {
return nil
}

var formParams: [String: Any] = [:]

if let dictionary = obj as? [String: Any] {
for (key, value) in dictionary {
let newPrefix = prefix.isEmpty ? key : "\(prefix)[\(key)]"
if let subFormParams = convertObjectToFormUrlEncoded(value, prefix: newPrefix) {
formParams.merge(subFormParams) { (_, new) in new }
}
}
} else if let array = obj as? [Any] {
for (index, value) in array.enumerated() {
let newPrefix = prefix.isEmpty ? "\(index)" : "\(prefix)[\(index)]"
if let subFormParams = convertObjectToFormUrlEncoded(value, prefix: newPrefix) {
formParams.merge(subFormParams) { (_, new) in new }
}
}
} else {
formParams[prefix] = obj
}

return formParams.isEmpty ? nil : formParams
}

private func encodeParamsToFormUrlEncoded(_ formParams: [String: Any]) -> String {
return formParams
.compactMap { (key, value) in
guard let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let encodedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}
return "\(encodedKey)=\(encodedValue)"
}
.joined(separator: "&")
}


struct HttpClientHelpers {
static func executeRequest(method: HttpMethod, url: String, payload: [String: Any]?, config: Config?, completion: @escaping ((_ request: URLResponse?, _ data: JSON?, _ error: Error?) -> Void)) -> Void {
guard let url = URL(string: url) else {
completion(nil, nil, HttpClientError.invalidURL)
return
}


var request = URLRequest(url: url)
request.httpMethod = method.rawValue

Expand All @@ -34,11 +91,18 @@ struct HttpClientHelpers {
return
}

let httpBody = try! JSONSerialization.data(withJSONObject: mutablePayload, options: [])
let contentType = config?.headers["Content-Type"] ?? "application/json"

let httpBody = mapObjToRequestBody(contentType: contentType, obj: mutablePayload)

request.httpBody = httpBody
}

if let config = config {
if(config.headers["Content-Type"] == nil) {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}

for header in config.headers {
request.setValue(header.value, forHTTPHeaderField: header.key)
}
Expand Down
4 changes: 4 additions & 0 deletions IntegrationTester/IntegrationTester.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
0A363AF22A1D74AE00DA5E69 /* RawProxyResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A363AF12A1D74AE00DA5E69 /* RawProxyResponseViewController.swift */; };
0AB437902A4C9FB4000099CD /* HttpClientViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4378F2A4C9FB4000099CD /* HttpClientViewController.swift */; };
428CB1062937C53000C8220E /* CardExpirationDateUITextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428CB1052937C53000C8220E /* CardExpirationDateUITextFieldTests.swift */; };
428CB10A293E398B00C8220E /* CardNumberUITextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428CB109293E398B00C8220E /* CardNumberUITextFieldTests.swift */; };
6286CEC129523C8200F7E8BD /* SplitCardElementsUITextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6286CEC029523C8200F7E8BD /* SplitCardElementsUITextFieldTests.swift */; };
Expand Down Expand Up @@ -62,6 +63,7 @@

/* Begin PBXFileReference section */
0A363AF12A1D74AE00DA5E69 /* RawProxyResponseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawProxyResponseViewController.swift; sourceTree = "<group>"; };
0AB4378F2A4C9FB4000099CD /* HttpClientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpClientViewController.swift; sourceTree = "<group>"; };
428CB1052937C53000C8220E /* CardExpirationDateUITextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardExpirationDateUITextFieldTests.swift; sourceTree = "<group>"; };
428CB109293E398B00C8220E /* CardNumberUITextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardNumberUITextFieldTests.swift; sourceTree = "<group>"; };
6286CEC029523C8200F7E8BD /* SplitCardElementsUITextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitCardElementsUITextFieldTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -163,6 +165,7 @@
921217F02A05C3E6008272E3 /* FailingDelegateViewController.swift */,
921217F22A05C6E2008272E3 /* SucceedingDelegateViewController.swift */,
0A363AF12A1D74AE00DA5E69 /* RawProxyResponseViewController.swift */,
0AB4378F2A4C9FB4000099CD /* HttpClientViewController.swift */,
);
path = IntegrationTester;
sourceTree = "<group>";
Expand Down Expand Up @@ -352,6 +355,7 @@
921217F32A05C6E2008272E3 /* SucceedingDelegateViewController.swift in Sources */,
924576CC2900519900A01E70 /* Configuration.swift in Sources */,
9245768B28FF46C100A01E70 /* SceneDelegate.swift in Sources */,
0AB437902A4C9FB4000099CD /* HttpClientViewController.swift in Sources */,
92352C8229805E63004E12C2 /* CardRevealViewController.swift in Sources */,
921217F12A05C3E6008272E3 /* FailingDelegateViewController.swift in Sources */,
);
Expand Down
79 changes: 78 additions & 1 deletion IntegrationTester/IntegrationTester/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,23 @@
<segue destination="9gn-d1-KR4" kind="show" id="KZY-NX-5nW"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HaO-DX-Fzt">
<rect key="frame" x="61" y="473" width="271" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="HTTP Client"/>
<connections>
<segue destination="TzD-Wj-3hS" kind="show" id="Zp8-nw-QvG"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="3kk-pw-Ckz"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zvg-9q-GmG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="361" y="-486"/>
<point key="canvasLocation" x="359.5419847328244" y="-486.61971830985919"/>
</scene>
<!--Split Card Elements View Controller-->
<scene sceneID="oDY-lw-nmO">
Expand Down Expand Up @@ -405,6 +414,74 @@
</objects>
<point key="canvasLocation" x="2180.9160305343512" y="407.74647887323948"/>
</scene>
<!--Http Client View Controller-->
<scene sceneID="lTM-Zn-Q2P">
<objects>
<viewController id="TzD-Wj-3hS" customClass="HttpClientViewController" customModule="IntegrationTester" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="dqR-IJ-epe">
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="sik-1Y-feY" customClass="CardVerificationCodeUITextField" customModule="BasisTheoryElements">
<rect key="frame" x="42" y="170" width="309" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="SKh-fh-1Sl" customClass="CardExpirationDateUITextField" customModule="BasisTheoryElements">
<rect key="frame" x="42" y="128" width="309" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rwQ-xP-Yvh">
<rect key="frame" x="42" y="236" width="141" height="51"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Create"/>
<connections>
<action selector="createPaymentMethod:" destination="TzD-Wj-3hS" eventType="touchUpInside" id="1uX-gd-5xO"/>
</connections>
</button>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="BG6-w5-MRM" customClass="CardNumberUITextField" customModule="BasisTheoryElements">
<rect key="frame" x="42" y="86" width="309" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1Fh-v5-0cP">
<rect key="frame" x="191" y="236" width="160" height="51"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Set Text"/>
<connections>
<action selector="printToConsoleLog:" destination="TzD-Wj-3hS" eventType="touchUpInside" id="SGU-cf-8th"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="YSS-tg-G4j">
<rect key="frame" x="111" y="45" width="240" height="33"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="ChU-7O-bej"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="QY0-dK-YRJ"/>
<connections>
<outlet property="cardBrand" destination="YSS-tg-G4j" id="UMN-R6-4th"/>
<outlet property="cardNumberTextField" destination="BG6-w5-MRM" id="JCu-ac-6mO"/>
<outlet property="cvcTextField" destination="sik-1Y-feY" id="rkQ-Lm-Q8U"/>
<outlet property="expirationDateTextField" destination="SKh-fh-1Sl" id="8mo-zJ-Edv"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Pje-zP-gZR" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2880" y="408"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
Expand Down
112 changes: 112 additions & 0 deletions IntegrationTester/IntegrationTester/HttpClientViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// HttpClientViewController.swift
// IntegrationTester
//
// Created by kevin on 28/6/23.
//

import Foundation
import UIKit
import BasisTheoryElements
import BasisTheory
import Combine

class HttpClientViewController: UIViewController {
private let lightBackgroundColor : UIColor = UIColor( red: 240/255, green: 240/255, blue: 240/255, alpha: 1.0 )
private let darkBackgroundColor : UIColor = UIColor( red: 200/255, green: 200/255, blue: 200/255, alpha: 1.0 )
private var cancellables = Set<AnyCancellable>()

@IBOutlet weak var cardNumberTextField: CardNumberUITextField!
@IBOutlet weak var expirationDateTextField: CardExpirationDateUITextField!
@IBOutlet weak var cvcTextField: CardVerificationCodeUITextField!
@IBOutlet weak var cardBrand: UITextView!

@IBAction func printToConsoleLog(_ sender: Any) {
cardNumberTextField.text = "4242424242424242"
expirationDateTextField.text = "10/26"
cvcTextField.text = "909"
}

@IBAction func createPaymentMethod(_ sender: Any) {
let body: [String: Any] = [
"type": "card",
"billing_details": [
"name": "Peter Panda"
],
"card": [
"number": self.cardNumberTextField,
"exp_month": self.expirationDateTextField.month(),
"exp_year": self.expirationDateTextField.format(dateFormat: "YY"),
"cvc": self.cvcTextField
]
]

BasisTheoryElements.post(
url: "https://api.stripe.com/v1/payment_methods",
payload: body,
config: Config.init(headers: ["Authorization" : "Bearer {{ Stripe's API Key }}", "Content-Type": "application/x-www-form-urlencoded"])) { data, error, completion in
DispatchQueue.main.async {
guard error == nil else {
print(error)
return
}
print(data)
}
}
}


override func viewDidLoad() {
super.viewDidLoad()

setStyles(textField: cardNumberTextField, placeholder: "Card Number")
setStyles(textField: expirationDateTextField, placeholder: "MM/YY")
setStyles(textField: cvcTextField, placeholder: "CVC")

let cvcOptions = CardVerificationCodeOptions(cardNumberUITextField: cardNumberTextField)
cvcTextField.setConfig(options: cvcOptions)

cardNumberTextField.subject.sink { completion in
print(completion)
} receiveValue: { message in
print("cardNumber:")
print(message)

if (!message.details.isEmpty) {
let brandDetails = message.details[0]

self.cardBrand.text = brandDetails.type + ": " + brandDetails.message
}
}.store(in: &cancellables)

expirationDateTextField.subject.sink { completion in
print(completion)
} receiveValue: { message in
print("expirationDate:")
print(message)
}.store(in: &cancellables)

cvcTextField.subject.sink { completion in
print(completion)
} receiveValue: { message in
print("CVC:")
print(message)
}.store(in: &cancellables)
}

private func setStyles(textField: UITextField, placeholder: String) {
textField.layer.cornerRadius = 15.0
textField.placeholder = placeholder
textField.backgroundColor = lightBackgroundColor
textField.addTarget(self, action: #selector(didBeginEditing(_:)), for: .editingDidBegin)
textField.addTarget(self, action: #selector(didEndEditing(_:)), for: .editingDidEnd)
}

@objc private func didBeginEditing(_ textField: UITextField) {
textField.backgroundColor = darkBackgroundColor
}

@objc private func didEndEditing(_ textField: UITextField) {
textField.backgroundColor = lightBackgroundColor
}
}
Loading

0 comments on commit 9d6dfc4

Please sign in to comment.