Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Java integration for the Google Wallet API #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions java/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
plugins {
id("com.android.application")
}

android {
namespace = "com.google.android.gms.samples.wallet"
compileSdk = 34

defaultConfig {
applicationId = "com.google.android.gms.samples.wallet"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

viewBinding {
enable = true
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {

// Google Wallet dependencies
implementation("com.google.android.gms:play-services-pay:16.4.0")

implementation("androidx.appcompat:appcompat:1.6.1")

val lifecycle_version = "2.5.1"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.4")
}
21 changes: 21 additions & 0 deletions java/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.google.android.gms.samples.wallet

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.google.android.gms.samples.wallet", appContext.packageName)
}
}
27 changes: 27 additions & 0 deletions java/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GoogleWalletSample">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GoogleWalletSample">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.gms.samples.wallet;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.activity.ComponentActivity;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.IntentSenderRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;

import com.google.android.gms.pay.PayClient;
import com.google.android.gms.samples.wallet.databinding.ActivitySavePassBinding;
import com.google.android.gms.samples.wallet.viewmodel.WalletViewModel;

public class MainActivity extends ComponentActivity {

private WalletViewModel model;

private static final int ADD_TO_GOOGLE_WALLET_REQUEST_CODE = 999;

private View addToGoogleWalletButtonContainer;
private View addToGoogleWalletButton;

/**
* Initialize the Google Pay API on creation of the activity
*
* @see android.app.Activity#onCreate(android.os.Bundle)
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializeUi();

// Check Google Pay availability
model = new ViewModelProvider(this).get(WalletViewModel.class);

// Check out Google Wallet availability
model.canAddPasses.observe(this, this::setAddToGoogleWalletAvailable);
}

private void initializeUi() {

// Use view binding to access the UI elements
ActivitySavePassBinding layoutBinding = ActivitySavePassBinding.inflate(getLayoutInflater());
setContentView(layoutBinding.getRoot());

addToGoogleWalletButton = layoutBinding.addToGoogleWalletButton.getRoot();
addToGoogleWalletButtonContainer = layoutBinding.passContainer;
addToGoogleWalletButton.setOnClickListener(v -> {
addToGoogleWalletButton.setClickable(false);
model.savePassesJwt(model.genericObjectJwt, this, ADD_TO_GOOGLE_WALLET_REQUEST_CODE);
});
}

/**
* If getPayApiAvailabilityStatus returned {@code true}, show the "Add to Google Wallet" button.
* Otherwise, notify the user that Google Wallet is not available. Please adjust to fit in with
* your current user flow. You are not required to explicitly let the user know if isReadyToPay
* returns {@code false}.
*
* @param available isReadyToPay API response.
*/
private void setAddToGoogleWalletAvailable(boolean available) {
if (available) {
addToGoogleWalletButtonContainer.setVisibility(View.VISIBLE);
} else {
Toast.makeText(
this,
R.string.google_wallet_status_unavailable,
Toast.LENGTH_LONG).show();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);

if (requestCode == ADD_TO_GOOGLE_WALLET_REQUEST_CODE) {
switch (resultCode) {
case RESULT_OK: {
Toast
.makeText(this, getString(R.string.add_google_wallet_success), Toast.LENGTH_LONG)
.show();
break;
}

case RESULT_CANCELED: {
// TODO Handle save canceled
break;
}

case PayClient.SavePassesResult.SAVE_ERROR: {
if (data != null) {
String apiErrorMessage = data.getStringExtra(PayClient.EXTRA_API_ERROR_MESSAGE);
// TODO Handle API error
}
break;
}

default: // TODO Handle other outcomes
}

addToGoogleWalletButton.setClickable(true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.gms.samples.wallet.viewmodel;

import android.app.Activity;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.google.android.gms.pay.Pay;
import com.google.android.gms.pay.PayApiAvailabilityStatus;
import com.google.android.gms.pay.PayClient;

public class WalletViewModel extends AndroidViewModel {

// A client to interact with the Google Wallet API
private final PayClient walletClient;

// LiveData with the result of whether the user can save passes with Google Wallet
private final MutableLiveData<Boolean> _canAddPasses = new MutableLiveData<>();
public final LiveData<Boolean> canAddPasses = _canAddPasses;

public WalletViewModel(Application application) {
super(application);
walletClient = Pay.getClient(application);

fetchCanAddPassesToGoogleWallet();
}

/**
* Determine whether the API to save passes to Google Pay is available on the device.
*/
private void fetchCanAddPassesToGoogleWallet() {
walletClient
.getPayApiAvailabilityStatus(PayClient.RequestType.SAVE_PASSES)
.addOnSuccessListener(
status -> _canAddPasses.setValue(status == PayApiAvailabilityStatus.AVAILABLE))
// If the API is not available, we recommend to either:
// 1) Hide the save button
// 2) Fall back to a different Save Passes integration (e.g. JWT link)
// Note that a user might become eligible in the future.

// Google Play Services is too old. API availability can't be verified.
.addOnFailureListener(exception -> _canAddPasses.setValue(false));
}

/**
* Exposes the `savePassesJwt` method in the wallet client
*/
public void savePassesJwt(String jwtString, Activity activity, int requestCode) {
walletClient.savePassesJwt(jwtString, activity, requestCode);
}

/**
* Exposes the `savePasses` method in the wallet client
*/
public void savePasses(String objectString, Activity activity, int requestCode) {
walletClient.savePasses(objectString, activity, requestCode);
}

// Test generic object used to be created against the API
// See https://developers.google.com/wallet/tickets/boarding-passes/web#json_web_token_jwt for more details
public final String genericObjectJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJnb29nbGUiLCJwYXlsb2FkIjp7ImdlbmVyaWNPYmplY3RzIjpbeyJpZCI6IjMzODgwMDAwMDAwMjIwOTUxNzcuZjUyZDRhZjYtMjQxMS00ZDU5LWFlNDktNzg2ZDY3N2FkOTJiIn1dfSwiaXNzIjoid2FsbGV0LWxhYi10b29sc0BhcHBzcG90LmdzZXJ2aWNlYWNjb3VudC5jb20iLCJ0eXAiOiJzYXZldG93YWxsZXQiLCJpYXQiOjE2NTA1MzI2MjN9.ZURFHaSiVe3DfgXghYKBrkPhnQy21wMR9vNp84azBSjJxENxbRBjqh3F1D9agKLOhrrflNtIicShLkH4LrFOYdnP6bvHm6IMFjqpUur0JK17ZQ3KUwQpejCgzuH4u7VJOP_LcBEnRtzZm0PyIvL3j5-eMRyRAo5Z3thGOsKjqCPotCAk4Z622XHPq5iMNVTvcQJaBVhmpmjRLGJs7qRp87sLIpYOYOkK8BD7OxLmBw9geqDJX-Y1zwxmQbzNjd9z2fuwXX66zMm7pn6GAEBmJiqollFBussu-QFEopml51_5nf4JQgSdXmlfPrVrwa6zjksctIXmJSiVpxL7awKN2w";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/accent_alpha26">
<item>
<shape>
<size android:width="270dp" android:height="48dp"/>
<solid android:color="#1f1f1f"/>
<stroke android:color="#747775" android:width="1dp"/>
<corners android:radius="24dp"/>
</shape>
</item>
</ripple>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<size android:width="270dp" android:height="48dp"/>
<solid android:color="#1f1f1f"/>
<stroke android:color="#747775" android:width="1dp"/>
<corners android:radius="24dp"/>
</shape>
</item>
</layer-list>
Loading