I'm new to swift and iOS programming. I want to add Admob consent/GDPR option to my app,
So I've setup an ad box and can display a test add, but need to add consent option next.
I've read
https://developers.google.com/admob/ios/eu-consent
but it's not clear to me, can anyone show me an example of code to do this step by step, or point me to GitHub source where it has been done.
Thanks
Can you please show an example of what you have tried that isn't working for you?
As I am unable to see your code example to see what you're doing or not doing, I'll give an example based on the docs.
import PersonalizedAdConsent
...
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
PACConsentInformation.sharedInstance.
requestConsentInfoUpdate(
forPublisherIdentifiers: ["pub-0123456789012345"])
{(_ error: Error?) -> Void in
if let error = error {
// Consent info update failed.
} else {
// Consent info update succeeded. The shared PACConsentInformation
// instance has been updated.
}
}
guard let privacyUrl = URL(string: "https://www.your.com/privacyurl"),
let form = PACConsentForm(applicationPrivacyPolicyURL: privacyUrl) else {
print("incorrect privacy URL.")
return
}
form.shouldOfferPersonalizedAds = true
form.shouldOfferNonPersonalizedAds = true
form.shouldOfferAdFree = true
form.load {(_ error: Error?) -> Void in
print("Load complete.")
if let error = error {
// Handle error.
print("Error loading form: \(error.localizedDescription)")
} else {
// Load successful.
}
}
//Finally present the consent form
form.present(from: self) { (error, userPrefersAdFree) in
if let error = error {
// Handle error.
} else if userPrefersAdFree {
// User prefers to use a paid version of the app.
} else {
// Check the user's consent choice.
let status =
PACConsentInformation.sharedInstance.consentStatus
}
}
}
Related
I am in the process of implementing UMP SDK into my iOS app. I have setup the GDPR and IDFA messages in the Google AdMob dashboard's Privacy and Messaging section. I am having trouble getting the GDPR message to show up. The IDFA and iOS' ATT messages work perfectly.
Below is the code that I am using. I have tested this on both simulator and physical device. Also, I am located in the EU.
static func trackingConsentFlow(completion: #escaping () -> Void) {
let umpParams = UMPRequestParameters()
let debugSettings = UMPDebugSettings()
debugSettings.geography = UMPDebugGeography.EEA
umpParams.debugSettings = debugSettings
umpParams.tagForUnderAgeOfConsent = false
UMPConsentInformation
.sharedInstance
.requestConsentInfoUpdate(with: umpParams,
completionHandler: { error in
if error != nil {
print("MYERROR #1 \(String(describing: error))")
completion()
} else {
let formStatus = UMPConsentInformation.sharedInstance.formStatus
print("FORM STATUS: \(formStatus)")
if formStatus == .available {
loadForm(completion)
} else {
completion()
}
}
})
}
private static func loadForm(_ completion: #escaping () -> Void) {
UMPConsentForm.load(completionHandler: { form, loadError in
if loadError != nil {
print("MYERROR #2 \(String(describing: loadError))")
completion()
} else {
print("CONSENT STATUS: \(UMPConsentInformation.sharedInstance.consentStatus)")
if UMPConsentInformation
.sharedInstance.consentStatus == .required {
guard let rootViewController = UIApplication.shared.currentUIWindow()?.rootViewController else {
return completion()
}
form?.present(from: rootViewController, completionHandler: { dismissError in
if UMPConsentInformation
.sharedInstance.consentStatus == .obtained {
completion()
}
})
}
}
})
}
Just to be clear:
With this code I am able to show the IDFA message, after which the AppTrackingTransparency alert is shown. But I am expecting to also see the GDPR consent form.
For anyone wondering the same thing. The GDPR message was not appearing because I had not finalised my Admob account setup. I had not added my payment method. After adding it (and sending the app for review in Admob) the GDPR message started to appear.
I have developed my game in Godot game engine and I'm now trying to implement googles gdpr consent form into my Xcode project.
I am using Xcode 11.5
The code below is my current code which does not result in the consent form popping up.
It might be something with the class I made but I'm not sure how that works.
import UIKit
import PersonalizedAdConsent
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
PACConsentInformation.sharedInstance.requestConsentInfoUpdate(
forPublisherIdentifiers: ["I have the id just not posted here"])
{(_ error: Error?) -> Void in
if let error = error {
// Consent info update failed.
} else {
// Consent info update succeeded. The shared PACConsentInformation
// instance has been updated.
}
}
guard let privacyUrl = URL(string: "My_privacy_policy(not_actual)"),
let form = PACConsentForm(applicationPrivacyPolicyURL: privacyUrl) else {
print("incorrect privacy URL.")
return
}
form.shouldOfferPersonalizedAds = true
form.shouldOfferNonPersonalizedAds = true
form.shouldOfferAdFree = false
form.load {(_ error: Error?) -> Void in
print("Load complete.")
if let error = error {
// Handle error.
print("Error loading form: \(error.localizedDescription)")
} else {
// Load successful.
}
}
//Finally present the consent form
form.present(from: self) { (error, userPrefersAdFree) in
if let error = error {
// Handle error.
} else if userPrefersAdFree {
// User prefers to use a paid version of the app.
} else {
// Check the user's consent choice.
let status =
PACConsentInformation.sharedInstance.consentStatus
}
}
}
}
Any help is appreciated :)
I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}
I'm trying and failing badly to implement the cool Firebase email link login feature. I successfully setup sending an email link. However, I can't get the email link to open up the app. It just opens up the preview page like it can't open the app.
I've tested the dynamic link I setup and I can get it to open up the app in a device. I just can't get the email link to do the same.
Code in my app:
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
// userEmail comes from a textField
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.firebaseapp.com/?email=%#", email))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
}
}
When I say I've successfully gotten my dynamic link to open the app what I mean is when I follow the link I created (mylinkname.page.link/emaillogin) on a device that has the app installed, it opens the app. Because of that and [this helpful Firebase video][1] on setting up a dynamic link it seems like I've got those details correct and the issue is with the code, but I'm new to this so I'm not sure.
I've spend few days going around in circles to figure this out, and trying to parse the dense Firebase documentation, so any ideas are greatly appreciated.
I finally figured it out. The code was fine. It was an issue related to the dynamic link. I had a couple links setup in Firebase because I had to create a new Bundle ID at one point. When I deleted out the old one in Firebase the email link started working.
It shows up in my app association site like this, and oddly still does even though I deleted out the old link, but at least it works now!
{"applinks":{"apps":[],"details":[{"appID":"TEAMID.com.OLDBUNDLEIDENTIFIER.APPNAME","paths":["NOT //*","/*"]},{"appID":"TEAMID.com.NEWBUNDLEIDENTIFIER.APPNAME","paths":["NOT //","/"]}]}}
UPDATE: My full code to implement passwordless email login is below. It was painful for me to piece together using the documentation so hopefully this saves you the trouble.
Key steps assuming you understand the basics of Firebase Setup.
1) Setup a Dynamic Link Using the Firebase Video tutorial.
2) Code in View Controller:
var userEmail: String?
var link: String?
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.page.link/emaillogin/?email=%#", email!))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email!,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
// TODO: Notify user to check email and click the link.
}
}
// Sign in user after they clicked email link called from AppDelegate
#objc func signInUserAfterEmailLinkClick() {
// Get link url string from the dynamic link captured in AppDelegate.
if let link = UserDefaults.standard.value(forKey: "Link") as? String {
self.link = link
}
// Sign user in with the link and email.
Auth.auth().signIn(withEmail: userEmail!, link: link!) { (result, error) in
if error == nil && result != nil {
if (Auth.auth().currentUser?.isEmailVerified)! {
print("User verified with passwordless email")
// TODO: Do something after user verified like present a new View Controller
}
else {
print("User NOT verified by passwordless email")
}
}
else {
print("Error with passwordless email verfification: \(error?.localizedDescription ?? "Strangely, no error avaialble.")")
}
}
}
3) Code in AppDelegate
// For Passwordless Email Login to Handle Dynamic Link after User Clicks Email Link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
// Parse incoming
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Found an error: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
}
else {
// Maybe do other things with dynamic links in future?
return false
}
}
return false
}
// Handles the link and saves it to userDefaults to assist with login.
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
print("My dynamic link object has no url")
return
}
print("Incoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Save link to userDefaults to help finalize login.
UserDefaults.standard.set(link, forKey: "Link")
// Send notification to ViewController to push the First Time Login VC
NotificationCenter.default.post(
name: Notification.Name("SuccessfulPasswordlessEmailNotification"), object: nil, userInfo: nil)
}
}
For anyone using SwiftUI with AppDelegate and SceneDelegate files instead of UIKit, here's what I've done:
Create a function to send a link to the user's email
func sendSignLink(email: String) async throws {
do {
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.url = URL(string: "*enter your Firebase Dynamic link here*")
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
try await Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings)
UserDefaults.standard.set(email, forKey: "email")
}
catch {
throw error
}
}
In the SceneDelegate file, import FirebaseDynamicLinks and add the below code
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
print("\n \nIncoming URL is \(incomingURL)")
_ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("\n \nError with handling incoming URL: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
guard let url = dynamicLink.url else {
print("\n \nDynamic link object has no url")
return
}
print("\n \nIncoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Send notification to trigger the rest of the sign in sequence
NotificationCenter.default.post(name: Notification.Name("Success"), object: nil, userInfo: ["link": link])
} else {
// Send error notification
NotificationCenter.default.post(name: Notification.Name("Error"), object: nil, userInfo: nil)
}
}
}
}
}
Create a function to handle the sign in after the user has clicked on the link in their email
func signInWithEmail(link: String) async throws {
do {
let email = UserDefaults.standard.value(forKey: "email")
try await Auth.auth().signIn(withEmail: email, link: link)
}
catch {
throw error
}
}
In a relevant view, handle the notifications which get posted
struct MyView: View {
var body: some View {
VStack {
Text("View")
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Success"))) { notificationInfo in
if let userInfo = notificationInfo.userInfo {
if let link = userInfo["link"] as? String {
Task.init {
do {
try await signInWithEmail(link: link)
} catch {
print(error)
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Error"))) { _ in
//do something with error
}
}
}
I am trying to implement a function to delete current user's account on iOS. Account deletion works properly but the problem is that I cannot delete the account's data from Database and Storage when deleting an account.
"currentUser.delete" deletes the account but I think there is no authentication to delete its data from Database and Storage. Permission denied message shows up in the log. After running this function, I get to see the account is gone in Firebase Console Authentication page but data from Database and Storage persists.
Is this the correct way to delete an account?
I tried to delete data from Database and Storage before deleting the account. However, Firebase asks for re-authentication if session is more than 5 minutes old. Re-login shows empty data to the user before performing account deletion again so this is misleading and very confusing.
Please let me know how to remove data when deleting an account.
private func deleteAccount() {
guard let currentUser = Auth.auth().currentUser else {
return print("user not logged in")
}
currentUser.delete { error in
if error == nil {
// 1. Delete currentUser's data from Database. Permission denied
// 2. Delete currentUser's data from Storage. Permission denied
// present login screen (welcome page)
self.presentLoginScreen()
} else {
guard let errorCode = AuthErrorCode(rawValue: error!._code) else { return }
if errorCode == AuthErrorCode.requiresRecentLogin {
self.showMessage("Please re-authenticate to delete your account.", type: .error)
do {
try Auth.auth().signOut()
self.presentLoginScreen()
} catch {
print("There was a problem logging out")
}
}
}
}
}
Swift 5 | Firebase 8.11.0
To solve the problems that you've mentioned (delete the data before deleting the actual user and potentially get the AuthErrorCode.requiresRecentLogin error), you may use DispatchGroup and check the lastSignInDate, like this (just call deleteUserProcess()):
let deleteDataGroup = DispatchGroup()
func deleteUserProcess() {
guard let currentUser = Auth.auth().currentUser else { return }
deleteUserData(user: currentUser)
// Call deleteUser only when all data has been deleted
deleteDataGroup.notify(queue: .main) {
self.deleteUser(user: currentUser)
}
}
/// Remove data from Database & Storage
func deleteUserData(user currentUser: User) {
// Check if `currentUser.delete()` won't require re-authentication
if let lastSignInDate = currentUser.metadata.lastSignInDate,
lastSignInDate.minutes(from: Date()) >= -5 {
deleteDataGroup.enter()
Database.database().reference().child(userId).removeValue { error, _ in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
// Delete folders from Storage isn't possible,
// so list and run over all files to delete each one independently
deleteDataGroup.enter()
Storage.storage().reference().child(userId).listAll { list, error in
if let error = error { print(error) }
list.items.forEach({ file in
self.deleteDataGroup.enter()
file.delete { error in
if let error = error { print(error) }
self.deleteDataGroup.leave()
}
})
deleteDataGroup.leave()
}
}
}
/// Delete user
func deleteUser(user currentUser: User) {
currentUser.delete { error in
if let error = error {
if AuthErrorCode(rawValue: error._code) == .requiresRecentLogin {
reauthenticate()
} else {
// Another error occurred
}
return
}
// Logout properly
try? Auth.auth().signOut()
GIDSignIn.sharedInstance.signOut()
LoginManager().logOut()
// The user has been deleted successfully
// TODO: Redirect to the login UI
}
}
func reauthenticate() {
// TODO: Display some UI to get credential from the user
let credential = ... // Complete from https://stackoverflow.com/a/38253448/8157190
Auth.auth().currentUser?.reauthenticate(with: credential) { _, error in
if let error = error {
print(error)
return
}
// Reload user (to update metadata.lastSignInDate)
Auth.auth().currentUser?.reload { error in
if let error = error {
print(error)
return
}
// TODO: Dismiss UI
// Call `deleteUserProcess()` again, this time it will delete the user
deleteUserProcess()
}
}
}
The minuets function can be added in an extension to Date (thanks to Leo Dabus):
extension Date {
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
}
you can first make your specific user deleted and and its value through its UID then you can deleted user and take him to root view controller or login screen after deleting it.
// removing user data from firebase and its specific user id
let user = Auth.auth().currentUser
user?.delete { error in
if let error = error {
// An error happened.
print(error.localizedDescription)
} else {
Database.database().reference().child("users").child(user?.uid ?? "").removeValue()
self.navigationController?.popToRootViewController(animated: true)
// Account deleted and logout user
// do {
// try Auth.auth().signOut()
// take you to root
// self.navigationController?.popToRootViewController(animated: true)
}