Continue handling receipts after log in - ios

I implemented auto renew subscriptions in my app. By default app tries to use production URL for handling receipts, if code returns 27001, app will use sandbox URL. So if I run app from XCode and sandbox account is specified in the iPhone Setting (iTunes/AppStore), app tries at second attempt to load product details for sandbox and does it successfully. Because product array is not empty, I am able to buy a subscription after confirmation prompt. It looks like code working as expected but not in all conditions.
App Review Team does the following: they log off from real and sandbox account in the device settings. In this case if they run app, app shows prompt to log in, because in the ViewController app checks for status of subscriptions. They press Cancel. Next they go to BuySubscriptionViewController and press Cancel again. So at this moment product array is empty and it is not possible to buy a product.
I added the following condition:
#IBAction func buySub(_ sender: Any) {
if (product_mysub != nil) {
StoreManager.shared.buy(product: product_mysub)
} else {
requestData()
}
}
So if a product wasn't found I am asking user to log in again if he tries to buy it.
func requestData() {
let receiptManager: ReceiptManager = ReceiptManager()
receiptManager.startValidatingReceipts()
}
from ReceiptManager.swift:
func startValidatingReceipts() {
do {
_ = try self.getReceiptURL()?.checkResourceIsReachable()
do {
let receiptData = try Data(contentsOf: self.getReceiptURL()!)
self.validateData(data: receiptData)
print("Receipt exists")
} catch {
print("Not able to get data from URL")
}
} catch {
guard UserDefaults.standard.bool(forKey: "didRefreshReceipt") == false else {
print("Stopping after second attempt")
return
}
UserDefaults.standard.set(true, forKey: "didRefreshReceipt")
let receiptRequest = SKReceiptRefreshRequest()
receiptRequest.delegate = self
receiptRequest.start()
print("Receipt URL Doesn't exist", error.localizedDescription)
}
}
If users taps on a button again, app doesn't ask him to log in again and gets data about products. If user taps third time on the button, app shows confirmation prompt to buy a product. How can I continue purchase flow after entering email and password without asking user to press the button 2 times yet?
extension ReceiptManager: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
// this func is not executed after entering account details in the prompt and pressing OK
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Error refreshing receipt", error.localizedDescription)
}
}

Related

How to detect if In-App purchases process is in progress?

Is it possible in Swift to detect if user is purchasing something? The process usually takes about 15-20 seconds and shows 3-4 different alerts(alert typing password for Apple ID, for confirming purchase, for information if it is successfully purchased or not etc.).
My problem is showing ads(OpenAd) whenever the app is about to become active, so it is really bad user experience to see ads when he tries to buy premium account to remove ads...
This is the part of code where I present them(AppDelegate):
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
tryToPresentAd()
}
And here are the methods that do the presenting(Also written in AppDelegate):
func tryToPresentAd() {
let ad: GADAppOpenAd? = self.appOpenAd
self.appOpenAd = nil
if ad != nil {
guard let rootController = self.window?.rootViewController else { return }
ad?.present(fromRootViewController: rootController)
} else {
requestAppOpenAd()
}
}
func requestAppOpenAd() {
self.appOpenAd = nil
GADAppOpenAd.load(withAdUnitID: Bundle.getValue(forKey: "xyzID"), request: GADRequest(), orientation: .portrait) { (ad, error) in
if error != nil {
debugPrint("Failed to load app open ad", error as Any)
} else {
self.appOpenAd = ad
}
}
}
Is there any way to detect it? Maybe like putting some kind of flags or something, or maybe Apple have some built-in way to detect it? Thanks.
Perhaps you could try to create a bool which would turn to true whenever an user decides to purchase something, and check if that bool is false in order to tryToPresentAd().

LocalAuthentication issue

I have an app where the user can authenticate either with TouchID / FaceID (if Available, enrolled and enabled) or with passcode. All those options can be set in the settings of the app and are stored in UserDefaults. Once the app loads, it checks if those Bool keys have true value in UserDefaults and act accordingly.
My problem comes when a user has a device with TouchID/FaceID but they haven't enabled & enrolled it. In this case, the app should show passcode screen only. But instead, I'm presented by TouchID on my iPhone, when I have disabled the option (for testing purposes). According to Apple's documentation, it says:
If Touch ID or Face ID is available, enrolled, and not disabled, the user is asked for that first. Otherwise, they are asked to enter the device passcode.
On a simulator, I see the Passcode screen, but on my iPhone, I see the TouchID pop up when it's disabled and UserDefaults returns false for that key. Why is that happening? What am I doing wrong?
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
func setUI() {
let faceTouchIdState = UserDefaults.standard.bool(forKey: DefaultsKeys.faceTouchIdState)
let passcodeState = UserDefaults.standard.bool(forKey: DefaultsKeys.passcodeState)
if faceTouchIdState {
print("Authenticate")
authenticate()
}
else {
print("Passscode")
showEnterPasscode()
}
}
func showEnterPasscode() {
let context = LAContext()
var errMess: NSError?
let policy = LAPolicy.deviceOwnerAuthentication
if context.canEvaluatePolicy(policy, error: &errMess) {
context.evaluatePolicy(policy, localizedReason: "Please authenticate to unlock the app.") { [unowned self] (success, err) in
DispatchQueue.main.async {
if success && err == nil {
self.performSegue(withIdentifier: "GoToTabbar", sender: nil)
}
else {
print(err?.localizedDescription)
}
}
}
}
else {
print("cannot evaluate")
}
}
There is nothing wrong with your code. I believe the issue is on how you did this "I have disabled the option (for testing purposes)".
Since you were prompted with the "Touch ID" pop up, then it proves that your biometric wasn't really disabled. I surmise that you toggled one of the "USE TOUCH ID FOR" switches and thought doing so will disable biometric in your app, which it won't.
If you want to test the fallback to passcode in your device:
try unenrolling all fingerprints
OR
disable your fingerprint via inputting unenrolled fingerprints on
your app multiple times.

Pop up ad works, but not once I implement in-app purchase to remove it?

The test ad works fine when I don't include the in app purchase. I click on the cell, it takes me to the next view controller, and the ad pops up. However, when I include in-app purchases the ad doesn't show up even if the user didn't pay to remove ads.
The ad shows up with this function:
func showAd() {
self.interstitial = createInterstitialAd()
}
But when I add this, the ad doesn't show even if the user hasn't paid to remove ads.
func showAd() {
if let purchased = UserDefaults.standard.value(forKey: "payment") as? Bool{
if purchased == true{
interstitial = nil
print("there is no ad!!!!")
}else{
self.interstitial = createInterstitialAd()
print("there is an ad!!!")
}
}
Your problem is that initially there will be no value in UserDefaults for the payment key. This will cause the outer if statement to fall through, resulting in no ad.
You can make your code simpler by using bool(forKey:) - This will return false where the key is not present in UserDefaults rather than nil:
func showAd() {
if UserDefaults.standard.bool(forKey: "payment") {
interstitial = nil
print("there is no ad!!!!")
} else {
self.interstitial = createInterstitialAd()
print("there is an ad!!!")
}
}

Can't insert message after TouchID authentication

So I'm playing around a bit with iMessage apps, and have hit a weird issue. I want to try and use TouchID authentication inside of iMessage, and am able to pop the TouchID alert fine from the iMessage app. However, when I go to insert a message showing the result of TouchID, it won't insert the message for me. Here's the relevant code:
#IBAction func authenticateTapped(_ sender: Any) {
let context = LAContext()
var wasSuccessful = false
self.group.enter()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Testing authentication") { (successful, _) in
wasSuccessful = successful
self.group.leave()
}
self.group.notify(queue: DispatchQueue.main) {
self.sendResult(wasSuccessful)
}
}
#IBAction func sendMessageTapped(_ sender: Any) {
sendResult(true)
}
func sendResult(_ successful: Bool) {
guard let conversation = self.activeConversation else { fatalError("expected conversation") }
var components = URLComponents()
components.queryItems = [URLQueryItem(name: "successful", value: successful.description)]
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "green_checkmark")
layout.caption = "Authentication Result"
let message = MSMessage(session: conversation.selectedMessage?.session ?? MSSession())
message.url = components.url!
message.layout = layout
print("queryParts: \(String(describing: components.queryItems))")
print("message: \(message)")
print("activeConversation: \(String(describing: conversation))")
conversation.insert(message) {
(error) in
print("in completion handler")
print(error ?? "no error")
}
}
When authenticateTapped is triggered, the TouchID prompt shows, I successfully authenticate, and then see every log message inside of the sendResult message, except for any of the ones in the completion handler of the insert method.
The weird thing is, when the sendMessageTapped method is fired, everything works as expected. Does anyone know what's going on here, and why I can't seem to insert a message after I successfully authenticate using TouchID?
The only thing I can think of that's different between the two is that the view controller is disappearing when the TouchID prompt comes up, however, if that were the cause, I would expect none of my print statements would show up in the console, when everyone does except those in the completion handler?
Edit: I've done a bit more digging. When presenting the Touch ID authentication in compact mode, your view controller resigns active. When presenting in expanded mode, it stays active, allowing you to insert the message.
Does anyone know if resigning active when presenting the Touch ID alert is a bug or intended behavior?

Firebase - iOS Swift: FIRAuth.auth().signOut() not signing out current user

I'm building an app using Firebase with an initial SignInViewController that loads a sign in page for users to authenticate with email which triggers the following methods:
#IBAction func didTapSignIn(sender: AnyObject) {
let email = emailField.text
let password = passwordField.text
FIRAuth.auth()?.signInWithEmail(email!, password: password!) { (user, error) in
if let error = error {
print(error.localizedDescription)
return
}
self.signedIn(user!)
}
}
func signedIn(user: FIRUser?) {
AppState.sharedInstance.displayName = user?.displayName ?? user?.email
AppState.sharedInstance.signedIn = true
NSNotificationCenter.defaultCenter().postNotificationName(Constants.NotificationKeys.SignedIn, object: nil, userInfo: nil)
performSegueWithIdentifier(Constants.Segues.SignInToHome, sender: nil)
}
The SignInViewController also checks if there is a cached current user when the app launches and, if so, signs that user in:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
//Synchronously gets the cached current user, or null if there is none.
if let user = FirebaseConfigManager.sharedInstance.currentUser {
self.signedIn(user)
}
}
Once the user is signed in, the app segues to a HomeScreenViewController which displays a "Sign Out" button at the top left of the navigation bar. When a user taps the "Sign Out" button, that user is supposed to get signed out and the app should segue back to the SignInViewController with the following method:
#IBAction func didTapSignOut(sender: UIBarButtonItem) {
print("sign out button tapped")
let firebaseAuth = FIRAuth.auth()
do {
try firebaseAuth?.signOut()
AppState.sharedInstance.signedIn = false
dismissViewControllerAnimated(true, completion: nil)
} catch let signOutError as NSError {
print ("Error signing out: \(signOutError)")
} catch {
print("Unknown error.")
}
}
When I tap the "Sign out" button, the didTapSignOut method gets called and gets executed.
However, after the try firebaseAuth?.signOut() line of code gets executed, the current user should be nil. But when I print out the current user in the Xcode console, the current user is still logged in:
po FIRAuth.auth()?.currentUser
▿ Optional<FIRUser>
- Some : <FIRUser: 0x7fde43540f50>
Since the current user doesn't get signed out after firebaseAuth?.signOut() gets called, once the app segues back to the SignInViewController the app still thinks there is a cached current user so that user gets signed in again.
Could this be a Keychain issue?
Does it have to do with NSNotificationCenter.defaultCenter().postNotificationName being called?
My code comes directly from the Google Firebase Swift Codelab so I'm not sure why it's not working:
https://codelabs.developers.google.com/codelabs/firebase-ios-swift/#4
You can add a listener in your viewDidAppear method of your view controller like so:
FIRAuth.auth()?.addStateDidChangeListener { auth, user in
if let user = user {
print("User is signed in.")
} else {
print("User is signed out.")
}
}
This allows you to execute code when the user's authentication state has changed. It allows you to listen for the event since the signOut method from Firebase does not have a completion handler.
GIDSignIn.sharedInstance().signOut()
Use exclamation points not question marks.
try! FIRAuth.auth()!.signOut()
I actually had this issue as well. I was also logging out the user (as you are) with the method's provided by Firebase but when I printed to the console it said that I still had a optional user.
I had to change the logic of setting the current user so that it is always configured by the authentication handler provided by Firebase:
var currentUser: User? = Auth.auth().currentUser
var handle: AuthStateDidChangeListenerHandle!
init() {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
self.currentUser = user
if user == nil {
UserDefaults.standard.setValue(false, forKey: UserDefaults.loggedIn)
} else {
UserDefaults.standard.setValue(true, forKey: UserDefaults.loggedIn)
}
}
}
As long as you are referencing the current user from this handle, it will update the current user no matter the authentication state.
Some answers are using a force unwrap when the firebase signing out method can throw an error. DO NOT DO THIS!
Instead the call should be done in a do - catch - block as shown below
do {
try Auth.auth().signOut()
} catch let error {
// handle error here
print("Error trying to sign out of Firebase: \(error.localizedDescription)")
}
You can then listen to the state change using Auth.auth().addStateDidChangeListener and handle accordingly.
I just had what I think is the same problem - Firebase + Swift 3 wouldn't trigger stateDidChangeListeners on logout, which left my app thinking the user was still logged in.
What ended up working for me was to save and reuse a single reference to the FIRAuth.auth() instance rather than calling FIRAuth.auth() each time.
Calling FIRAuth.auth()?.signOut() would not trigger stateDidChangeListeners that I had previously attached. But when I saved the variable and reused it in both methods, it worked as expected.

Resources