diff --git a/java/app/build.gradle.kts b/java/app/build.gradle.kts new file mode 100644 index 0000000..7668573 --- /dev/null +++ b/java/app/build.gradle.kts @@ -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") +} \ No newline at end of file diff --git a/java/app/proguard-rules.pro b/java/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/java/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/java/app/src/androidTest/java/com/google/android/gms/samples/wallet/ExampleInstrumentedTest.kt b/java/app/src/androidTest/java/com/google/android/gms/samples/wallet/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bdf0662 --- /dev/null +++ b/java/app/src/androidTest/java/com/google/android/gms/samples/wallet/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/java/app/src/main/AndroidManifest.xml b/java/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..50bb1b9 --- /dev/null +++ b/java/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/app/src/main/java/com/google/android/gms/samples/wallet/MainActivity.java b/java/app/src/main/java/com/google/android/gms/samples/wallet/MainActivity.java new file mode 100644 index 0000000..ef975fd --- /dev/null +++ b/java/app/src/main/java/com/google/android/gms/samples/wallet/MainActivity.java @@ -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); + } + } +} \ No newline at end of file diff --git a/java/app/src/main/java/com/google/android/gms/samples/wallet/viewmodel/WalletViewModel.java b/java/app/src/main/java/com/google/android/gms/samples/wallet/viewmodel/WalletViewModel.java new file mode 100644 index 0000000..318e338 --- /dev/null +++ b/java/app/src/main/java/com/google/android/gms/samples/wallet/viewmodel/WalletViewModel.java @@ -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 _canAddPasses = new MutableLiveData<>(); + public final LiveData 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"; +} \ No newline at end of file diff --git a/java/app/src/main/res/drawable-v21/add_to_google_wallet_button_background.xml b/java/app/src/main/res/drawable-v21/add_to_google_wallet_button_background.xml new file mode 100644 index 0000000..18ae7ca --- /dev/null +++ b/java/app/src/main/res/drawable-v21/add_to_google_wallet_button_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/drawable-xxhdpi/ts_10_11019a.jpg b/java/app/src/main/res/drawable-xxhdpi/ts_10_11019a.jpg new file mode 100644 index 0000000..f744070 Binary files /dev/null and b/java/app/src/main/res/drawable-xxhdpi/ts_10_11019a.jpg differ diff --git a/java/app/src/main/res/drawable/add_to_google_wallet_button_background.xml b/java/app/src/main/res/drawable/add_to_google_wallet_button_background.xml new file mode 100644 index 0000000..b49cbe8 --- /dev/null +++ b/java/app/src/main/res/drawable/add_to_google_wallet_button_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/drawable/add_to_google_wallet_button_content.xml b/java/app/src/main/res/drawable/add_to_google_wallet_button_content.xml new file mode 100644 index 0000000..41adec8 --- /dev/null +++ b/java/app/src/main/res/drawable/add_to_google_wallet_button_content.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/java/app/src/main/res/drawable/check_circle.xml b/java/app/src/main/res/drawable/check_circle.xml new file mode 100644 index 0000000..324bc8b --- /dev/null +++ b/java/app/src/main/res/drawable/check_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/java/app/src/main/res/drawable/ic_launcher_background.xml b/java/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/java/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/app/src/main/res/drawable/ic_launcher_foreground.xml b/java/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/java/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/layout/activity_save_pass.xml b/java/app/src/main/res/layout/activity_save_pass.xml new file mode 100644 index 0000000..5bf27b7 --- /dev/null +++ b/java/app/src/main/res/layout/activity_save_pass.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/layout/add_to_google_wallet_button.xml b/java/app/src/main/res/layout/add_to_google_wallet_button.xml new file mode 100644 index 0000000..8995f0a --- /dev/null +++ b/java/app/src/main/res/layout/add_to_google_wallet_button.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/java/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/java/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/java/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/java/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/java/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/java/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/java/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/java/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/java/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/java/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/java/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/java/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/java/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/java/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/java/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/java/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/java/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/java/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/java/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/java/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/java/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/java/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/java/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/java/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/java/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/java/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/java/app/src/main/res/values/colors.xml b/java/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..26999d8 --- /dev/null +++ b/java/app/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #42CCCCCC + \ No newline at end of file diff --git a/java/app/src/main/res/values/dimens.xml b/java/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..2bb0c32 --- /dev/null +++ b/java/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 48dp + \ No newline at end of file diff --git a/java/app/src/main/res/values/strings.xml b/java/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..326af08 --- /dev/null +++ b/java/app/src/main/res/values/strings.xml @@ -0,0 +1,23 @@ + + + + + Google Wallet Sample + Unfortunately, Google Wallet is not available on this phone. + The pass was added successfully! + Add to Google Wallet + diff --git a/java/app/src/main/res/values/themes.xml b/java/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..8a14273 --- /dev/null +++ b/java/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +