SwiftUI ask Push Notifications Permissions again - firebase

So I have push notifications implemented in my App and when the app first starts up, its asks users if they would like to allow push notifications (this implementation works fine as expected).
If this user disallows the push notifications, is it possible to have a button in the app which allows the user to click on and it would ask to allow permissions again?
This is what im trying to achieve:
SettingsView
//IF PUSH NOTIFICATIONS NOT ENABLED, SHOW THIS SECTION
Section (header: Text("Push Notifications")) {
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Enable Push Notifications").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
checkPushNotifications()
}) {
Text("View").font(.system(size:12))
}
}
}
In my Push Notification Function:
class PushNotificationService: NSObject, MessagingDelegate {
static let shared = PushNotificationService()
private let SERVER_KEY = "myserverkey"
private let NOTIFICATION_URL = URL(string: "https://fcm.googleapis.com/fcm/send")!
private let PROJECT_ID = "my project name"
private override init() {
super.init()
Messaging.messaging().delegate = self
}
func askForPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted: Bool, error: Error?) in
if granted {
self.refreshFCMToken()
} else {
// Maybe tell the user to go to settings later and re-enable push notifications
}
}
}
func refreshFCMToken() {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("Error fetching remote instance ID: \(error)")
} else if let result = result {
print("Remote instance ID token: \(result.token)")
self.updateFCMToken(result.token)
}
}
}
func updateFCMToken(_ token: String) {
guard let currentUser = Auth.auth().currentUser else { return }
let firestoreUserDocumentReference = Firestore.firestore().collection("users").document(currentUser.uid)
firestoreUserDocumentReference.updateData([
"fcmToken" : token
])
}
}
What im trying to achieve is if the user HAS NOT enabled notification only then ask them the option to reenable in SettingsView.

No you cannot. However, a good UI/UX design will be careful before burning the one-time chance of asking for permissions. Instead, use a user friendly UI to explain why you need certain permissions. For example, I often found it frustrating to implement a permission display view, and handle various async permission requests in a seperate view model. So I recently made a SwiftUI package:
PermissionsSwiftUI
                  
PermissionSwiftUI is a package to beautifully display and handle permissions.
EmptyView()
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
For a SINGLE line of code, you get a beautiful UI and the permission dialogs.
It already supports 7 OUT OF 12 iOS system permissions. More features coming 🙌
Full example
struct ContentView: View {
#State var showModal = false
var body: some View {
Button(action: {showModal=true},
label: {Text("Ask user for permissions")})
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
}
}
To use PermissionsSwiftUI, simply add the JMPermission modifier to any view.
Pass in a Binding to show the modal view, and add whatever permissions you want to show.

The short answer is no, you can't ask the user again if he once disabled the push-notifications for your app.
What you can do, is navigating the user to the settings in their phone to allow push-notifications again.
The code snippet in SwiftUI for the button would be:
Button(action: {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}, label: {
Text("Allow Push")
})
I would also refer to this question: How to ask notifications permissions if denied?

Related

Firebase Login and Login with Apple not linking to same user account

Using SwiftUI, Xcode12.5.1, Swift5.4.2, iOS14.7.1,
My Firebase-Login page shall be extended with other Login possibilities such as Apple-Login (eventually Google-login, Facebook-login etc).
I have an implementation of Firebase-Login that works well.
I extended the LoginView with the Sign in with Apple Button.
And this new Apple Login in its basic implementation also works.
Now the problem:
If I log in with Apple, I need to access the corresponding Firebase-user in order to query the correct user-data. Right now, login in with Apple works but the retrieved data is not the user-data of the corresponding Firebase-user.
What I want to achieve:
From a logout-state, I want to
a) Being able to log in with Firebase Email/Password and sometimes later want to log-out and log in again with Apple.
--> and for both cases, I would like to get the same user-data
b) Being able to log in with Apple and sometimes later want to log-out and log in again with Firebase Email/Password
--> and for both cases, I would like to get the same user-data
--- THE IDEA ----------
I learned from the Firebase documentation that there is a way to link two login-accounts that we are able to know that these two accounts are corresponding.
--- THE IMPLEMENTATION -----------
Below is my current implementation for the Apple login:
I learned that you can get userInformation of the corresponding other account in the error of the link-callback. But in my case, I get the wrong linkError:
My linkError:
The email address is already in use by another account.
Instead of:
AuthErrorCode.credentialAlreadyInUse
For me this doesn't make sense. Especially since I know that I already did log in before with Firebase-Email/Password. Then I logged out and now I tried to log in with Apple.
Shouldn't the link method recognise that I am allowed to have been logged in via Firebase-Email/Password before and shouldn't it be ok to have that email being used before ?? I don't understand this linkError.
Questions:
In the link-callback, why do I get the linkError The email address is already in use by another account. instead of AuthErrorCode.credentialAlreadyInUse ??
What do I need to change in order to make a) work ??
How does the implementation look for the b) workflow (i.e. if user logs in to Apple, then logs-out and logs in again with Firebase-Email/Password ??). How do I link the two accounts then ??
Here my code:
switch state {
case .signIn:
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print("Error authenticating: \(error.localizedDescription)")
return
}
do {
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
if let user = authResult?.user {
user.link(with: credential) { (result, linkError) in
if let linkError = linkError, (linkError as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(user.uid)] tasks.")
if let updatedCredential = (linkError as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let user = result?.user {
// eventually do a data-migration
user.getIDToken { (token, error) in
if let _ = token {
// do data migration here with the token....
self.doSignIn(appleIDCredential: appleIDCredential, user: user)
}
}
}
}
}
}
else if let linkError = linkError {
// I END UP HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// WHY WHY WHY WHY WHY WHY WHY WHY ????????????????????????
print("Error trying to link user: \(linkError.localizedDescription)")
}
else {
if let user = result?.user {
self.doSignIn(appleIDCredential: appleIDCredential, user: user)
}
}
}
}
}
} catch {
print(error.localizedDescription)
}
if let user = authResult?.user {
if let onSignedInHandler = self.onSignedInHandler {
onSignedInHandler(user)
}
}
}
case .link:
// t.b.d.
case .reauth:
// t.b.d.
}

How can you add a custom image to a notification in iOS with react-native-firebase

I want to use the data payload in a firebase cloud messaging to present an image in the notification. The image is specified as an url to a website where the image is hosted.
It appears as what I want to do is to add the image as an attachment, see row 8 below. There are however no image present, other than the application icon.
const notification = new firebase.notifications.Notification()
.setNotificationId("notification_id")
.setTitle(notification.data.title)
.setBody(notification.data.body)
.setData({ url: notification.data.url })
.ios.setLaunchImage(notification.data.icon)
.android.setBigPicture(notification.data.icon)
.ios.addAttachment("some_id", notification.data.icon, {});
The problem is that there are no error messages that can help me. The notification displays with the title and body as expected, but no image is present. From what I can read of the documentation what I want to do is possible.
The short answer is that react-native on iOS does not support "rich push notifications" i.e. notifications with images.
The longer answer is that it is rather simple to add support for an image to a react-native project if you add a little swift code.
Work around:
Open your xcode project and go to "editor"->"Add Target...". Select the "Application Extension" named "Notification Service Extension".
You can name it whatever you want but make sure that the correct project is selected if you are using CocoaPods.
Once it is created replace the content of override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) with:
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
// get the variables that is needed later.
guard let bestAttemptContent = bestAttemptContent,
let attachmentURLAsString = bestAttemptContent.userInfo["icon"] as? String,
// "icon" is the key for the image url in the notification. It
// could be named whatever you want.
let attachmentURL = URL(string: attachmentURLAsString) else {
return
}
// call a custom function to download the image before attaching
// it to the notification and presenting it.
downloadImageFrom(url: attachmentURL) { (attachment) in
if let attachment = attachment {
bestAttemptContent.attachments = [attachment]
contentHandler(bestAttemptContent)
}
}
Then the downloadImageFrom function needs to be created:
private func downloadImageFrom(url: URL, with completionHandler: #escaping (UNNotificationAttachment?) -> Void) {
let task = URLSession.shared.downloadTask(with: url) { (downloadedUrl, response, error) in
//verify that a url exists.
guard let downloadedUrl = downloadedUrl else {
completionHandler(nil)
return
}
// create a local unique filepath.
var urlPath = URL(fileURLWithPath: NSTemporaryDirectory())
let uniqueURLEnding = ProcessInfo.processInfo.globallyUniqueString + ".png"
urlPath = urlPath.appendingPathComponent(uniqueURLEnding)
// fetch the image from the url
try? FileManager.default.moveItem(at: downloadedUrl, to: urlPath)
// if successful, return the image as an attachment.
do {
let attachment = try UNNotificationAttachment(identifier: "picture", url: urlPath, options: nil)
completionHandler(attachment)
} catch {
completionHandler(nil)
}
}
task.resume()
}
When you build the application it will use this code instead for loading notifications.
When sending the notification you have to remember to include the "icon" value. An example of what is needed to send a notification:
"notification": {
"body": "body",
"title": "title"
"mutable_content": true // this row is required for the notification to work!
},
"data": {
"icon":"https://pusher.com/static_logos/320x320.png", // change to your image url.
},

How to force logout firebase auth user from app remotely

I have a project which uses firebase auth with firebaseUI to authenticate users. I have enabled Google, Facebook and email providers. What I need is to remotely logout or disable some of the users.
I want the users to logout from the app on doing so. I tried disabling the user in the firebase console and also used the firebase admin SDK (https://firebase.google.com/docs/auth/admin/manage-sessions) to revoke the refresh tokens.
I waited for more than 2 days and still noticed that the user was logged in and could access the firestore data.
I have also gone through and tried
Firebase still retrieving authData after deletion
Can anyone point to what I am doing wrong ?
You also cannot remotely force a user to be signed out. Any sign out will have to happen from the device that the user is signed in on.
There is no way to revoke an access token once that is minted. This means that even if you disable the user's account, they may continue to have access for up to an hour.
If that is too long, the trick (as also mentioned in my answer to the question you linked) is to maintain a list of blocked users in your database (or elsewhere) and then check against that in your security rules (or other authorization layer).
For example in the realtime database, you could create a list of blocked user's UIDs:
banned_uids: {
"uid1": true
"uid2": true
}
And then check against that in your security rules with:
".read": "auth.uid !== null && !root.child('banned_uids').child(auth.uid).exists()"
You can send a message data with FCM to force to log out.
For example, if the users use android application.
Save the FCM token in a collection in firebase Realtime.
configure the Android client app, in the service. LINK You have to make when receive a message with especial string, force to log out.
make the trigger you need in cloud functions, to send the data LINK when you need the user log out.
SUCCESS!
As per your scenarios, i assume that you need to make user logout when user is disabled.
Use One global variable to store TokenNo (might be in shared preference or sqlite):
Add following code to your manifest:
<service android:name=".YourFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Add following code in your
public class LogoutOntokenchange extends FirebaseMessagingService{
#Override
public void onNewToken (String token){
if(TokenNo=>1){ //if tokenNo >=1 means he already logged in
TokenNo=0;
FirebaseAuth.getInstance().signOut(); //Then call signout method
}
else{
TokenNo=1; //store token no in db
}
}
}
What Happens here:
When user logged in first time onNewToken is called then It goes into else then TokenNo is updated to 1 from 0.
When You disable any user then automatically token is refreshed.Then OnNewToken is called then TokenNo>=1 so user will be logged out.
NOTE: When user log in for first time i.e if TokenNo variable is not stored then store it as 0.
For reference: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService
The only way I can think about is adding a if-else block in your starting activity.
Store the that status of user (verified/banned/deleted) in Firebase Real-time database. Then retrieve the status of user at start of application and add the code:
if (currentUserStatus.equals("banned"))
{
currentUser.logout();
}
What I've done is I created for each user upon registration a Firestore document with the UID as document ID. In this document I store an array which stores all fcm tokens the individual user receives when logging into a new device. That way I always keep track where the user is logged in. When the user logs out manually the fcm token will be deleted from the document in Firestore as well as on the device.
In order to be able to log out the user everywhere they are signed in I did the following. When starting the app and once the user is logged in I start a snapshot listener that listens to all changes in the users document. As soon as there is a change I retrieve the new array of fcm tokens, search inside the array for the local current device fcm token. If found, I do nothing. If the fcm token is no longer in the array I will call the local logout method and go back to the login screen.
Here are the methods I used in swift on iOS. The closures (passOnMethod) will just trigger an unwind segue to the login view controller.
import Foundation
import Firebase
class FB_Auth_Methods {
let db = Firestore.firestore()
var listener: ListenerRegistration?
func trackLoginStatus(passOnMethod: #escaping () -> () ) {
listener?.remove()
if let loggedInUserA_UID = Auth.auth().currentUser?.uid {
listener = db.collection(K.FStore.collectionOf_RegisteredUsers_Name)
.document(loggedInUserA_UID)
.addSnapshotListener { (snapshotDocument, error) in
if let error = error {
print(error)
} else {
if let document = snapshotDocument {
if let data = document.data() {
if let fcmTokens = data[K.FStore.Users.fcmTokens] as? [String] {
print("Found the following tokens: \(fcmTokens)")
self.compareTokensAgainstCurrentDeviceToken(fcmTokens: fcmTokens, passOnMethod: { () in
passOnMethod()
})
}
}
}
}
}
}
}
func compareTokensAgainstCurrentDeviceToken(fcmTokens: [String], passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
if fcmTokens.contains(result.token) {
print("Token found, doing nothing")
} else {
print("Token no longer found, logout user")
do {
try Auth.auth().signOut()
InstanceID.instanceID().deleteID { error in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
} catch let signOutError as NSError {
print (signOutError)
}
}
}
}
}
}
And here is the method I use when logging out the user everywhere but at the current device.
func deleteAllFcmTokensExceptCurrent(loggedInUserA: User, passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
let batch = self.db.batch()
let deleteAllFcmRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.delete()], forDocument: deleteAllFcmRef)
let updateFcmTokenRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.arrayUnion([result.token])], forDocument: updateFcmTokenRef)
batch.commit { (error) in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
}
}
}
Not tested yet, as our backend programmer, who is in charge of setting up Firestore rules was gone for the day, but in theory this should work: (and it's something I'll test tomorrow)
Having a FirebaseAuth.AuthStateListener in charge of serving UI based on the status of the user
This combined with rules in firestore
match /collection
allow read: if isAuth();
Where isAuth is:
function isAuth() {
return request.auth.uid != null;
}
If the user is then disabled, while being logged in, whenever the user tries to read data from the collection, he should be denied, and a signOut() call should be made.
The AuthStateListener will then detect it, and sign the user out.

Check if user is logged in: Firebase iOS Swift 4

I am still having difficulty in checking whether the user is logged in with Google or Facebook to read and write on Firebase Database. I want to present a log in screen to a first time user and when the user authenticates, the log in screen is dismissed and it sent to the tabViewControllers. Here's my Swift 4 code below, which is placed in the AppDelegate, application(application:didFInishLaunchingWithOptions launchOptions:).
if Auth.auth().currentUser == nil {
print("NO USER") // this does print out in the console before the app crashes
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginVC = storyboard.instantiateViewController(withIdentifier: "loginVC") as! LogInViewController
self.window?.rootViewController = loginVC
} else {
let tabController = window!.rootViewController as! UITabBarController
if let tabViewControllers = tabController.viewControllers {
// First tab (only one so far...)
let navController = tabViewControllers[0] as! UINavigationController
let controller1 = navController.viewControllers.first as! UserProfileViewController
controller1.coreDataStack = coreDataStack
}
}
}
Please note the LogInViewController Scene is created in the Main.storyboard file and it has a Storyboard ID of "loginVC". When I try to run this, the program crashes at the part where the tabViewController[0] tries to fetch from the coreDataStack.
Hi you need to store UID of the user here is my code for login screen as you said if user open app for first time he have to login / authenticate and second time is automatically.
override func viewDidLoad() {
super.viewDidLoad()
if let uid = KeychainWrapper.standard.string(forKey: KEY_UID) {
autoLoginWithUID(uid: uid)
}
}
after app launches try it to auto login him if have stored his UID otherwise screen stays
func autoLoginWithUID(uid: String) {
KeychainWrapper.standard.set(uid, forKey: KEY_UID)
print(uid)
//Keep db and userRef as class constants shouldn't be here
let db = Firestore.firestore()
let userRef = db.collection("Users").document(uid)
userRef.getDocument { (document, error) in
if let document = document {
print("User data: \(document.data())")
self.performSegue(withIdentifier: "LogIn", sender: nil)
} else {
print("User does not exist")
}
}
}
Here I look in db if I have user with this UID if I got it its stored in global variable and continue. You can also store users credentials and log user by them. But dont know which way is more secure.
Auth.auth().signIn(with: credential) { (user, error) in
if let error = error {
// ...
return
}
// User is signed in
// ...
}
}
If you want more code from UserRequest let me know ;)

How to populate client-side Meteor.user.services after OAuth with built-in accounts-ui package in Meteor v1.4+?

I'm using accounts-ui and accounts-google in Meteor v1.4.1. I can't get the user.services object to appear scoped in the client code. In particular, I need google's profile picture.
I've configured the server-side code to authenticate with Google like so:
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';
const services = Meteor.settings.private.oauth;
for (let service of Object.keys(services)) {
ServiceConfiguration.configurations.upsert({
service
}, {
$set: {
clientId: services[service].app_id,
secret: services[service].secret,
loginStyle: "popup"
}
});
}
...and the client side code to configure permissions like so:
Accounts.ui.config({
requestPermissions: {
google: ['email', 'profile']
},
forceApprovalPrompt: {
google: true
},
passwordSignupFields: 'EMAIL_ONLY'
});
When users click the 'Sign-In with Google' button, a pop-up appears and they can authenticate. No prompt appears, however, despite forceApprovalPrompt being set to true for google.
The big issue is that when I execute this,
const user = Meteor.user();
console.log(user.services);
anywhere in client code, I do not see the expected user services information. I check my database and it is definitely there for the taking:
$ mongo localhost:27017
> db.users.find({})
> ... "services" : { "google" : { "accessToken" : ... } } ...
I'm curious what I'm missing? Should I explicitly define a publish function in order for user services data to exist in the client?
The services property is intentionally hidden on the client side for security reasons. There are a couple of approaches here :
Suggestions
My preferred one would be to expose a meteor method to bring you the
public keys and avatars you might need in the few places you'd need
them.
On a successful login, you could record the data you need somewhere in the user object, but outside of the services property.
As you said, you could make a new publication which explicitly specifies which fields to retrieve and which ones to hide. You have to be careful what you publish, though.
Code Examples
Meteor methods:
// server
Meteor.methods({
getProfilePicture() {
const services = Meteor.user().services;
// replace with actual profile picture property
return services.google && services.google.profilePicture;
}
});
// client
Meteor.call('getProfilePicture', (err, profilePicture) => {
console.log('profile picture url', profilePicture);
});
Update on successful user creation (you might want to have a login hook as well to reflect any avatar/picture changes in google):
// Configure what happens with profile data on user creation
Accounts.onCreateUser((options, user) => {
if (!('profile' in options)) { options.profile = {}; }
if (!('providers' in options.profile)) { options.profile.providers = {}; }
// Define additional specific profile options here
if (user.services.google) {
options.profile.providers.google = {
picture: user.services.google.picture
}
}
user.profile = options.profile;
return user;
});
Publish only select data...
// Server
Meteor.publish('userData', function () {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, {
fields: { other: 1, things: 1 }
});
} else {
this.ready();
}
});
// Client
Meteor.subscribe('userData');

Resources