From 882ce45d71dd822dc77d8703adde1b6674599fca Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:41:07 +0300 Subject: [PATCH 1/5] Developer guide: four more sentences that promise code and do not deliver Deep-Link Routing twice says "the plugin's produces the payload:" and then shows nothing. Both payloads are now what the builders write: AasaBuilder and AssetLinksBuilder were run against the chapter's own example -- the /users/:id route it documents, a Services ID, two signing fingerprints -- and their output committed. Note addRouterPattern turns /users/:id into /users/*, which is the conversion the chapter describes in prose and now shows. Advertising promised the initialize-then-consent-then-load order and then described AdConfig's flags as if the reader had just seen them. That listing is restored, with the reason the order matters beside it: requestConsent presents the GDPR form and, on iOS, the App Tracking Transparency prompt, and both need the SDK up, so consent sits between initialize and the first load rather than before either. The fourth is not restorable and is rewritten instead. Each provider's install() lives in its own cn1lib, none of which the docs module depends on, so a compiled listing would mean a build dependency for one static call. The sentence now names the four calls and points at the table right above it that gives the class for each. Ratchet drops from 34 to 30. Co-Authored-By: Claude Opus 5 (1M context) --- .../generated/AdvertisingJava009Snippet.java | 95 +++++++++++++++++++ .../developer-guide/deep-links-routing.json | 30 ++++++ docs/developer-guide/Advertising.asciidoc | 12 ++- .../Deep-Links-Routing.asciidoc | 10 ++ .../missing-code-blocks-baseline.txt | 4 - 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java create mode 100644 docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.json diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java new file mode 100644 index 00000000000..556378bf77c --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.generated; + +import com.codename1.gpu.*; +import com.codename1.ui.*; +import com.codename1.ui.animations.*; +import com.codename1.ui.events.*; +import com.codename1.ui.geom.*; +import com.codename1.ui.layouts.*; +import com.codename1.ui.list.*; +import com.codename1.ui.plaf.*; +import com.codename1.ui.util.*; +import com.codename1.components.*; +import com.codename1.charts.models.*; +import com.codename1.charts.renderers.*; +import com.codename1.charts.views.*; +import com.codename1.capture.*; +import com.codename1.io.*; +import com.codename1.l10n.*; +import com.codename1.location.*; +import com.codename1.maps.*; +import com.codename1.media.*; +import com.codename1.messaging.*; +import com.codename1.payment.*; +import com.codename1.processing.*; +import com.codename1.properties.*; +import com.codename1.push.*; +import com.codename1.security.*; +import com.codename1.social.*; +import com.codename1.ui.spinner.*; +import java.io.*; +import com.codename1.analytics.*; +import com.codename1.appreview.*; +import com.codename1.ads.*; +import com.codename1.util.*; +import java.util.*; + + +class AdvertisingJava009Snippet { + + Form form; + + // tag::advertising-java-009[] + void startAds() { + AdConfig cfg = new AdConfig() + .testMode(true) + .addTestDevice("YOUR_TEST_DEVICE_ID") + .tagForChildDirectedTreatment(AdConfig.TAG_FALSE) + .maxAdContentRating(AdConfig.RATING_G); + + AdManager.initialize(cfg, initialized -> { + // Consent has to be settled before the first load, not before + // initialize: requestConsent presents the GDPR form and, on iOS, + // the App Tracking Transparency prompt, and both need the SDK up. + AdConsent.requestConsent(status -> { + if (AdConsent.canRequestAds()) { + loadAds(); + } else { + // STATUS_REQUIRED with consent withheld. Personalized ads + // are off the table; show the app without them rather + // than blocking on a prompt the user already declined. + showAdFreeUi(); + } + }); + }); + } + + void loadAds() { + } + + void showAdFreeUi() { + } + // end::advertising-java-009[] +} diff --git a/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.json b/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.json new file mode 100644 index 00000000000..401fd82fa84 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.json @@ -0,0 +1,30 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::deep-links-routing-json-001[] +{ + "applinks": { + "details": [ + { + "appIDs": ["ABCDE12345.com.example.app"], + "components": [ + { "/": "/users/*" }, + { "/": "/promo/*" } + ] + } + ] + } +} +// end::deep-links-routing-json-001[] + +// tag::deep-links-routing-json-002[] +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.example.app", + "sha256_cert_fingerprints": ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5", "7B:5A:1F:C9:0E:52:44:7C:82:19:6E:38:0A:B7:D4:11:5C:63:9F:28:E1:47:8B:30:A6:C2:5D:94:F0:6B:38:19"] + } + } +] +// end::deep-links-routing-json-002[] diff --git a/docs/developer-guide/Advertising.asciidoc b/docs/developer-guide/Advertising.asciidoc index cc5be36ea3f..7adefc9fcae 100644 --- a/docs/developer-guide/Advertising.asciidoc +++ b/docs/developer-guide/Advertising.asciidoc @@ -74,8 +74,11 @@ include::../demos/common/src/main/snippets/developer-guide/advertising.xml[tag=a === Enabling a provider -Enable a provider once at startup. Each library exposes a static `install()` -method that registers its provider with `AdManager`: +Enable a provider once at startup, from your app's `init(Object)`, by calling +the static `install()` method on the provider class named in the table above -- +`AdMobProvider.install()`, `AppLovinProvider.install()`, +`LevelPlayProvider.install()` or `MockAdProvider.install()`. Each one registers +its provider with `AdManager` and takes no arguments. That single call binds the provider; the rest of your code uses only the @@ -92,6 +95,11 @@ wrap Google's User Messaging Platform or an equivalent), and on iOS the App Tracking Transparency prompt must be presented to access the advertising identifier. The recommended order is to initialize, gather consent, then load: +[source,java] +---- +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java[tag=advertising-java-009,indent=0] +---- + `AdConfig` also carries the global compliance flags every network requires: test mode, test device ids, child directed treatment, under-age-of-consent diff --git a/docs/developer-guide/Deep-Links-Routing.asciidoc b/docs/developer-guide/Deep-Links-Routing.asciidoc index 74758fb9dab..23d98d2d913 100644 --- a/docs/developer-guide/Deep-Links-Routing.asciidoc +++ b/docs/developer-guide/Deep-Links-Routing.asciidoc @@ -85,6 +85,11 @@ Host an `apple-app-site-association` JSON file at `https://your.domain/.well-known/apple-app-site-association` over HTTPS without redirects. The plugin's `AasaBuilder` produces the payload: +[source,json] +---- +include::../demos/common/src/main/snippets/developer-guide/deep-links-routing.json[tag=deep-links-routing-json-001,indent=0] +---- + Tell iOS which domains your app claims by setting the `ios.associatedDomains` build hint -- a comma-separated list of @@ -103,6 +108,11 @@ Host an `assetlinks.json` file at `https://your.domain/.well-known/assetlinks.json`. The plugin's `AssetLinksBuilder` produces the payload: +[source,json] +---- +include::../demos/common/src/main/snippets/developer-guide/deep-links-routing.json[tag=deep-links-routing-json-002,indent=0] +---- + Tell Android which URLs to intercept by setting the `android.xintent_filter` build hint with a verified intent filter for diff --git a/scripts/developer-guide/missing-code-blocks-baseline.txt b/scripts/developer-guide/missing-code-blocks-baseline.txt index 2d64765057a..23ba36a6adc 100644 --- a/scripts/developer-guide/missing-code-blocks-baseline.txt +++ b/scripts/developer-guide/missing-code-blocks-baseline.txt @@ -1,12 +1,8 @@ # Prose that promises a code block where none follows. # A ratchet: entries may be removed as holes are filled, never added. # Regenerate with check-missing-code-blocks.py --write-baseline. -Advertising.asciidoc identifier. The recommended order is to initialize, gather consent, then load: -Advertising.asciidoc method that registers its provider with `AdManager`: Annotation-JSON-XML-Mapping.asciidoc Hand-write a `Mapper` and register it at startup: Authentication-And-Identity.asciidoc Firebase Auth isn't an OIDC provider -- it issues Google-Identity-Toolkit-style tokens via REST endpoints. `com.codename1.social.FirebaseAuth` wraps those endpoints: -Deep-Links-Routing.asciidoc `AssetLinksBuilder` produces the payload: -Deep-Links-Routing.asciidoc without redirects. The plugin's `AasaBuilder` produces the payload: Maven-Creating-CN1Libs.adoc Now try it out. Try adding the following code to your application project's main class (or anywhere in the application project, for that matter): Maven-Creating-CN1Libs.adoc The simulator dispatches every action on the Codename One EDT through `Display.callSerially`, so your method can call `Display.getInstance()`, `Form.show()`, `Dialog.show()`, `ToastBar.showInfoMessage()` and any other CN1 API. Reflection uses the same classloader that loaded `Display`, so cn1lib internals (including package-private classes) resolve normally: Miscellaneous-Features.asciidoc To solve this sort of used case you have two APIs in `Display`: From e94b0df5dace2f01ed2085ee1d9be05147ecfa64 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:44:33 +0300 Subject: [PATCH 2/5] Developer guide: show the Android App Link filter, not just name it The Android half of deep linking said to set android.xintent_filter "with a verified intent filter for your domain" and left the reader to work out what that is. The hint's value is injected into the manifest verbatim, so it is the whole element with escaped line breaks -- not something to guess at -- and the three parts that decide whether it works are now spelled out: autoVerify is what makes Android fetch the assetlinks.json beside it rather than show an app chooser, the data element needs the https scheme and host, and the hint holds one value so a second filter goes in the same string. This is also what the Authentication chapter's Apple-on-Android note points at: the Services ID callback is an HTTPS URL, so the custom scheme registered there cannot receive it and this is the recipe that can. Co-Authored-By: Claude Opus 5 (1M context) --- .../deep-links-routing.properties | 4 ++++ .../Deep-Links-Routing.asciidoc | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.properties b/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.properties index 092d29f5379..c662c1b3389 100644 --- a/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.properties +++ b/docs/demos/common/src/main/snippets/developer-guide/deep-links-routing.properties @@ -3,3 +3,7 @@ // tag::deep-links-routing-properties-001[] codename1.arg.ios.associatedDomains=applinks:example.com,applinks:www.example.com // end::deep-links-routing-properties-001[] + +// tag::deep-links-routing-properties-002[] +codename1.arg.android.xintent_filter=\n \n \n \n \n +// end::deep-links-routing-properties-002[] diff --git a/docs/developer-guide/Deep-Links-Routing.asciidoc b/docs/developer-guide/Deep-Links-Routing.asciidoc index 23d98d2d913..48f3ddce80a 100644 --- a/docs/developer-guide/Deep-Links-Routing.asciidoc +++ b/docs/developer-guide/Deep-Links-Routing.asciidoc @@ -115,8 +115,24 @@ include::../demos/common/src/main/snippets/developer-guide/deep-links-routing.js Tell Android which URLs to intercept by setting the -`android.xintent_filter` build hint with a verified intent filter for -your domain. The Android builder injects the filter into the manifest. +`android.xintent_filter` build hint. The Android builder injects the +value into the manifest verbatim, so it's the whole `` +element, with `\n` for the line breaks: + +[source,properties] +---- +include::../demos/common/src/main/snippets/developer-guide/deep-links-routing.properties[tag=deep-links-routing-properties-002,indent=0] +---- + +Three parts of that carry weight. `android:autoVerify="true"` is what +makes Android fetch the `assetlinks.json` above at install time and open +the link in your app without an app chooser; without it the filter still +matches but the user is asked every time. The `` element needs +`android:scheme="https"` and the host, and narrowing it with +`android:pathPrefix` keeps the app from claiming every URL on the domain. +And the hint holds one value, so an app that also registers a custom +scheme puts both `` elements in the same string rather +than setting the hint twice. The SHA-256 fingerprint comes from `keytool -list -v -keystore ...`, or from the Play Console under **Setup > App integrity** when using Play From 56a8181fd171e6cef26d61db4c5c8f3bdc5f9d91 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:46:45 +0300 Subject: [PATCH 3/5] Developer guide: a failed ad initialize has to stop the flow AdManager.initialize reports false when no provider was installed or the network's own initialize failed, and the listing ignored the flag. With no provider AdConsent reports consent as not required and canRequestAds() answers true, so the recommended order walked straight into loadAds() with nothing behind it -- the one arrangement where the graceful degradation the chapter describes hides the mistake instead of surfacing it. Co-Authored-By: Claude Opus 5 (1M context) --- .../snippets/generated/AdvertisingJava009Snippet.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java index 556378bf77c..d3dcb4d8f56 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java @@ -69,7 +69,16 @@ void startAds() { .tagForChildDirectedTreatment(AdConfig.TAG_FALSE) .maxAdContentRating(AdConfig.RATING_G); - AdManager.initialize(cfg, initialized -> { + AdManager.initialize(cfg, ready -> { + // false when no provider was installed, or the network's own + // initialize failed. Stop here: with no provider AdConsent reports + // consent as not required and canRequestAds() answers true, so + // carrying on would walk into loadAds() with nothing behind it. + if (!ready) { + showAdFreeUi(); + return; + } + // Consent has to be settled before the first load, not before // initialize: requestConsent presents the GDPR form and, on iOS, // the App Tracking Transparency prompt, and both need the SDK up. From 115248250b9ed546374f124a90481e5dd45e05f9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:52:06 +0300 Subject: [PATCH 4/5] Developer guide: say what the two ad callbacks actually promise Both are weaker than the chapter implied, and both are in the providers rather than in the listing, so the listing keeps its shape and the chapter now says what it is looking at. The readiness flag is not a report that the network came up. Every provider -- AdMob, AppLovin, LevelPlay -- hands the config to its native bridge and calls back true on the next line, while the SDK behind it initializes asynchronously. An invalid SDK key therefore surfaces as failing loads later, never as false here. And requestConsent presents a form only where the provider implements one. AdMob does, through Google's User Messaging Platform. AppLovin MAX and Unity LevelPlay read consent from an external CMP and the IAB TCF string, so their native requestConsent reports STATUS_NOT_REQUIRED immediately and canRequestAds() always answers true -- which means on those two the EEA/UK consent flow is the CMP's and has to be in place before the first load. A reader following the recommended order without knowing that would have believed the framework was collecting consent it never collects. Co-Authored-By: Claude Opus 5 (1M context) --- .../generated/AdvertisingJava009Snippet.java | 11 +++++++---- docs/developer-guide/Advertising.asciidoc | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java index d3dcb4d8f56..20d8581e837 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/AdvertisingJava009Snippet.java @@ -70,10 +70,13 @@ void startAds() { .maxAdContentRating(AdConfig.RATING_G); AdManager.initialize(cfg, ready -> { - // false when no provider was installed, or the network's own - // initialize failed. Stop here: with no provider AdConsent reports - // consent as not required and canRequestAds() answers true, so - // carrying on would walk into loadAds() with nothing behind it. + // false when no provider was installed. It is not a report that + // the network is up: every provider hands its config to the native + // bridge and answers true straight away, while the SDK behind it + // initializes asynchronously. Worth checking anyway, because with + // no provider at all AdConsent reports consent as not required and + // canRequestAds() answers true, so carrying on would walk into + // loadAds() with nothing behind it. if (!ready) { showAdFreeUi(); return; diff --git a/docs/developer-guide/Advertising.asciidoc b/docs/developer-guide/Advertising.asciidoc index 7adefc9fcae..3a8df9968ee 100644 --- a/docs/developer-guide/Advertising.asciidoc +++ b/docs/developer-guide/Advertising.asciidoc @@ -101,6 +101,22 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/g ---- +Two things about that flow are worth knowing before you rely on it. + +The readiness flag says a provider was installed and took the config; it isn't +a report that the network came up. Every provider passes the config to its +native bridge and answers `true` immediately, while the SDK behind it +initializes asynchronously -- so an invalid SDK key surfaces as failing loads +later, not as `false` here. + +And `AdConsent.requestConsent` presents a form only where the provider +implements one. AdMob does, through Google's User Messaging Platform. AppLovin +MAX and Unity LevelPlay read consent from an external CMP and the IAB TCF +string instead, so their implementations report `STATUS_NOT_REQUIRED` at once +and `canRequestAds()` always answers `true`. On those two the consent flow is +the CMP's, not this one, and it has to be in place before the first load for +EEA/UK users. + `AdConfig` also carries the global compliance flags every network requires: test mode, test device ids, child directed treatment, under-age-of-consent treatment and a maximum ad content rating. From 2768f5ff4837c989dea593d09fd6fad1201e499d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:57:55 +0300 Subject: [PATCH 5/5] Developer guide: two more claims the implementations do not support On iOS 14+ the AdMob provider starts ATTrackingManager with an empty completion handler and fires its consent callback from the UMP path independently, so the callback can arrive while the system tracking prompt is still on screen. The recommended order in this chapter then loads ads before tracking authorization is settled, which is the one thing that ordering exists to prevent. Documented as a warning with what to do instead. The provider itself needs a change -- the UMP request belongs inside the ATT completion handler -- and that is a cn1-admob fix rather than a docs one. And the App Link paragraph said an unverified filter still matches and asks the user each time. From Android 12 that is not what happens: an unverified web link goes to the default browser and the filter never sees it until the user turns the association on by hand. A reader who skipped autoVerify on the strength of the old sentence would have found deep links silently not working at all. Co-Authored-By: Claude Opus 5 (1M context) --- docs/developer-guide/Advertising.asciidoc | 7 +++++++ .../Deep-Links-Routing.asciidoc | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/developer-guide/Advertising.asciidoc b/docs/developer-guide/Advertising.asciidoc index 3a8df9968ee..f865e920ab7 100644 --- a/docs/developer-guide/Advertising.asciidoc +++ b/docs/developer-guide/Advertising.asciidoc @@ -117,6 +117,13 @@ and `canRequestAds()` always answers `true`. On those two the consent flow is the CMP's, not this one, and it has to be in place before the first load for EEA/UK users. +WARNING: On iOS 14 and later the AdMob provider starts the App Tracking +Transparency prompt alongside the User Messaging Platform request and fires its +consent callback when UMP finishes, without waiting for the tracking answer. The +callback can therefore arrive while the system prompt is still on screen, and a +load started from it runs before tracking authorization is settled. Present the ATT +prompt yourself at a point you control, and load after the user has answered. + `AdConfig` also carries the global compliance flags every network requires: test mode, test device ids, child directed treatment, under-age-of-consent treatment and a maximum ad content rating. diff --git a/docs/developer-guide/Deep-Links-Routing.asciidoc b/docs/developer-guide/Deep-Links-Routing.asciidoc index 48f3ddce80a..0f9029a2e0c 100644 --- a/docs/developer-guide/Deep-Links-Routing.asciidoc +++ b/docs/developer-guide/Deep-Links-Routing.asciidoc @@ -124,15 +124,17 @@ element, with `\n` for the line breaks: include::../demos/common/src/main/snippets/developer-guide/deep-links-routing.properties[tag=deep-links-routing-properties-002,indent=0] ---- -Three parts of that carry weight. `android:autoVerify="true"` is what -makes Android fetch the `assetlinks.json` above at install time and open -the link in your app without an app chooser; without it the filter still -matches but the user is asked every time. The `` element needs +Three parts of that carry weight. `android:autoVerify="true"` is what makes +Android fetch the `assetlinks.json` above at install time and route the link +straight into your app. Leaving it out doesn't fall back to an app chooser: +from Android 12 onward an unverified web link goes to the default browser, and +your filter never sees it until the user turns the association on by hand under +**Settings > Apps > Open by default**. The `` element needs `android:scheme="https"` and the host, and narrowing it with -`android:pathPrefix` keeps the app from claiming every URL on the domain. -And the hint holds one value, so an app that also registers a custom -scheme puts both `` elements in the same string rather -than setting the hint twice. +`android:pathPrefix` keeps the app from claiming every URL on the domain. And +the hint holds one value, so an app that also registers a custom scheme puts +both `` elements in the same string rather than setting the hint +twice. The SHA-256 fingerprint comes from `keytool -list -v -keystore ...`, or from the Play Console under **Setup > App integrity** when using Play