Explore our sample app
Get the sample code used during this guide, you can either download it our explore it directly on GitHub.
Download Code View on Github

Building a (native) mobile app

Introduction

This guide will walk you trough the process of building your own native mobile application (Android) which will interact with a blockchain via Arkane. After this intro you will understand the different Arkane components and be able to integrate all features from the Reference documentation.

What you’ll build

We’ll build an Android app that will:

  • Authenticate the user with Arkane (OAuth2)

  • Enable the user to manage his wallets connected to our application

  • Fetch a list of the user’s wallets

In this example, the following components are used Arkane Identity Arkane API Arkane Connect.

Prerequisities

In order to follow this quick start guide we expect you have the Android Development tools installed and have a minimal understanding of Java / Android apps.

Let’s get started

We’ll start by setting up our development environment. Open a terminal window, checkout the sample project, called mobile-android-example, from GitHub and build the project.

git clone https://github.com/ArkaneNetwork/mobile-android-example.git
cd mobile-android-example

Building your project:

Make Project

Running the project:

Run

After clicking run, you have to setup a virtual device (or connect your android phone). When everything is setup correctly, the app should start on your device:

Device

Authentication

Arkane uses OpenID connect for user authentication. For more information on the authorization flow, please check our Reference documentation.

You can choose to implement the OpenId connect flow yourself or use an existing library:

In our example we will be using AppAuth. First we need to add it as a dependency inside the build.gradle of the app:

Configure AppAuth library

/package.json
dependencies {
    implementation group: 'net.openid', name: 'appauth', version: '0.7.1'
}

We also need a config file that will configure the library:

/package.json
{
  "client_id": "Arketype",
  "redirect_uri": "network.arkane.arketype://oauth2redirect",
  "authorization_scope": "openid",
  "discovery_uri": "https://login-staging.arkane.network/auth/realms/Arkane/.well-known/openid-configuration",
  "https_required": true
}
Table 1. Explanation of the fields
Field Explanation

client_id

The id of the client, on staging you can use Arkane. For production please request a personal client id.

redirect_uri

To where does it need to redirect when authentication is done. Important note, this must be equal to the value inside the defaultConfig of the app.gradle: manifestPlaceholders = ["appAuthRedirectScheme": "network.arkane.arketype"]

discovery_uri

Discovery endpoint for all other endpoints that are necessary for authentication.

Implement LoginActivity

onCreate

In the onCreate section we will setup everything that is necessary for the authentication:

/app/src/main/java/LoginActivity.java
        (1)
        mAuthStateManager = AuthStateManager.getInstance(this);
        mConfiguration = Configuration.getInstance(this);

        (2)
        if (mAuthStateManager.getCurrent().isAuthorized()
                && !mConfiguration.hasConfigurationChanged()) {
            Log.i(TAG, "User is already authenticated, proceeding to token activity");
            startActivity(new Intent(this, TokenActivity.class));
            finish();
            return;
        }

        (3)
        if (!mConfiguration.isValid()) {
            displayError(mConfiguration.getConfigurationError(), false);
            return;
        }

        (4)
        if (getIntent().getBooleanExtra(EXTRA_FAILED, false)) {
            displayAuthCancelled();
        }

        (5)
        mExecutor.submit(this::initializeAppAuth);

        @WorkerThread
        private void initializeAppAuth() {
            Log.i(TAG, "Initializing AppAuth");
            recreateAuthorizationService();

            if (mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration() != null) {
                // configuration is already created, skip to client initialization
                Log.i(TAG, "auth config already established");
                initializeClient();
                return;
            }

            (6)
            // if we are not using discovery, build the authorization service configuration directly
            // from the static configuration values.
            if (mConfiguration.getDiscoveryUri() == null) {
                Log.i(TAG, "Creating auth config from res/raw/auth_config.json");
                AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(
                        mConfiguration.getAuthEndpointUri(),
                        mConfiguration.getTokenEndpointUri(),
                        mConfiguration.getRegistrationEndpointUri());

                mAuthStateManager.replace(new AuthState(config));
                initializeClient();
                return;
            }

            (7)
            // WrongThread inference is incorrect for lambdas
            // noinspection WrongThread
            runOnUiThread(() -> displayLoading("Retrieving discovery document"));
            Log.i(TAG, "Retrieving OpenID discovery doc");
            AuthorizationServiceConfiguration.fetchFromUrl(
                    mConfiguration.getDiscoveryUri(),
                    this::handleConfigurationRetrievalResult,
                    mConfiguration.getConnectionBuilder());
        }

        (8)
        private void recreateAuthorizationService() {
            if (mAuthService != null) {
                Log.i(TAG, "Discarding existing AuthService instance");
                mAuthService.dispose();
            }
            mAuthService = createAuthorizationService();
            mAuthRequest.set(null);
            mAuthIntent.set(null);
        }

        private AuthorizationService createAuthorizationService() {
            Log.i(TAG, "Creating authorization service");
            AppAuthConfiguration.Builder builder = new AppAuthConfiguration.Builder();
            builder.setBrowserMatcher(AnyBrowserMatcher.INSTANCE); // <5.c.i>
            builder.setConnectionBuilder(mConfiguration.getConnectionBuilder());

            return new AuthorizationService(this, builder.build());
        }

        (9)
        @WorkerThread
        private void initializeClient() {
            if (mConfiguration.getClientId() != null) {
                Log.i(TAG, "Using static client ID: " + mConfiguration.getClientId());
                // use a statically configured client ID
                mClientId.set(mConfiguration.getClientId());
                runOnUiThread(this::initializeAuthRequest);
                return;
            }

            RegistrationResponse lastResponse =
                    mAuthStateManager.getCurrent().getLastRegistrationResponse();
            if (lastResponse != null) {
                Log.i(TAG, "Using dynamic client ID: " + lastResponse.clientId);
                // already dynamically registered a client ID
                mClientId.set(lastResponse.clientId);
                runOnUiThread(this::initializeAuthRequest);
                return;
            }


            // WrongThread inference is incorrect for lambdas
            // noinspection WrongThread
            runOnUiThread(() -> displayLoading("Dynamically registering client"));
            Log.i(TAG, "Dynamically registering client");

            RegistrationRequest registrationRequest = new RegistrationRequest.Builder(
                    mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
                    Collections.singletonList(mConfiguration.getRedirectUri()))
                    .setTokenEndpointAuthenticationMethod(ClientSecretBasic.NAME)
                    .build();

            mAuthService.performRegistrationRequest(
                    registrationRequest,
                    this::handleRegistrationResponse);
        }

        (11)
        @MainThread
        private void initializeAuthRequest() {
            createAuthRequest("");
            warmUpBrowser();
        }

        (12)
        private void createAuthRequest(@Nullable String loginHint) {
            Log.i(TAG, "Creating auth request for login hint: " + loginHint);
            AuthorizationRequest.Builder authRequestBuilder = new AuthorizationRequest.Builder(
                    mAuthStateManager.getCurrent().getAuthorizationServiceConfiguration(),
                    mClientId.get(),
                    ResponseTypeValues.CODE,
                    mConfiguration.getRedirectUri())
                    .setScope(mConfiguration.getScope());

            if (!TextUtils.isEmpty(loginHint)) {
                authRequestBuilder.setLoginHint(loginHint);
            }
            HashMap<String, String> additionalParameters = new HashMap<>();
            // you can enforce your users to use a specific IDP like: google or facebook
            // additionalParameters.put("kc_idp_hint", "google");
            authRequestBuilder.setAdditionalParameters(additionalParameters);
            mAuthRequest.set(authRequestBuilder.build());
        }

        (13)
        private void warmUpBrowser() {
            mAuthIntentLatch = new CountDownLatch(1);
            mExecutor.execute(() -> {
                Log.i(TAG, "Warming up browser instance for auth request");
                CustomTabsIntent.Builder intentBuilder =
                        mAuthService.createCustomTabsIntentBuilder(mAuthRequest.get().toUri());
                intentBuilder.setToolbarColor(getColorCompat(R.color.colorPrimary));
                mAuthIntent.set(intentBuilder.build());
                mAuthIntentLatch.countDown();
            });
        }

        (10)
        @MainThread
        private void handleRegistrationResponse(
                RegistrationResponse response,
                AuthorizationException ex) {
            mAuthStateManager.updateAfterRegistration(response, ex);
            if (response == null) {
                Log.i(TAG, "Failed to dynamically register client", ex);
                displayErrorLater("Failed to register client: " + ex.getMessage(), true);
                return;
            }

            Log.i(TAG, "Dynamically registered client: " + response.clientId);
            mClientId.set(response.clientId);
            initializeAuthRequest();
        }
  • Create the state manager for the authentication and parse the configuration (the json file) 1

  • If the user is already authenticated, start the next intent 2

  • If configuration is invalid, show an error 3

  • When auth failed, call a function to handle this 4

  • Setup of the AppAuth library 5

    • You can specify each endpoint seperatly if preferred 6

    • Use discovery endpoint for getting all the correct endpoints 7

    • Create the authorization service 8

      • Select which browser to use for authentication, use ANY for auto selection 9

    • Initialize the client with client id etc.

      • Initialize the authentication request 11

      • Create the authentication request 12

      • Warmup browser (performance optimization) 13

    • Handle the registration response 10

This code block only contains snippets, please checkout the full source on GitHub

Authenticating a user

When a user clicks a button, a custom tab should open where the user can log into Arkane. When this is done correctly, he will return to the app.

/app/src/main/java/LoginActivity.java
    (1)
    findViewById(R.id.start_auth).setOnClickListener((View view) -> startAuth());

    (2)
    @MainThread
    void startAuth() {
        displayLoading("Making authorization request");

        // WrongThread inference is incorrect for lambdas
        // noinspection WrongThread
        mExecutor.submit(this::doAuth);
    }

    (3)
    @WorkerThread
    private void doAuth() {
        try {
            mAuthIntentLatch.await();
        } catch (InterruptedException ex) {
            Log.w(TAG, "Interrupted while waiting for auth intent");
        }
        Intent intent = mAuthService.getAuthorizationRequestIntent(
                mAuthRequest.get(),
                mAuthIntent.get());
        startActivityForResult(intent, RC_AUTH);
    }
1 Add an on click listener when a user wants to authenticate
2 Submit the authentication to the executor (separate thread)
3 Create an authentication intent and start it

Wallets

Manage wallets

As an application, it is possible to have a user manage his wallets for a specific blockchain. During this action, the user can link existing wallets or import a wallet. When the user returns to the app, a wallet will be linked to your application for the given blockchain. When a user does not have any wallets, he can indicate to create a new wallet.

To manage wallets, a specific url needs to be opened in the browser (using custom tabs). It is not possible to do this directly in the background since the user needs to verify each change using his PIN. For security reasons, we cannot allow the confirmation to happen through a 3rd party application.

The endpoint to manage wallets:

GET https://connect-staging.arkane.network/wallets/manage?redirectUri={redirectUri}&bearerToken={bearerToken}&data={data}
Table 2. Query parameters
Name Description Example

redirectUri

Needs to be replaced with a URI to which should be redirected after, in our example we will use this url to give focus back to our app

network.arkane://callback

bearerToken

The bearer token (access token) you get back from the authentication service

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiS…​

data

a Base64 encoded json object containing the chain you would like to manage. Possible values are ethereum and vechain. E.g. Base 64 encoded {"chain": "ethereum"}

eyJjaGFpbiI6ICJldGhlcmV1bSJ9

Make sure all query parameters are url encoded (ex. https://www.urlencoder.org/)

When the user returns to the application, the status (SUCCESS or ABORTED) of the user’s actions will be returned as a requestParameter added to the redirectUri (= Intent). In our ArkaneCallbackActivity you can access this by calling getIntent().getData().getQueryParameter(“status”).

Example

/app/src/main/java/TokenActivity.java
    (1)
    Button manageWalletsButton = (Button) findViewById(R.id.manage_wallets);
            manageWalletsButton.setOnClickListener((View view) -> manageWallets());

    public void manageWallets() {
        AuthState state = mStateManager.getCurrent(); (2)
        state.performActionWithFreshTokens(mAuthService, (accessToken, idToken, ex) -> { (3)
            String url = "https://connect-staging.arkane.network/wallets/manage?redirectUri=network.arkane://callback&data=eyJjaGFpbiI6ICJldGhlcmV1bSJ9&bearerToken=" + accessToken;
            CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
            CustomTabsIntent customTabsIntent = builder.build();
            customTabsIntent.launchUrl(this, Uri.parse(url)); (4)
        });
    }
1 Add a click listener to manage wallets
2 Get current authentication state
3 Get a valid access token
4 Open a custom tabs intent with the correct URL

Upon returning to the application, we can handle the status as follows:

/app/src/main/java/network/arkane/arketype/ArkaneCallbackActivity.java
    @Override
    public void onCreate(Bundle savedInstanceBundle) {
        super.onCreate(savedInstanceBundle);

        if (getIntent().getData() != null) { (1)
            final String status = getIntent().getData().getQueryParameter("status"); (2)
            if ("success".equalsIgnoreCase(status)) { (3)
                Toast.makeText(getApplicationContext(),
                               "You successfully linked wallets to this application",
                               Toast.LENGTH_LONG)
                     .show();
            } else if ("aborted".equalsIgnoreCase(status)) { (4)
                Toast.makeText(getApplicationContext(),
                               "You did not change the wallets linked to this application",
                               Toast.LENGTH_LONG)
                     .show();
            }
        }

        Intent intent = new Intent(this, TokenActivity.class);
        startActivity(intent);
        finish();
    }
1 Check if the intent has data
2 Extract the status from the intent data
3 Handle status is 'success'
4 Handle status is 'aborted'

This allows users to link their existing wallets with your application.

The difference with Manage wallets:

  • A user can only link wallets, it is not possible to create or import a wallet

  • A list of all blockchain wallets is returned. (It is possible to filter based on blockchain).

An example would be a portfolio app where a user wants to quickly link all his wallets to get an overview of his complete portfolio.

The endpoint to link wallets:

GET https://connect-staging.arkane.network/wallets/link?redirectUri={redirectUri}&bearerToken={bearerToken}
Table 3. Query parameters
Name Description Example

redirectUri

Needs to be replaced with a URI to which should be redirected after, in our example we will use this url to give focus back to our app

network.arkane://callback

bearerToken

The bearer token (access token) you get back from the authentication service

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiS…​

Example

/app/src/main/java/network/arkane/arketype/TokenActivity.java
    (1)
    Button linkWalletsButton = (Button) findViewById(R.id.link_wallets);
    linkWalletsButton.setOnClickListener((View view) -> linkWallets());

    public void linkWallets() {
        AuthState state = mStateManager.getCurrent(); (2)
        state.performActionWithFreshTokens(mAuthService, (accessToken, idToken, ex) -> { (3)
            String url = "https://connect-staging.arkane.network/wallets/link?redirectUri=network.arkane://callback&bearerToken=" + accessToken;
            CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
            CustomTabsIntent customTabsIntent = builder.build();
            customTabsIntent.launchUrl(this, Uri.parse(url)); (4)
        });
    }
1 Add a click listener to link wallets
2 Get current authentication state
3 Get a valid access token
4 Open a custom tabs intent with the correct URL

Upon returning to the application, we can handle the status as follows:

/app/src/main/java/network/arkane/arketype/ArkaneCallbackActivity.java
    @Override
    public void onCreate(Bundle savedInstanceBundle) {
        super.onCreate(savedInstanceBundle);

        if (getIntent().getData() != null) { (1)
            final String status = getIntent().getData().getQueryParameter("status"); (2)
            if ("success".equalsIgnoreCase(status)) { (3)
                Toast.makeText(getApplicationContext(),
                               "You successfully linked wallets to this application",
                               Toast.LENGTH_LONG)
                     .show();
            } else if ("aborted".equalsIgnoreCase(status)) { (4)
                Toast.makeText(getApplicationContext(),
                               "You did not change the wallets linked to this application",
                               Toast.LENGTH_LONG)
                     .show();
            }
        }

        Intent intent = new Intent(this, TokenActivity.class);
        startActivity(intent);
        finish();
    }
1 Check if the intent has data
2 Extract the status from the intent data
3 Handle status is 'success'
4 Handle status is 'aborted'

View wallets

If you want to retrieve the wallets for a user, you can call the API endpoint for listing user wallets.

Example

/app/src/main/java/network/arkane/arketype/TokenActivity.java
    (1)
    Button getWalletsButton = (Button) findViewById(R.id.get_wallets);
    getWalletsButton.setOnClickListener((View view) -> getWallets());

    private void getWallets() {
        mExecutor.submit(() -> {
            AuthState state = mStateManager.getCurrent(); (2)
            state.performActionWithFreshTokens(mAuthService, (accessToken, idToken, ex) -> { (3)
                List<Wallet> wallets = arkaneClient.getWallets(accessToken); (4)
                runOnUiThread(() -> {
                    openWallets(wallets);
                });
            });
        });
    }

    @MainThread
    private void openWallets(List<Wallet> wallets) {
        Intent intent = new Intent(this, WalletListActivity.class);
        intent.putExtra("wallets", new WalletListIntentData(wallets));
        startActivity(intent); (5)
    }
/app/src/main/java/network/arkane/arketype/client/ArkaneClient.java
    public List<Wallet> getWallets(String accessToken) {
        try {
            URL walletsEndpoint = new URL("https://api-staging.arkane.network/api/wallets");
            HttpURLConnection conn =
                    (HttpURLConnection) walletsEndpoint.openConnection();
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);
            conn.setInstanceFollowRedirects(false);
            String response = Okio.buffer(Okio.source(conn.getInputStream()))
                    .readString(Charset.forName("UTF-8"));
            return mapToWallets(response);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
1 Add a click listener to get wallets
2 Get current authentication state
3 Get a valid access token
4 Use the arkane client to retrieve the wallets
5 Open new intent with wallets

Summary

Congratulations! You’ve just built an Android app that is able to:

  • Authenticate a user with Arkane (OAuth2)

  • Enable a user to manage his wallets connected to our application

  • Fetch a list of a user’s wallets

In this example, the following components were used Arkane Identity Arkane API Arkane Connect.

The sample code used during this guide can either be downloaded or explored on GitHub.

What’s next

Now that you’ve mastered the basics you can dive deeper in the different building blocks or explore all our functionalities to transform the sample app into your own personal wallet.

If at any time you get stuck and need some help or advise, don’t hesitate to join our Telegram channel, we are glad to help!

^