I have Apple Pay implemented in my SwiftUI app and I'd like to call a function after payment status returns a success. However, when I try to call the function, I run into error "Type () cannot conform to View". Relevant code:
PaymentView.swift (SwiftUI view where Apple Pay button is displayed):
#StateObject var applePayModel = ApplePayModel()
//....code....//
var body: some View {
PaymentButton(){
// Apple Pay Model
applePayModel.pay(clientSecret: PaymentConfig.shared.paymentIntentClientSecret)
}
if let paymentStatus = applePayModel.paymentStatus {
switch paymentStatus {
case .success:
postPaymentSetup() // trying to call this function!!
Text("Payment complete!")
case .error:
Text("Payment failed")
case .userCancellation:
Text("Payment canceled")
#unknown default:
Text("Unknown status")
}
}
}
ApplePayModel.swift
import Foundation
import Stripe
import PassKit
class ApplePayModel : NSObject, ObservableObject, STPApplePayContextDelegate {
#Published var paymentStatus: STPPaymentStatus?
#Published var lastPaymentError: Error?
var clientSecret: String?
func pay(clientSecret: String?) {
self.clientSecret = clientSecret
// Configure a payment request
let pr = StripeAPI.paymentRequest(withMerchantIdentifier: "merchant.com.app", country: "US", currency: "USD")
// You'd generally want to configure at least `.postalAddress` here.
// We don't require anything here, as we don't want to enter an address
// in CI.
pr.requiredShippingContactFields = []
pr.requiredBillingContactFields = []
// Configure shipping methods
pr.shippingMethods = []
// Build payment summary items
// (You'll generally want to configure these based on the selected address and shipping method.
pr.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Widget)", amount: NSDecimalNumber(string: "0.00")),
PKPaymentSummaryItem(label: "Merchant", amount: NSDecimalNumber(string: "0.00")),
]
// Present the Apple Pay Context:
let applePayContext = STPApplePayContext(paymentRequest: pr, delegate: self)
applePayContext?.presentApplePay()
}
func applePayContext(_ context: STPApplePayContext, didCreatePaymentMethod paymentMethod: STPPaymentMethod, paymentInformation: PKPayment, completion: #escaping STPIntentClientSecretCompletionBlock) {
// Confirm the PaymentIntent
if (self.clientSecret != nil) {
// Call the completion block with the PaymentIntent's client secret.
completion(clientSecret, nil)
} else {
completion(nil, NSError())
}
}
func applePayContext(_ context: STPApplePayContext, didCompleteWith status: STPPaymentStatus, error: Error?) {
// When the payment is complete, display the status.
self.paymentStatus = status
self.lastPaymentError = error
}
}
ApplePayModel is an observable object so I know there should be a way to check when its paymentStatus has changed. Just haven't figured out a way yet. Any ideas?
Thanks!
EDIT:
Here's the function I'm trying to call, located in PaymentView.swift:
func postPaymentSetup(){
DataService.instance.updateUser(userID: currentUserID)
// send notifications to seller
AuthService.instance.getUserOnesignalID(forUserID: selectedSellerID) { onesignalID in
OneSignal.postNotification(["contents": ["en": "Your payment has succeeded!"], "include_player_ids": ["\(onesignalID ?? "")"]])
}
showToast = true
toastMessage = "Payment Completed ✅"
isShowingSheet = false
isShowingSheet_Reservation = false
showSelectButton = false
presentationMode.wrappedValue.dismiss()
}
Related
I'm developing a MVVM structure with API calls.
I have this structure now:
//Get publisher
loginPublisher = LoginService.generateLoginPublisher()
//Create a subscriber
loginSubscriber = loginPublisher!
.sink { error in
print("Something bad happened")
self.isLoading = false
} receiveValue: { value in
self.saveClient(value)
self.client = value
self.isLoading = false
}
//Asking service to start assync task and notify its result on publisher
LoginService.login(email, password, loginPublisher!)
Basically what I do is obtain certain publisher from a LoginService, then I create a subscriber on loginPublisher, and then I tell LoginService to make some assync logic and send it result to loginPublisher this way I manage sent data with loginSubscriber.
I would like to execute LoginService.login() internally when I execute LoginService.generateLoginPublisher(), but if I do that, there is a chance that LoginService.login() logic finish before I create loginSubscriber, that's why I was forced to control when to call LoginService.login().
How could I detect from LoginService when its publisher has a new subscriber?
This is my LoginService class:
class LoginService{
static func generateLoginPublisher() -> PassthroughSubject<Client, NetworkError>{
return PassthroughSubject<Client, NetworkError>()
}
static func login(_ email: String,_ password: String,_ loginPublisher: PassthroughSubject<Client, NetworkError>){
let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
print(url)
let parameters: [String: String] = [
"password": password,
"login": email
]
print(parameters)
let request = AF.request(
url, method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default
)
request.validate(statusCode: 200...299)
request.responseDecodable(of: Client.self) { response in
if let loginResponse = response.value{//Success
loginPublisher.send(loginResponse)
}
else{//Failure
loginPublisher.send(completion: Subscribers.Completion<NetworkError>.failure(.thingsJustHappen))
}
}
}
}
If you want full control over subscriptions, you can create a custom Publisher and Subscription.
Publisher's func receive<S: Subscriber>(subscriber: S) method is the one that gets called when the publisher receives a new subscriber.
If you simply want to make a network request when this happens, you just need to create a custom Publisher and return a Future that wraps the network request from this method.
In general, you should use Future for one-off async events, PassthroughSubject is not the ideal Publisher to use for network requests.
I finally solved my problem using Future instead of PassthroughtSubject as Dávid Pásztor suggested. Using Future I don't have to worried about LoginService.login() logic finish before I create loginSubscriber.
LoginSevice.login() method:
static func login(_ email: String,_ password: String) -> Future<Client, NetworkError>{
return Future<Client, NetworkError>{ completion in
let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
print(url)
let parameters: [String: String] = [
"password": password,
"login": email
]
print(parameters)
let request = AF.request(
url, method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default
)
request.validate(statusCode: 200...299)
request.responseDecodable(of: Client.self) { response in
if let loginResponse = response.value{//Success
completion(.success(loginResponse))
}
else{//Failure
completion(.failure(NetworkError.thingsJustHappen))
}
}
}
}
Implementation:
loginSubscriber = LoginService.login(email, password)
.sink { error in
print("Something bad happened")
self.isLoading = false
} receiveValue: { value in
self.saveClient(value)
self.client = (value)
self.isLoading = false
}
You can handle publisher events and inject side effect to track subscriptions, like
class LoginService {
static func generateLoginPublisher(onSubscription: #escaping (Subscription) -> Void) -> AnyPublisher<Client, NetworkError> {
return PassthroughSubject<Client, NetworkError>()
.handleEvents(receiveSubscription: onSubscription, receiveOutput: nil, receiveCompletion: nil, receiveCancel: nil, receiveRequest: nil)
.eraseToAnyPublisher()
}
}
so
loginPublisher = LoginService.generateLoginPublisher() { subscription in
// do anything needed here when on new subscription appeared
}
I have been experimenting with some new swift architectures and patterns and I have noticed a strange issue with RxSwift where it seems if I am making a service call and an error occurs - e.g. user enters wrong password - then it seems to dispose of my subscriptions so I cannot make the service call again
I am unsure as to why this happening. I made a quick mini project demonstrating the issue with a sample login app.
My ViewModel looks like this
import RxSwift
import RxCocoa
import RxCoordinator
import RxOptional
extension LoginModel : ViewModelType {
struct Input {
let loginTap : Observable<Void>
let password : Observable<String>
}
struct Output {
let validationPassed : Driver<Bool>
let loginActivity : Driver<Bool>
let loginServiceError : Driver<Error>
let loginTransitionState : Observable<TransitionObservables>
}
func transform(input: LoginModel.Input) -> LoginModel.Output {
// check if email passes regex
let isValid = input.password.map{(val) -> Bool in
UtilityMethods.isValidPassword(password: val)
}
// handle response
let loginResponse = input.loginTap.withLatestFrom(input.password).flatMapLatest { password in
return self.service.login(email: self.email, password: password)
}.share()
// handle loading
let loginServiceStarted = input.loginTap.map{true}
let loginServiceStopped = loginResponse.map{_ in false}
let resendActivity = Observable.merge(loginServiceStarted, loginServiceStopped).materialize().map{$0.element}.filterNil()
// handle any errors from service call
let serviceError = loginResponse.materialize().map{$0.error}.asDriver(onErrorJustReturn: RxError.unknown).filterNil()
let loginState = loginResponse.map { _ in
return self.coordinator.transition(to: .verifyEmailController(email : self.email))
}
return Output(validationPassed : isValid.asDriver(onErrorJustReturn: false), loginActivity: resendActivity.asDriver(onErrorJustReturn: false), loginServiceError: serviceError, loginTransitionState : loginState)
}
}
class LoginModel {
private let coordinator: AnyCoordinator<WalkthroughRoute>
let service : LoginService
let email : String
init(coordinator : AnyCoordinator<WalkthroughRoute>, service : LoginService, email : String) {
self.service = service
self.email = email
self.coordinator = coordinator
}
}
And my ViewController looks like this
import UIKit
import RxSwift
import RxCocoa
class TestController: UIViewController, WalkthroughModuleController, ViewType {
// password
#IBOutlet var passwordField : UITextField!
// login button
#IBOutlet var loginButton : UIButton!
// disposes of observables
let disposeBag = DisposeBag()
// view model to be injected
var viewModel : LoginModel!
// loader shown when request is being made
var generalLoader : GeneralLoaderView?
override func viewDidLoad() {
super.viewDidLoad()
}
// bindViewModel is called from route class
func bindViewModel() {
let input = LoginModel.Input(loginTap: loginButton.rx.tap.asObservable(), password: passwordField.rx.text.orEmpty.asObservable())
// transforms input into output
let output = transform(input: input)
// fetch activity
let activity = output.loginActivity
// enable/disable button based on validation
output.validationPassed.drive(loginButton.rx.isEnabled).disposed(by: disposeBag)
// on load
activity.filter{$0}.drive(onNext: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.generalLoader = UtilityMethods.showGeneralLoader(container: strongSelf.view, message: .Loading)
}).disposed(by: disposeBag)
// on finish loading
activity.filter{!$0}.drive(onNext : { [weak self] _ in
guard let strongSelf = self else { return }
UtilityMethods.removeGeneralLoader(generalLoader: strongSelf.generalLoader)
}).disposed(by: disposeBag)
// if any error occurs
output.loginServiceError.drive(onNext: { [weak self] errors in
guard let strongSelf = self else { return }
UtilityMethods.removeGeneralLoader(generalLoader: strongSelf.generalLoader)
print(errors)
}).disposed(by: disposeBag)
// login successful
output.loginTransitionState.subscribe().disposed(by: disposeBag)
}
}
My service class
import RxSwift
import RxCocoa
struct LoginResponseData : Decodable {
let msg : String?
let code : NSInteger
}
class LoginService: NSObject {
func login(email : String, password : String) -> Observable<LoginResponseData> {
let url = RequestURLs.loginURL
let params = ["email" : email,
"password": password]
print(params)
let request = AFManager.sharedInstance.setupPostDataRequest(url: url, parameters: params)
return request.map{ data in
return try JSONDecoder().decode(LoginResponseData.self, from: data)
}.map{$0}
}
}
If I enter valid password, request works fine. If I remove the transition code for testing purposes, I could keep calling the login service over and over again as long as password is valid. But as soon as any error occurs, then the observables relating to the service call get disposed of so user can no longer attempt the service call again
So far the only way I have found to fix this is if any error occurs, call bindViewModel again so subscriptions are setup again. But this seems like very bad practice.
Any advice would be much appreciated!
At the place where you make the login call:
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
}
.share()
You can do one of two things. Map the login to a Result<T> type.
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.map(Result<LoginResponse>.success)
.catchError { Observable.just(Result<LoginResponse>.failure($0)) }
}
.share()
Or you can use the materialize operator.
let loginResponse = input.loginTap
.withLatestFrom(input.password)
.flatMapLatest { [unowned self] password in
self.service.login(email: self.email, password: password)
.materialize()
}
.share()
Either method changes the type of your loginResponse object by wrapping it in an enum (either a Result<T> or an Event<T>. You can then deal with errors differently than you do with legitimate results without breaking the Observable chain and without loosing the Error.
Another option, as you have discovered is to change the type of loginResponse to an optional but then you loose the error object.
The behavior is not strange, but works as expected: As stated in the official RxSwift documentation documentation:
"When a sequence sends the completed or error event all internal resources that compute sequence elements will be freed."
For your example that means, a failed login attempt, will cause method func login(email : String, password : String) -> Observable<LoginResponseData> to return an error, i.e. return Observable<error>, which will:
on the one hand fast forward this error to all its subscribers (which will be done your VC)
on the other hand dispose the observable
To answer your question, what you can do other than subscribing again, in order to maintain the subscription: You could just make use of .catchError(), so the observable does not terminate and you can decide yourself what you want to return after an error occurs. Note, that you can also check the error for a specific error domain and return errors only for certain domains.
I personally see the responsibility of the error handling in the hand of the respective subscribers, i.e. in your case your TestController (so you could use .catchError() there), but if you want to be sure the observable returned from from func login(email : String, password : String) -> Observable<LoginResponseData> does not even fast forward any errors for all subscriptions, you could also use .catchError() here, although I'd see issues for potential misbehaviors.
I am busy with making a chat app in Swift. I have a create a model of Conversation. This model will have two parameters for now: message (String) and isSender (Bool). If isSender is true, the message will appear at the right side of the view.
For most of the conversation objects I would like to have a certain action that should be called. By creating a new object, I want to tell it's type of conversation action, so I can determine what kind of action is linked to the question/message. Actions can be like, ask for gps permission or to show something else depending on a bool (yes or no).
I thought to create multiple classes that inherits from the model class. In my example for asking permission for gps and for now a Boolean value.
Adding a new parameter, so I can choose the type of action and for some actions an optional closure.
But I am struggling with getting the right result and structure. How can I fix this?
My model class looks like this:
class Conversation: NSObject {
var message: String?
var isSender: Bool?
required init(message: String?, isSender: Bool?) {
self.message = message
self.isSender = isSender
}
}
class gpsPermission: Conversation {
var hasPermission: Bool?
func askPermissionForGPS() -> Bool {
print("Permission for GPS \(hasPermission)")
return hasPermission!
}
}
I would think about it that way:
Base message - generic and contains only common props.
// Sender enum (More readable)
enum MessageSender {
case me
case counterparty
}
// Message class
class Message {
let text: String
let sender: MessageSender
required init(with text: String, sender: MessageSender) {
self.text = text
self.sender = sender
}
}
let message = Message(with: "Hi there", sender: .me)
Subclass messages - permission for example:
// Permission Type
enum PermissionType {
case location
case contacts
case notifications
}
// Permission Message
class PermissionMessage: Message {
let permissionType: PermissionType
var permissionGiven = false
required init(with text: String, sender: MessageSender, permission: PermissionType) {
self.permissionType = permission
super.init(with: text, sender: sender)
}
required init(with text: String, sender: MessageSender) {
fatalError("Permission type must be provided")
}
}
let permissionMessage = PermissionMessage(with: "requesting location", sender: .me, permission: .location)
Model must not contain control logic inside it. only trigger UI changes and be changed by it.
EDIT:
About requesting permissions from the user and informing model object.
IMO, this logic should be totally separated from the model (Message, PermissionMessage, ...). I would recommend that u implement it in a separate stateless manager / controller, send the function a completion handler - something like this:
let contactsPermission = PermissionMessage(with: "requesting contact access", sender: .me, permission: .contacts)
PermissionManager.requestContactsPermission {
contactsPermission.permissionGiven = true
}
class PermissionManager {
static func requestContactsPermission(withCompletion successHandler: ()->()) {
var approved = false
/*
Request contact permission
When done set 'approved' to true, call handler
*/
if approved {
successHandler()
}
}
}
I'm working on a local game which relies on turn order.
Rules;
There are a number of phases in the game (ie: Buy, Sell)
During each phase, a player takes a single turn
Each phase is not considered complete until every player (in turn order) has completed their turn.
I'm not sure how to manage this data. There are a number of things to track.
The phase we are in
The current player on turn
When all players have completed their turns
When the end of the turn order has been reached so we can move to next phase.
Resetting all turn completions when all phases are complete
I'm thinking that a subscription model is the best approach to this, but I'm not used to such a pattern.
Currently I'm using a similar system to a to-do where the phase itself can be marked complete or incomplete.
This is the way I'm currently handling turn orders and phases in Swift playground.
// Turn order management
class TurnOrderManager: NSObject
{
static var instance = TurnOrderManager()
var turnOrder: [Player] = [Player]()
private var turnOrderIndex = 0
var current: Player {
return turnOrder[turnOrderIndex]
}
func next() {
if (self.turnOrderIndex < (self.turnOrder.count-1)) {
turnOrderIndex += 1
}
else {
print("Reached end of line")
}
}
}
class Player: NSObject {
var name: String = ""
override var description: String {
return self.name
}
init(name: String) {
super.init()
self.name = name
}
}
let p1:Player = Player.init(name: "Bob")
let p2:Player = Player.init(name: "Alex")
TurnOrderManager.instance.turnOrder = [p1,p2]
print (TurnOrderManager.instance.current)
TurnOrderManager.instance.next()
print (TurnOrderManager.instance.current)
TurnOrderManager.instance.next()
print (TurnOrderManager.instance.current)
// ---------------------------------
// Phase management
enum PhaseType: Int {
case buying = 1
case selling
}
struct Phase {
var id: PhaseType
var title: String
var completed: Bool = false {
didSet {
// Notify subscribers of completion
guard completed else { return }
handlers.forEach { $0(self) }
}
}
var description:String {
return "Phase: \(self.title), completed: \(completed)"
}
// Task queue
var handlers = [(Phase) -> Void]()
init(id: PhaseType, title: String, initialSubscription: #escaping (Phase) -> Void =
{_ in})
{
self.id = id
self.title = title
subscribe(completion: initialSubscription)
}
mutating func subscribe(completion: #escaping (Phase) -> Void) {
handlers.append(completion)
}
}
class MyParentController {
lazy var phase1: Phase = {
return Phase(id: .buying, title: "Phase #1") {
print("Do something with phase: \($0.title)")
}
}()
}
let controller = MyParentController()
controller.phase1.completed = true
Question:
I'm wanting to notify:
Turn is complete
All turns are complete (so that it can move to next phase)
How do I make my TurnOrderManager alert the PhaseManager that the current turn is complete.
How do I make my PhaseManager know that when all turns are complete to move to the next phase.
I apologize for the verboseness of my query.
Many thanks
You're going to want to define a delegate relationship PhaseManager and your TurnOrderManager.
Here is the Apple docs on protocols.
Here is a great article on delegation.
First you'll need to define a protocol. Something like this:
protocol TurnManagerDelegate {
func complete(turn: objectType)
func allTurnsComplete()
}
Next you'll have to conform your PhaseManager to the protocol:
class PhaseManager: TurnManagerDelegate {
...
func complete(turn: objectType) {
// implement
}
func allTurnsComplete() {
// implement
}
...
}
Last you'll have to add a property on your TurnOrderManager with the delegate:
class TurnOrderManager {
...
var delegate: TurnManagerDelegate
...
}
and call the functions whenever needed in your TurnOrderManager:
...
delegate?.allTurnsComplete() //example
...
You'll also have to set your PhaseManager as the delegate before your TurnOrderManager would try to call any of the delegate methods.
I have successfully figured out push notifications in Cloud Code in the basic sense. I can send a test message from my app and it shows up on my phone.
But I don't understand how to change the values of request.params.message in my Cloud Code.
Also, what I have below causes a Cloud Code error of "invalid type for key message, expected String, but got array." What array?
In case it isn't clear, I am trying to send a push notification to users who subscribe to a particular channel.
My Swift code:
import UIKit
import Parse
import Bolts
class ViewController: UIViewController {
var channels = "B08873"
var minyanID = "bethel08873"
var message = "This is a test message."
override func viewDidLoad() {
super.viewDidLoad()
let currentInstallation = PFInstallation.currentInstallation()
currentInstallation.addUniqueObject( (minyanID), forKey:"channels")
currentInstallation.addUniqueObject(message, forKey: "message")
currentInstallation.saveInBackground()
}
#IBAction func sendPush(sender: AnyObject) {
if minyanID == channels {
PFCloud.callFunctionInBackground("alertUser", withParameters: [channels:message], block: {
(result: AnyObject?, error: NSError?) -> Void in
if ( error === nil) {
NSLog("Rates: \(result) ")
}
else if (error != nil) {
NSLog("error")
}
});
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
My Cloud Code
Parse.Cloud.define("alertUser", function(request,response){
var query = new Parse.Query(Parse.Installation);
var theMessage = request.params.channels;
var theChannel = request.params.message;
query.equalTo(theChannel)
Parse.Push.send({
where: query,
data : { alert: theMessage, badge: "Increment", sound: "", } },
{ success: function() { response.success() },
error: function(error) { response.error(err) }
});
});
Because you are sending B08873 as key and This is a test message. as its value. If you want to send both channel and message key/value pairs you need to do it like this instead:
PFCloud.callFunctionInBackground("alertUser", withParameters: ["channels": channels , "message": message], .....
In your Cloud function you should be bale to access these paramerts like this:
var theChannels = request.params.channels; // returns B08873
var theMessage = request.params.message; // returns This is a test message.
Then call the Push function like this:
Parse.Push.send({
channels: [ theChannels ],
data: {
alert: theMessage ,
badge: "Increment"
}}, {
success: function() {
response.success();
},
error: function(error) {
response.error(error);
}
});