I have a local notification working fine and it delivers notifications with a content attachment using a url referencing an image in the app bundle. That works fine.
The issue I have is when I have an action that reissues the notification some time later.
Within the delegate function for Notification Center (didReceive response), I make a mutable copy of the response content and create a new tigger before creating a new request like this snippet shows.
let content = response.notification.request.content
let newContent = content.mutableCopy() as! UNMutableNotificationContent
// If I uncomment this next line, the notification gets added but without the attachment
// newContent.attachments = []
let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: snooozeInterval, repeats: false)
let request = UNNotificationRequest(identifier: response.notification.request.identifier,
content: newContent,
trigger: newTrigger)
do {
try await UNUserNotificationCenter.current().add(request)
} catch {
print(error)
}
When I do this, I get the following error:
Error Domain=UNErrorDomain Code=100 "Invalid attachment file URL" UserInfo={NSLocalizedDescription=Invalid attachment file URL}
if, before forming the request, I replace the newContent.attachments with no attachments, the request is formed OK and I get no error and the request is added. However, obviously, no attachment.
Is there a way to get a properly formed attachment so that I an add it and reissue with with the new request.
Related
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
I have an S3Service which is a singleton that manages all the S3 related uploads and downloads.
When I upload the first image it works fine but if I try to upload an Image consecutively It gives me this warning and the completion block never gets called.
A background URLSession with identifier com.amazonaws.AWSS3TransferUtility.Identifier.TransferManager already exists.
This is how I upload method looks:
if let data = image.jpegData(compressionQuality: 0.5) {
let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: S3Service.TRANSFER_MANAGER_KEY)
transferUtility.uploadUsingMultiPart(data: data, bucket: EnvironmentUtils.getBucketName(), key: filename, contentType: "image/jpg", expression: nil, completionHandler: { task,error in
if let error = error {
print(error.localizedDescription)
} else {
print("Image upload success")
}
})
}
The call to register transfer utility AWSS3TransferUtility.register(with: serviceconfig, forKey: KEY) was causing the above issue. There are two things that should be kept in mind.
The AWSS3TransferUtility should be registered only once per Application session. Then we can use AWSS3TransferUtility.S3TransferUtilityForKey to get the instance wherever needed.
If these are for different users within the app, ( e.g. sign-up) and if we want to keep AWSS3TransferUtility separate for each user, register AWSS3TransferUtility with a different key (preferably the same key for the same user) and look up using that key.
I created an macOS ShareExtension which I want to use to upload pictures.
I'm still testing this so any requests will be sent to https://beeceptor.com.
The share extension works fine and it shows up in Preview, once I run it:
I add some text and hit "Post"
But the image is then not uploaded.
This is my code that initiates the background upload:
let sc_uploadURL = "https://xyz.free.beeceptor.com/api/posts" // https://beeceptor.com/console/xyz
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = URLSessionConfiguration.background(withIdentifier: configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.CreateDaily"
let session = URLSession(configuration: sessionConfig)
// Prepare the URL Request
let request = urlRequestWithImage(image: attachedImage, text: contentText)
// Create the task, and kick it off
let task = session.dataTask(with: request! as URLRequest)
task.resume()
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
extensionContext?.completeRequest(returningItems: [AnyObject](), completionHandler: nil)
}
private func urlRequestWithImage(image: NSImage?, text: String) -> NSURLRequest? {
let url = URL(string: sc_uploadURL)!
let request: NSMutableURLRequest? = NSMutableURLRequest(url: url as URL)
request?.addValue("application/json", forHTTPHeaderField: "Content-Type")
request?.addValue("application/json", forHTTPHeaderField: "Accept")
request?.httpMethod = "POST"
let jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image: image)
}
// Create the JSON payload
let jsonData = try! JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions.prettyPrinted)
request?.httpBody = jsonData
return request
}
Please note that the sharedContainerIdentifier is present in the entitlements of the app as well as in the sharing extensions entitlements.
The ShareExtensions is in the respective App Group and has outgoing connections enabled.
Performing a background upload
Once the user has completed their entry, and clicks the Post button, then the extension should upload the content to some web service somewhere. For the purposes of this example, the URL of the endpoint is contained within a property on the view controller:
let sc_uploadURL = "http://requestb.in/oha28noh"
This is a URL for the Request Bin service, which gives you a temporary URL to allow you to test network operations. The above URL (and the one in the sample code) won’t work for you, but if you visit requestb.in then you can get hold of your own URL for testing.
As mentioned previously, it’s important that extensions put very little strain on the limited system resources. Therefore, at the point the Post button is tapped, there is no time to perform a synchronous, foreground network operation. Luckily, NSURLSession provides a simple API for creating background network operations, and that’s what you’ll need here.
The method which gets called when the user taps post is didSelectPost(), and in its simplest form it must look like this:
override func didSelectPost() {
// Perform upload
...
// Inform the host that we're done, so it un-blocks its UI.
extensionContext?.completeRequestReturningItems(nil, completionHandler: nil)
}
Setting up an NSURLSession is pretty standard:
let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.ShareAlike"
let session = NSURLSession(configuration: sessionConfig)
The important part to note of the above code segment is the line which sets the sharedContainerIdentifier on the session configuration. This specifies the name of the container that NSURLSession can use as a cache (since extensions don’t have their own writable disc access). This container needs to be set up as part of the host application (i.e. ShareAlike in this demo), and can be done through Xcode:
Go to the capabilities tab of the app’s target
Enable App Groups
Create a new app group, entitled something appropriate. It must
start with group.. In the demo the group is called group.ShareAlike
Let Xcode go through the process of creating this group for you.
Then you need to go to the extension’s target, and follow the same process. Note that you won’t need to create a new app group, but instead select the one that you created for your host application.
These app groups are registered against your developer ID, and the signing process ensures that only your apps are able to access these shared containers.
Xcode will have created an entitlements file for each of your projects, and this will contain the name of the shared container it has access to.
Now that you’ve got your session set up correctly, you need to create a URL request to perform:
// Prepare the URL Request
let request = urlRequestWithImage(attachedImage, text: contentText)
This calls a method which constructs a URL request which uses HTTP POST to send some JSON, which includes the string content, and some metadata properties about the image:
func urlRequestWithImage(image: UIImage?, text: String) -> NSURLRequest? {
let url = NSURL.URLWithString(sc_uploadURL)
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.HTTPMethod = "POST"
var jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image)
}
// Create the JSON payload
var jsonError: NSError?
let jsonData = NSJSONSerialization.dataWithJSONObject(jsonObject, options: nil, error: &jsonError)
if jsonData {
request.HTTPBody = jsonData
} else {
if let error = jsonError {
println("JSON Error: \(error.localizedDescription)")
}
}
return request
}
This method doesn’t actually create a request which uploads the image, although it could be adapted to do so. Instead, it extracts some details about the image using the following method:
func extractDetailsFromImage(image: UIImage) -> NSDictionary {
var resultDict = [String : AnyObject]()
resultDict["height"] = image.size.height
resultDict["width"] = image.size.width
resultDict["orientation"] = image.imageOrientation.toRaw()
resultDict["scale"] = image.scale
resultDict["description"] = image.description
return resultDict
}
Finally, you can ask the session to create a task associated with the request you’ve built, and then call resume() on it to kick it off in the background:
// Create the task, and kick it off
let task = session.dataTaskWithRequest(request!)
task.resume()
If you run through this process now, with your own requestb.in URL in place, then you can expect to see results like this:
An App Group identifier must start with "group." and must match everywhere it is used - in the entitlements files, in your code, and on the Apple Dev portal.
In your app and share extension entitlement definitions, you have $(TeamIdentifierPrefix).group.CreateDaily. This is not valid, since it does not begin with "group.".
In your code, you just have "group.CreateDaily". This would be fine if it matched what was in your entitlement files, though Apple recommends using reverse domain name notation to avoid conflicts.
My recommendation would be to go to the Apple Dev portal under Certificates, Identifiers & Profiles/ Identifiers/ AppGroups and define your app groups. Apple will not let you enter something that does not begin with "group.". Once that has been setup, make sure that what you have in your entitlement files and code (config.sharedContainerIdentifier) match and then everything should work.
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.
first I'd like to say that I've been searching for the answer to my question quite a bit but the only things I've found so far are answers for older versions of Swift or answers that don't specifically answer my question.
Background info:
I'm trying to develop an app that can remind you in a set interval. Now this works, given that you only set 1 reminder. However if I set the interval to be 20 seconds, launch the app, set 2 notifications and close the app only the second notification shows in 20 seconds. The first notification is being overwritten by the second one.
Question: How can I make sure that all of my notifications, requested by the user, actually get sent and that no notification overrides the previous one?
Code for the notification:
let tijd = 20 //20 is just for the test, normally there is more code to it
// Notification
let content = UNMutableNotificationContent()
content.title = "Notification title"//title
content.body = "Notification body" //body
content.badge = 1
content.sound = UNNotificationSound.default()
// Timer
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(tijd), repeats: false)
let request = UNNotificationRequest(identifier: "timerDone", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
This code is stored in a UITableView cell.
Okay I figured it out!
https://stackoverflow.com/a/41892828/7385440
This answer lead to the same problem I had. I had to make the identifier different for every notification! So my code now is:
let request = UNNotificationRequest(identifier: bezigheid, content: content, trigger: trigger)
and bezigheid is something that is different in every single cell. Tested it and now I get 2 different notifications!