I'm currently working on integrating Stripe into my iOS application via firebase cloud functions. I'm running into a weird problem where when I try to add a card It tells me that my API key is missing when I definitely configured it in my cloud functions.
One thing I noticed is on the client side if I don't include a STPPaymentConfiguration(), then the code works correctly and a payment source gets added to firebase and stripe. Am I missing something here?
I think it's something on the front end side that I'm not quite grasping because with
let addCardViewController = STPAddCardViewController()
my code works fine and as it should but now the view controller doesn't have billing address options.
My front end swift code:
#objc func addPaymentPressed(_ sender:UIButton) {
// Setup add card view controller
let config = STPPaymentConfiguration()
config.requiredBillingAddressFields = .full
let addCardViewController = STPAddCardViewController(configuration: config, theme: theme.stpTheme)
//Creating VC without configuration and theme works just fine
//let addCardViewController = STPAddCardViewController()
addCardViewController.delegate = self
let navigationController = UINavigationController(rootViewController: addCardViewController)
navigationController.navigationBar.stp_theme = theme.stpTheme
present(navigationController, animated: true, completion: nil)
}
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
// Dismiss add card view controller
dismiss(animated: true)
}
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: #escaping STPErrorBlock) {
dismiss(animated: true)
let cardObject = token.allResponseFields["card"]
print("Printing Strip Token:\(token.tokenId)")
CustomerServices.instance.addPaymentToDB(uid: currentUserId, payment_token: token.tokenId, stripe_id: token.stripeID, cardInfo: cardObject as Any) { (success) in
if success {
print("successfully added card info to subcollection!")
} else {
print("TODO: add error message handler")
}
}
}
My Cloud function code:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const stripe = require('stripe')(functions.config().stripe.token);
const currency = functions.config().stripe.currency || 'USD';
// Add a payment source (card) for a user by writing a stripe payment source token to database
exports.addPaymentSource = functions.firestore
.document('Customers/{userId}/paymentSources/{paymentId}')
.onWrite((change, context) => {
let newPaymentSource = change.after.data();
let token = newPaymentSource.payment_token;
return admin.firestore().collection("Customers").doc(`${context.params.userId}`).get()
.then((doc) => {
return doc.data().customer_id;
}).then((customer) => {
return stripe.customers.createSource(customer, {"source" : token});
});
});
When adding configuration to me STPAddCardViewController it gives me an "You did not provide an API key" error.
The issue looks to be that you are creating a new instance of STPPaymentConfiguration (which does not have your Stripe publishable key set to it), instead of using the shared instance (which you probably set your publishable key elsewhere in your code).
You need to make this change:
let config = STPPaymentConfiguration.shared()
The reason just instantiating let addCardViewController = STPAddCardViewController() works is because the initializer actually uses STPPaymentConfiguration.shared() for its configuration.
I got the same error. I was creating an instance of STPAPIClient() and setting publishableKey key.
let client = STPAPIClient()
client.publishableKey = ""
The right way is to use the shared instance of STPAPIClient()
STPAPIClient.shared().publishableKey = ""
let cardParams = STPCardParams()
cardParams.number = cardTextField.cardNumber
cardParams.expMonth = (cardTextField.expirationMonth)
cardParams.expYear = (cardTextField.expirationYear)
cardParams.cvc = cardTextField.cvc
STPAPIClient.shared().createToken(withCard: cardParams) { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
print(error?.localizedDescription)
}
}
Related
i'm using this pod to use Google reCaptcha when users authenticate to an iOS app.
The captcha is shown, but not clickable.
in my loginViewController:
private var recaptcha: ReCaptcha?
private var locale: Locale = Locale(identifier: "fr-FR")
private var endpoint = ReCaptcha.Endpoint.default
override func viewDidLoad() {
super.viewDidLoad()
do {
recaptcha = try ReCaptcha(apiKey: Config.API_KEY, baseURL: URL(string: "http://localhost"), endpoint: .default, locale: locale)
}
catch {
print("error")
}
recaptcha?.configureWebView { [weak self] webview in
webview.frame = self?.view.bounds ?? CGRect.zero
}
recaptcha?.forceVisibleChallenge = true
}
func validate(){
recaptcha?.validate(on: view, resetOnError: true, completion: { (ReCaptchaResult) in
switch ReCaptchaResult {
case .error:
print("error")
case .token:
//WHAT TO DO WITH THE TOKEN HERE?
guard let userName = self.userNameTextField.text, let password = self.passwordTextField.text else {
return
}
self.authentificationPresenter.authenticate(userName: userName, password: password)
}
})
}
#IBAction func loginButtonTapped() {
validate()
}
The result is this. I don't understand how to go from here, and interpret the user's result.
Since the Captcha is loaded in a webview and to my knowledge would not be directly displayed on a ViewController is it possible that your Captcha is is somehow covered by an empty view? This may be why it is not "clickable". Either that or perhaps you need to enable interactions inside of your webview - you can try adding this when you configure the web view:
webview.isUserIntractionEnabled = true
As far as interpreting the result, if the user successfully completes the Captcha test you should receive a client token which you will use alongside your API secret key in a post request to the endpoint provided. Here is the documentation for verifying the user following the completion of a Captcha test: https://developers.google.com/recaptcha/docs/verify. Hope this helped.
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 am facing an issue with auto sign-in with ADAL v2.5.4 in my iOS App.
When a user wants to login to MSA account, we call acquireTokenWithResource with the required params and promptBehavior as AD_PROMPT_AUTO.
In the first run of the app, the user is shown the webview from which login flow is working as expected as user is getting logged in successfully.
On clicking ‘Sign Out’ in my app, I am removing all tokens that have my app’s ClientID. At this point I see that there is still one token present in the cache with ClientID ‘foci-1’.
Additionally I’m clearing the cookie storage of my app so that the webview doesn’t reuse any the cookies.
The issue arises when the user wishes to login again. When the same flow is triggered again for login, now the user is automatically signed in. In the logs I see ‘1 token found for query’.
Ideally since the user signed out earlier, they should be prompted for their credentials again.
What is the right way to handle this scenario?
Should sign-out be handled differently? Should there be any additional checks before login is retriggered? What is the impact of promptBehavior in this scenario?
This is the code I use to perform a "logout" from an app that uses ADAL.
It calls the logout endpoint to invalidate the refresh token on the server side and deletes all of the relevant cookies and keychain entries.
fileprivate var safariModal = false
fileprivate var safariHostVC: UIViewController?
public func logout(presentOn viewController: UIViewController?, modal: Bool) {
let client = "xyzzy" // Your app client id here
let redirect = "youruri://somepath/" // Your redirect URI here
ADKeychainTokenCache.defaultKeychain().removeAll(forClientId: clientid, error: nil)
if let url = URL(string:"https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=\(redirect)") {
let safari = SFSafariViewController(url: url)
safari.toolbarItems = nil
safari.delegate = self
if #available(iOS 11.0, *) {
safari.dismissButtonStyle = .close
}
guard let vc = viewController else {
return
}
self.safariHostVC = vc
self.safariModal = modal
safari.modalPresentationStyle = .overFullScreen
safari.modalTransitionStyle = .coverVertical
if modal {
vc.present(safari, animated: true, completion: nil)
} else {
vc.navigationController?.pushViewController(safari, animated: true)
}
let cookieJar = HTTPCookieStorage.shared
guard let cookies = cookieJar.cookies else { return }
let cookiesArr = Array(cookies)
for cookie: HTTPCookie in cookiesArr {
if (cookie.name == "SignInStateCookie" || cookie.name == "ESTSAUTHPERSISTENT" || cookie.name == "ESTSAUTHLIGHT" || cookie.name == "ESTSAUTH" || cookie.name == "ESTSSC") {
cookieJar.deleteCookie(cookie)
}
}
}
}
You also need to implement an SFSafariViewControllerDelegate function
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
guard let vc = self.safariHostVC else {
return
}
if self.safariModal {
vc.dismiss(animated: true, completion: nil)
} else {
vc.navigationController?.popViewController(animated: true)
}
self.safariHostVC = nil
}
I want to get Customerid of Stripe After Saving Card I got STPToken .I am unable create customerid from STPToken From Swift. Please Help. Here is my code snippet.
let paymentConfig = STPPaymentConfiguration.init();
paymentConfig.requiredBillingAddressFields = STPBillingAddressFields.none;
paymentConfig.publishableKey = "pk_test_whRD827lMXvFb1MtY9T7bRzW"
let theme = STPTheme.default();
let addCardViewController = STPAddCardViewController.init(configuration: paymentConfig, theme: theme);
addCardViewController.delegate = self;
let navigationController = UINavigationController(rootViewController: addCardViewController);
self.present(navigationController, animated: true, completion: nil);
func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: #escaping STPErrorBlock) {
print(token)
dismiss(animated: true)
}
You can not get customer id from the client. It should be generated on your server. Here is the reference for your server to generate customer,
https://stripe.com/docs/api/dotnet#create_customer
You should have an API to send stptoken on your server and then your server should create a customer and return the customerId in the response.
I am trying to find a way to skip provider options screen in FirebaseUI.
I just need phone authentication and there is no need to show user provider options.
Is there a way to take user directly to phone authentication screen?
Here is my code on viewcontroller
override func viewDidLoad() {
super.viewDidLoad()
//createGradientLayer()
checkLoggedIn()
}
func checkLoggedIn() {
Auth.auth().addStateDidChangeListener { auth, user in
if user != nil {
// User is signed in.
} else {
// No user is signed in.
self.login()
}
}
}
func login() {
let authUI = FUIAuth.defaultAuthUI()
authUI?.delegate = self as? FUIAuthDelegate
let providers: [FUIAuthProvider] = [
FUIPhoneAuth(authUI:FUIAuth.defaultAuthUI()!),
]
authUI?.providers = providers
FUIAuth.defaultAuthUI()?.isSignInWithEmailHidden = true
let authViewController = authUI?.authViewController()
self.present(authViewController!, animated: true, completion: nil)
}
func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?) {
if error != nil {
//Problem signing in
login()
}else {
//User is in! Here is where we code after signing in
}
}
You were almost there. After FUIAuthProvider initialization start Phone Auth flow directly:
FUIPhoneAuth *provider = self.authUI.providers.firstObject;
[provider signInWithPresentingViewController:self];
Here is sample code.
In order to add logo to Welcome screen subclass FUIAuthPickerViewController and implement FUIAuthDelegate delegate method:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController
Here is one more sample for this.
Let's supose that you have a view controller with a button to start the phone validation. This is the code that should be included in the button (obj-c)
- (IBAction)btnPhoneValidation:(id)sender {
FUIAuth *authUI = [FUIAuth defaultAuthUI];
authUI.delegate = self;
//The following array may contain diferente options for validate the user (with Facebook, with google, e-mail...), in this case we only need the phone method
NSArray<id<FUIAuthProvider>> * providers = #[[[FUIPhoneAuth alloc]initWithAuthUI:[FUIAuth defaultAuthUI]]];
authUI.providers = providers;
//You can present the screen asking for the user number with the following method.
FUIPhoneAuth *provider = authUI.providers.firstObject;
[provider signInWithPresentingViewController:self phoneNumber:nil];
//This is the default way to present several options.
// UINavigationController *authViewController = [authUI authViewController];
// [self presentViewController:authViewController animated:YES completion:nil];
}
The same process but with e-mail authentication, replacing the provider type:
NSArray<id<FUIAuthProvider>> * providers = #[[[FUIEmailAuth alloc]init]];
authUI.providers = providers;
FUIEmailAuth *provider = authUI.providers.firstObject;
[provider signInWithPresentingViewController:self email:nil];
According to the FirebaseAuthUI documentation, you cannot customize the flow. (See the section on custom email/password screens)