Cannot receive results from Square Point of Sale in a small Swift app - swift

I have written a small app in Swift using Xcode 12.5 by following the information and code samples provided here ... https://github.com/square/SquarePointOfSaleSDK-iOS
The app polls a server to see if there is a charge to be made. The output from the server is in JSON format. When a charge comes in, the JSON results are providing a customer id, amount to be charged, and a note to the Square Point of Sale SDK.
Using the SCCAPIRequest example from the GitHub page ...
// Replace with your app's URL scheme.
let callbackURL = URL(string: "<#T##Your URL Scheme##String#>://")!
// Your client ID is the same as your Square Application ID.
// Note: You only need to set your client ID once, before creating your first request.
SCCAPIRequest.setApplicationID(<#T##Application ID##String#>)
do {
// Specify the amount of money to charge.
let money = try SCCMoney(amountCents: 100, currencyCode: "USD")
// Create the request.
let apiRequest =
try SCCAPIRequest(
callbackURL: callbackURL,
amount: money,
userInfoString: nil,
locationID: nil,
notes: "Coffee",
customerID: nil,
supportedTenderTypes: .all,
clearsDefaultFees: false,
returnsAutomaticallyAfterPayment: false,
disablesKeyedInCardEntry: false,
skipsReceipt: false
)
// Open Point of Sale to complete the payment.
try SCCAPIConnection.perform(apiRequest)
} catch let error as NSError {
print(error.localizedDescription)
}
The app successfully switches to Square POS, displays the amount due, and knows which customer I am wanting to charge (via customer id). I can process the payment and Square POS switches back to my app just fine.
This is where I am running in to trouble. I am also using the UIApplication delegate method example on that same page. Under the comment "Handle a successful request" ...
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
guard SCCAPIResponse.isSquareResponse(url) else {
return
}
do {
let response = try SCCAPIResponse(responseURL: url)
if let error = response.error {
// Handle a failed request.
print(error.localizedDescription)
} else {
// Handle a successful request.
}
} catch let error as NSError {
// Handle unexpected errors.
print(error.localizedDescription)
}
return true
}
I have added the following ...
print("Transaction successful: \(response)")
From what I understand, the response should include the transaction id, and anything that was passed along in the userInfoString. It appears that this code example isn't even firing when Square POS returns to my app. I cannot see anything in the Xcode console.
I have assigned a callback URL within Xcode using the documentation on the link above, and it's also added in the Square Developer Portal under the Point of Sale API.
What am I missing? Where should the UIApplication delegate method be placed, in AppDelegate.swift or should it reside in ViewController.swift, or somewhere else? Any insight would be greatly appreciated.

#ElTomato provided me with the hint that I needed to solve the problem I was having. I needed to delete SceneDelegate.swift, remove Application Scene Manifest from Info.plist, and remove some code from AppDelegate.swift
I found detailed instructions on THIS site ...
iOS 13: Swift - 'Set application root view controller programmatically' does not work
Thank you kindly for the fantastic help #ElTomato

Related

Why is my Plaid Link Integration not opening the Link UI

I am attempting to integrate with Plaid, their documentation is a link confusing to follow and their support team informed me they are working on it. I also reached out to them to see if they could help me with my current integration but they informed me they are not able to look at my code because every integration is different.
Per the Plaid documentation I added my ngrok url to the redirect uri section in the dashboard (This works with the Plaid Link demo app: https://github.com/plaid/plaid-link-ios).
Also I added allow arbitrary load to the info plist.
I was able to get their Plaid Link-demo app up and running but when I try to place this code in my project I get the following error:
Thread 12: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
// MARK: Start Plaid Link using a Link token
// For details please see https://plaid.com/docs/#create-link-token
func presentPlaidLinkUsingLinkToken() {
#warning("Replace <#GENERATED_LINK_TOKEN#> below with your link_token")
// In your production application replace the hardcoded linkToken below with code that fetches an link_token
// from your backend server which in turn retrieves it securely from Plaid, for details please refer to
// https://plaid.com/docs/#create-link-token
let linkToken = "TOKEN HERE"
// <!-- SMARTDOWN_PRESENT_LINKTOKEN -->
// With custom configuration using a link_token
var linkConfiguration = LinkTokenConfiguration(token: linkToken) { success in
print("public-token: \(success.publicToken) metadata: \(success.metadata)")
}
linkConfiguration.onExit = { exit in
if let error = exit.error {
print("exit with \(error)\n\(exit.metadata)")
} else {
print("exit with \(exit.metadata)")
}
}
let result = Plaid.create(linkConfiguration)
switch result {
case .failure(let error):
print("Unable to create Plaid handler due to: \(error)")
case .success(let handler):
// UI Update code here
handler.open(presentUsing: .viewController(self))
self.linkHandler = handler
}
// <!-- SMARTDOWN_PRESENT_LINKTOKEN -->
}
#IBAction func refreshButtonAction(_ sender: Any) {
// UI Update code here
self.presentPlaidLinkUsingLinkToken()
}
I tried to place the self.presentPlaidLinkUsingLinkToken() into the following snippet:
DispatchQueue.main.async {
self.presentPlaidLinkUsingLinkToken()
}
Also per the Plaid documentation I added the following method within my App delegate swift file:
// MARK: Continue Plaid Link for iOS to complete an OAuth authentication flow
// <!-- SMARTDOWN_OAUTH_SUPPORT -->
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let webpageURL = userActivity.webpageURL else {
return false
}
// Check that the userActivity.webpageURL is the oauthRedirectUri
// configured in the Plaid dashboard.
guard let linkOAuthHandler = window?.rootViewController as? LinkOAuthHandling,
let handler = linkOAuthHandler.linkHandler,
webpageURL.host == linkOAuthHandler.oauthRedirectUri?.host &&
webpageURL.path == linkOAuthHandler.oauthRedirectUri?.path
else {
return false
}
// Continue the Link flow
if let error = handler.continueFrom(redirectUri: webpageURL) {
print("Unable to continue from redirect due to: \(error)")
}
return true
}
// <!-- SMARTDOWN_OAUTH_SUPPORT -->
But the same error occurs. Any suggestions will be appreciated.
In case anyone is having issues with this, I was able to figure it out. It is something incorrect in their cocoapod. Once, I downloaded and dragged the LinkKit Framework everything worked as expected. Also Plaid does not use their own cocoapod in their demo project, they do the manual install. Maybe they are working on this issue.
It's hard to tell without seeing the code in your application calling presentPlaidUsingLinkToken(), but your application is crashing on Thread 12. UIKit is not multi-threaded, so if you are presenting a view controller on a background thread you can expect crashes.
I see you tried a DispatchQueue.main.async, however I suspect you may have another threading issue going on.
Just putting this out there since this is one of few results for this issue: My issue was that I wasn't retaining the Handler object. As a result, the Plaid UI would never appear and no events would fire, leaving me with an endless spinner. Simply maintaining the reference in the class did the trick (as shown in the sample project and mentioned in passing in the documentation).
Kona Farry's solution above works. From Plaid docs:
Create a Handler - A Handler is a one-time use object used to open a Link session. The Handler must be retained for the duration of the Plaid SDK flow.

Kentico Cloud Swift SDK not returning items

I'm testing out the Kentico Cloud Swift SDK to return some 'article' content types (I have created two of them and they are published).
I am using the Boilerplate code as described here:
The result I get is : [Kentico Cloud] Getting items action has succeeded. Received nil items.
My code:
let client = DeliveryClient.init(projectId: <project id>, previewApiKey: <preview key>, secureApiKey: <secure key>, enableDebugLogging: true)
func getArticles(){
// Note: Using "items" as custom query returns all content items,
// but to map them to a single model, a filter is needed.
let customQuery = "items?system.type=article"
// More about strongly-typed models https://github.com/Kentico/cloud-sdk-swift#using-strongly-typed-models
client.getItems(modelType: Article.self, customQuery: customQuery) { (isSuccess, itemsResponse, error) in
if isSuccess {
// We get here and itemsResponse != nil but items == nil
if let articles = itemsResponse?.items {
for article in articles {
}
}
} else {
if let error = error {
print(error)
}
}
}
}
I believe this error message would appear before ObjectMapper is triggered to convert the JSON into Article objects. I could be wrong though.
Anyone have any ideas?
UPDATE
Interestingly, if I request a single article object like so ...
client.getItem(modelType: Article.self, itemName: <codename>) { (isSuccess, itemResponse, error) in
if isSuccess {
if let article = itemResponse?.item {
// Use your item here
}
} else {
if let error = error {
print(error)
}
}
}
... then it works. I get the Article object. It's just asking for all of the articles that fails.
I'm going to investigate the issue later today, however, from your description, it might be caused by the Delivery API item readiness delay - the project was not fully synced with the delivery API yet. After the publishing/unpublishing item or creating/generating the project, there might be a small delay in processing messages by Delivery API which could cause unavailability of item. This delay might be variable - from my experience, it may vary from a couple of seconds to 2-3 minutes. Nevertheless, I'm going to check it just to be sure. I'll keep you updated.
Edit: I'm pretty sure the project was not synced and processed on the Delivery API at the time you were requested the items. The API returned 200, which caused isSuccess in the callback to be true, however, there might have been none or just a subset of items available - I've reproduced this behavior (screenshot below), although it's by design (the content/messages in Event Hub must be processed asynchronously).
I've also suggested the improvement for Kentico Cloud's documentation to mention/explain the possible delay caused by processing events queue messages from Event Hubs.
Just to be sure - could you try it again with your getArticles custom query?
Edit2: Back to your question about the ObjectMapper. This is not an error just a debug message, however, there shouldn't be probably nil but 0 (zero) in the debug message. This message came from:
private func sendGetItemsRequest<T>(url: String, completionHandler: #escaping (Bool, ItemsResponse<T>?, Error?) -> ()) where T: Mappable {
sessionManager.request(url, headers: self.headers).responseObject { (response: DataResponse<ItemsResponse<T>>) in
switch response.result {
case .success:
if let value = response.result.value {
let deliveryItems = value
if self.isDebugLoggingEnabled {
print("[Kentico Cloud] Getting items action has succeeded. Received \(String(describing: deliveryItems.items?.count)) items.")
}
completionHandler(true, deliveryItems, nil)
}
case .failure(let error):
if self.isDebugLoggingEnabled {
print("[Kentico Cloud] Getting items action has failed. Check requested URL: \(url)")
}
completionHandler(false, nil, error)
}
}
}
Ok. This is very weird. After checking the API by requesting an individual item (see the update in the post above), and getting a result (woot). It now seems the original code (unchanged) now works.
I'm wondering if it takes a while for the data to propagate and be available in the API?
Who knows. Weird.

Receipt validation iOS - The file could not be opened because there is no such file

I'm working on a subscription IAP. I set all purchases up, their details come back well, I can do the purchases in sandbox and get all the messages alright. The problem I have now is checking the receipt. I always get a URL returned alright, but when I try to read it I keep getting error that the file does not exist. So I try and refresh with SKReceiptRefreshRequest. Try again, still same.
I have uninstalled app on simulator and two real devices, try again from new install and same problem. One thing I realised, one of the real devices displays the password prompt request with [Sandbox] mention. However after two prompts (including accepting password), instead of purchase completed I get a "user/password don't match" message. On simulator when prompted for itunes account and password it all goes through but the actual purchase confirmation never comes (I waited 4 minutes, stable internet connection).
This is the validation process (I have changed it quite a few times, from different tutorials and other people's problems)
let receiptURL = Bundle.main.appStoreReceiptURL
func receiptValidation() {
print("1")
print("2", receiptURL)
do {
print("3")
let receiptData = try Data(contentsOf: receiptURL!, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
let dict = ["receipt-data" : receiptString, "password" : "\(password)"] as [String : Any]
do {
print("4")
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
if let sandboxURL = Foundation.URL(string:"https://sandbox.itunes.apple.com/verifyReceipt") {
print("5")
var request = URLRequest(url: sandboxURL)
request.httpMethod = "POST"
request.httpBody = jsonData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request) { data, response, error in
print("6")
if let receivedData = data,
let httpResponse = response as? HTTPURLResponse,
error == nil,
httpResponse.statusCode == 200 {
print("7")
do {
print("8")
if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject> {
print(jsonResponse, jsonResponse.count)
// parse and verify the required informatin in the jsonResponse
} else { print("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
}
catch { print("Couldn't serialize JSON with error: " + error.localizedDescription) }
}
}
print("51")
task.resume()
} else { print("Couldn't convert string into URL. Check for special characters.") }
}
catch { print("Couldn't create JSON with error: " + error.localizedDescription) }
}
catch {
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
func requestDidFinish(_ request: SKRequest) {
print("???")
do {
let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
print(receipt)
} catch {
print("WTF NO RECEIPT")
// still no receipt, possible but unlikely to occur since this is the "success" delegate method
}
}
And this is the Debugging output from running the app. receiptURL varies between simulator/real device, but other then that everything remains the same.
1
2 Optional(file:///Users/apple/Library/Developer/CoreSimulator/Devices/47EA3293-9B13-4808-BD0B-13D884D14BFE/data/Containers/Data/Application/2F1B7E4E-C523-4270-BF46-6D77F7A2220C/StoreKit/receipt)
3
Couldn't read receipt data with error: The file “receipt” couldn’t be opened because there is no such file.
???
WTF NO RECEIPT
???
WTF NO RECEIPT
Why can't I get the receipt created, or found? Is it a device problem, a bug or am I oblivious to something?
The whole IAP process works asynchronously due to which you will not receive the receipt data unless the whole process has been completed successfully. I can't see the whole code based on what you have pasted in your question above but if you are trying to access the receipt data immediately on the action of a button or something similar, you will not get it.
The correct way to access receipt data is to try accessing the receipt based on success completion handler callback of your IAP request. Once you submit the IAP request there is a server side process which takes care of processing the IAP and then a callback handler from IAP SKPaymentTransactionObserver class is triggered. Using the notification handler from this class you can send the update to your ViewController to check for receipt data.
While further researching, I have found the following article, which solved the problem
article
Important: If you mistakenly use a sandbox tester account to log in to a production environment on your test device instead of your test environment, the sandbox account becomes invalid and can’t be used again. If this happens, create a new sandbox tester account with a new email address.

"An error occurred with the `activeEnergyQuery`. The error was: Authorization not determined."

I'm using Apple's demo HealthKit app called ActivityRings. I have set up the bundle identifiers and entitlements correctly. The iOS app and Watch Extension are working and it's recording data seemingly ok. It should be ok as I haven't touched any code.
However console log says, "An error occurred with the activeEnergyQuery. The error was: Authorization not determined."
As you can see in the reporting query and handler assignment Apple has written to print for this error.
I'd like to know what this is for. Is there broken functionality?
// Create a query to report new Active Energy Burned samples to our app.
let activeEnergyQuery = HKAnchoredObjectQuery(type: activeEnergyType, predicate: predicate, anchor: nil, limit: Int(HKObjectQueryNoLimit)) { query, samples, deletedObjects, anchor, error in
if let error = error {
print("An error occurred with the `activeEnergyQuery`. The error was: \(error.localizedDescription)")
return
}
// NOTE: `deletedObjects` are not considered in the handler as there is no way to delete samples from the watch during a workout.
guard let activeEnergySamples = samples as? [HKQuantitySample] else { return }
sampleHandler(activeEnergySamples)
}
// Assign the same handler to process future samples generated while the query is still active.
activeEnergyQuery.updateHandler = { query, samples, deletedObjects, anchor, error in
if let error = error {
print("An error occurred with the `activeEnergyQuery`. The error was: \(error.localizedDescription)")
return
}
// NOTE: `deletedObjects` are not considered in the handler as there is no way to delete samples from the watch during a workout.
guard let activeEnergySamples = samples as? [HKQuantitySample] else { return }
sampleHandler(activeEnergySamples)
}
currentQuery = activeEnergyQuery
healthStore.executeQuery(activeEnergyQuery)
}
func endWorkoutOnDate(endDate: NSDate) {
workoutEndDate = endDate
workoutButton.setTitle("Begin Workout")
activeEnergyBurnedLabel.setText("0.0")
if let query = currentQuery {
healthStore.stopQuery(query)
}
saveWorkout()
}
requestAuthorizationToShareTypes function
override func willActivate() {
// This method is called when watch view controller is about to be visible to user.
super.willActivate()
// Only proceed if health data is available.
guard HKHealthStore.isHealthDataAvailable() else { return }
// We need to be able to write workouts, so they display as a standalone workout in the Activity app on iPhone.
// We also need to be able to write Active Energy Burned to write samples to HealthKit to later associating with our app.
let typesToShare = Set([
HKObjectType.workoutType(),
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)!])
let typesToRead = Set([
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned)!])
healthStore.requestAuthorizationToShareTypes(typesToShare, readTypes: typesToRead) { success, error in
if let error = error where !success {
print("You didn't allow HealthKit to access these read/write data types. In your app, try to handle this error gracefully when a user decides not to provide access. The error was: \(error.localizedDescription). If you're using a simulator, try it on a device.")
}
}
}
AppDelegate.swift
import UIKit
import HealthKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let healthStore: HKHealthStore = HKHealthStore()
func applicationShouldRequestHealthAuthorization(application: UIApplication) {
healthStore.handleAuthorizationForExtensionWithCompletion { success, error in
if let error = error where !success {
print("You didn't allow HealthKit to access these read/write data types. In your app, try to handle this error gracefully when a user decides not to provide access. The error was: \(error.localizedDescription). If you're using a simulator, try it on a device.")
}
}
}
}
Have you setup your iOS app to handle the healthkit authorization from your watch app? When you request permission to use healthkit types from your Apple Watch, a permission dialog shows up on your iOS app. But, you need to tell your iOS app that you are expecting your apple watch to request it. You do this with the following code in your AppDelegate file:
func applicationShouldRequestHealthAuthorization(application: UIApplication) {
let healthStore = HKHealthStore()
healthStore.handleAuthorizationForExtensionWithCompletion { (success, error) -> Void in
//...
}
}
Note that data can get sent directly from the watch's sensors (like heart rate and calories burned) to healthkit without needing permission from your app. It sounds like your permission errors are because you are trying to read the data (which you don't have permission to do yet).
Your app needs to request authorization to read and write active energy samples. Until the user has chosen whether to authorize your app, authorization will be "not determined". See the HKHealthStore documentation for more information about requesting authorization with requestAuthorizationToShareTypes:readTypes:completion:.

Any way to get the name of iPhone user?

Outside of asking the user to input their name, is there any way to get it off the device?
I tried this library, which attempts to extract the name from [UIDevice currentDevice] name], but that doesn't work in a lot of situations:
https://github.com/tiboll/TLLNameFromDevice
Is the user's name present in the phonebook or anywhere else that we have access to in iOS 6?
Well you could go through all the contacts in the AddressBook and see if any of them are marked with the owner flag.
Just be aware that doing this will popup the "this app wants access to the address book" message. Also Apple isn't very keen on these kind of things. In the app review guide it is specified that an app can not use personal information without the user's permission.
You could use Square's solution:
Get the device's name (e.g. "John Smith's iPhone").
Go through the contacts on the phone and look for a contact named "John Smith".
JBDeviceOwner and ABGetMe will both do this for you.
You could use CloudKit. Following a snippet in Swift (ignoring errors):
let container = CKContainer.defaultContainer()
container.fetchUserRecordIDWithCompletionHandler(
{
(recordID, error) in
container.requestApplicationPermission(
.PermissionUserDiscoverability,
{
(status, error2) in
if (status == CKApplicationPermissionStatus.Granted)
{
container.discoverUserInfoWithUserRecordID(
recordID,
completionHandler:
{
(info, error3) in
println("\(info.firstName) \(info.lastName)")
}
)
}
}
)
}
)
The above code was based on the code at http://www.snip2code.com/Snippet/109633/CloudKit-User-Info
to save folks time. in swift4:
let container = CKContainer.default()
container.fetchUserRecordID(
completionHandler: {
(recordID, error) in
guard let recordID = recordID else {
return
}
container.requestApplicationPermission(
.userDiscoverability,
completionHandler: {
(status, error2) in
if (status == CKContainer_Application_PermissionStatus.granted)
{
if #available(iOS 10.0, *) {
container.discoverUserIdentity(withUserRecordID:
recordID,
completionHandler:
{
(info, error3) in
guard let info = info else {
return
}
print("\(info.firstName) \(info.lastName)")
}
)
}
}
}
)
}
)
however: CKUserIdentity no longer exposes either first or last name
So this answer no longer works.
You can use:
NSLog(#"user == %#",[[[NSHost currentHost] names] objectAtIndex:0]);
I did receive compiler warnings that the methods +currentHost and -names were not found. Given the warning, I’m not sure of Apple’s intention to make this available (or not) as a publicly accessible API, however, everything seemed to work as expected without the need to include any additional header files or linking in additional libraries/frameworks.
Edit 1:
You may also take a look at this Link
Edit 2:
If you have integrated your app with Facebook you can easily retrieve the user info, see Facebook Fetch User Data
For SWIFT you can use
NSUserName() returns the logon name of the current user.
func NSUserName() -> String