May I please ask how can I write the presentationWindow function in the PKPaymentAuthorizationControllerDelegate protocol of the PassKit?
I wrote as below, it worked in iOS when the presentationWindow function is optional, so I did not write it at all, but in Mac catalyst, it must included, so it does not work when I wrote this, may I please ask what is wrong? Thank you very much for your time!
func presentationWindow(for controller: PKPaymentAuthorizationController) -> UIWindow? {
let paymentWindow:UIWindow=UIWindow(frame: CGRect(x: 0,y: 0,width: 200,height: 200))
return paymentWindow
}
and startPayment function I wrote:
func startPayment(completion: #escaping PaymentCompletionHandler) {
let amount = PKPaymentSummaryItem(label: "Ammount", amount: NSDecimalNumber(string: price), type: .final)
let tax = PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(string: "1.12"), type: .final)
let total = PKPaymentSummaryItem(label: "ToTal", amount: NSDecimalNumber(string: "10.00"), type: .pending)
paymentSummaryItems = [amount, tax, total];
completionHandler = completion
// Create our payment request
let paymentRequest = PKPaymentRequest()
paymentRequest.paymentSummaryItems = paymentSummaryItems
paymentRequest.merchantIdentifier = "merchant.com.YOURDOMAIN.YOURAPPNAME"
paymentRequest.merchantCapabilities = .capability3DS
paymentRequest.countryCode = "US"
paymentRequest.currencyCode = "USD"
paymentRequest.requiredShippingContactFields = [.phoneNumber, .emailAddress]
paymentRequest.supportedNetworks = PaymentHandler.supportedNetworks
// Display our payment request
paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
paymentController?.delegate = self
//self.presentationWindow(for: paymentController!)
paymentController?.present(completion: { (presented: Bool) in
if presented {
NSLog("Presented payment controller")
} else {
NSLog("Failed to present payment controller")
self.completionHandler!(false)
}
})
}
Related
I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.
I'm currently learning Swift and following some tutorials but I'm stuck on a StoreKit issue.
The code works when I provide a single productIdentifier, but when I provide more than 1 in the Set, the entire app hangs on loading. This is in the iOS Simulator, and on a device. I've got 2 identifiers in the set, and both of these work individually, but not at the same time. My code looks the same as the original tutorial (video) so I don't know where I'm going long.
Entire Store.swift file below. Problem appears to be in the fetchProducts function, but I'm not sure. Can anyone point me in the right direction?
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allRecipes = [Recipe]() {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
for index in self.allRecipes.indices {
self.allRecipes[index].isLocked = !self.completedPurchases.contains(self.allRecipes[index].id)
}
}
}
}
private let allProductIdentifiers = Set(["com.myname.ReceipeStore.test", "com.myname.ReceipeStore.test2"])
private var completedPurchases = [String]()
private var productsRequest: SKProductsRequest?
private var fetchedProducts = [SKProduct]()
private var fetchCompletionHandler: FetchCompletionHandler?
private var purchaseCompletionHandler: PurchaseCompletionHandler?
override init() {
super.init()
startObservingPaymentQueue()
fetchProducts { products in
self.allRecipes = products.map { Recipe(product: $0) }
}
}
private func startObservingPaymentQueue() {
SKPaymentQueue.default().add(self)
}
private func fetchProducts(_ completion: #escaping FetchCompletionHandler) {
guard self.productsRequest == nil else { return }
fetchCompletionHandler = completion
productsRequest = SKProductsRequest(productIdentifiers: allProductIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
private func buy(_ product: SKProduct, competion: #escaping PurchaseCompletionHandler) {
purchaseCompletionHandler = competion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
extension Store {
func product(for identififier: String) -> SKProduct? {
return fetchedProducts.first(where: { $0.productIdentifier == identififier })
}
func purchaseProduct(_ product: SKProduct) {
buy(product) { _ in }
}
}
extension Store: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
var shouldFinishTransactions = false
switch transaction.transactionState {
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
shouldFinishTransactions = true
case .failed:
shouldFinishTransactions = true
case .deferred, .purchasing:
break
#unknown default:
break
}
if shouldFinishTransactions {
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
// loading products from the store
extension Store: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let loadedProducts = response.products
let invalidProducts = response.invalidProductIdentifiers
guard !loadedProducts.isEmpty else {
print("Could not load the products!")
if !invalidProducts.isEmpty {
print("Invalid products found: \(invalidProducts)")
}
productsRequest = nil
return
}
// cache the feteched products
fetchedProducts = loadedProducts
// notify anyone waiting on the product load (swift UI view)
DispatchQueue.main.async {
self.fetchCompletionHandler?(loadedProducts)
self.fetchCompletionHandler = nil
self.productsRequest = nil
}
}
}```
It looks like you're running all of your requests on the main DispatchQueue, this will block other main queue work until completed. You should consider handling some of these tasks with a custom concurrent queue. This bit of sample code should get the ball rolling.
func requestProducts(_ productIdentifiers: Set<ProductIdentifier>, handler: #escaping ProductRequestHandler) {
// Set request handler
productRequest?.cancel()
productRequestHandler = handler
// Request
productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest?.delegate = self
productRequest?.start()
}
func requestPrices() {
// Retry interval, 5 seconds, set this to your liking
let retryTimeOut = 5.0
var local1: String? = nil
var local2: String? = nil
let bundleIdentifier = Bundle.main.bundleIdentifier!
let queue = DispatchQueue(label: bundleIdentifier + ".IAPQueue", attributes: .concurrent)
// Request price
queue.async {
var trying = true
while(trying) {
let semaphore = DispatchSemaphore(value: 0)
requestProducts(Set(arrayLiteral: SettingsViewController.pID_1000Credits, SettingsViewController.pID_2000Credits)) { (response, error) in
local1 = response?.products[0].localizedPrice
local2 = response?.products[1].localizedPrice
semaphore.signal()
}
// We will keep checking on this thread until completed
_ = semaphore.wait(timeout: .now() + retryTimeOut)
if(local2 != nil) { trying = false }
}
// Update with main thread once request is completed
DispatchQueue.main.async {
self.price1 = local1 ?? "$0.99"
self.price2 = local2 ?? "$1.99"
}
}
}
extension SKProduct {
// Helper function, not needed for this example
public var localizedPrice: String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = self.priceLocale
return formatter.string(from: self.price)
}
Today again one combine problem I currently run in and I hope that someone of you can help. How can normal unit tests be written for ObservableObjects classes which contain #Published attributes? How can I subscribe in my test to them to get the result object which I can assert?
The injected mock for the web service works correctly, loadProducts() function set exactly the same elements from the mock in the fetchedProducts array.
But I don't know currently how to access this array in my test after it is filled by the function because it seems that I cannot work with expectations here, loadProducts() has no completion block.
The code looks like this:
class ProductsListViewModel: ObservableObject {
let getRequests: GetRequests
let urlService: ApiUrls
private let networkUtils: NetworkRequestUtils
let productsWillChange = ObservableObjectPublisher()
#Published var fetchedProducts = [ProductDTO]()
#Published var errorCodeLoadProducts: Int?
init(getRequestsHelper: GetRequests, urlServiceClass: ApiUrls = ApiUrls(), utilsNetwork: NetworkRequestUtils = NetworkRequestUtils()) {
getRequests = getRequestsHelper
urlService = urlServiceClass
networkUtils = utilsNetwork
}
// nor completion block in the function used
func loadProducts() {
let urlForRequest = urlService.loadProductsUrl()
getRequests.getJsonData(url: urlForRequest) { [weak self] (result: Result<[ProductDTO], Error>) in
self?.isLoading = false
switch result {
case .success(let productsArray):
// the products filled async here
self?.fetchedProducts = productsArray
self?.errorCodeLoadProducts = nil
case .failure(let error):
let errorCode = self?.networkUtils.errorCodeFrom(error: error)
self?.errorCodeLoadProducts = errorCode
print("error: \(error)")
}
}
}
}
The test I try to write looks like this at the moment:
import XCTest
#testable import MyProject
class ProductsListViewModelTest: XCTestCase {
var getRequestMock: GetRequests!
let requestManagerMock = RequestManagerMockLoadProducts()
var productListViewModel: ProductsListViewModel!
override func setUp() {
super.setUp()
getRequestMock = GetRequests(networkHelper: requestManagerMock)
productListViewModel = ProductsListViewModel(getRequestsHelper: getRequestMock)
}
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
}
}
The Mock looks like this:
class RequestManagerMockLoadProducts: NetworkRequestManagerProtocol {
var isSuccess = true
func makeNetworkRequest<T>(urlRequestObject: URLRequest, completion: #escaping (Result<T, Error>) -> Void) where T : Decodable {
if isSuccess {
let successResultDto = returnedProductedArray() as! T
completion(.success(successResultDto))
} else {
let errorString = "Cannot create request object here"
let error = NSError(domain: ErrorDomainDescription.networkRequestDomain.rawValue, code: ErrorDomainCode.unexpectedResponseFromAPI.rawValue, userInfo: [NSLocalizedDescriptionKey: errorString])
completion(.failure(error))
}
}
func returnedProductedArray() -> [ProductDTO] {
let product1 = ProductDTO(idFromBackend: "product-1", name: "product-1", description: "product-description", price: 3.55, photo: nil)
let product2 = ProductDTO(idFromBackend: "product-2", name: "product-2", description: "product-description-2", price: 5.55, photo: nil)
let product3 = ProductDTO(idFromBackend: "product-3", name: "product-3", description: "product-description-3", price: 8.55, photo: nil)
return [product1, product2, product3]
}
}
Maybe this article can help you
Testing your Combine Publishers
To solve your issue I will use code from my article
typealias CompetionResult = (expectation: XCTestExpectation,
cancellable: AnyCancellable)
func expectValue<T: Publisher>(of publisher: T,
timeout: TimeInterval = 2,
file: StaticString = #file,
line: UInt = #line,
equals: [(T.Output) -> Bool])
-> CompetionResult {
let exp = expectation(description: "Correct values of " + String(describing: publisher))
var mutableEquals = equals
let cancellable = publisher
.sink(receiveCompletion: { _ in },
receiveValue: { value in
if mutableEquals.first?(value) ?? false {
_ = mutableEquals.remove(at: 0)
if mutableEquals.isEmpty {
exp.fulfill()
}
}
})
return (exp, cancellable)
}
your test needs to use this function
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
/// The expectation here can be extended as needed
let exp = expectValue(of: productListViewModel .$fetchedProducts.eraseToAnyPublisher(), equals: [{ $0[0].idFromBackend == "product-1" }])
productListViewModel.loadProducts()
wait(for: [exp.expectation], timeout: 1)
}
The easy and clearest way for me is simply to test #published var after X seconds. An example bellow :
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
let expectation = XCTestExpectation()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
XCTAssertEqual(self.productListViewModel.fetchedProducts, ["Awaited values"])
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
I hope that helps !
I am using PaypalSDK(2.16.2) for iOS,I can login and pay in PayPalEnvironmentSandbox,when i switch app to PayPalEnvironmentProduction, I get following error in xcode response:
PayPal SDK: Request has failed with error: invalid_user - Incorrect username/password. Please try again。 (401) | PayPal Debug-ID: b2cedad5b6842, b2cedad5b6842 [live, PayPal iOS SDK 2.16.2] | Details: (
{
"error_description" = "Invalid user credentialenter code heres";
}
).
I'm sure my account and password are correct(Android app and web pages can log on),I am unable to understand the reason for invalid credentials?
Code
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
successView.isHidden = true
// Set up payPalConfig
payPalConfig.acceptCreditCards = false
payPalConfig.merchantName = "shopins"
payPalConfig.merchantPrivacyPolicyURL = URL(string: "https://www.paypal.com/webapps/mpp/ua/privacy-full")
payPalConfig.merchantUserAgreementURL = URL(string: "https://www.paypal.com/webapps/mpp/ua/useragreement-full")
payPalConfig.languageOrLocale = Locale.preferredLanguages[0]
payPalConfig.payPalShippingAddressOption = .payPal;
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
PayPalMobile.preconnect(withEnvironment: PayPalEnvironmentProduction)
}
#IBAction func buyClothingAction(_ sender: AnyObject) {
resultText = ""
let item1 = PayPalItem(name: "Old jeans with holes", withQuantity: 2, withPrice: NSDecimalNumber(string: "84.99"), withCurrency: "USD", withSku: "Hip-0037")
let item2 = PayPalItem(name: "Free rainbow patch", withQuantity: 1, withPrice: NSDecimalNumber(string: "0.00"), withCurrency: "USD", withSku: "Hip-00066")
let item3 = PayPalItem(name: "Long-sleeve plaid shirt (mustache not included)", withQuantity: 1, withPrice: NSDecimalNumber(string: "37.99"), withCurrency: "USD", withSku: "Hip-00291")
let items = [item1, item2, item3]
let subtotal = PayPalItem.totalPrice(forItems: items)
let shipping = NSDecimalNumber(string: "5.99")
let tax = NSDecimalNumber(string: "2.50")
let paymentDetails = PayPalPaymentDetails(subtotal: subtotal, withShipping: shipping, withTax: tax)
let total = subtotal.adding(shipping).adding(tax)
let payment = PayPalPayment(amount: total, currencyCode: "USD", shortDescription: "Hipster Clothing", intent: .sale)
payment.items = items
payment.paymentDetails = paymentDetails
if (payment.processable) {
let paymentViewController = PayPalPaymentViewController(payment: payment, configuration: payPalConfig, delegate: self)
present(paymentViewController!, animated: true, completion: nil)
}
}
func payPalPaymentDidCancel(_ paymentViewController: PayPalPaymentViewController) {
resultText = ""
successView.isHidden = true
paymentViewController.dismiss(animated: true, completion: nil)
}
func payPalPaymentViewController(_ paymentViewController: PayPalPaymentViewController, didComplete completedPayment: PayPalPayment) {
paymentViewController.dismiss(animated: true, completion: { () -> Void in
self.resultText = completedPayment.description
self.showSuccess()
})
}
I am very puzzled why PayPalEnvironmentSandbox is no problem but the PayPalEnvironmentProduction can not be used properly
Currently working on submitting my application to Apple, and I'm experiencing a strange issue where my product identifiers are not loading immediately. Sometimes if you load the store quick enough, the prices are not loaded and you can't make a purchase. Apple is reporting that they can't make a purchase on their ipv6 network, but I've also seen this issue on ipv4.
Do you see any logic issues which would be causing this issue and/or ipv6 non-friendly code? Thank you.
Code Follows:
IAPManager:
import Foundation
import StoreKit
import RealmSwift
import AVFoundation
class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver
{
static let sharedInstance = IAPManager()
private let realm = try! Realm()
// SKRequest
var request:SKProductsRequest!
// Array of SKProducts
var products:[SKProduct] = []
// Audio
var audioPlayer: AVAudioPlayer!
// Received Response From Store
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
// Check for Response
self.products = response.products
}
// Creates String Array of Product Identifiers
func getStoreProductIdentifiers() -> [String]
{
var identifiers: [String] = []
let hintsPackXS = "net.identitywithheld.HintsPackXS"
let hintsPackS = "net.identitywithheld.HintsPackS"
let hintsPackM = "net.identitywithheld.HintsPackM"
let hintsPackL = "net.identitywithheld.HintsPackL"
let hintsPackXL = "net.identitywithheld.HintsPackXL"
let removeAds = "net.identitywithheld.RemoveAds"
identifiers.append(hintsPackXS)
identifiers.append(hintsPackS)
identifiers.append(hintsPackM)
identifiers.append(hintsPackL)
identifiers.append(hintsPackXL)
identifiers.append(removeAds)
return identifiers
}
// Perform Request with Identifiers
func performProductRequestForIdentifiers(identifiers:[String]){
// Create Set Out of String Array and Type Cast as Set<String>
// Note that sets are not in any paritcular order.
let products = NSSet(array: identifiers) as! Set<String>
// Set request to call based on products identifier
self.request = SKProductsRequest(productIdentifiers: products)
// Set Delegate to self (this class)
self.request.delegate = self
// Start Request
self.request.start()
}
// Request Store Products
// Gets Identifiers and Performs Requests (Ties Together)
func requestStoreProducts(){
self.performProductRequestForIdentifiers(identifiers: self.getStoreProductIdentifiers())
}
// Checks to see if user can make purchases
func setupPurchases() -> Bool
{
// Check to See if Can Make Payments
if SKPaymentQueue.canMakePayments(){
// Sets Self As Observer
SKPaymentQueue.default().add(self)
return true
}
else
{
return false
}
}
// Requests Payment Request
func createPaymentRequestForProduct(product:SKProduct)
{
let payment = SKMutablePayment(product: product)
payment.quantity = 1
// Starts Payment Process
SKPaymentQueue.default().add(payment)
}
// Payment Observer - Is Updated Whenever There's An Update
//MARK - Transaction Observer
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// For Each Transaction in Transaction Array [SKPaymentTransaction]
for transaction in transactions {
switch transaction.transactionState{
case .purchasing:
print("purchasing")
break
case .purchased:
print("purchased")
complete(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
case .deferred:
print("deferred")
break
case .failed:
print("failed")
fail(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
case .restored:
print("restored")
restore(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
}
}
}
// Complete Purchase
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
// Check Whether Sounds Are On
let gameDataObj = realm.objects(GameData.self).filter("id == 0").first
let soundOn = gameDataObj!.sound
if(soundOn == true)
{
let hintSound = NSURL(fileURLWithPath: Bundle.main.path(forResource: "purchaseComplete", ofType: "caf")!)
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try? AVAudioSession.sharedInstance().setActive(true)
try? audioPlayer = AVAudioPlayer(contentsOf: hintSound as URL)
audioPlayer.volume = 0.5
audioPlayer!.prepareToPlay()
audioPlayer!.play()
}
// If On, Play Sound
}
// Restore Purchases
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
let gameDataObj = realm.objects(GameData.self).filter("id == 0")
var hints = gameDataObj.first!.hints
var hintsPurchase = false
if(identifier == "net.identitywithheld.HintsPackXS")
{
print("Extra Small Pack")
hints = hints + 10
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackS")
{
print("Small Pack")
hints = hints + 45
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackM")
{
print("Medium Pack")
hints = hints + 125
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackL")
{
print("Large Pack")
hints = hints + 200
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackXL")
{
print("Extra Large Pack")
hints = hints + 500
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.RemoveAds")
{
print("Remove Ads")
try! realm.write {
gameDataObj.first!.ads = false
}
}
// If Hint Purchase, Set Hints
if(hintsPurchase == true)
{
try! realm.write {
gameDataObj.first!.hints = hints
}
}
}
func getTextWidth(font: UIFont, text: String) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: 17, height: 17))
label.text = text
label.font = font
label.sizeToFit()
return label.frame.width
}
func getTextHeight(font: UIFont, text: String, width: CGFloat) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: 17))
label.text = text
label.font = font
label.numberOfLines = 0
label.sizeToFit()
return label.frame.height
}
}
App Delegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
openRealm()
// Override point for customization after application launch.
// Use Firebase library to configure APIs
FIRApp.configure()
GADMobileAds.configure(withApplicationID: adMobAppID)
// Store Call to Get Products if Can Make Purchases
if (IAPManager.sharedInstance.setupPurchases() == true)
{
IAPManager.sharedInstance.requestStoreProducts()
UserDefaults.standard.set(true, forKey: "IAPCapable")
UserDefaults.standard.synchronize()
}
else
{
UserDefaults.standard.set(false, forKey: "IAPCapable")
UserDefaults.standard.synchronize()
}
// Initialize the Chartboost library
Chartboost.start(withAppId: "identitywithheld", appSignature: "identitywithheld", delegate: nil)
return true
}
After testing this vigorously, I realized that I wasn't delaying the store load and so therefore the products were not fully loaded. This isn't an ipv6 issue.
To resolve this issue, I added an activity indicator and implemented a timer to check and see if the products were loaded before displaying the store.