Skip to content

Conversation

@JorgeMucientes
Copy link
Contributor

@JorgeMucientes JorgeMucientes commented Dec 11, 2025

Part of WOOMOB-1859

⚠️ Do not merge until we figure out were exactly this changes must be added (hotfix, betafix?)

Description

This PR integrates Play Age Signals API in Woo Android.

The added logic works as follows:

  • Every time the app is opened (after being previously killed) we'll check the Google Age Signal API values to verify if the user is still eligible to use the app.
  • If the user is ineligible (more on when this could happen) the we'll log the user out and show a dialog that the access to the app is restricted for them based on their age.

When is a user ineligible to use WooCommerce app? Discussion p2y3YZ-anO-p2#comment-24048.
According to WooCommerce TOS our app usage is expected for 13+ years old users:

All of our apps have a required age of 13+ and so we would need the app stores to return a result showing the user is at least that age. Parents can also revoke consent for a minor in their family account; the API should be capable of informing us of such revocations and we’ll need to figure out how to honor those.

With the TOS in mind the logic to determine if a user is eligible will be based on the 4 different user statuses the API provides:

Screenshot 2025-12-12 at 18 11 47
  • Verified users will be granted access bu default
  • Supervised users above +13 plus we'll be granted access by default.
  • Supervised users whose access was denied will be log out and access restricted via a blocking dialog. See screen recording below.
  • Any other case were we fail to retrieve the userStatus, we'll grant access by default.

Here's a diagram to help better understand the different flows:

Screenshot 2025-12-22 at 17 37 32

Test Steps

We currently can't test in prod, because we app needs to be downloaded from Google Play in order for the API to work. For that Google provides a FakeAgeSignalsManager() that enables us to test the different scenarios.

Apply the following patch in order to simulate the different API responses we'll get from the Play Signals API:

Use_FakeAgeSignalsManager_to_simulate_supervised_users.patch

For each of the following tests you'll need to open AgeSignalsClient.kt. and update the mocked userStatus

Test Case 1: User 18+ (Verified)

  • Set userStatus to VERIFIED
  • Open the app
  • Log in as usual

Test Case 2: Approved 13+ user (Supervised)

  • Set userStatus to SUPERVISED
  • Set ageUpper to 14 (use setAgeUpper(14))
  • Open the app
  • Log in as usual

Test Case 3: Supervised user under 13

  • Set userStatus to SUPERVISED_APPROVAL_PENDING
  • Set ageUpper to 12
  • Open the app
  • Logged out and restricted access for being below 13+ years old

Test Case 4: Supervised user approval denied

  • Set userStatus to SUPERVISED_APPROVAL_DENIED
  • No matter what the age is.
  • Open the app
  • You'll be logged out and a dialog displayed saying you Google account is restricted from using the app.

Images/gif

Screen_recording_20251219_165448.mp4
  • I have considered if this change warrants release notes and have added them to RELEASE-NOTES.txt if necessary. Use the "[Internal]" label for non-user-facing changes.

@dangermattic
Copy link
Collaborator

dangermattic commented Dec 11, 2025

1 Error
🚫 This PR is tagged with status: do not merge label(s).
1 Warning
⚠️ Class GoogleAgeSignalsClient is missing tests, but unit-tests-exemption label was set to ignore this.
1 Message
📖

This PR contains changes to Tracks-related logic. Please ensure (author and reviewer) the following are completed:

  • The tracks events must be validated in the Tracks system.
  • Verify the internal Tracks spreadsheet has also been updated.
  • Please consider registering any new events.
  • The PR must be assigned the category: tracks label.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 11, 2025

Project dependencies changes

list
+ New Dependencies
com.google.android.play:age-signals:0.0.2

! Upgraded Dependencies
com.google.android.gms:play-services-basement:18.9.0, (changed from 18.5.0)
tree
 +--- com.google.firebase:firebase-messaging -> 25.0.1
 |    +--- com.google.firebase:firebase-common:22.0.1
 |    |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0 -> 1.10.2
 |    |    |    \--- com.google.android.gms:play-services-tasks:16.0.1 -> 18.2.0
-|    |    |         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0
+|    |    |         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0
-|    |    \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.9.0 (*)
 |    +--- com.google.firebase:firebase-iid-interop:17.1.0
-|    |    \--- com.google.android.gms:play-services-basement:17.0.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:17.0.0 -> 18.9.0 (*)
 |    +--- com.google.firebase:firebase-measurement-connector:19.0.0
-|    |    \--- com.google.android.gms:play-services-basement:17.0.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:17.0.0 -> 18.9.0 (*)
 |    +--- com.google.android.gms:play-services-base:18.1.0 -> 18.5.0
-|    |    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
-|    +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.5.0 (*)
+|    +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.9.0 (*)
 |    +--- com.google.android.gms:play-services-cloud-messaging:17.2.0
-|    |    \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.9.0 (*)
 |    \--- com.google.android.gms:play-services-stats:17.0.2
-|         \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.9.0 (*)
 +--- com.google.firebase:firebase-config -> 23.0.1
 |    \--- com.google.firebase:firebase-abt:21.1.1
-|         \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 +--- com.google.firebase:firebase-analytics -> 23.0.0
 |    +--- com.google.android.gms:play-services-measurement:23.0.0
 |    |    +--- com.google.android.gms:play-services-ads-identifier:18.0.0
-|    |    |    \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+|    |    |    \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.9.0 (*)
-|    |    +--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    |    +--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    |    +--- com.google.android.gms:play-services-measurement-base:23.0.0
-|    |    |    \--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    |    |    \--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    |    +--- com.google.android.gms:play-services-measurement-impl:23.0.0
-|    |    |    \--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    |    |    \--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    |    \--- com.google.android.gms:play-services-measurement-sdk-api:23.0.0
-|    |         \--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    |         \--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    +--- com.google.android.gms:play-services-measurement-api:23.0.0
-|    |    \--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    \--- com.google.android.gms:play-services-measurement-sdk:23.0.0
-|         \--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-auth:21.4.0
 |    +--- com.google.android.gms:play-services-auth-api-phone:18.0.2
-|    |    \--- com.google.android.gms:play-services-basement:18.0.2 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.0.2 -> 18.9.0 (*)
 |    +--- com.google.android.gms:play-services-auth-base:18.0.10
-|    |    \--- com.google.android.gms:play-services-basement:18.2.0 -> 18.5.0 (*)
+|    |    \--- com.google.android.gms:play-services-basement:18.2.0 -> 18.9.0 (*)
-|    +--- com.google.android.gms:play-services-basement:18.5.0 (*)
+|    +--- com.google.android.gms:play-services-basement:18.5.0 -> 18.9.0 (*)
 |    \--- com.google.android.gms:play-services-fido:20.0.1 -> 21.0.0
-|         \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.3.0 -> 18.9.0 (*)
++--- com.google.android.play:age-signals:0.0.2
+|    +--- com.google.android.gms:play-services-basement:18.9.0 (*)
+|    +--- com.google.android.gms:play-services-tasks:18.2.0 (*)
+|    \--- com.google.android.play:core-common:2.0.4
 +--- project :libs:login
 |    \--- androidx.credentials:credentials-play-services-auth:1.5.0
 |         +--- com.google.android.gms:play-services-auth-blockstore:16.4.0
-|         |    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|         |    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 |         \--- com.google.android.gms:play-services-identity-credentials:16.0.0-alpha02
-|              \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|              \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- project :libs:cardreader
 |    \--- com.stripe:stripeterminal-taptopay:4.7.5
 |         \--- com.google.android.play:integrity:1.1.0
-|              \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|              \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 +--- com.google.android.play:app-update:2.1.0
-|    \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 +--- com.google.android.play:review:2.0.2
-|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-code-scanner:16.1.0
-|    +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|    +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 |    \--- com.google.mlkit:barcode-scanning-common:17.0.0
-|         +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+|         +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.9.0 (*)
 |         \--- com.google.mlkit:vision-common:17.0.0 -> 17.3.0
-|              +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|              +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 |              \--- com.google.mlkit:common:18.6.0 -> 18.11.0
-|                   \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|                   \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.mlkit:text-recognition:16.0.1
-|    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 |    +--- com.google.android.gms:play-services-mlkit-text-recognition:19.0.1
-|    |    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    |    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 |    |    \--- com.google.android.gms:play-services-mlkit-text-recognition-common:19.1.0
-|    |         +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    |         +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 |    |         \--- com.google.mlkit:vision-interfaces:16.3.0
-|    |              \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|    |              \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 |    \--- com.google.mlkit:text-recognition-bundled-common:17.0.0
-|         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-mlkit-text-recognition-japanese:16.0.1
-|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-mlkit-text-recognition-chinese:16.0.1
-|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-mlkit-text-recognition-korean:16.0.1
-|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1
-|    \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.9.0 (*)
 +--- com.google.mlkit:barcode-scanning:17.3.0
-|    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    +--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 |    \--- com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1
-|         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|         \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)
 +--- com.google.android.gms:play-services-wearable:19.0.0
-|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.5.0 (*)
+|    \--- com.google.android.gms:play-services-basement:18.4.0 -> 18.9.0 (*)

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 11, 2025

📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
App NameWooCommerce-Wear Android
Platform⌚️ Wear OS
FlavorJalapeno
Build TypeDebug
Commit31dfd3c
Direct Downloadwoocommerce-wear-prototype-build-pr15085-31dfd3c.apk

Refactor the legacy viewmodel implementation that was previously added to LoginActivity
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 11, 2025

🤖 Test Failure Analysis

Your tests failed. Claude has analyzed the failures - check the annotation for details.

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 11, 2025

📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.

App NameWooCommerce Android
Platform📱 Mobile
FlavorJalapeno
Build TypeDebug
Commit31dfd3c
Direct Downloadwoocommerce-prototype-build-pr15085-31dfd3c.apk

@JorgeMucientes JorgeMucientes modified the milestones: 23.8 ❄️, 23.9 Dec 12, 2025
@JorgeMucientes JorgeMucientes marked this pull request as ready for review December 12, 2025 18:22
Copy link
Contributor

@irfano irfano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • It's worth adding RELEASE-NOTES for this update.
  • When I change the upper age limit from 12 to 13 and open the app for the first time, the app still applies the previous behavior (age 12) and blocks access.
    Similarly, when the upper age limit is changed from 14 to 12 and the app is restarted, on the first launch it behaves as if the limit were still 14 and allows access, but on the second launch it correctly treats it as 12 and blocks the app.
Screen_recording_20251217_155709.webm

All other parts look good. 👍🏻 Thanks for adding this feature.

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Dec 18, 2025

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@JorgeMucientes
Copy link
Contributor Author

I'm moving this to draft as I need to add bigger changes than I expected. I'll let you know once everything is ready again @irfano. And thanks for the first round 👍🏼

@JorgeMucientes JorgeMucientes marked this pull request as draft December 18, 2025 18:25
These changes ensure the restricted access dialog is shown or hide based on any changes happening to age eligibility checks
@JorgeMucientes JorgeMucientes added the status: do not merge Dependent on another PR, ready for review but not ready for merge. label Dec 19, 2025
@JorgeMucientes JorgeMucientes added the category: tracks Related to analytics, including Tracks Events. label Dec 19, 2025
@JorgeMucientes
Copy link
Contributor Author

Hey @irfano this is ready for another round 🙏🏼

  • Applied all of your small suggested changes
  • Addressed the main bug you reported by making the age eligibility status observable
  • Added tracking so we can figure out the results the API returns and how often this dialog is shown \
  • Some other minor refactor in AgeEligibilityChecker to avoid unnecessary checks

It's worth adding RELEASE-NOTES for this update.

I haven't added release notes yet as I'm still not sure where these changes are going to land (23.8.1 or 23.9)

@JorgeMucientes JorgeMucientes marked this pull request as ready for review December 19, 2025 16:34
if (ageUpper == null) {
true // If we can't determine the age return true
} else {
ageUpper >= WOOCOMMERCE_TOS_MINIMUM_AGE_FOR_APP_USE
Copy link
Contributor

@malinajirka malinajirka Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this on slack (p1766407567026729-slack-C03L1NF1EA3). From what I understand, if the state is SUPERVISED (approved by parent) we shouldn't check their age against WOOCOMMERCE_TOS_MINIMUM_AGE_FOR_APP_USE. For example, 12 years old user whose parents approved the app should be able to use it, right?

Copy link
Contributor

@irfano irfano Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to WooCommerce TOS our app usage is expected for 13+ years old users:

Given that, the current behavior seems expected: we shouldn’t allow 12-year-old users to use the WooCommerce platform.

Test Case 3: Supervised user under 13

  • Set userStatus to SUPERVISED_APPROVAL_PENDING
  • Set ageUpper to 12
  • Open the app
  • Log in as usual

I think the last step is misleading for this case. If the user is under 13, they shouldn’t be able to “log in as usual”; they should be logged out.
@JorgeMucientes, can you confirm my understanding is correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to WooCommerce TOS our app usage is expected for 13+ years old users:

Given that, the current behavior seems expected: we shouldn’t allow 12-year-old users to use the WooCommerce platform.

Yes, that's it. Replied on Slack as well and shared the diagram that might help (will ad it to the PR desc):

Image

I think the last step is misleading for this case. If the user is under 13, they shouldn’t be able to “log in as usual”; they should be logged out.

Well, spotted those instructions are wrong. The user should be logged out and access restricted. Updated the PR desc with the correct steps.

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Is the singleton annotation needed here? From what I understand we inject the checker just once in the initializer. If we declare it as singleton, it'll never be garbage collected. It's a minor memory optimization, but unless we need it I'd personally replace it with @Reusable.

}

private fun keepTrackOfAgeEligibility() {
val dialog = AlertDialog.Builder(this)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 The dialog is leaking after configuration change, but considering how rare this dialog will be I think it's not worth fixing.

}

@Test
fun `given user is supervised and age is under 13, when checkAge called, then user is NOT eligible`() =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is related to the behavior I mentioned above - I believe the user should be allowed to use the app, since they have been approved by their parent. Wdyt?

Copy link
Contributor

@malinajirka malinajirka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @JorgeMucientes! Overall looks good to me. I've left some comments with suggestions + questions + one potential bug.

Copy link
Contributor

@irfano irfano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incorrect “previous app state” issue is fixed now. 👍🏻 Thanks for the change. I added more comments that needs your attention. Also some reminders

  • RELEASE-NOTES for the change,
  • Register new tracks

if (isUserAgeRangeEligible.value.not()) {
accountRepository.logout()
}
trackingProperties["access_restricted"] = _isUserAgeRangeEligible.value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! is needed here.
_isUserAgeRangeEligible.value!_isUserAgeRangeEligible.value

Comment on lines 4351 to 4352
<string name="age_restriction_dialog_title">Account Access Required</string>
<string name="age_restriction_dialog_message">Your Google Account settings indicate that you need a parent or guardian's permission to continue. They can grant access using their Google account to get you back online.</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we show the same message to supervised users who are under 13? In that case, the restriction isn’t related to their Google account. So I think we should either show a different message or make this one more generic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: tracks Related to analytics, including Tracks Events. status: do not merge Dependent on another PR, ready for review but not ready for merge. unit-tests-exemption woocommerce-android

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants