Conference App Secrets #2: Custom Credentials Dialog

October 29, 2013 — 1 Comment

This is part #2 of my series ‘Conference App Secrets’.

In part#1 I demonstrated HowTo dispay a EULA at first start of the application.

This time I will talk about a Custom Credentials Dialog. There’s already a SystemCredentialsPrompt for Cascades, but we ran into an issue with the password. There were situations where the password field was not masked.

BBJam Asia Login

q10_bbjam_home

So I developed a Custom Credentials Dialog to do the Login into BBJam Asia Conference App.

There are different use-cases to do a Login – for the BBJam Asia Conference App Login is optional.

Everyone can use the App and take a look at Sessions, Speakers, Venue, …

To get access to personal schedule, bookmark Sessions as Favorites or fill out Surveys you must be logged in.

Login needs a Username and Password and there should be an easy way to request a new Password directly from Login Dialog.

Login credentials are sent to a Server and feedback if Login was successfull or not comes back asynchron.

If you need a successful Login to start your App you can implement your Login logic similar to the EULA license dialog.

I have extracted the code to do a Custom Login into a sample App and – as usual – all is available at Github (Apache 2 License).

You should download the BBJam Asia Conference App to see it there in action.

UI (QML)

We need a Dialog with a Title, two entry fields and some Buttons. To solve this the easiest way, we create a Custom Dialog with some properties: alias properties to get the values from fields and a result string.

Dialog {
    id: loginDialog
    property string result
    property alias usernameEntry: usernameField.text
    property alias passwordEntry: passwordField.text

The Dialog gets a Container with a solid background color, so nothing will shine through. The color depends from Theme:

Container {
        id: dialogContainer
        background: isDark() ? Color.Black : Color.White
        horizontalAlignment: HorizontalAlignment.Fill
        verticalAlignment: VerticalAlignment.Fill

To detect the Theme we use a function:

function isDark() {
        return Application.themeSupport.theme.colorTheme.style == VisualStyle.Dark
    }

Inside the Dialog Container we have a Container for the Title and some Container for the content to display the entry fields and Buttons.

The Title Container in the sample app has a blue background – the conference app uses the special JAM color. We use a fixed height for the Title and a white Label.

Here’s the Title Container:

Container {
            id: titleContainer
            background: Color.Blue
            topPadding: 5
            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Fill
            minHeight: 90
            maxHeight: 90
            Label {
                text: "Login to your App"
                textStyle.base: SystemDefaults.TextStyles.TitleText
                textStyle.color: Color.White
                horizontalAlignment: HorizontalAlignment.Center
                verticalAlignment: VerticalAlignment.Center
                translationY: 14
            }
        }

In your application all text values like “Login to …” should be translated values qsTr(“Login to …”)

The Content is placed inside a ScrollView, so it also works and all is accessible if you’re using a Touch Device in Landscape mode with only 768 (Z10) or 720 (Z30) height and virtual keyboard. The Content Container (id: body) has some padding values – you can adjust them if you want to redesign the layout.

Inside the Container there are the two entry fields for Username (id: usernamefield) and Password (id: passwordField).

Remember that we have access to the values from the Dialog alias properties.

               TextField {
                    id: usernameField
                    hintText: qsTr("Email Address or Username")
                    text: ""
                    onTextChanging: {
                        loginButton.enabled = passwordField.text.length > 0 && text.length > 0
                        forgotButton.enabled = text.length > 0
                    }
                    inputMode: TextFieldInputMode.Url
                }
                TextField {
                    id: passwordField
                    hintText: "My Password"
                    topMargin: 30
                    text: ""
                    onTextChanging: {
                        loginButton.enabled = usernameField.text.length > 0 && text.length > 0
                    }
                    onTextChanged: {
                        loginButton.requestFocus()
                    }
                    inputMode: TextFieldInputMode.Password
                }

There’s some UI-logic implemented:

  • Login Button is only enabled if there are values entered for Username and Password
  • Forgot Password Button is only enabled if there’s a value entered for Username

So we’re watching the slot onTextChanging{}: if text is eneterd we enable / disable the buttons.

Both textFields have a different inputMode: Url for Username and Password for PasswordField.

Also the second field (PasswordField) watches onTextChanged{}. This event happened if user has entered all characters and typically hit the RETURN key.

The Buttons are sending different results back – here’s the Login Button:

Button {
         id: loginButton
         enabled: false
         text: qsTr("Login")
         preferredWidth: Infinity
         focusPolicy: FocusPolicy.KeyAndTouch
         onClicked: {
           loginDialog.result = "LOGIN"
           loginDialog.close()
          }
}

per default the button is disabled. if clicked the result is set to LOGIN and the Dialog will close.

Here’s the LoginDialog from dark Theme on Q10 and light Theme on Z10 using different backgrounds:

q10_custom_login

z10_custom_login

The logic to show the Dialog is inside the

assets/main.qml

which is a TabbedPane in ConferenceApp and a simple Page in the sample App.

At first we have to add the Custom Dialog as an attachedObject. Here you also see what happens, if the Dialog was closed. We test for the result and call a method from C++: app.login(…) or app.forgotPassword(…) using the entered values as parameters.

    attachedObjects: [
        LoginDialog {
            id: myLoginDialog
            onClosed: {
                if (myLoginDialog.result == "LOGIN") {
                    console.log("LOGIN");
                    app.login(myLoginDialog.usernameEntry, myLoginDialog.passwordEntry);
                    myLoginDialog.usernameEntry = ""
                    myLoginDialog.passwordEntry = ""
                    return
                }
                if (myLoginDialog.result == "FORGOT") {
                    console.log("FORGOT PASSWORD");
                    app.forgotPassword(myLoginDialog.usernameEntry)
                    myLoginDialog.passwordEntry = ""
                    return
                }
                console.log("CANCEL LOGIN");
                myLoginDialog.usernameEntry = ""
                myLoginDialog.passwordEntry = ""
            }
        },
        SystemToast {
            id: mailSentToast
            body: "Mail send to request a new Password."
        }
    ]

There’s also a SystemToast to send a notification to the user that a new password was requested.

To open the Dialog we’re using a Button:

        Button {
            id: loginButton
            visible: ! rootPage.loginDone
            text: qsTr("  Login") + Retranslate.onLanguageChanged
            imageSource: "asset:///images/buttons/login.png"
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center
            onClicked: {
                myLoginDialog.open();
            }
        }

If the Button was clicked, we open the LoginDialog.

Our Application should work on devices with light theme (Q5, Z10) and dark theme (Q10, Z30), so we need the login.png for both.

Cascades uses static asset selectors for this:

static_assets

As you can see there are two login.png. And here’s how it looks from Q10 and Z10:

q10_custom_main

z10_custom_main

To receive the signals from C++ we have to connect to them:

    function onLoginDone(success) {
        if (success) {
            rootPage.loginDone = true
            return
        }
        // send toast to user that login failed
    }
    function onMailSent(){
        mailSentToast.show()
    }
    onCreationCompleted: {
        // we have to wait for the signal that login was done
        app.loginDone.connect(onLoginDone)
        app.mailSent.connect(onMailSent);
    }

The scret to connect is done inside the onCreationCompleted{}: we connect the signals loginDone() and maiSent() from the C++ Application (app) and call a corresponding function. if login was done with success we set a property, whcih makes another label visible. if a new password was requested we show the SystemToast.

Business Logic (C++)

Now let’s see what happens at C++ side.

we need the ‘app’ context property:

    // access to the Application
    qml->setContextProperty("app", this);

Setting the context property isn’t enough – we must tell Qt that some of our methods can be invoked. This is done in the applicationUi.hpp Headerfile:

	Q_INVOKABLE
	void forgotPassword(const QString& email);

	Q_INVOKABLE
	void login(const QString& user, const QString& pw);

We’re also sending some signals:

signals:
	void loginDone(const bool& success);
	void mailSent();

Remember: we connected to these signals from main.qml.

Here’s the implementation of the Q_INVOKABLE methods:

void ApplicationUI::forgotPassword(const QString& email) {
	qDebug() << "CASCADES CREDENTIALS | ApplicationUI::forgotPassword | email address: " << email;
	// send the information to the server
	// or send an email to admin
	// let user know that password was requested
	emit mailSent();
}

void ApplicationUI::login(const QString& user, const QString& pw) {
	qDebug() << "CASCADES CREDENTIALS | ApplicationUI::login for user: " << user;
	// test if login OK or call server
	// in this app all logins are OK
	emit loginDone(true);
}

signals must NOT be implemented: using emit loginDone(true) sends them out to all connected to this signal, which can be from C++ and/or QML.

emitting this signals gets the function onLoginDone() called in main.qml.

This is only a sample app: it’s up to you to verify if username and password are valid. Can be done checking a local database or calling a server. If you want to store the username and password you should use QSettings. I explained the use of QSettings in part#1 of this series.

Don’t forget

Please don’t forget to import the bb.system libraries in QML to make the SystemToast run.

import bb.system 1.0

Summary

from this sample you have learned

  • HowTo reate a Custom Credentials Dialog
  • Enable / Disable Buttons depending from text Input
  • Use of SystemToast in QML
  • Use static asset selectors for Images to support dark and light themes
  • Connect QML functions to C++ Signals

Download and Discuss

The Sample APP is available at GitHub Open Source (Apache 2 License): https://github.com/ekke/cascades_credentials_prompt

I also created a Thread in the Forums, where you can ask or discuss this.

Have fun with the sample app and copy/paste what you need to implement Custom Login Dialogs into your own apps.

Trackbacks and Pingbacks:

  1. Conference App Secrets Part 3: JSON vs XML | BlackBerry Developer Blog - January 10, 2014

    […] Part 2 was about a┬áCustom Credentials Dialog. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s