ShoppingGate Travel SDK For Android Native

Android Native Integration Guide (AAR + Native Facade).

Complete technical guide for embedding the ShoppingGate Travel SDK into native Android apps.

SDK Downloads

Core Flutter SDK

Compiled AAR artifacts, engine binaries, and required assets.

Download Latest SDK
Reference Project

Android Studio starter project with implementation.

View Samples

1. Prerequisites

Verification of the following environment settings is required for a successful build:

2. API & Required Parameters

To initialize the SDK, you must provide the following required payload values:

phoneNumber: User mobile number (without +).
countryCode: Example: +91, +966.
environment: development or production.
language: en or ar.
auth_code: Generated from the authentication API (expires in 5 minutes).
host_id: Your application identifier (e.g., "barq").
Validation Rule: SDK accepts only development or production in environment. Any other value will fail initialization.

3. Repository Setup

Place the ShoppingGateSDK folder in your project's root directory and keep the native helper file in host app source.

Project-Root/
├── app/
├── ShoppingGateSDK/ <-- ShoppingGate SDK folder (AAR repo)

4. Host Configuration

A. Settings Gradle

settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()

        // Local ShoppingGate SDK
        maven {
            url = uri("${rootProject.projectDir}/ShoppingGateSDK") 
        }

        // Flutter engine binaries
        maven { 
            url = uri("https://storage.googleapis.com/download.flutter.io") 
        }
    }
}

B. App-Level Gradle

app/build.gradle.kts
android {
    compileSdk = 36

    defaultConfig {
        minSdk = 24
        targetSdk = 35
        multiDexEnabled = true
    }

    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    kotlinOptions {
        jvmTarget = "11"
    }
}

dependencies {
    debugImplementation("com.shoppingate.sdk.travel:flutter_release:1.0")
    releaseImplementation("com.shoppingate.sdk.travel:flutter_release:1.0")
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}

5. Android Manifest Registry

Register the Flutter host activity. Add Google Maps key if your flow uses maps.

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <application>
        <!-- Optional: Required when map screens are used -->
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR_MAPS_KEY_HERE"/>

        <activity
            android:name="io.flutter.embedding.android.FlutterActivity"
            android:exported="false"
            android:launchMode="singleTop"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize" />
    </application>
</manifest>

6. Launching the SDK

Use the SDK native facade helper (SgTravelNative). Do not implement MethodChannel wiring directly in partner app.

Testing Credentials:
Use the following details for your sandbox environment:

phoneNumber: 554433220
countryCode: +966
auth_code: Generate using the authentication API endpoint below

Note: You must call the auth code generation API before initializing the SDK. See Authentication Flow section for details.

Authentication Flow:

To enable user login via auth code (without OTP verification), follow the steps below:

  • Step 1: Generate Auth Code
    Call the ShoppingGate API from your backend to generate an auth code. This auth code is required for silent login without OTP verification.
    API Request
    curl -X 'POST' \
      'https://microservices.shoppinggate.app/users/users/sdk/generate-auth-code' \
      -H 'accept: */*' \
      -H 'Content-Type: application/json' \
      -d '{
      "host_id": "barq",
      "host_secret": "barq-secret",
      "phone_number": "554433221",
      "phone_code": "+966",
      "device_id": "string"
    }'
    API Response
    {
      "status": true,
      "message": "Auth code generated successfully",
      "data": {
        "auth_code": "5bd9f0fb-0c7a-4250-8b44-0b35e5a1a129",
        "expires_in": 300
      }
    }
  • Step 2: Pass Auth Code to SDK
    Once you receive the auth_code from the API response, pass it to the SDK during initialization along with your host_id. The SDK will perform a silent login without requiring OTP verification.
    Kotlin Example
    SgTravelNative.initialize(
        request = SgTravelNative.SgTravelRequest(
            phoneNumber = "554433220",
            countryCode = "+966",
            environment = "development",
            language = "en",
            auth_code = "5bd9f0fb-0c7a-4250-8b44-0b35e5a1a129", // From API response
            host_id = "barq" // Your host identifier
        )
    )
    Note: The auth code expires in 300 seconds (5 minutes). Ensure you initialize the SDK within this timeframe.

A. Native Facade Helper

SgTravelNative.kt
package com.sg.travelsdkdemo

import android.app.Activity
import android.content.Context
import android.content.Intent
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.lang.ref.WeakReference

object SgTravelNative {
    const val CHANNEL_NAME: String = "shoppingate_travel_sdk/channel"
    private const val ENGINE_ID: String = "sg_travel_sdk_engine"

    // Native-to-Flutter methods.
    const val METHOD_INITIALIZE: String = "initialize"
    const val METHOD_OPEN_BOOKING_FLOW: String = "openBookingFlow"
    const val METHOD_OPEN_ORDER_FLOW: String = "openOrderFlow"

    // Flutter-to-native methods.
    const val METHOD_HOST_READY: String = "hostReady"
    const val METHOD_SET_USER_DATA: String = "setUserData"
    const val METHOD_CLOSE_SDK: String = "closeSDK"
    const val METHOD_CONTINUE_TO_PAYMENT: String = "continueToPayment"

    // Native-to-Flutter payment method.
    private const val METHOD_INVOKE_PAYMENT: String = "invokePaymentInitiation"

    private const val ERROR_MISSING_INITIAL_DATA = "MISSING_INITIAL_DATA"
    private const val ERROR_INVALID_ARGUMENT = "INVALID_ARGUMENT"
    private const val ERROR_INVALID_ENVIRONMENT = "INVALID_ENVIRONMENT"

    private const val KEY_PHONE = "phoneNumber"
    private const val KEY_COUNTRY = "countryCode"
    private const val KEY_ENV = "environment"
    private const val KEY_LANG = "language"
    private const val KEY_AUTH_CODE = "auth_code"
    private const val KEY_HOST_ID = "host_id"

    private val lock = Any()
    private var latestData: MutableMap = mutableMapOf()
    private var isInitialized: Boolean = false
    private var isFlutterReady: Boolean = false
    private var shouldDestroyEngineOnHostDestroy: Boolean = false
    private var methodChannel: MethodChannel? = null
    private val pendingCalls: MutableList = mutableListOf()
    private var hostActivityRef: WeakReference? = null

    private data class PaymentPayload(
        val amount: Double,
        val orderId: String?,
        val requestPaymentType: String?,
    )

    private data class ChannelCall(
        val method: String,
        val arguments: Any?,
    )

    data class SgTravelRequest(
        val phoneNumber: String,
        val countryCode: String,
        val environment: String,
        val language: String,
        val auth_code: String,
        val host_id: String,
    )

    @JvmStatic
    fun initialize(request: SgTravelRequest) {
        val normalized = normalizeRequest(request)

        synchronized(lock) {
            latestData = normalized
            isInitialized = true
        }

        dispatchOrQueue(
            method = METHOD_INITIALIZE,
            arguments = HashMap(normalized),
        )
    }

    @JvmStatic
    fun openBooking(context: Context) {
        ensureInitialized()
        val createdNewEngine = ensureEngineStarted(context)
        if (createdNewEngine) {
            dispatchInitialize()
        }
        launchHost(context)
        dispatchOrQueue(method = METHOD_OPEN_BOOKING_FLOW, arguments = null)
    }

    @JvmStatic
    fun openOrder(context: Context) {
        ensureInitialized()
        val createdNewEngine = ensureEngineStarted(context)
        if (createdNewEngine) {
            dispatchInitialize()
        }
        launchHost(context)
        dispatchOrQueue(method = METHOD_OPEN_ORDER_FLOW, arguments = null)
    }

    @JvmStatic
    fun setHostActivity(activity: Activity) {
        synchronized(lock) { hostActivityRef = WeakReference(activity) }
    }

    @JvmStatic
    fun clearHostActivity() {
        synchronized(lock) { hostActivityRef = null }
    }

    @JvmStatic
    fun invokePaymentInitiation(paymentResult: String) {
        val (channel, ready) = synchronized(lock) {
            Pair(methodChannel, isFlutterReady)
        }
        if (channel != null && ready) {
            channel.invokeMethod(METHOD_INVOKE_PAYMENT, paymentResult)
        }
    }

    @JvmStatic
    fun attachChannel(engine: FlutterEngine) {
        val channel = MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL_NAME)
        synchronized(lock) {
            methodChannel = channel
            isFlutterReady = false
        }

        channel.setMethodCallHandler(::handleMethodCall)
        flushPendingCallsIfReady()
    }

    private fun launchHost(context: Context) {
        val intent = SgFlutterActivity
            .withCachedEngine(ENGINE_ID)
            .destroyEngineWithActivity(false)
            .build(context)

        if (context !is Activity) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }

        context.startActivity(intent)
    }

    private fun ensureEngineStarted(context: Context): Boolean {
        if (FlutterEngineCache.getInstance().get(ENGINE_ID) != null) {
            return false
        }

        val appContext = context.applicationContext
        val loader = FlutterInjector.instance().flutterLoader()
        loader.startInitialization(appContext)
        loader.ensureInitializationComplete(appContext, emptyArray())

        val engine = FlutterEngine(appContext)
        registerPluginsSafely(engine)
        attachChannel(engine)

        val entrypoint = DartExecutor.DartEntrypoint(
            loader.findAppBundlePath(),
            "travelSdkHostEntryPoint",
        )
        engine.dartExecutor.executeDartEntrypoint(entrypoint)
        FlutterEngineCache.getInstance().put(ENGINE_ID, engine)
        return true
    }

    private fun dispatchInitialize() {
        val payload = synchronized(lock) {
            if (!isInitialized || latestData.isEmpty()) {
                null
            } else {
                HashMap(latestData)
            }
        }

        if (payload != null) {
            dispatchOrQueue(
                method = METHOD_INITIALIZE,
                arguments = payload,
            )
        }
    }

    private fun registerPluginsSafely(engine: FlutterEngine) {
        try {
            val generatedPluginRegistrant = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant")
            val registrationMethod = generatedPluginRegistrant.getDeclaredMethod(
                "registerWith",
                FlutterEngine::class.java,
            )
            registrationMethod.invoke(null, engine)
        } catch (_: Throwable) {
            // Plugin auto-registration fallback varies by embedding configuration.
        }
    }

    private fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            METHOD_HOST_READY -> {
                synchronized(lock) {
                    isFlutterReady = true
                }
                result.success(null)
                flushPendingCallsIfReady()
            }

            METHOD_SET_USER_DATA -> {
                val incoming = call.arguments as? Map<*, *>
                if (incoming == null) {
                    result.error(ERROR_INVALID_ARGUMENT, "Expected map payload", null)
                    return
                }

                try {
                    val normalized = normalizePayload(incoming)
                    synchronized(lock) {
                        latestData = normalized
                    }
                    result.success(null)
                } catch (e: ValidationException) {
                    result.error(e.code, e.message, null)
                }
            }

            METHOD_CLOSE_SDK -> {
                result.success(null)
                val host = synchronized(lock) {
                    shouldDestroyEngineOnHostDestroy = true
                    hostActivityRef?.get()
                }

                if (host != null) {
                    host.runOnUiThread { host.finish() }
                } else {
                    // No active host activity, so reset immediately.
                    resetEnginePreservingInitialization()
                }
            }

            METHOD_CONTINUE_TO_PAYMENT -> {
                val payload = parsePaymentPayload(call.arguments)
                result.success(null)
                val host = synchronized(lock) { hostActivityRef?.get() }
                host?.runOnUiThread {
                    host.startActivity(
                        PaymentActivity.newIntent(
                            context = host,
                            amount = payload.amount,
                            orderId = payload.orderId,
                            requestPaymentType = payload.requestPaymentType,
                        ),
                    )
                }
            }

            else -> result.notImplemented()
        }
    }

    private fun dispatchOrQueue(method: String, arguments: Any?) {
        val activeChannel = synchronized(lock) {
            if (methodChannel == null || !isFlutterReady) {
                pendingCalls.add(ChannelCall(method = method, arguments = arguments))
                null
            } else {
                methodChannel
            }
        }

        activeChannel?.invokeMethod(method, arguments)
    }

    private fun parsePaymentPayload(arguments: Any?): PaymentPayload {
        val mapPayload = arguments as? Map<*, *>
        if (mapPayload != null) {
            val amount = (mapPayload["total_amount"] as? Number)?.toDouble() ?: 0.0
            val orderId = mapPayload["order_id"]?.toString()
            val requestPaymentType = mapPayload["request_payment_type"]?.toString()
            return PaymentPayload(
                amount = amount,
                orderId = orderId,
                requestPaymentType = requestPaymentType,
            )
        }

        val amount = (arguments as? Number)?.toDouble() ?: 0.0
        return PaymentPayload(amount = amount, orderId = null, requestPaymentType = null)
    }

    private fun flushPendingCallsIfReady() {
        val (channel, calls) = synchronized(lock) {
            if (methodChannel == null || !isFlutterReady || pendingCalls.isEmpty()) {
                return
            }

            val snapshot = pendingCalls.toList()
            pendingCalls.clear()
            Pair(methodChannel!!, snapshot)
        }

        calls.forEach { call ->
            channel.invokeMethod(call.method, call.arguments)
        }
    }

    private fun ensureInitialized() {
        val initialized = synchronized(lock) { isInitialized }
        if (!initialized) {
            throw ValidationException(
                ERROR_MISSING_INITIAL_DATA,
                "Initialize request not found. Call SgTravelNative.initialize first.",
            )
        }
    }

    private fun normalizeRequest(request: SgTravelRequest): MutableMap {
        val raw = mapOf(
            KEY_PHONE to request.phoneNumber,
            KEY_COUNTRY to request.countryCode,
            KEY_ENV to request.environment,
            KEY_LANG to request.language,
            KEY_AUTH_CODE to request.auth_code,
            KEY_HOST_ID to request.host_id,
        )
        return normalizePayload(raw)
    }

    private fun normalizePayload(raw: Map<*, *>): MutableMap {
        val phone = readRequiredString(raw, KEY_PHONE)
        val country = readRequiredString(raw, KEY_COUNTRY)
        val env = readRequiredString(raw, KEY_ENV)
        val language = readRequiredString(raw, KEY_LANG)
        val authCode = readRequiredString(raw, KEY_AUTH_CODE)
        val hostId = readRequiredString(raw, KEY_HOST_ID)

        if (env != "development" && env != "production") {
            throw ValidationException(
                ERROR_INVALID_ENVIRONMENT,
                "Use environment: development or production",
            )
        }

        val normalized = mutableMapOf(
            KEY_PHONE to phone,
            KEY_COUNTRY to country,
            KEY_ENV to env,
            KEY_LANG to language,
            KEY_AUTH_CODE to authCode,
            KEY_HOST_ID to hostId,
        )
        return normalized
    }

    private fun readRequiredString(raw: Map<*, *>, key: String): String {
        val value = raw[key]?.toString()?.trim().orEmpty()
        if (value.isEmpty()) {
            throw ValidationException(
                ERROR_INVALID_ARGUMENT,
                "Required keys: phoneNumber, countryCode, environment, language, auth_code, host_id",
            )
        }
        return value
    }

    private class ValidationException(val code: String, message: String) : IllegalArgumentException(message)

    @JvmStatic
    fun onHostActivityDestroyed(activity: Activity) {
        val shouldReset = synchronized(lock) {
            val host = hostActivityRef?.get()
            if (host == activity) {
                hostActivityRef = null
            }

            val pending = shouldDestroyEngineOnHostDestroy
            shouldDestroyEngineOnHostDestroy = false
            pending
        }

        if (shouldReset) {
            resetEnginePreservingInitialization()
        }
    }

    private fun resetEnginePreservingInitialization() {
        synchronized(lock) {
            methodChannel?.setMethodCallHandler(null)
            methodChannel = null
            isFlutterReady = false
            shouldDestroyEngineOnHostDestroy = false
            pendingCalls.clear()
            hostActivityRef = null
        }

        FlutterEngineCache.getInstance().get(ENGINE_ID)?.destroy()
        FlutterEngineCache.getInstance().remove(ENGINE_ID)
    }

    @JvmStatic
    fun destroy() {
        synchronized(lock) {
            methodChannel?.setMethodCallHandler(null)
            methodChannel = null
            isInitialized = false
            isFlutterReady = false
            shouldDestroyEngineOnHostDestroy = false
            pendingCalls.clear()
            latestData.clear()
            hostActivityRef = null
        }

        FlutterEngineCache.getInstance().get(ENGINE_ID)?.destroy()
        FlutterEngineCache.getInstance().remove(ENGINE_ID)
    }
}

B. Create Flutter Activity

SgFlutterActivity.kt
package com.sg.travelsdkdemo

import io.flutter.embedding.android.FlutterActivity

/**
 * A thin [FlutterActivity] subclass that keeps [SgTravelNative] aware of the
 * currently visible host activity.
 *
 * This allows [SgTravelNative] to:
 *  - Properly call [finish] when the SDK requests `closeSDK`.
 *  - Start the native [PaymentActivity] when the SDK requests `continueToPayment`.
 */
class SgFlutterActivity : FlutterActivity() {

    override fun onResume() {
        super.onResume()
        SgTravelNative.setHostActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        SgTravelNative.onHostActivityDestroyed(this)
    }

    companion object {
        fun withCachedEngine(engineId: String): CachedEngineIntentBuilder =
            CachedEngineIntentBuilder(SgFlutterActivity::class.java, engineId)
    }
}

C. Host Activity Usage

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        SgTravelNative.initialize(
            request = SgTravelNative.SgTravelRequest(
                phoneNumber = "554433220",
                countryCode = "+966",
                environment = "development", // or production
                language = "en",
                auth_code = "5bd9f0fb-0c7a-4250-8b44-0b35e5a1a129", // Generated from API
                host_id = "barq", // Your host identifier
            ),
        )

        findViewById<Button>(R.id.openBooking).setOnClickListener {
            SgTravelNative.openBooking(context = this)
        }

        findViewById<Button>(R.id.openOrder).setOnClickListener {
            SgTravelNative.openOrder(context = this)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        SgTravelNative.destroy()
    }
}

D. Payment Screen UI

activity_payment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:fitsSystemWindows="true">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="20dp">

        <ImageView
            android:id="@+id/btnClose"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:src="@android:drawable/ic_menu_revert"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <TextView
            android:id="@+id/tvTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Payment Method"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintTop_toTopOf="@id/btnClose"
            app:layout_constraintBottom_toBottomOf="@id/btnClose"
            app:layout_constraintStart_toEndOf="@id/btnClose" />

        <TextView
            android:id="@+id/tvAmount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <Button
            android:id="@+id/btnPay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Pay Now"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

E. Payment Screen Logic

PaymentActivity.kt
package com.sg.travelsdkdemo

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity

/**
 * Native payment screen shown when the Travel SDK invokes `continueToPayment`.
 *
 * Displays the booking amount and lets the user initiate payment. On confirming,
 * it calls [SgTravelNative.invokePaymentInitiation] to signal back to the SDK.
 */
class PaymentActivity : AppCompatActivity() {

    private var isPaymentInitiated = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_payment)

        val amount = intent.getDoubleExtra(EXTRA_AMOUNT, 0.0)
        val orderId = intent.getStringExtra(EXTRA_ORDER_ID)

        val tvAmount = findViewById(R.id.tvAmount)
        val btnPay = findViewById(R.id.btnPay)
        val btnClose = findViewById(R.id.btnClose)

        tvAmount.text = getString(R.string.payment_amount_label, amount)

        btnPay.setOnClickListener {
            if (!isPaymentInitiated) {
                isPaymentInitiated = true
                btnPay.isEnabled = false
                btnPay.text = getString(R.string.payment_initiated_label)
                val transactionId = buildTransactionId(orderId)
                SgTravelNative.invokePaymentInitiation(transactionId)
                // Automatically close the payment screen after payment initiation
                btnClose.postDelayed({ finish() }, 300)
            }
        }

        btnClose.setOnClickListener {
            finish()
        }
    }

    companion object {
        private const val EXTRA_AMOUNT = "extra_amount"
        private const val EXTRA_ORDER_ID = "extra_order_id"
        private const val EXTRA_REQUEST_PAYMENT_TYPE = "extra_request_payment_type"

        fun newIntent(
            context: Context,
            amount: Double,
            orderId: String?,
            requestPaymentType: String?,
        ): Intent =
            Intent(context, PaymentActivity::class.java)
                .putExtra(EXTRA_AMOUNT, amount)
                .putExtra(EXTRA_ORDER_ID, orderId)
                .putExtra(EXTRA_REQUEST_PAYMENT_TYPE, requestPaymentType)

        private fun buildTransactionId(orderId: String?): String {
            val timestamp = System.currentTimeMillis()
            return if (!orderId.isNullOrBlank()) {
                "TXN_${orderId}_$timestamp"
            } else {
                "TXN_$timestamp"
            }
        }
    }
}

7. Troubleshooting

  • Initialize First: Call initialize before openBooking/openOrder.
  • Environment Validation: Use only development or production.
  • No Direct MethodChannel: Keep partner app on SgTravelNative facade API only.
  • Maps Issues: Ensure API key is enabled for Maps SDK and restricted with package/SHA.