how can I create a token for Apple Pay with Stripe - ios

So my ultimate goal is to be able to just make test payments with Apple Pay on Stripe, whether that be with a token or something else.
**EDIT **I have this function I use to gather card info from the stored card in the Wallet:
func applePayContext(_ context: STPApplePayContext, didCreatePaymentMethod paymentMethod: STPPaymentMethod, paymentInformation: PKPayment, completion: #escaping STPIntentClientSecretCompletionBlock) {
guard let paymentIntentClientSecret = paymentIntentClientSecret else {
return;
}
let backendUrlForToken = "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createStripeToken"
let url = URL(string: backendUrlForToken)
let pkToken = String(data: paymentInformation.token.paymentData, encoding: .utf8)
guard let name = paymentInformation.billingContact?.name else { return }
let nameFormatter = PersonNameComponentsFormatter()
nameFormatter.string(from: name)
let json: [String: Any] = [
"card": [
"address_city": paymentInformation.billingContact?.postalAddress?.city,
"address_country": paymentInformation.billingContact?.postalAddress?.country,
"address_line1": paymentInformation.billingContact?.postalAddress?.street,
"address_state": paymentInformation.billingContact?.postalAddress?.state,
"address_zip": paymentInformation.billingContact?.postalAddress?.postalCode,
"name": name
],
"muid": UIDevice.current.identifierForVendor?.uuidString,
"pk_token": pkToken,
"pk_token_instrument_name": paymentInformation.token.paymentMethod.displayName,
"pk_token_payment_network": paymentInformation.token.paymentMethod.network?.rawValue,
"pk_token_transaction_id": paymentInformation.token.transactionIdentifier
]
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: json)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let token = json["id"] as? String else {
print("Error getting Stripe token")
return
}
print("\(token)")
self.stripeToken = token
}
task.resume()
let error = NSError()
completion(paymentIntentClientSecret, error)
}
I didn't have an endpoint for the API call so I created this request for a token. This is the function I have at my API endpoint:
exports.createStripeToken = functions.https.onRequest(async (req, res) => {
var cardAddressCity = req.body.card.address_city;
var cardAddressCountry = req.body.card.address_country;
var cardAddressLine = req.body.card.address_line1;
var cardAddressProvince = req.body.card.address_state;
var cardAddressPostalCode = req.body.card.address_zip;
var cardHolderName = req.body.card.name;
var muid = req.body.muid;
var pkToken = req.body.pk_token;
var pkTokenInstrument = req.body.pk_token_instrument_name;
var pkTokenNetwork = req.body.pk_token_payment_network;
var pkTokenTransactionID = req.body.pk_token_transaction_id;
const token = await stripe.tokens.create({
card: {
"address_city": cardAddressCity,
"address_country": cardAddressCountry,
"address_line1": cardAddressLine,
"address_state": cardAddressProvince,
"address_zip": cardAddressPostalCode,
"name": cardHolderName
},
"muid": muid,
"pk_token": pkToken,
"pk_token_instrument_name": pkTokenInstrument,
"pk_token_payment_network": pkTokenNetwork,
"pk_token_transaction_id": pkTokenTransactionID
});
res.send({
id: token.id,
});
});
I'm not sure if this is the correct way, I can't find much on the internet about this at all.
I figured this was the parameters I seen in my api logs so I used the same ones and I also seen a rare post on gist.github of a guy making an API call similar to this one, but for some reason it still doesn't work at all.

Your statements/logs aren't being hit because Token creation internally in STPApplePayContext is failing. For background, STPApplePayContext does:
Create a Token from the Apple Pay encrypted card details.
Using the Token, creates a PaymentMethod object.
Then triggers the didCreatePaymentMethod() delegate implementation.
And step 1 is failing.
Typically when I've seen a Token creation request 500, that typically means there might be a mismatch on the merchant ID used on XCode vs the certificate set up on Stripe account. Or it could be some combination of a PKPaymentNetwork not being supported on a Stripe account country.
I would re-do the steps of setting up the merchant ID and Apple Pay certificate, ideally on a fresh project to do away with any confusion or conflicts etc.
Since this was a 500, Stripe Support would be the right people to deal with that, as the error possibly lies on their end. I would provide them request IDs for your /v1/tokens creation request.

Related

How to Decode Apple App Attestation Statement?

I'm trying to perform the Firebase App Check validation using REST APIs since it's the only way when developing App Clips as they dont' support sockets. I'm trying to follow Firebase docs. All I'm having trouble with is the decoding of the App Attestation Statement.
So far I've been able to extract the device keyId, make Firebase send me a challenge to be sent to Apple so they can provide me an App Attest Statement using DCAppAttestService.shared.attestKey method.
Swift:
private let dcAppAttestService = DCAppAttestService.shared
private var deviceKeyId = ""
private func generateAppAttestKey() {
// The generateKey method returns an ID associated with the key. The key itself is stored in the Secure Enclave
dcAppAttestService.generateKey(completionHandler: { [self] keyId, error in
guard let keyId = keyId else {
print("key generate failed: \(String(describing: error))")
return
}
deviceKeyId = keyId
print("Key ID: \(deviceKeyId.toBase64())")
})
}
func requestAppAttestChallenge() {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):generateAppAttestChallenge?key=\(FIREBASE_API_KEY)") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
return
}
guard let data = data, error == nil else {
return
}
let response = try? JSONDecoder().decode(AppAttestChallenge.self, from: data)
if let response = response {
let challenge = response.challenge
print("Response app check challenge: \(challenge)")
print("Response app check keyID: \(deviceKeyId)")
let hash = Data(SHA256.hash(data: Data(base64Encoded: challenge)!))
dcAppAttestService.attestKey(deviceKeyId, clientDataHash: hash, completionHandler: {attestationObj, errorAttest in
let string = String(decoding: attestationObj!, as: UTF8.self)
print("Attestation Object: \(string)")
})
}
}
task.resume()
}
I tried to send the attestation object to Firebase after converting it in a String, although I wasn't able to properly format the String. I see from Apple docs here the format of the attestation, but it isn't really a JSON so I don't know how to handle it. I was trying to send it to Firebase like this:
func exchangeAppAttestAttestation(appAttestation : String, challenge : String) {
guard let url = URL(string: "\(PROJECT_PREFIX_BETA):exchangeAppAttestAttestation?key=\(FIREBASE_API_KEY)") else {
return
}
let requestBody = ExchangeAttestChallenge(attestationStatement: appAttestation.toBase64(), challenge: challenge, keyID: deviceKeyId)
let jsonData = try! JSONEncoder().encode(requestBody)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) {data, response, error in
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("Exchange App Attestation \(String(describing: response))")
return
}
guard let data = data, error == nil else {
return
}
print("Exchange App Attestation: \(data)")
}
task.resume()
}

Challenge fetched from sever does not match challenge created by ASAuthorizationPlatformPublicKeyCredentialProvider credential creation request

I followed WWDC 2022 passkeys video and I am trying to register a passkey for my service in iOS as described in that video.
Below is the function where I obtain challenge from server and then make use of ASAuthorizationPlatformPublicKeyCredentialProvider to generate passkey.
func signUpWith(userName: String, anchor: ASPresentationAnchor) {
self.authenticationAnchor = anchor
self.userName = userName
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: self.domain)
// Fetch the challenge from the server. The challenge needs to be unique for each request.
// The userID is the identifier for the user's account.
var urlRequst = URLRequest(url: URL(string: "https://<domain>/registration")!)
urlRequst.httpMethod = "POST"
urlRequst.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
let httpBody = try JSONSerialization.data(withJSONObject: ["registration": ["username": userName, "nickname": userName]], options: [])
urlRequst.httpBody = httpBody
} catch let error {
print(error)
}
let urlSession = URLSession(configuration: .default)
var task: URLSessionDataTask?
task = urlSession.dataTask(with: urlRequst) { data, response, error in
let challengeJson = try? JSONDecoder().decode(Challenge.self, from: data!)
let challengeString = challengeJson!.challenge
let userIdString = challengeJson!.user.id
let challengeData = Data(challengeString.utf8)
let userID = Data(userIdString.utf8)
let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challengeData,
name: userName, userID: userID)
// Use only ASAuthorizationPlatformPublicKeyCredentialRegistrationRequests or
// ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequests here.
let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] )
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
self.isPerformingModalReqest = true
}
task?.resume()
}
This works and I am able to obtain challenge and initiate local biometric authentication on iPhone to generate passkey for the service for given username.
Below is console print of challenge received from server :-
{
"challenge":"fS-mfyjb3_sBjgU2X3xp99jxdFcNVq2l1Yn-097FWL8",
"timeout":120000,
"rp":{
"name":"Passkeys demo app"
},
"user":{
"name":"letsbondiway",
"id":"EU1BXzOQUYAE0_WbIM1LEdbhE2Y7tA-o8-gl6P27mAe_cV-Q3xKxFovyOV5cY_0kJm1z_mvOHft1AKE2AaW1sQ",
"displayName":"letsbondiway"
},
"pubKeyCredParams":[
{
"type":"public-key",
"alg":-7
},
{
"type":"public-key",
"alg":-37
},
{
"type":"public-key",
"alg":-257
}
]
}
However, in the delegate method when I decode the cliendDataJSON object, the challenge value is different.
Below is the delegate method handling :-
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
let logger = Logger()
switch authorization.credential {
case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
logger.log("A new passkey was registered: \(credentialRegistration)")
// Verify the attestationObject and clientDataJSON with your service.
// The attestationObject contains the user's new public key to store and use for subsequent sign-ins.
let attestationObject = credentialRegistration.rawAttestationObject
let clientDataJSON = credentialRegistration.rawClientDataJSON
let credentialId = credentialRegistration.credentialID
print(String(data: clientDataJSON, encoding: .utf8) as Any)
// After the server verifies the registration and creates the user account, sign in the user with the new account.
didFinishSignIn()
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
logger.log("A passkey was used to sign in: \(credentialAssertion)")
// Verify the below signature and clientDataJSON with your service for the given userID.
// let signature = credentialAssertion.signature
// let clientDataJSON = credentialAssertion.rawClientDataJSON
// let userID = credentialAssertion.userID
// After the server verifies the assertion, sign in the user.
didFinishSignIn()
case let passwordCredential as ASPasswordCredential:
logger.log("A password was provided: \(passwordCredential)")
// Verify the userName and password with your service.
// let userName = passwordCredential.user
// let password = passwordCredential.password
// After the server verifies the userName and password, sign in the user.
didFinishSignIn()
default:
fatalError("Received unknown authorization type.")
}
isPerformingModalReqest = false
}
The print in the delegate method outputs :-
{
"type":"webauthn.create",
"challenge":"ZlMtbWZ5amIzX3NCamdVMlgzeHA5OWp4ZEZjTlZxMmwxWW4tMDk3RldMOA",
"origin":"https://<domain>"
}
What is it that I am doing wrong here? Why are the challenge values different?
I think you're missing a base64url-decode before passing the challenge to the API. The challenge in the resulting clientDataJSON is base64url encoded itself:
ZlMtbWZ5amIzX3NCamdVMlgzeHA5OWp4ZEZjTlZxMmwxWW4tMDk3RldMOA
If we base64url-decode that, we get:
fS-mfyjb3_sBjgU2X3xp99jxdFcNVq2l1Yn-097FWL8
Which is the challenge from your server. So you should base64url-decode the challenge from the server yourself and pass a binary challenge into the API. Then the value in the clientDataJSON should match what you expect.

Display data from POST request Swift

New to swift and wanted to display some data from a post request each time a user presses a button.
Class and struct for Post request:
import Foundation
struct Response: Codable, Identifiable{
var id = UUID()
var model = String()
var choices = String()
var generatedtext: [GeneratedText]
}
struct GeneratedText: Codable{
var finish_reason = String()
var index = Int()
var logprobs = String()
var text = String()
}
class GPT3TextComepletion: ObservableObject{
#Published var response = [Response]()
func textCompletion(promptText:String, completion:#escaping ([Response]) -> () ){
let token = "redacted"
let url = URL(string: "redacted URL")!
// prepare json data
var json: [String: Any] = [
"temperature": 0.8,
"max_tokens": 64,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"stop": ["\n\n"]]
json["prompt"] = promptText
let jsonData = try? JSONSerialization.data(withJSONObject: json)
// create post request
var request = URLRequest(url: url)
request.httpMethod = "POST"
// insert json data to the request
request.httpBody = jsonData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.setValue( "Bearer \(token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
//TODO: Remove this once i get the response to display
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
}
let response = try! JSONDecoder().decode([Response].self, from: data)
DispatchQueue.main.async {
completion(response)
}
}
task.resume()
}
}
View:
Removed the unnecessary styling and other code from the below view:
#State var prompt: String = "This is some editable text..."
#State var response = [Response]()
utton(action: {GPT3TextComepletion().textCompletion(promptText: prompt, completion: {(response) in self.response = response })})
List (response){ response in
VStack{
Text(response.model)}}
However upon running the emulator and attempting the post call there seems to be an issue with how i am decoding the data and most likely how i have organized my structs, error message:
"Expected to decode Array but found a dictionary instead."
JSON:
["id": cmpl-4PCJZrxEm8YlCwFSTZlfLpnAUcMPl, "created": 1641910885, "object": text_completion, "model": davinci:2020-05-03, "choices": <__NSSingleObjectArrayI 0x6000012f1dc0>(
{
"finish_reason" = stop;
index = 0;
logprobs = "<null>";
text = "";
}
)
]
Obviously i am doing something wrong with how i am defining my struct however I have tried a number of different things with no success over the last few days.
Two main questions:
Is there an alternate way to display response of the post request without using List and making the struct identifiable?
What I am doing wrong with my structs here?
Thanks!

Cannot parse ephemeral key, how do I send the unmodified JSON

POST EDITED
I have a CheckoutViewController that I copied from the Stripe git repository and I implemented it into my app. At first I was trying to setup the STPPaymentContext in a different vc but it was giving a bad access but now when I try to set it up in the Checkout vc, I get an assertion failure saying the JSON couldn't be parsed.
This is what I have in my MyAPIClient class:
class MyAPIClient: NSObject, STPCustomerEphemeralKeyProvider {
let baseURL = "https://us-central1-attend-41f12.cloudfunctions.net/createEphemeralKeys"
let ticketVC: TicketFormViewController = TicketFormViewController()
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
ticketVC.getUsersStripeCustomerID { (customerid) in
if let id = customerid {
let url = URL(string:self.baseURL)!
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!
urlComponents.queryItems = [URLQueryItem(name: "api_version", value: apiVersion), URLQueryItem(name: "customer_id", value: id)]
var request = URLRequest(url: urlComponents.url!)
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let json = ((try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]) as [String : Any]??) else {
completion(nil, error)
return
}
completion(json, nil)
})
task.resume()
}
}
}
}
This is my Cloud Functions function that I created to try and get the ephemeral key:
exports.createEphemeralKeys = functions.https.onRequest(async (req,
res) => {
var api_version = req.body.api_version;
var customerId = req.body.customer_id;
if (!api_version) {
res.status(400).end();
return;
}
let key = await stripe.ephemeralKeys.create(
{ customer: customerId },
{ stripe_version: api_version }
);
res.json(key);
});
This is looking like it's causing the problem, I know it's not the URL because I made another API call this morning with a paymentIntent Cloud Function and it had a 200 Status Code. I copied the code from the Stripe docs and had faith that it would work but it let me down. Any suggestions.
UPDATE I also checked the Cloud Functions server logs, and the function is executing with a 400 Error Status Code, so it's definitely the Cloud Function that is the issue, isn't the json in the exports.createEphemeralKeys function unmodified already, what else do I need to add for it to work?
After days of just trying to figure it out, I finally got help from a member in the Slack chat.
I got rid of the if statement, changed stripe_version to apiVersion because it was deprecated.
exports.createEphemeralKeys = functions.https.onRequest(async (req, res) => {
var api_version = req.query.api_version;
var customerId = req.query.customer_id;
const key = await stripe.ephemeralKeys.create(
{ customer: customerId },
{ apiVersion: api_version}
);
res.json(key);
});
I also changed req.body to req.query. The JSON now prints in the console, but there's still a lot left I need to figure out.

Perform POST request in iOS Swift

I am trying perform a POST request and the request does not go through. I have looked through Perform POST request in Swift already but it does not contain what I'm looking for.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
var request = NSMutableURLRequest(URL: NSURL(string: "https://us1.lacunaexpanse.com"))
println("request url https://us1.lacunaexpanse.com")
var session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
let apikey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
println("apikey",apikey)
let username = "username"
let password = "password"
var login = Array(["username", "password", "apikey"])
let jsonDictionary = ["2.0", "jsonrpc", "1", "id", "login", "method", "login", "params"]
println("jsonDictionary",jsonDictionary)
var writeError: NSError?
let jsonData = NSJSONSerialization.dataWithJSONObject(jsonDictionary, options: NSJSONWritingOptions(), error: NSErrorPointer())
var resultAsString = NSString(data: jsonData, encoding: NSUTF8StringEncoding)
resultAsString = resultAsString.stringByAppendingString("empire")
let url = NSURL.URLWithString("string")
println("url",url)
var request2 = NSMutableURLRequest()
println("Post url =%#",url)
request2 = NSMutableURLRequest(URL:url)
request2.HTTPMethod = "POST"
var connection = NSURLConnection(request: request, delegate: self, startImmediately: false)
return true
There are a whole bunch of tactical issues here:
You're creating URLSession, but then issuing NSURLConnection request. Just use URLSession.
Your “request dictionary” isn't a dictionary, but rather an array. For example, to issue the JSON-RPC request, the proper format of the dictionary is:
let requestDictionary: [String: Any] = [
"jsonrpc" : "2.0",
"id" : 1,
"method" : "login",
"params" : ["myuserid", "mypassword", "mykey"]
]
Minor issue, but you are using a lot of variables (via var) where a constant (via let) would be fine. In the spirit of Swift’s safety, use let wherever possible.
According to the Lacuna Expanse API, your URL should be including the module name.
So, for example if doing POST requests in the "Empire" module, the URL is:
let url = URL(string: "https://us1.lacunaexpanse.com/empire")!
You're likely to be doing a lot of requests, so I'd suggest putting the bulk of that in a single function that you can call again and again, without repeating code all over the place. Perhaps a function like the following that takes the following parameters:
module (e.g. “empire” vs “alliance”);
method (e.g. “login” vs “fetch_captcha”);
the parameters appropriate for that request (e.g. for “login”, that would be the “name”, “password”, and the “api_key”); and
closure that will be called when the asynchronous request finishes.
This function then prepares the JSON-RPC request and calls the closure when the request finishes:
#discardableResult
func submitLacunaRequest(module: String, method: String, parameters: Any, completion: #escaping (Result<[String: Any], Error>) -> Void) -> URLSessionTask? {
let session = URLSession.shared
let url = URL(string: "https://us1.lacunaexpanse.com")!
.appendingPathComponent(module)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json-rpc", forHTTPHeaderField: "Content-Type")
let requestDictionary: [String: Any] = [
"jsonrpc": "2.0",
"id" : 1,
"method" : method,
"params" : parameters
]
request.httpBody = try! JSONSerialization.data(withJSONObject: requestDictionary)
let task = session.dataTask(with: request) { data, response, error in
// handle fundamental network errors (e.g. no connectivity)
guard error == nil, let data = data else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
// check that http status code was 200
guard
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(URLError(.badServerResponse)))
return
}
// parse the JSON response
do {
guard let responseObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.badServerResponse)
}
completion(.success(responseObject))
} catch let parseError {
completion(.failure(parseError))
}
}
task.resume()
return task
}
This does all of the necessary wrapping of the method and parameters within a JSON-RPC request. Then, all you need to do to call that method is something like so:
submitLacunaRequest(module: "empire", method: "login", parameters: ["myuserid", "mypassword", "mykey"]) { result in
switch result {
case .failure(let error):
print("error = \(error)")
case .success(let value):
if let errorDictionary = value["error"] as? [String: Any] {
print("error logging in (bad userid/password?): \(errorDictionary)")
} else if let resultDictionary = value["result"] as? [String: Any] {
print("successfully logged in, refer to resultDictionary for details: \(resultDictionary)")
} else {
print("we should never get here")
print("responseObject = \(value)")
}
}
}
For a request that requires a dictionary, such as "create", just go ahead and supply the dictionary:
submitLacunaRequest(module:"empire", method: "create", parameters: [
"name" : "user",
"password" : "password",
"password1" : "password",
"captcha_guid" : "305...dd-....-....-....-e3706...73c0",
"captcha_solution" : "42",
"email" : "test#gmail.com"
]) { result in
switch result {
case .failure(let error):
print("error = \(error)")
case .success(let value):
print("responseObject = \(responseObject)")
}
}
Clearly, in these above, I am just doing minimal error handling, so you could beef this up, but your question was about issuing POST request, and hopefully the above illustrates how that is done.
For Swift 2 version, see previous revision of this answer.
As #jcaron points out, this post is full of bad edits. There's a lot of variables that have different names later in the function and so on. Not to mention you should NEVER post your api key in a SO question or anywhere on the internet for that matter.
To answer your question on to do a post request in Swift, unless you need incredibly granular control over the process, take a look at Alamofire (same guy that wrote AFNetworking).
A POST request is as simple as Alamofire.request(.POST, "http://someapiurl.com") You can also pass in a dictionary of body parameters if you so choose.

Resources