Skip to content

[pull] develop from duckduckgo:develop - #350

Open
pull[bot] wants to merge 7849 commits into
Rachelmorrell:developfrom
duckduckgo:develop
Open

pull[bot] wants to merge 7849 commits into
Rachelmorrell:developfrom
duckduckgo:develop

Conversation

@pull

@pull pull Bot commented Aug 19, 2021 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot]

Can you help keep this open source service alive? 💖 Please sponsor : )

karlenDimla and others added 19 commits October 26, 2026 16:29
)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1219760542418633?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
Visual polish for the rich Duck.ai card in the tab switcher grid:
- Content background: chat and image content now use the primary surface
color (daxColorSurface). The `daxColorTabDuckAiContentContainer` token
is no longer used, so it's removed from the design system.
- Label: the Chat, Transcript and Voice chat labels now use secondary
text color, and their icons are tinted to match.
- Layout: the content (snippet or image thumbnail) and the label are
anchored to each other, so the label always sits directly under
whichever one is shown. The voice face and its label are anchored the
same way.
- Spacing: removed the top margin on the card titles and adjusted the
thumbnail margins (4dp top, 8dp bottom).

### Steps to test this PR
Smoke test tab switcher

### UI changes
| Before  | After |
| ------ | ----- |
!<img width="323" height="367" alt="Screenshot 2026-10-22 at 12 26 05"
src="https://github.com/user-attachments/assets/bafb60a0-7abc-4b91-a45d-05192d667560"
/>|<img width="328" height="367" alt="Screenshot 2026-10-22 at 11 58 35"
src="https://github.com/user-attachments/assets/3b086d18-a6f4-4e2f-896b-f1126dde7a2c"
/>|



---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/414730916066338/task/1219890577216994

- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [ ] All tests must pass
- [ ] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/414730916066338/task/1219921002387479

- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [ ] All tests must pass
- [ ] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
…10186)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1219798206568457?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

- The camera launcher now calls `onResult` on the main thread in order
to fix a `CalledFromWrongThreadException` when focussing the
`NativeInputModeWidget`

### Steps to test this PR

- [ ] Open Duck.ai, tap the attach icon and choose "Take Photo"
- [ ] Verify that the photo is attached and the app does not crash
Task/Issue URL: https://app.asana.com/0/0/1215373984475612/f
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable): None

### Description

This makes `omnibarTextInput` display only: it shows the URL or query
and the click catcher opens the native input. It removes the omnibar's
focus, back key, IME and touch listeners, the back arrow and clear
button, every `hasFocus`/`isEditing` branch, and the autofill autoprompt
gate that relied on them. For reviewers: the native input never cancels
a pending autofill credential prompt. That was not reproduced on a
device (the emulator has no screen lock, so autofill is off there) and
is left open. `from_focused_ntp` on the tab manager, fire and menu
pixels had been false since the native input rolled out, because it read
the legacy field's focus. It now comes from the native input: true on
the new tab page when the native input's text field has focus
(`NativeInputWidget.hasInputFocus()`). I didn't use
`isNativeInputShown()`, which is true whenever the widget is attached,
not only when the field has focus.

Retired pixels (no firing site left; `m_addressbar_click_aichat` also
removed from the registry): `m_addressbar_focus_ntp`,
`m_addressbar_click_ntp`, `m_addressbar_click_website`,
`m_addressbar_click_serp`, `m_addressbar_click_aichat`,
`m_addressbar_focus_clear_entry_ntp`,
`m_addressbar_focus_clear_entry_website`,
`m_addressbar_focus_clear_entry_serp`, `m_addressbar_focus_cancel_ntp`,
`m_addressbar_focus_cancel_website`, `m_addressbar_focus_cancel_serp`,
`m_addressbar_focus_close_ntp`, `m_addressbar_focus_close_website`,
`m_addressbar_focus_close_serp`, `m_keyboard_go_click_ntp`,
`m_keyboard_go_click_website`, `m_keyboard_go_click_serp`,
`m_aichat_legacy_omnibar_shown`, `m_aichat_legacy_omnibar_shown_count`,
`m_aichat_legacy_omnibar_back_button_pressed`.

### Steps to test this PR

_Address bar_
- [x] Install an internal build, open a site and check the address bar
shows the domain and can't be typed into
- [x] Tap the address bar on a page and on the new tab page, with the
address bar at the top and then at the bottom, and check the native
input opens each time
- [x] Repeat with Settings > AI Features set to Search Only, then to
Toggle between Search and Duck.ai
- [x] With two tabs open, swipe the address bar sideways and check the
tab changes

_Duck.ai_
- [x] Open https://duck.ai from the address bar, press back, and check
the previous page shows with no keyboard and no input open

### UI changes
| Before  | After |
| ------ | ----- |
| No UI changes | No UI changes |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1201807753394693/task/1219813152401991?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
- Remove subscribe pixel events from switches
- Replace with switch pixel

### Steps to test this PR
- [x] Apply patch on
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true
- [x] Install from branch
- [x] Purchase a test subscription
- [x] Verify `m_subscribe` pixel is sent
- [x] Verify `m_privacy-pro_app_subscription-purchase_success` pixel is
sent
- [x] Go to App Settings > Subscription Settings
- [x] Tap on 'View All Plans'
- [x] Switch  to another subscription
- [x] Verify `m_subscribe` pixel is **not** sent
- [x] Verify `m_privacy-pro_app_subscription-purchase_success` pixel is
**not** sent
- [x] Verify `subscription_switch` is sent

### No UI changes

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212608036467427/task/1212839176825188
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
SQLite corruption makes the default open-helper callback delete app.db,
which wipes bookmarks and everything else stored there with no signal.
Wrap app.db's open helper so onCorruption fires m_database_corruption
(count + daily) with the database name, process name and whether the
file had been opened, before the default handling runs.

### Steps to test this PR
- [x] open the app
- [x] create a bookmark
- [x] close the app
- [x] manually corrupt the DB file.
- [x] open the app
- [x] no bookmarks visible, and the pixel is fired.

### UI changes
None.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1219776338950932
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/481882893211075/task/1219704925344696
API Proposals URL(s) (if applicable):
https://app.asana.com/1/137249556945/project/1212087397361015/task/1219775711271918

### Description

Adds a plugin point that lets other features show a promo card on the
redesigned Google passwords import success screen, and a temporary dummy
sync plugin to exercise it. This is the base for the Sync promotion and
the cross-import promos from the designs.

The new public API is `PasswordsImportScreenPromotionPlugin` in
`autofill-api`.

### Steps to test this PR

_Promo on the success screen_
- [ ] Install an internal build.
- [ ] Enable the `autofillImportRedesign` feature flag.
- [ ] Open Settings.
- [ ] Open "Passwords".
- [ ] Open the overflow menu.
- [ ] Tap "Import Passwords From Google".
- [ ] Tap "Import From Google".
- [ ] Complete the Google flow.
- [ ] Verify the importing screen is shown until the import finishes.
- [ ] Verify the completion screen shows the import results with the
blue "Dummy plugin" card below them.
- [ ] Tap the "Dummy plugin" card.
- [x] Verify the import sheet closes.

_Redesign disabled_
- [ ] Disable the `autofillImportRedesign` feature flag.
- [ ] Import passwords from Google again.
- [x] Verify the legacy result screen is shown without any promo card.

### UI changes
N/A

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1219816597348860
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Moves the state of the Google bookmarks import screen into a ViewModel.
The import flow looks and behaves the same as before. The change
prepares the screen for the upcoming redesign and fixes a few cases
where the screen lost its state when the activity was recreated.

- Rotating the device on the success or error screen keeps that screen.
Previously it restarted the import from the Google page.
- "Close" and the toolbar close button on the success or error screen
keep working after the device is rotated. Previously they stopped
responding.
- Loading updates that the Google page sends after the import has
finished no longer move the user away from the success or error screen.
- Closing the screen with a result is delivered once the screen is ready
to handle it, so it is not lost if it arrives while the activity is
being recreated.

### Steps to test this PR

_Successful import_
- [ ] Open the bookmarks screen.
- [ ] Tap "Import" in the overflow menu.
- [ ] Tap "Import From Google".
- [ ] Sign in to Google if asked.
- [ ] Wait for the import to finish.
- [ ] Verify the success screen shows the number of imported bookmarks.
- [ ] Tap "Close".
- [ ] Verify the imported bookmarks appear in the "Imported from Chrome"
folder.

_Rotate on the result screen_
- [ ] Start an import from Google.
- [ ] Wait for the success screen.
- [ ] Rotate the device.
- [ ] Verify the success screen is still shown.
- [ ] Tap "Close".
- [x] Verify the screen closes and the bookmarks screen is shown.

_Cancel the import_
- [ ] Start an import from Google.
- [ ] Tap the close button in the toolbar.
- [ ] Tap "Cancel Import" in the "Cancel import?" dialog.
- [x] Verify the screen closes and the bookmarks screen is shown.

### UI changes
N/A

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1219816597348861
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Adds the redesigned importing, success and error screens to the Google
bookmarks import flow, behind the `autofillImportRedesign` feature flag,
which now defaults to enabled on internal builds.

> [!note]
> On a fresh launch the activity reads
`importRedesignFeature.self().isEnabled()` on the main thread and picks
the design from it. The value is saved in `onSaveInstanceState` and
restored from `savedInstanceState`, so the design stays the same after
rotation and after process death, and a remote flag update only applies
to the next import. The read is cheap enough for the main thread because
toggle state is cached in memory by `CachedToggleStore`.
`DuckDuckGoActivity.onCreate` already reads two
`AppBrandDesignUpdateToggles` flags synchronously for every activity,
and the permissions redesign reads its flag on the main thread to choose
between screens.
> 
> I considered reading the flag on a background thread in
`ImportGoogleBookmarksViewModel` with a loading state until it arrives
meant the toolbar, which is shown in one design and hidden in the other,
either flickered or had to stay hidden until the flag was known, and the
ViewModel had to know about the design. Delaying `setContentView` until
a background read finished breaks restoration, because after rotation or
process death the system restores the fragments before the flag arrives
and they crash without their container, and fixing that needs the choice
saved in state anyway. Reading the flag in
`ImportFromGoogleLauncherImpl.launchBookmarksImport()`, which already
runs off the main thread, would add more data to the request that the
contract casts from the opaque `BookmarksImportRequest` and move a
screen decision out of the screen. Letting the launcher choose between
two separate activities through a resolver keeps the read off the main
thread but needs a second activity and more wiring for a single flag.

### Steps to test this PR

_Successful bookmarks import_
- [ ] Install an internal build.
- [ ] Open the bookmarks screen.
- [ ] Tap "Import" in the overflow menu.
- [ ] Tap "Import From Google".
- [ ] Sign in to Google if asked.
- [ ] Wait for the importing screen.
- [ ] Verify the importing screen is shown without a toolbar, with the
info rows and the "Cancel" button at the bottom.
- [ ] Wait for the import to finish.
- [ ] Verify the success screen shows "Import complete!" and the number
of imported bookmarks.
- [ ] Tap "Done".
- [x] Verify the imported bookmarks appear in the "Imported from Chrome"
folder.

_Design stays constant during an import_
- [ ] Start an import from Google.
- [ ] Wait for the importing screen.
- [ ] Rotate the device.
- [x] Verify the redesigned importing screen is still shown.

_Cancel from the importing screen_
- [ ] Start an import from Google.
- [ ] Wait for the importing screen.
- [ ] Tap "Cancel".
- [ ] Tap "Cancel Import" in the "Cancel import?" dialog.
- [x] Verify the screen closes and the bookmarks screen is shown.

_Legacy bookmarks import_
- [ ] Disable `autofillImportRedesign` in the feature flag inventory.
- [ ] Start an import from Google.
- [ ] Verify the importing, success and error screens look as before,
with the toolbar visible.

### UI changes
| In progress | Success | Failure |
| - | - | - |
| <img width="540" alt="image"
src="https://github.com/user-attachments/assets/aeb093dd-d9d4-4722-801b-b1d2c780eb98"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/a87b8991-1a41-47fe-b3d7-00149ac04e9c"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/a3d7427a-c669-4440-9582-1e38fdf09a8f"
/> |

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1219865357934686
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219704925344696
API Proposals URL(s) (if applicable):
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219808767736471

### Description

Adds a plugin point that lets other features show a promo card on the
redesigned Google bookmarks import success screen, and a temporary dummy
sync plugin to exercise it. This is the bookmarks counterpart of the
passwords import plugin and will carry the Sync promotion and the
cross-import promos from the designs.

The new public API is `BookmarksImportScreenPromotionPlugin` in
`autofill-api`. Like the passwords one, a plugin answers `isEligible`
before the success screen shows and builds its card in `createView`
whenever the screen renders.

### Steps to test this PR

_Promo on the success screen_
- [ ] Install an internal build.
- [ ] Enable the `autofillImportRedesign` feature flag.
- [ ] Open Bookmarks.
- [ ] Open the overflow menu.
- [ ] Tap "Import".
- [ ] Tap "Import From Google".
- [ ] Complete the Google flow.
- [ ] Verify the importing screen is shown until the import finishes.
- [ ] Verify the "Import complete!" screen shows the number of imported
bookmarks with the blue "Dummy plugin" card below it.
- [ ] Switch the system dark mode setting while the screen is open.
- [ ] Verify the "Dummy plugin" card is still shown.
- [ ] Tap the "Dummy plugin" card.
- [x] Verify the import screen closes.

_Redesign disabled_
- [ ] Disable the `autofillImportRedesign` feature flag.
- [ ] Import bookmarks from Google again.
- [x] Verify the legacy result screen is shown without any promo card.

### UI changes
N/A

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219573627842074
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Shows a Sync promo on the redesigned passwords import success screen, so
users who just imported their passwords are nudged to turn on Sync &
Backup and access them on their other devices.

Promo card:
- The card shows below the import results with a sync illustration, the
title "Access your passwords across Mobile and Desktop." and a "Turn on
Sync & Backup" button.
- The card has no close button and no elevation.
- Tapping "Turn on Sync & Backup" opens the Sync screen and closes the
import sheet.

When the promo shows:
- At least one password was imported in this import.
- Sync is available and the user is not syncing yet.
- The `syncPromotion` and `syncPromotion.passwordsImport` flags are on.
`passwordsImport` is enabled only in internal builds for now.
- The promo only appears in the redesigned import UI. The legacy import
UI is unchanged.

The card is built with a new `PromoMessageView` in `sync-impl`. It
follows the Message component design, with an optional close button and
optional elevation, because the design system `MessageCta` always has
both. Without elevation the card uses a translucent container fill, and
with elevation it uses the secondary surface color.

### Steps to test this PR

_Promo after importing passwords_
- [ ] Install an internal build.
- [ ] Make sure Sync is turned off.
- [ ] Open the Passwords screen.
- [ ] Tap "Import Passwords".
- [ ] Import passwords from a Google account that has passwords not yet
saved in the app.
- [ ] Verify the "Import complete!" screen shows the promo card below
the results.
- [ ] Tap "Turn on Sync & Backup".
- [ ] Verify the import sheet closes.
- [ ] Verify the Sync screen opens (I'll add direct sync action in a
future PR).

_Nothing imported_
- [ ] Import passwords from the same Google account again.
- [ ] Verify the "Import complete!" screen shows no promo card.

_Already syncing_
- [ ] Turn on Sync & Backup.
- [ ] Import passwords from a Google account that has passwords not yet
saved in the app.
- [ ] Verify the "Import complete!" screen shows no promo card.

### UI changes
| Light  | Dark |
| ------ | ----- |
| <img width="540" alt="image"
src="https://github.com/user-attachments/assets/f7c76699-a47c-42ab-ac50-a254f6154456"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/64bc1a36-9731-475e-bd8e-44dc3572c205"
/> |

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219573859775891
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Makes Sync promotions start a Sync flow directly instead of only opening
the Sync & Backup settings screen. The Sync screen decides which flow to
start based on whether this device is already synced, so every promotion
behaves the same way.

- When this device is not synced, the "Sync This Device" flow starts
right away, the same as turning on the "Sync This Device" toggle.
- When this device is already synced, the "Add Another Device" flow
starts right away.
- While the flow is starting, the Sync & Backup settings screen stays
hidden, so it no longer flashes before the flow opens. It shows again
when the user returns from the flow, or when a device authentication
prompt has to appear over it.

All current promotions are only shown when this device is not synced, so
in practice they start "Sync This Device". The "Add Another Device" path
is there for upcoming promotions aimed at users who already sync.

The AI chat promo is unchanged, because it explicitly opens the "Sync
With Another Device" flow.

### Steps to test this PR

_Bookmarks screen banner_
- [ ] Install an internal build.
- [ ] Make sure Sync is turned off.
- [ ] Save at least one bookmark.
- [ ] Open the Bookmarks screen.
- [ ] Tap "Set Up Sync" on the Sync banner.
- [ ] Verify the Sync & Backup settings screen does not flash before the
flow opens.
- [x] Verify the "Sync This Device" flow opens on top of the Sync &
Backup settings screen.

_Passwords screen banner_
- [ ] Save at least one password.
- [ ] Open the Passwords screen.
- [ ] Tap "Set Up Sync" on the Sync banner.
- [x] Verify the "Sync This Device" flow opens on top of the Sync &
Backup settings screen.

_Add Bookmark sheet promo_
- [ ] Bookmark a website.
- [ ] Tap "Sync Bookmarks Across Devices" in the Add Bookmark sheet.
- [x] Verify the "Sync This Device" flow opens on top of the Sync &
Backup settings screen.

_Cancel the flow_
- [ ] Start the flow from any of the promotions above.
- [ ] Close the "Sync This Device" flow without finishing it.
- [ ] Verify the Sync & Backup settings screen shows with the "Sync This
Device" toggle off.

### UI changes
N/A


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219573627888415
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Shows a Sync promo on the redesigned bookmarks import success screen, so
users who just imported their bookmarks are nudged to turn on Sync &
Backup and access them on their other devices. It mirrors the promo on
the passwords import success screen.

Promo card:
- The card shows below the import results with a sync illustration, the
title "Access your bookmarks across Mobile and Desktop." and a "Turn on
Sync & Backup" button.
- The card has no close button and no elevation.
- Tapping "Turn on Sync & Backup" starts the "Sync This Device" flow and
closes the import screen.

When the promo shows:
- At least one bookmark was imported in this import.
- Sync is available and the user is not syncing yet.
- The `syncPromotion` and `syncPromotion.bookmarksImport` flags are on.
`bookmarksImport` is enabled only in internal builds for now.
- The promo only appears in the redesigned import UI. The legacy import
UI is unchanged.

### Steps to test this PR

_Promo after importing bookmarks_
- [ ] Install an internal build.
- [ ] Make sure Sync is turned off.
- [ ] Open the Bookmarks screen.
- [ ] Tap "Import Bookmarks" in the overflow menu.
- [ ] Tap "Import From Google".
- [ ] Import bookmarks from a Google account that has bookmarks.
- [ ] Verify the "Import complete!" screen shows the promo card below
the results.
- [ ] Tap "Turn on Sync & Backup".
- [ ] Verify the import screen closes.
- [x] Verify the "Sync This Device" flow opens.

_Already syncing_
- [ ] Turn on Sync & Backup.
- [ ] Import bookmarks from a Google account that has bookmarks.
- [x] Verify the "Import complete!" screen shows no promo card.

### UI changes
| Light  | Dark |
| ------ | ----- |
| <img width="540" alt="image"
src="https://github.com/user-attachments/assets/683c79ec-2a3a-44cc-9d22-7d5b63bd1842"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/aa3e6775-5959-419e-8504-f5438ca338ea"
/> |

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1216103556496795/task/1219899746084432
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Makes Sync keep track of the entry point that opened it for the whole
setup flow, so a finished or abandoned setup can be attributed to the
promotion that started it.

- `SyncActivity` resolves the launch source it is opened with into a
`SyncLaunchSource`. A source that is not a known promotion, or no source
at all, resolves to `SyncLaunchSource.Other`.
- The launch source is passed to every setup screen opened from Sync:
"Sync This Device", "Sync With Another Device" with its "Scan QR" and
"Enter Code" tabs, "Show QR Code", code processing, and "I Have a
Recovery Code".
- The "Sync Bookmarks Across Devices" promo in the bookmark added sheet
now opens Sync with its own launch source instead of none.
- Promotions build their launch source from `SyncLaunchSource`, so the
values are defined in one place.

### Steps to test this PR

_Setup_
- [ ] Install an internal debug build.
- [ ] Run `adb logcat | grep --line-buffered -E "Pixel sent:
.*(sync_promotion_(displayed|confirmed)|sync_setup_ended_(successful|failed|abandoned)|m_sync_signup_(direct|connect)|wide_sync-setup)"`.

_Passwords import promo_
- [ ] Make sure Sync is off.
- [ ] Import passwords from Google.
- [ ] Verify `sync_promotion_displayed` is logged with
`source=passwords_import`.
- [ ] Tap "Turn on Sync & Backup".
- [ ] Verify `sync_promotion_confirmed` is logged with
`source=passwords_import`.
- [ ] Tap "Sync This Device Only".
- [ ] Complete the device authentication prompt.
- [ ] Verify `m_sync_signup_direct` is logged with
`source=promotion_passwords_import`.
- [ ] Finish the flow.
- [x] Verify `wide_sync-setup` is logged with
`context.name=promotion_passwords_import`.

_Bookmarks import promo_
- [ ] Make sure Sync is off.
- [ ] Enable the `bookmarksImport` toggle of `SyncPromotionFeature`.
- [ ] Import bookmarks from Google.
- [ ] Verify `sync_promotion_displayed` is logged with
`source=bookmarks_import`.
- [ ] Tap "Turn on Sync & Backup".
- [ ] Verify `sync_promotion_confirmed` is logged with
`source=bookmarks_import`.
- [ ] Tap "Sync This Device Only".
- [ ] Complete the device authentication prompt.
- [ ] Verify `m_sync_signup_direct` is logged with
`source=promotion_bookmarks_import`.
- [ ] Verify `wide_sync-setup` is logged with
`context.name=promotion_bookmarks_import`.

_Pairing from an import promo_
- [ ] Make sure Sync is off.
- [ ] Import passwords from Google.
- [ ] Tap "Turn on Sync & Backup".
- [ ] Tap "Sync With Another Device".
- [ ] Sync with another device.
- [ ] Verify `sync_setup_ended_successful` is logged with
`launch_source=promotion_passwords_import`.

_Pairing from Sync settings_
- [ ] Close and open "Sync & Backup".
- [ ] Tap "Sync With Another Device".
- [ ] Sync with another device.
- [ ] Verify `sync_setup_ended_successful` is logged with
`launch_source=other`.

_Abandoned setup_
- [ ] Make sure Sync is off.
- [ ] Open "Sync & Backup".
- [ ] Tap "Sync With Another Device".
- [ ] Close the scanner.
- [ ] Verify `sync_setup_ended_abandoned` is logged with
`launch_source=other`.

_Bookmark added sheet promo_
- [ ] Make sure Sync is off.
- [ ] Bookmark a page.
- [ ] Tap "Sync Bookmarks Across Devices" in the bookmark added sheet.
- [ ] Tap "Sync This Device Only".
- [ ] Complete the device authentication prompt.
- [ ] Verify `m_sync_signup_direct` is logged with
`source=promotion_bookmark_added_dialog`.

### UI changes
N/A(Upload before screenshot)|(Upload after screenshot)|

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1142021229838617/task/1219899197827051
Tech Design URL (if applicable): N/A
API Proposals URL(s) (if applicable): N/A

### Description

Updates the redesigned passwords and bookmarks import screens so the
Sync promo shown after a successful import is the main call to action.
When the promo is on screen, the "Done" button is shown as a secondary
button instead of a primary one.

### Steps to test this PR

QA optional.

### UI changes
| Passwords in progress | Passwords no promo | Passwords promo |
| - | - | - |
| <img width="540" alt="image"
src="https://github.com/user-attachments/assets/e7e97ba1-efa5-47bc-b023-f4032c4ef5f4"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/e0e35cfb-49e6-44fb-9b92-5cb9fe81e10d"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/2eaa536c-2bf5-4466-88a9-b5c922a70459"
/> |



| Bookmarks no promo | Bookmarks promo |
| - | - |
| <img width="540" alt="image"
src="https://github.com/user-attachments/assets/d65fb652-1671-4611-b5e4-9d456ab36ec0"
/> | <img width="540" alt="image"
src="https://github.com/user-attachments/assets/aabf098e-88fc-41e0-8d6d-2d5f2e78674a"
/> |

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
YoussefKeyrouz and others added 30 commits November 12, 2026 01:59
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211654189969294/task/1220455198841234?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

Opening the tab manager with the address bar focused collapsed the input
back to the address bar just before the tab manager appeared.

The input is now dismissed once the tab manager covers the browser (when
the tab stops), so the collapse happens off screen. If the tab manager
closes before covering the browser, it's dismissed when the tab resumes
instead. Duck.ai mode is unchanged

### Steps to test this PR

_Focused input_
- [ ] Open a new tab, then tap the tabs button next to the focused
input: the input doesn't collapse before the tab manager opens
- [ ] Press back: the New Tab Page shows as before, focused
- [ ] Repeat, then pick a tab with a page: the page shows with the
address bar
- [ ] Repeat, then tap +: the new tab opens as before


### UI changes
| Before  | After |
| ------ | ----- |
|
https://github.com/user-attachments/assets/a2ac7683-1c3f-46d4-91e7-52725105eb90
|
https://github.com/user-attachments/assets/377c1c14-46c2-4df4-936d-0be09f1fba06
|
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1219721209616437?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220041118787533?focus=true
API Proposals URL(s) (if applicable):

### Description

This is part 1/5 of the new PR transitions

This PR adds the engine on its own, in `/browser/tabs/transition/`.
Nothing calls it yet; the browser wiring, tab manager, intent entry
points and internal settings follow.

- **Flag:** `TabManagerFeatureFlags.newTabTransitions()`, off by
default. `NewTabTransitionConfigProvider` returns null while it's off,
and every entry point starts from that.
- **Picture:** `Window.capturePicture()` copies the window with
`PixelCopy`, waiting at most 50 ms; on failure there's no picture and no
transition. Tab appears instantly.
- **Handing over:** `PendingNewTabTransitions` holds one new tab's
picture and motion from the tap until that tab shows (for 3s at most).
It's keyed by tab id, so only that tab can animate.
- **Coordinating:** `NewTabTransitionCoordinator` receives the browser's
signals (tab showing, tab list changed, tab registered, browser started,
resumed and stopped) and starts, covers, or cancels:
- When the list gets the new tab before the selection does, the picture
covers the pager until the selection arrives.
- If a tab is listed before its picture is ready, it waits for it. It
only apply a cover if it's still hidden when the picture arrives.
  - Any other selection ends a running cover, transition, or wait.
- **Playing:** `NewTabTransitionPlayer` shows the picture in a cover
view behind the tab container, then grows the container in over it.
- The clock advances at most 20 ms per frame, so a stall while the new
tab builds pauses the motion instead of skipping it.
- It follows the system animator duration scale; with animations off,
including Battery Saver, it ends at once.
- **Motion values:** `NewTabTransitionConfig` and `NewTabMotionFrames`
hold the timing, scales, offsets and dim, with one profile per origin
(browser, tab manager).
- **Running state:** `NewTabTransitionState` exposes whether a
transition runs and which tab is shown. `TabPagerSwipeGate` pauses pager
swiping while one runs.


### Steps to test this PR

_No UX change yet_
- [ ] The app builds and behaves as before: nothing calls the engine yet
- [ ] Unit tests succeed

### UI changes
No UI changes
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220221287444491
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220041118787533?focus=true
API Proposals URL(s) (if applicable):

### Description

The second of five PRs for new tab transitions. Stacked on the
transition engine PR.

This PR wires the engine (introduced in #10365) into the browser, so new
tabs opened from inside a tab grow in.

- `BrowserTabFragment`: the new tab entry points ask for the transition.
- `BrowserActivity`: takes the picture at the tap, hands the transition
to the new tab once it exists, and passes the engine the browser's tab
and lifecycle signals, with swiping tabs on and off.
- `activity_browser.xml`: the cover view that shows the picture.

### Steps to test this PR

Enable `tabManager` → `newTabTransitions` in the feature flag inventory.

_Swiping tabs on_
- [ ] Browser menu → New tab: the new tab grows in over the page from
the right side, and old one dims and slides away
- [ ] Long-press on the tabs button: same transition
- [ ] In a Duck.ai tab, click on + menu → Search (Or "New tab" when
`duckAiPlusMenuUpdate` feature flag is off): same transition
- [ ] Long-press a link on a website → Open in new tab: same transition
- [ ] Any link that opens a new tab (`target="_blank"`): same transition
- [ ] Turn Animations off (In your phone system settings - developer
options or Battery Saver depending on your phone model ): the new tab
shows at once without animation

_Swiping tabs off_ (disable `swipingTabs` in the feature flag inventory)
- [ ] Repeat all the steps above

_Flag off_
- [ ] New tabs open exactly as before

### UI changes



https://github.com/user-attachments/assets/1d5c3a40-fbe0-43f3-b4b0-c41f29c51517
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1219721209616438?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220041118787533?focus=true
API Proposals URL(s) (if applicable): None

### Description

The third of five PRs for new tab transitions. Stacked on the browser
wiring PR.

This PR connects the tab manager to the transition engine, so a new tab
opened from it grows in from below over a picture of the tab manager.

- `TabSwitcherNewTabTransition`: takes the picture at the tap and hands
it to the browser when the tab manager closes. Only the latest tap can
hand over, since the tab operations can finish in any order.
- `TabSwitcherViewModel`: passes each tap's request id with the tab
operation and closes with `CloseToNewTab`. A reused tab (a blank New Tab
Page or an empty Duck.ai chat) closes without the transition.
- `TabSwitcherActivity`: starts a request on the new tab and Duck.ai
taps, and finishes without a window animation when the new tab grows in.

**Known limitation:** after switching between Regular and Fire while in
the tab manager, or rotating the device a new tab shows without the
transition.
The browser rebuilds itself for the new mode after you switch back to it
(becuase it was in the background), so the transition is skipped.
This only happen when we switch, subsequent tabs would play the
animation.

### Steps to test this PR

Enable `tabManager` → `newTabTransitions` in the feature flag inventory.

_Swiping tabs on_
- [ ] Open the tab manager, and close any already opened NTP (so we
don't switch to it).
- [ ] Tap + to open a new tab: the new tab grows in from below over the
tab manager
- [ ] Same with + in the bottom bar (bottom or split address bar)
- [ ] In Tab manager → Duck.ai button → New Chat, or picking a recent
chat: same transition.
- [ ] With a blank New Tab Page already open, tap +: the tab manager
closes to it without the transition
- [ ] Tap + twice quickly: the new tab grows in, once
- [ ] Tap an existing tab: the tab manager closes as before
- [ ] Switch between Regular and Fire in the tab manager, then tap +:
the new tab shows without the transition, nothing left on screen

_Swiping tabs off_ (disable `swipingTabs` in the feature flag inventory)
- [ ] Repeat all the steps above. Exception: with a blank New Tab Page
already open, + opens a new tab with the transition instead of reusing
it

_Flag off_
- [ ] The tab manager closes as before in all cases

### UI changes


https://github.com/user-attachments/assets/32ec94f0-99b0-496f-b2dd-f9a242b069ac
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1220221287444492?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220041118787533?focus=true
API Proposals URL(s) (if applicable): None

### Description

The fourth of five PRs for new tab transitions. Stacked on the tab
manager PR.

This PR makes new tabs opened through an intent animate in when the
browser is on screen. This covers Duck.ai's own entries (New Chat,
Image, a chat from the menu) and links opened in a new tab by an intent.

- `BrowserActivity.intent()`: marks in-app launches, so a launch from a
widget or from text selected in another app doesn't grow in, even with
the browser beside it in split screen.
- `BrowserActivity.processIntent()`: the Duck.ai and open-in-new-tab
branches take the picture and register the new tab's transition. Nothing
animations in unless the browser is on screen, or for an intent held
back until data clearing finished. A link needs the tab that asked to be
on screen; a Duck.ai request carries no tab, so the tab on screen stands
in.

### Steps to test this PR

Enable `tabManager` → `newTabTransitions` in the feature flag inventory.

_Swiping tabs on_
- [ ] On the New Tab Page, Duck.ai icon in the address bar → New Chat:
the chat grows in from the right
- [ ] In a Duck.ai chat, + → Image: same transition
- [ ] On the New Tab Page, Duck.ai menu → a recent chat: same transition
- [ ] On a page, type duck.ai in the address bar → New Chat: same
transition
- [ ] On a page, open the contextual Duck.ai sheet and open its chat in
a new tab: same transition, over a picture of the page with the sheet

_Swiping tabs off_ (disable `swipingTabs` in the feature flag inventory)
- [ ] Repeat all the steps above

_Flag off_
- [ ] All the above open as before

### UI changes


https://github.com/user-attachments/assets/fc6e45f5-0f97-45c2-9353-9085631d4ee8
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1219721209616436?focus=true
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220041118787533?focus=true
API Proposals URL(s) (if applicable): None

### Description

The last of five PRs for new tab transitions. Stacked on the intent
entry points PR.

This PR adds a screen to the internal build's developer settings for
tuning the transition on a device. Internal builds only.

- `NewTabTransitionDevSettingsActivity`: shows whether the flag is on,
picks where the new tab grows in from (browser and tab manager
separately), and toggles slow motion and reset.
- `NewTabTransitionOriginDevSettingsActivity`: sliders for one origin
(start size, offset, scale back, dim, slide left, duration).
- `NewTabTransitionDevStore`: saves the settings, keeping saved values
within the sliders' ranges.
- `InternalNewTabTransitionConfigProvider`: replaces the default config
with the saved one in internal builds.

### Steps to test this PR

Internal build. Enable `tabManager` → `newTabTransitions` in the feature
flag inventory.

- [ ] Settings → Developer settings → New Tab Transitions: the screen
opens and shows the flag as On
- [ ] Turn on slow motion, open a new tab from the browser menu: the
transition plays slowly
- [ ] Change the browser origin to Below center, open a new tab: it
grows in from below
- [ ] Open an origin's animation page, move each slider, open a new tab:
the changes apply
- [ ] Reset: the defaults are back
- [ ] Turn the flag off: the screen says the settings apply once it's
on, and new tabs open as before


### UI changes



https://github.com/user-attachments/assets/8dd696f3-29ee-436f-a985-0a09e314d495
Task/Issue URL:
https://app.asana.com/1/137249556945/project/414730916066338/task/1220457634550540

- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [x] All tests must pass
- [x] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [x] All tests must pass
- [x] Privacy tests do not need to run

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/414730916066338/task/1220477803409733

- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [ ] All tests must pass
- [ ] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220481274722966
Autoconsent Release:
https://github.com/duckduckgo/autoconsent/releases/tag/v18.3.0


## Description
Updates Autoconsent to version
[v18.3.0](https://github.com/duckduckgo/autoconsent/releases/tag/v18.3.0).

### Autoconsent v18.3.0 release notes
See release notes
[here](https://github.com/duckduckgo/autoconsent/blob/v18.3.0/CHANGELOG.md)

## Steps to test
This release has been tested during Autoconsent development. You can
check the release notes for more information.
1. Make sure that there's no unexpected failures in CI checks
2. (optional) smoke test some of the sites mentioned in the release
notes
3. If there are problems, reach out to a CPM DRI

Co-authored-by: sammacbeth <248111+sammacbeth@users.noreply.github.com>
)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220271890253683?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

### Steps to test this PR

_Pre steps_
- [x] Apply patch on
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true
- [x] Follow steps in
https://app.asana.com/1/137249556945/task/1219969199295542 to get a
staging ROW stripe account

_Withdrawal date_
- [x] Install from branch
- [x] Go to Settings > I have a Subscription
- [x] Add staging account you purchased
- [x] Go to Subscription Settings
- [x] Tap on Withdrawal option
- [x] Withdrawal dialog will be displayed
- [x] Tap on 'Confirm Withdrawal'
- [x] Verify the withdrawal date in the not subscribed screen looks
correct

### No UI changes

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1198194956794324/task/1220386721157988
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable):

### Description

Removes NetP's pcap recording. It never captured anything:
`WgVpnNetworkStack` always passed `pcapConfig = null`, so the internal
settings toggle had no effect.

```diff
 WgVpnNetworkStack
-  wgProtocol.startWg(tunFd, config, pcapConfig = null)
+  wgProtocol.startWg(tunFd, config)
 RealWgProtocol.startWg
   safelyStartWg(tunFd, config)
-  safelyConfigurePcap(pcapConfig)   // GoBackend.wgPcap, never reached
```

Removed:
- `GoBackend.wgPcap` (JNI declaration), `PcapConfig`,
`NetPDefaultConfigProvider.pcapConfig()`
- Internal: `enablePcapRecording` toggle, `ExportPcapContract`, the
"PCAP" section in NetP internal settings (layout, strings,
export/delete/share handlers), and the lint baseline entries for that
code

### Steps to test this PR

_NetP_
- [ ] Internal build: open NetP internal settings, the PCAP section is
gone and the screen works
- [ ] Enable NetP, it connects and traffic flows
- [ ] Disable / re-enable NetP, no crash

### UI changes
| Before  | After |
| ------ | ----- |
| PCAP section in NetP internal settings | Section removed |
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220473431098918?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

After #10425 With the selected-tab debounce removed, the pager moves to
a tab by its ID instead of its index. That path didn't tell the
transition that the new tab is showing, so the cover went up and timed
out with no grow-in.

This makes the ID path call `onTabShowing` the same way the index path
does, and adds a test for it.

### Steps to test this PR

_New tab transition with the debounce removed_
- [ ] Fresh install of an internal build
- [ ] Enable tabManager newTabTransitions in the feature flags
inventory, then restart the app
- [ ] Open a new tab from the browser menu: it animates
- [ ] Open a new tab from the tab manager: it animates
- [ ] Open a link in a new tab from a page: it animates

### UI changes
No UI Changes
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212810093780571/task/1220460098860759?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

Turns on new tab transitions by default in internal builds. The
newTabTransitions flag now defaults to INTERNAL instead of FALSE. Play
builds still have it off, and remote config can still turn it off.

### UI changes
No UI changes
Task/Issue URL:
https://app.asana.com/1/137249556945/project/38424471409662/task/1220303775212997?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
- Moved the new chat hint strings (newChatHintDocuments,
newChatHintPhoto, newChatHintImage) from donottranslate.xml to
strings-duckchat.xml so Smartling translates them.
- Changed the photo hint copy to the plural "Ask about photos" to match
"Analyze files", following copy review.

### Steps to test this PR
QA-optional

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: smartling <smartling@host-vm>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212227266948491/task/1220177860617495
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable): None

### Description

Adds the `sponsorBlock` remote feature to `ad-blocking-impl`, mirroring
the structure added for Apple in duckduckgo/privacy-configuration#6347:

- `SponsorBlockFeature`: `self()` and the `mvpEnabled` subfeature, both
defaulting to `internal`.
- `SponsorBlockSettingsProvider`: reads `categories` and `actionTypes`
from the feature settings, falling back to `["sponsor"]` and `["skip"]`
when the settings are missing, malformed or lack a key. Other keys in
the settings (such as the selectors read by C-S-S) are ignored.

Nothing consumes these yet.
n/a

### UI changes
n/a

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1220177860617500
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/481882893211075/task/1219201061224389?focus=true
API Proposals URL(s) (if applicable): None

### Description

Adds the native side of the `sponsorBlock` C-S-S messaging:

- `SponsorBlockContentScopeJsMessageHandler` handles `getSegments` and
`getConfig` and responds directly from the handler.
- `getSegments` validates `videoID` (11 chars, `[A-Za-z0-9_-]`) before
looking it up and returns segments sorted by start. It returns an empty
list if the ID is invalid, the feature is disabled or the lookup fails.
- `getConfig` returns `enabled` (from `mvpEnabled`), `skipMode`
(`manual` for now) and the contract `version`.
- Allowed domains are `youtube.com`, `m.youtube.com` and the Duck Player
embed host. The messaging layer checks them against the main frame's
host.
- `SponsorBlockSegmentsProvider` with an empty implementation. The real
provider will be plugged in later.
- `@ContentScopePrivacyFeature("sponsorBlock")`, so the remote config
for the feature is passed to C-S-S.

### Steps to test this PR
- [ ] Test along with next one

### UI changes
n/a

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212227266948491/task/1220201780433055
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable): None

### Description

Replaces the empty `SponsorBlockSegmentsProvider` with
`RealSponsorBlockSegmentsProvider`:

- Sends only the first 4 hex characters of the video ID's SHA-256 to the
SponsorBlock proxy (`SponsorBlockService`, through the `api` Retrofit
client, which sends no cookies). The hash uses the existing
`String.sha256` from `common-utils`.
- Keeps only the requested video from the response, then only the
categories and action types allowed by the `sponsorBlock` remote config
settings.
- Treats segments as untrusted: drops any without exactly two finite
numbers, with a negative start, with an end not after the start, or
without a category or action type. A non-positive `videoDuration`
becomes `null`.
- Resolves to an empty list on any failure. A 404 is cached as "no
segments"; other failures aren't cached, so the next call retries.
- Keeps a 50-entry LRU cache of each video's valid segments and applies
the remote config filter on every call, so a config change applies
without refetching. Concurrent lookups for the same video share one
request, which runs in the app scope so one caller cancelling doesn't
cancel it for the others.
- Adds `SponsorBlockHashPrefixGate`, which decides whether a lookup
needs the network. It always answers yes until the local hash prefix
list is available.
- Never logs the video ID or the hash.

### Steps to test this PR

_Pre-requisites_
- [x] Apply patch 1 below
- [x] Run `npm i
github:duckduckgo/content-scope-scripts#pr-releases/cris/sponsorblock-android`


_Segments lookup_
- [x] Install `internalDebug` 
- [x] Override remote config URL with
https://duckduckgo.github.io/privacy-configuration/pr-6373/v5/android-config.json
- [x] Open a YouTube video with sponsored content
- [x] Check sponsored segment is highlighted in green in the progress
bar
- [x] The skip button shows during the sponsor segment
- [x] Test it also works on Duck Player, and fullscreen mode
- [x] Disable Sponsorblock > mvpEnabled flag
- [x] Go back to the video, check no segments are hidhlighted now 

### Patches
_Patch 1_ 
```
Subject: [PATCH] Embedded privacy-config
---
Index: privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json
--- a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json	(revision 4d7a8a5)
+++ b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json	(date 1793967627046)
@@ -1,6 +1,6 @@
 {
     "readme": "https://github.com/duckduckgo/privacy-configuration",
-    "version": 1652275711516,
+    "version": 9999999999999,
     "features": {
         "adBlockingExtension": {
             "exceptions": [],
@@ -1179,6 +1179,15 @@
                     ]
                 }
             }
+        },
+        "sponsorBlock": {
+            "state": "internal",
+            "exceptions": [],
+            "features": {
+                "mvpEnabled": {
+                    "state": "internal"
+                }
+            }
         }
     },
     "unprotectedTemporary": []

```

### UI changes
| Before  | After |
| ------ | ----- |
| No UI changes | No UI changes |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1220201946095698
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable): None

### Description

Adds the user setting for SponsorBlock and sends it to C-S-S:

- Adds a "Skip sponsored content" row to the Ad Blocking settings, above
Duck Player, with a footer explaining what it does. Tapping it opens a
radio dialog with Always Ask, Always Skip and Off. The row only shows
while `sponsorBlock.mvpEnabled` is on. Strings live in
`donottranslate.xml` for now.
- Stores the mode in the ad blocking DataStore
(`SponsorBlockUserPreferences`). It defaults to Always Ask.
- Maps the mode into the config sent to C-S-S: Always Ask → `skipMode:
"manual"`, Always Skip → `skipMode: "auto"`, Off → `enabled: false`.
- `getSegments` reads the stored mode before any lookup, so Off never
fetches segments. `config()` can't suspend, so it uses a cached copy of
the mode, which holds the default until the first DataStore read.
- The change reaches open YouTube tabs through the existing
`onConfigChanged` push on tab resume.

### Steps to test this PR

_Pre-requisites_
- [x] Same as #10384: apply its embedded privacy-config patch and run
`npm i
github:duckduckgo/content-scope-scripts#pr-releases/cris/sponsorblock-android`
- [x] Install `internalDebug`

_Settings_
- [x] Open Settings > Ad Blocking
- [x] Check "Skip sponsored content" shows above Duck Player with
"Always Ask" and the footer below it
- [x] Tap it, pick each option, and check the row shows the new value
- [x] Disable Sponsorblock > mvpEnabled, go back to Ad Blocking
settings, and check the row and footer are hidden

_Always Ask_
- [x] Open a YouTube video with sponsored content
- [x] Check the segment is highlighted and the skip button shows during
it

_Always Skip_
- [x] Switch to Always Skip and go back to the video tab
- [x] Check the sponsored segment is skipped without tapping anything

_Off_
- [x] Switch to Off and go back to the video tab
- [x] Check no segments are highlighted and nothing is skipped

### UI changes
| Before  | After |
| ------ | ----- |
|<img width="575" height="1284" alt="Image"
src="https://github.com/user-attachments/assets/4a49fec7-26ad-4258-bccf-ca6870a73aaa"
/>|<img width="573" height="1290" alt="Image"
src="https://github.com/user-attachments/assets/8673cd2e-22fa-49fd-a284-77d9535c096a"
/>|

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220357840317513
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable): None

### Description

Sends each segment's SponsorBlock `UUID` to C-S-S in `getSegments`.

C-S-S remembers the segment the user seeked into by its `uuid` and shows
the skip button for it instead of auto-skipping. Without the `uuid` that
check never matched, so in Always Skip mode, dragging the seekbar back
into a skipped segment skipped it again.

- `SegmentResponse` reads `UUID` from the response. It uses
`@field:Json` because the app's Moshi has no Kotlin adapter, so a plain
`@Json` on the constructor parameter isn't picked up.
- `SponsorBlockSegment` carries `uuid`, and segments without one are
dropped, since the C-S-S `getSegments` response schema requires it.

### Steps to test this PR

_Pre-requisites_
- [x] `npm i
github:duckduckgo/content-scope-scripts#pr-releases/cris/sponsorblock-android`
- [x] Install `internalDebug`

_Seeking back in Always Skip_
- [x] Set Settings > Ad Blocking > Skip sponsored content to Always Skip
- [x] Open a YouTube video with sponsored content and let it play into a
segment
- [x] Check the segment is skipped automatically
- [x] Drag the seekbar back into the middle of that segment
- [x] Check playback continues inside the segment and the skip button
shows
- [x] Let it play past the segment, then into the next one, and check
that one is skipped automatically

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1220122635402769
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
The empty Duck.ai Chats screen labelled its button "Open Duck.ai",
although tapping it starts a new chat. The button now reuses the already
translated `browserMenuNewChat` string, so no new translations are
needed, and the unused string is removed. The purple illustration is
unchanged; design is tracking that separately.

### Steps to test this PR

_Empty Chats screen_
- [ ] Install a build with no saved Duck.ai chats
- [ ] Open the All Chats menu
- [ ] Check the button under "No chats yet" reads "New Chat"
- [ ] Tap it and check a new Duck.ai chat opens

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…terceptor (#10508)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1219784082433694
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/project/1211850753229323/task/1217926121070145
API Proposals URL(s) (if applicable): None

### Description

Extracts the request exemption checks (trusted sites, request blocklist
and its allowlists) and the request rewrite decision (GPC headers, user
agent) out of `WebViewRequestInterceptor` into two classes in
`request-interception-impl`, each with its own unit tests. Behaviour is
unchanged; the interceptor keeps the WebView side effects and drops from
20 to 16 dependencies.

### Steps to test this PR

QA optional, these steps are covered by CI.

_Behaviour unchanged_
- [ ] Run `./gradlew :app:testPlayDebugUnitTest` → confirm
`WebViewRequestInterceptorTest` (55), `DomainsReferenceTest` (122),
`SurrogatesReferenceTest` (12) and `RequestBlocklistReferenceTest` (41)
pass with only their constructor wiring changed
- [ ] Run `./gradlew :request-interception-impl:testDebugUnitTest` →
confirm `RequestExemptionsTest`, `RequestRewriteDecisionTest` and
`TrustedSitesTest` pass

_Smoke test_
- [ ] Install the app, open a site with trackers → confirm trackers are
still blocked in the privacy dashboard
- [ ] Add the site to the allowlist (disable protections) and reload →
confirm trackers are no longer blocked
- [ ] Toggle "Request desktop site" on any page → confirm the page
reloads with the desktop version

### UI changes

No user-facing UI changes.

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220098860376945?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description
Fix the failing validation for the subscription onboarding step pixels

### Steps to test this PR
- [ ] N/A

### No UI changes

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220384931554018
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/task/1220218515935564
API Proposals URL(s) (if applicable):
https://app.asana.com/1/137249556945/project/1212087397361015/task/1220342074337452

### Description

A migrated install (one whose data arrived through a device-to-device
transfer) now gets a short flow on first start: intro, notification
permission, a default browser prompt and "You're all set".

- The default browser step shows only when DuckDuckGo was the default
browser on the previous phone, so it is these users' one prompt while
their staged default browser prompts stay stopped.
- `DeviceMigration` gains `isMigrationFlowPending()`,
`markMigrationFlowCompleted()` and `sourceDeviceWasDefaultBrowser()`.
The flow is pending only for installs first detected by a version that
has it, so an install migrated earlier never gets it late.
- `PendingOnboardingStarter` picks the migration flow or the new-user
plan. A user who was still new on the previous phone continues new-user
onboarding afterwards, minus the steps the flow already showed. A
returning user gets no further onboarding.
- New pixels: `m_device_migration_all_set_prompt_shown` and
`m_device_migration_all_set_prompt_button_clicked`.

Transfers are only turned on by the top PR of this stack, so on its own
this PR changes nothing for users.

### Steps to test this PR

Use a build from the top of the stack
(`feature/aibrahim/device-migration-transfer`), which turns transfers
on, and two phones. On the new phone, transfer apps and data during
setup (Google's "Copy apps & data", or Smart Switch on Samsung) and
don't open DuckDuckGo until the transfer has finished.

_Migrated install, DuckDuckGo was the default browser_
- [ ] On the old phone, set DuckDuckGo as the default browser and open
the app once
- [ ] Transfer to the new phone and open the app
- [ ] The intro shows, then the notification permission step (if not
already granted), the default browser step and "You're all set"
- [ ] `m_device_migration_all_set_prompt_shown` fires, and
`m_device_migration_all_set_prompt_button_clicked` fires on tapping its
button

_Migrated install, DuckDuckGo was not the default browser_
- [ ] Same as above, with another default browser on the old phone
- [ ] The flow skips the default browser step

_Old phone was still a new user_
- [ ] On the old phone, reinstall the app and leave onboarding
unfinished
- [ ] After the transfer, the migration flow shows, then new-user
onboarding continues without repeating the steps the flow showed

_Not migrated_
- [ ] Fresh install: onboarding is unchanged

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1220384931554018
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/task/1220218515935564
API Proposals URL(s) (if applicable):
https://app.asana.com/1/137249556945/project/1212087397361015/task/1220342074337452

### Description

On a migrated install without Sync, the new tab page suggests turning
Sync on once the migration flow has ended, until the user dismisses it
or turns Sync on.

- sync-api: `SyncAfterDeviceTransferPrompt`, and
`SyncActivityStartSetupFlow` to open Sync setup directly. Setup leads
with "Sync With Another Device", so a user whose previous account wasn't
restored can join it again.
- `DeviceMigration` gains `previousDeviceHadSync()`. True when it found
an account to restore. When nothing was found to restore, it is false,
so that user isn't left without Sync and without the suggestion.
- The Sync stamp for a future transfer is also refreshed when the app
goes to the background, so a backup taken right after turning Sync on
carries it.
- New pixels: `m_device_migration_sync_prompt_shown` and
`m_device_migration_sync_prompt_clicked`.

### Steps to test this PR

Use a build from the top of the stack
(`feature/aibrahim/device-migration-transfer`) and two phones,
transferring as described in the PR below this one.

_Previous phone without Sync_
- [ ] After the transfer, finish the migration flow
- [ ] The new tab page shows the "Oh before I forget…" suggestion and
`m_device_migration_sync_prompt_shown` fires
- [ ] "Turn on Sync" opens Sync setup and
`m_device_migration_sync_prompt_clicked` fires
- [x] Once dismissed, or once Sync is on, the suggestion doesn't show
again

_Previous phone with Sync, account restored_
- [ ] Transfer with the Google account included ("Accounts" ticked in
Smart Switch)
- [x] Sync is restored silently and the suggestion doesn't show

_Previous phone with Sync, nothing restored_
- [ ] Transfer without the Google account, so the restore key doesn't
arrive
- [ ] Sync stays off and the suggestion shows

_Not migrated_
- [ ] Fresh install: the suggestion never shows

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1220342074337450
Tech Design URL (if applicable):
https://app.asana.com/1/137249556945/task/1220218515935564
API Proposals URL(s) (if applicable): None

### Description

Turns on device-to-device transfer of app data.

- Backup is enabled with our own `DeviceMigrationBackupAgent` and
`fullBackupOnly`. With the `deviceMigration` remote flag off, the agent
sends nothing.
- Cloud backup still excludes every domain, so nothing goes to Google
Drive.
- A device transfer sends only the stores listed in the rules: tabs and
other app data in `app.db`, history, site permissions and preferences,
Duck.ai chats, downloads, dismissed remote messages, settings, WebView
data and the migration's own stamps. A store added later stays on the
old phone until it is listed.
- `BackupRulesTest` checks that cloud backup excludes everything, that
both rules files list the same stores, that no whole domain is included,
and that no credential, keystore-encrypted or telemetry store is.

### Steps to test this PR

Two phones are needed: Android's command-line tools can make a
device-transfer backup but can't restore one. On the new phone, transfer
apps and data during setup (Google's "Copy apps & data", or Smart Switch
on Samsung) and don't open DuckDuckGo until the transfer has finished.

_Transfer_
- [ ] On the old phone, open tabs, visit sites, add bookmarks and
favorites, start a Duck.ai chat, change a few settings, download a file,
save a password and sign in to Email Protection
- [ ] Transfer to the new phone and open the app
- [ ] Tabs, history, bookmarks and favorites, the Duck.ai chat, the
settings and the downloads list arrived
- [ ] Saved passwords, Email Protection and the subscription did not

_Kill switch_
- [ ] With the `deviceMigration` remote flag off on the old phone, a
transfer brings no app data

### UI changes
| Before  | After |
| ------ | ----- |
| No UI changes | No UI changes |

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211850753229323/task/1220382189916774?focus=true
Tech Design URL (if applicable): None
API Proposals URL(s) (if applicable): None

### Description

Treats a tapped login form submission as user-initiated for the redirect
that returns to the app, so the app link is no longer silently dropped.

### Steps to test this PR

> [!NOTE]
> Prerequisites:
> - [x] Download the test tools:
[auth-handoff-test-tools.zip](https://github.com/user-attachments/files/34807459/auth-handoff-test-tools.zip)
and unzip.
> - [x] Install this branch's internal debug build,
> - [x] Install the auth handoff test apk to simulate all scenarios from
the unzipped folder.
> - [x] Start the local server from the unzipped folder:
>   ```
> python3 server/server.py --port 8080 --scenarios
server/scenarios_auth.py
>   ```
> - [x] Configure the device:
>   ```
>   adb reverse tcp:8080 tcp:8080
>   adb install -r auth-handoff-test.apk
> adb shell pm set-app-links-allowed --user 0 --package
com.duckduckgo.authhandofftest true
> adb shell pm set-app-links --package com.duckduckgo.authhandofftest 2
localtest.me
> adb shell pm set-app-links-user-selection --user 0 --package
com.duckduckgo.authhandofftest true localtest.me
>   adb shell pm get-app-links com.duckduckgo.authhandofftest
>   ```
> The last command should list `localtest.me` as approved (Android 12+;
the host cannot be auto-verified, hence the manual approval).

_Happy path_
- [x] Login redirect reaches app under 'Ask Each Time': Tap preset
**Form POST -> 302 -> callback** (Callback: App Link), set Open Links in
Apps to **Ask Each Time**, tap **view**, tap Submit → confirm the "Open
in app" snackbar appears and its action opens the test app.
- [x] Login redirect opens app directly under 'Always': Same preset and
Callback, set it to **Always**, tap **view**, tap Submit → confirm the
test app opens directly.
- [x] Hand-off after DuckDuckGo was closed in background: Same preset
(Callback: App Link), **Always**, tap **ct_session** once, run `adb
shell am force-stop com.duckduckgo.mobile.android.debug`, tap
**ct_stale_session**, tap Submit → confirm the test app opens (under
**Ask Each Time**, the snackbar appears instead).

_Guards_
- [x] No app opens without a tap: Tap preset **No tap: auto-submit
POST** (Callback: App Link), any setting, tap **view** (no Submit) →
confirm no snackbar appears and the test app does not open.
- [x] Late script redirect still opens no app: Tap preset **POST -> page
-> script delayed (6s)** (Callback: App Link), **Ask Each Time**, tap
**view**, tap Submit → confirm no snackbar and no app when the script
navigates to the callback seconds later.

_Edge cases_
- [x] Login redirect through multiple redirects: Tap preset **Form POST
-> 2 redirects** (Callback: App Link), tap **view**, tap Submit →
confirm the snackbar (**Ask Each Time**) or the test app (**Always**) as
in the happy path.
- [x] Multi-page login (email, then password): Tap preset **Two-step
login (email, password)** (Callback: App Link), **Ask Each Time**, tap
**view**, submit the email page then the password page → confirm the
snackbar after the second submit.
- [x] OAuth consent form returning to the app: Tap preset **Consent
(OAuth-like)** (Callback: App Link), **Ask Each Time**, tap **view**,
tap the consent button → confirm the snackbar; tapping it opens the test
app with `code` and `state` in the logged callback.
- [x] App's own link returns weren't affected: Switch Callback to
**Custom scheme**, tap preset **Form POST -> 302 -> callback**, **Ask
Each Time**, tap **view**, tap Submit → confirm the existing "open in
app" dialog, as before this PR.

### UI changes

No user-facing UI changes.

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1149059203486286/task/1220522358296710
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

Part of the Node 24 upgrade campaign. This is the required change for
`duckduckgo/Android`: declare a Node version, and make CI read it.

## What this does

```
add   .nvmrc                                 24.22.1
edit  .github/workflows/pixel-validation.yml node-version: 20.x
                                          -> node-version-file: .nvmrc
```

`pixel-validation.yml` is the only workflow here that runs a project
install —
`npm ci --verbose` in `PixelDefinitions/` — and it was pinned to **Node
20.x**,
below the campaign floor of 24.14.1. Pointing it at `.nvmrc` means the
version
lives in one place rather than being repeated in YAML.

**24.22.1** rather than the bare floor: it is a security release (seven
CVEs,
three High in TLS and DNS) and updates Node's root certificates to NSS
3.131.1.

## Three workflows deliberately left alone

`drift-audit.lock.yml`, `android-maintenance-worker.lock.yml` and
`workflow-failure-classifier.lock.yml` each set `node-version: '24'`,
which is a
bare major and does not strictly clear the floor — a bare `24` can
resolve to
`24.0.0`.

**They are compiled output.** Each is generated by GitHub Agentic
Workflows from
its `.md` source, and none of the three sources mentions Node at all —
the
version is injected by the compiler. Hand-editing the `.lock.yml` files
would be
reverted on the next recompile, so this PR does not touch them. If the
bare
major matters, that is a question for the agentic-workflow toolchain,
not for
this repo.

Those three run `npm install -g @anthropic-ai/claude-code@latest` rather
than a
project install, so they are not installing this repo's dependencies.

## Not included

No `.npmrc` / npm cooldown. That is a separate piece of work and it
needs a
decision here rather than a default: this repo has **three**
`package.json` +
lockfile pairs (root, `PixelDefinitions/`,
`.github/actions/pr-asana-sync/`),
and a root `.npmrc` with `min-release-age` would also govern the
`npm install -g @anthropic-ai/claude-code@latest` steps above — meaning
that
tooling would stop picking up releases newer than seven days. Worth
agreeing
before doing, not bundling in here.

## Checking

```bash
git clone --depth 1 git@dub.duckduckgo.com:duckduckgo/node-upgrade.git
python3 node-upgrade/check_repo.py --path .
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1218312816344693/task/1219837194779269?focus=true
Tech Design URL (if applicable): 
API Proposals URL(s) (if applicable):

### Description

Adds API quality scoring for `-api` modules, with GitHub workflows to
report on it. Nothing in the app or its build
changes: everything is new files under `scripts/api-quality/` and
`.github/`.

- **Scorer** (`scripts/api-quality/`): parses every `-api` module with
the Kotlin compiler's parser and scores it
against eight mechanical rules. It runs standalone, outside Gradle, in
about a second.
- **API quality check** (PR): compares the PR with the commit it
branched from and keeps one comment up to date,
listing each new finding with its line, details and fix. It fails on a
regression but isn't a required check, so it
  never blocks a merge.
- **API quality weekly report** (Mondays): compares `develop` with last
week's report and an optional baseline, and
  posts a summary plus one task per new finding group to Asana.
- **API quality scorecard** and **API quality compare** (manual): score
one ref, or compare two refs.

Asana posting needs the `GH_API_QUALITY_*` repository variables; until
they're set, the workflows only report on their artifact run pages.

**Edit - New changes after Review comments:**
- M1: also flags type names starting with "Real" followed by an
uppercase letter (RealFoo, not Realm).
- M7: also flags the Java mutable collections (ArrayList, HashMap,
HashSet, LinkedHashMap, LinkedHashSet, LinkedList, ConcurrentHashMap,
CopyOnWriteArrayList), the Atomic types, and the coroutines Channel and
SendChannel. Because we have our own data class Channel for
notifications, Channel only counts when the file imports it from
`kotlinx.coroutines.channels` (by name or with a wildcard) or writes the
full name.
- Source sets: every non-test source set is scored (src/main plus any
flavor folder like src/play), so flavor-only APIs aren't skipped.
- Rule count: no more hardcoded 8; the scorecard uses the scorer's rule
list, and compare.py counts the rules in rules.json.
- One source for rule texts: the names, explanations and fixes live in
scripts/api-quality/rules.json, which compare.py and the Asana scripts
read. The Asana rules task will link to it.
- Compiler setup: added a comment explaining why `MESSAGE_COLLECTOR_KEY`
is set to `MessageCollector.NONE`.
- score.sh: `SCORER_ROOT` can be set, which the re-baseline procedure
needs in order to run the old scorer against the same code.
- README: documents source sets, M1, M8 and rules.json. 

### Steps to test this PR

_Scripts, locally (needs Java 21)_
- [x] run `scripts/api-quality/score.sh` writes a scorecard to
`scripts/api-quality/output/`. Check the scorecard and CSV files are
properly generated
- [x] Run tests with `python3 -m venv /tmp/aq && /tmp/aq/bin/pip install
-r scripts/api-quality/requirements.txt && (cd scripts/api-quality &&
/tmp/aq/bin/python -m pytest)`

_Tests to do after merge (manual workflows only appear once on
`develop`)_
- [ ] **Actions → API quality scorecard → Run workflow** with no input
shows the scorecard on the run page and as an
  artifact.
- [ ] **API quality compare** with `develop` against an older release
tag shows the comparison and fails if `develop`
  regressed.
- [ ] **API quality weekly report → Run workflow**, box unticked, shows
the full report on the run page.

### UI changes

None.

---------

Co-authored-by: Łukasz Paczos <lukas.paczos@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.