Compare commits

..

43 Commits

Author SHA1 Message Date
Lakr
8df7353722 chore(ios): iap paywall update (#13669)
This pull request introduces several improvements and refactors to the
iOS frontend, with a focus on the paywall system, configuration, and
developer experience. The most significant changes include dynamic
pricing updates for subscription packages, the introduction of a
centralized pricing configuration, and enhanced developer documentation
and settings for Claude Code. There are also minor fixes and
improvements to restore purchase flows, App Store syncing, and protocol
usage guidance.

**Paywall System Improvements**

* Subscription package pricing and display is now dynamically updated
based on App Store data, ensuring users see accurate, localized pricing
and descriptions. This includes new logic for calculating monthly prices
and updating package button text. (`ViewModel.swift`,
`ViewModel+Action.swift`, `SKUnit+Pro.swift`, `SKUnit+AI.swift`)
[[1]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0R83-R160)
[[2]](diffhunk://#diff-cb192a424400265435cb06d86b204aa17b4e8195d9dd811580f51faeda211ff0L102-R199)
[[3]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL58-R73)
[[4]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL74-R94)
[[5]](diffhunk://#diff-ea535c02550f727587e74521da8fd90dec23cbe3c685f9c4aa4923ce0bbdb363L19-R35)
[[6]](diffhunk://#diff-a5fef660f959bbb52ce3f19bba8bfbd0bb00d66c9f18a20a998101b5df6c8f60L18-R22)
* Introduced a new `PricingConfiguration.swift` file to centralize
product identifiers, default selections, and display strings for
subscription products, improving maintainability and consistency.
(`PricingConfiguration.swift`, `SKUnit+Pro.swift`, `SKUnit+AI.swift`)
[[1]](diffhunk://#diff-de4566ecd5bd29f36737ae5e5904345bd1a5c8f0a73140c3ebba41856bae3e86R1-R54)
[[2]](diffhunk://#diff-ea535c02550f727587e74521da8fd90dec23cbe3c685f9c4aa4923ce0bbdb363L19-R35)
[[3]](diffhunk://#diff-a5fef660f959bbb52ce3f19bba8bfbd0bb00d66c9f18a20a998101b5df6c8f60L18-R22)

**Developer Experience and Documentation**

* Added `AGENTS.md` to provide comprehensive guidance for Claude Code
and developers, including project overview, build commands,
architecture, native bridge APIs, Swift code style, and dependencies.
(`AGENTS.md`)
* Added a local settings file (`settings.local.json`) to configure
permissions for Claude Code, allowing specific Bash commands for iOS
builds. (`settings.local.json`)
* Updated Swift architecture guidelines to discourage protocol-oriented
design unless necessary, favoring dependency injection and composition.
(`AGENTS.md`)

**User Experience Improvements**

* The purchase footer now includes an underline for "Restore Purchase"
and a clear message about subscription auto-renewal and cancellation
flexibility. (`PurchaseFooterView.swift`)
* Improved restore purchase and App Store sync logic to better handle
user sign-in prompts and error handling. (`ViewModel+Action.swift`,
`Store.swift`)
[[1]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL45-R49)
[[2]](diffhunk://#diff-df2cb61867b4ff10dee98d534cf3c94fe8d48ebaef3f219450a9fba26725fdcbL58-R73)
[[3]](diffhunk://#diff-9f18fbbf15591c56380ce46358089c663ce4440f596db8577de76dc6cd306b54R26-R28)

**Minor Fixes and Refactoring**

* Made `docId` in `DeleteSessionInput` optional to match GraphQL schema
expectations. (`DeleteSessionInput.graphql.swift`)
[[1]](diffhunk://#diff-347e5828e46f435d7d7090a3e3eb7445af8c616f663e8711cd832f385f870a9bL14-R14)
[[2]](diffhunk://#diff-347e5828e46f435d7d7090a3e3eb7445af8c616f663e8711cd832f385f870a9bL25-R25)
* Minor formatting and dependency list updates in `Package.swift`.
(`Package.swift`)
* Fixed concurrency usage in event streaming for chat manager.
(`ChatManager+Stream.swift`)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* New Features
* Paywall options now dynamically reflect product data with clearer
labels and monthly price calculations.
* Added an auto‑renewal note (“cancel anytime”) and underlined “Restore
Purchase” for better clarity.

* Refactor
* Improved purchase/restore flow reliability and UI updates for a
smoother experience.

* Documentation
* Added a comprehensive development guide and updated architecture/style
guidance for iOS.

* Chores
* Introduced local build permissions configuration for iOS development.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 09:18:47 +00:00
Cats Juice
12daefdf54 fix(core): prevent emoji being clipped and adjust icon-picker default color (#13664)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Style
- Updated icon picker to use the primary icon color, improving visual
consistency (including SVG icons).
- Improved emoji rendering in the document icon picker by applying an
emoji-specific font for elements marked as emoji, matching existing size
and line-height.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 02:25:31 +00:00
Wu Yue
9f94d5c216 feat(core): support ai chat delete action (#13655)
<img width="411" height="205" alt="截屏2025-09-26 10 58 39"
src="https://github.com/user-attachments/assets/c3bce144-7847-4794-b766-5a3777cbc00d"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Delete icon added to AI session history with tooltip and confirmation
prompt; deleting current session opens a new session.
- Session deletion wired end-to-end (toolbar → provider → backend) and
shows notifications.

- Improvements
- Cleanup now supports deleting sessions with or without a document ID
(document-specific or workspace-wide).
- UI tweaks for cleaner session item layout and safer click handling
(delete won’t trigger item click).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-27 11:58:58 +00:00
Lakr
8d6f7047c2 fix(ios): build project (#13656)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Access Tokens screen now shows revealed access tokens, including the
token value where available.

- Chores
  - Updated iOS Paywall package to use Swift tools version 5.9.
  - Removed an unused internal iOS package to streamline the app.
- Aligned access token data model to the latest backend schema for
improved consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 10:10:30 +00:00
github-actions[bot]
a92894990d chore(i18n): sync translations (#13651)
New Crowdin translations by [Crowdin GH
Action](https://github.com/crowdin/github-action)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-26 09:13:17 +00:00
L-Sun
6af1f6ab8d fix(core): infinitied loop (#13653)
Fix #13649 

#### PR Dependency Tree


* **PR #13653** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Streamlined internal async handling to depend only on specified
inputs, reducing unnecessary updates and improving responsiveness.
  * Preserved existing error handling for async operations.

* **Chores**
* Adjusted lint configuration/comments to align with the updated
dependency strategy, reducing false-positive warnings.

No user-facing UI changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 08:59:33 +00:00
Rokas
e7f76c1737 chore: update mermaid (#13510)
https://github.com/toeverything/AFFiNE/issues/13509

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Upgraded Mermaid dependency to v11.1.0 in the frontend core package.

* **Impact**
* Improved diagram rendering and compatibility with newer Mermaid
syntax.
* Potential performance and security improvements from upstream updates.
  * No UI changes expected; existing diagrams should continue to work.
  * Please verify critical diagram views for any rendering differences.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: L-Sun <zover.v@gmail.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-26 07:40:42 +00:00
Xun Sun
5b52349b96 feat: implement textAlign property (#11790)
for paragraph blocks, image blocks, list blocks, and table blocks

Should fix #8617 and #11254.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added text alignment options (left, center, right) for paragraph,
list, image, note, and table blocks.
- Introduced alignment controls in toolbars and slash menus for easier
formatting.
- Enabled keyboard shortcuts for quick text alignment changes (supports
Mac and Windows).
- **Localization**
- Added English, Simplified Chinese, and Traditional Chinese
translations for new alignment commands and shortcuts.
- **Style**
  - Blocks now visually reflect selected text alignment in their layout.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: L-Sun <zover.v@gmail.com>
2025-09-26 07:23:28 +00:00
renovate[bot]
bf87178c26 chore: bump up @googleapis/androidpublisher version to v31 (#13633)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[@googleapis/androidpublisher](https://redirect.github.com/googleapis/google-api-nodejs-client)
| [`^28.0.0` ->
`^31.0.0`](https://renovatebot.com/diffs/npm/@googleapis%2fandroidpublisher/28.0.1/31.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@googleapis%2fandroidpublisher/31.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@googleapis%2fandroidpublisher/28.0.1/31.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>googleapis/google-api-nodejs-client
(@&#8203;googleapis/androidpublisher)</summary>

###
[`v31.0.0`](https://redirect.github.com/googleapis/google-api-nodejs-client/blob/HEAD/CHANGELOG.md#13100-2024-01-05)

[Compare
Source](https://redirect.github.com/googleapis/google-api-nodejs-client/compare/v30.0.0...v31.0.0)

##### ⚠ BREAKING CHANGES

- **serviceconsumermanagement:** This release has breaking changes.
- **playintegrity:** This release has breaking changes.

##### Features

- **chromepolicy:** update the API
([8429e3c](8429e3c9d6))
- **chromeuxreport:** update the API
([6d52abb](6d52abb902))
- **customsearch:** update the API
([1169e4c](1169e4c607))
- **dialogflow:** update the API
([4b1e073](4b1e0734d9))
- **displayvideo:** update the API
([45b61b5](45b61b5d20))
- **oslogin:** update the API
([cfc90e7](cfc90e7c9c))
- **playintegrity:** update the API
([767af5f](767af5f12e))
- regenerate index files
([4246fd1](4246fd1c64))
- **serviceconsumermanagement:** update the API
([a68206a](a68206a211))

##### Bug Fixes

- **accesscontextmanager:** update the API
([845c716](845c7168e9))
- **admin:** update the API
([4664d6b](4664d6bb4c))
- **backupdr:** update the API
([19b0192](19b019219b))
- **calendar:** update the API
([0ca9bbc](0ca9bbc4e4))
- **cloudbuild:** update the API
([31158a2](31158a226c))
- **cloudidentity:** update the API
([22610b3](22610b3d15))
- **cloudprofiler:** update the API
([2c5cbc4](2c5cbc4299))
- **cloudtrace:** update the API
([2a811d5](2a811d5fe8))
- **iap:** update the API
([ec596c1](ec596c1b87))
- **playdeveloperreporting:** update the API
([7181840](7181840daf))
- **servicenetworking:** update the API
([50c7dbd](50c7dbd323))
- **spanner:** update the API
([0e40d67](0e40d67436))

###
[`v30.0.0`](https://redirect.github.com/googleapis/google-api-nodejs-client/blob/HEAD/CHANGELOG.md#13000-2024-01-03)

##### ⚠ BREAKING CHANGES

- **networksecurity:** This release has breaking changes.
- **metastore:** This release has breaking changes.
- **gmail:** This release has breaking changes.
- **gkehub:** This release has breaking changes.
- **drivelabels:** This release has breaking changes.
- **dialogflow:** This release has breaking changes.
- **datacatalog:** This release has breaking changes.
- **content:** This release has breaking changes.
- **connectors:** This release has breaking changes.
- **cloudbuild:** This release has breaking changes.
- **chat:** This release has breaking changes.
- **batch:** This release has breaking changes.
- **artifactregistry:** This release has breaking changes.
- **aiplatform:** This release has breaking changes.
- **advisorynotifications:** This release has breaking changes.

##### Features

- **accesscontextmanager:** update the API
([26d496e](26d496e416))
- **adexchangebuyer2:** update the API
([31c0066](31c006606f))
- **admin:** update the API
([79ce913](79ce9133d7))
- **advisorynotifications:** update the API
([0f44091](0f440919dd))
- **aiplatform:** update the API
([66739ce](66739ce624))
- **alloydb:** update the API
([590f835](590f835773))
- **analyticsdata:** update the API
([25d0b67](25d0b6763e))
- **analyticshub:** update the API
([8279edf](8279edf154))
- **androidpublisher:** update the API
([c6d69a0](c6d69a049d))
- **artifactregistry:** update the API
([6fda22c](6fda22c487))
- **assuredworkloads:** update the API
([41debeb](41debeba59))
- **backupdr:** update the API
([1018945](1018945770))
- **batch:** update the API
([9ef21e0](9ef21e0459))
- **bigquery:** update the API
([f1deeab](f1deeabbb0))
- **blockchainnodeengine:** update the API
([07ac2e7](07ac2e721d))
- **chat:** update the API
([88428f0](88428f0d91))
- **checks:** update the API
([2d78a72](2d78a72c71))
- **cloudbilling:** update the API
([857a51e](857a51e47b))
- **cloudbuild:** update the API
([ddf4c10](ddf4c10cf4))
- **cloudchannel:** update the API
([aecac6b](aecac6be45))
- **clouddeploy:** update the API
([62d7fd6](62d7fd6070))
- **cloudfunctions:** update the API
([c5aae9a](c5aae9a7cf))
- **cloudprofiler:** update the API
([2933bff](2933bff415))
- **cloudsupport:** update the API
([feb88b5](feb88b5521))
- **composer:** update the API
([53b83d6](53b83d65b1))
- **compute:** update the API
([ffbf00b](ffbf00b1c1))
- **connectors:** update the API
([f433bd6](f433bd6284))
- **container:** update the API
([cac432f](cac432f882))
- **content:** update the API
([c0dd4c0](c0dd4c0bc2))
- **datacatalog:** update the API
([a939d7e](a939d7eaf2))
- **dataflow:** update the API
([9721cda](9721cda955))
- **dataform:** update the API
([d2bfeab](d2bfeabcbe))
- **datafusion:** update the API
([413c94e](413c94e5db))
- **dataplex:** update the API
([8da4b12](8da4b128b1))
- **dataproc:** update the API
([5a60626](5a606262b3))
- **dialogflow:** update the API
([8829da4](8829da4a7e))
- **discoveryengine:** update the API
([567c02d](567c02d288))
- **dlp:** update the API
([7cbdc6a](7cbdc6aaf4))
- **dns:** update the API
([f783244](f7832440a5))
- **documentai:** update the API
([01cc7b5](01cc7b5994))
- **drivelabels:** update the API
([50a1b75](50a1b75751))
- **drive:** update the API
([c07f193](c07f193c33))
- **file:** update the API
([324d0f6](324d0f69b3))
- **firebaseappcheck:** update the API
([c8fb050](c8fb050246))
- **firebaserules:** update the API
([2a44570](2a445705f0))
- **gkehub:** update the API
([044e086](044e0861ed))
- **gkeonprem:** update the API
([6c9398e](6c9398e54e))
- **gmail:** update the API
([c7698bd](c7698bda1d))
- **healthcare:** update the API
([d34ee61](d34ee618f9))
- **metastore:** update the API
([6887f67](6887f67506))
- **migrationcenter:** update the API
([e890439](e890439ac6))
- **monitoring:** update the API
([738848d](738848dcb6))
- **networkmanagement:** update the API
([d8a3556](d8a35563fc))
- **networksecurity:** update the API
([166232f](166232fe14))
- **networkservices:** update the API
([076de17](076de17ce5))
- **notebooks:** update the API
([a08d104](a08d104800))
- **orgpolicy:** update the API
([5c8f8c7](5c8f8c727c))
- **oslogin:** update the API
([f1475c5](f1475c544f))
- **paymentsresellersubscription:** update the API
([d79cf5a](d79cf5a6cf))
- **playdeveloperreporting:** update the API
([6ef5718](6ef5718e6e))
- **policysimulator:** update the API
([58e6545](58e654547c))
- **prod\_tt\_sasportal:** update the API
([99b92fe](99b92fe5d9))
- **pubsub:** update the API
([f17fac3](f17fac34c0))
- **recaptchaenterprise:** update the API
([7952baa](7952baabbe))
- **recommender:** update the API
([76b9501](76b9501327))
- **redis:** update the API
([fd4636b](fd4636b1c9))
- regenerate index files
([33f2d78](33f2d78b2c))
- **retail:** update the API
([0aa095b](0aa095b51a))
- **run:** update the API
([48a19bf](48a19bf416))
- **sasportal:** update the API
([2459cce](2459cce1e4))
- **script:** update the API
([0520e5e](0520e5efd5))
- **securitycenter:** update the API
([74c634a](74c634a34a))
- **serviceconsumermanagement:** update the API
([0552119](05521190fe))
- **servicemanagement:** update the API
([429940b](429940b1b4))
- **servicenetworking:** update the API
([42a1422](42a142249e))
- **serviceusage:** update the API
([c2ad070](c2ad070ce4))
- **storage:** update the API
([c0609c9](c0609c901b))
- **translate:** update the API
([77a0522](77a05229d2))
- **vault:** update the API
([db163fd](db163fd3b3))
- **vision:** update the API
([77a0a91](77a0a9136e))
- **vpcaccess:** update the API
([8db5275](8db52757e6))
- **workloadmanager:** update the API
([4c49597](4c4959752e))
- **workstations:** update the API
([174cd20](174cd20129))

##### Bug Fixes

- **accessapproval:** update the API
([227915d](227915d92f))
- **analyticsadmin:** update the API
([b858170](b858170642))
- **androidmanagement:** update the API
([35f8862](35f886254c))
- **apphub:** update the API
([e5a7c92](e5a7c92a2a))
- **binaryauthorization:** update the API
([7f20317](7f20317264))
- **calendar:** update the API
([e6ba462](e6ba462408))
- **chromepolicy:** update the API
([a5a5351](a5a5351998))
- **classroom:** update the API
([9d2ed12](9d2ed12202))
- **cloudasset:** update the API
([20a91d5](20a91d5cb6))
- **cloudidentity:** update the API
([5155e11](5155e11cd2))
- **cloudkms:** update the API
([90bab2c](90bab2c738))
- **cloudscheduler:** update the API
([2c7b902](2c7b90229a))
- **cloudtasks:** update the API
([a8d66db](a8d66db055))
- **contactcenterinsights:** update the API
([828c5d3](828c5d3e08))
- **datamigration:** update the API
([56a65a8](56a65a8590))
- **deploymentmanager:** update the API
([b48abef](b48abef098))
- **displayvideo:** update the API
([299cf97](299cf97f91))
- **firebaseappdistribution:** update the API
([b102fcc](b102fccab5))
- **gkebackup:** update the API
([30ca612](30ca612728))
- **iam:** update the API
([4e12124](4e121245a3))
- **iap:** update the API
([65c644e](65c644e9de))
- **language:** update the API
([77252e1](77252e1b9c))
- **logging:** update the API
([1b4dc67](1b4dc6732c))
- **mybusinessbusinessinformation:** update the API
([5e4c0fe](5e4c0fe093))
- **places:** update the API
([6bbdf72](6bbdf72e3e))
- **policytroubleshooter:** update the API
([ad18f3b](ad18f3b0f6))
- **privateca:** update the API
([b230959](b23095912e))
- **runtimeconfig:** update the API
([0dfe961](0dfe9610eb))
- **secretmanager:** update the API
([a202268](a202268db9))
- **servicedirectory:** update the API
([ddc06a2](ddc06a219b))
- **sourcerepo:** update the API
([1965102](19651026ae))
- **spanner:** update the API
([ce99980](ce99980e71))
- **sqladmin:** update the API
([de59e8d](de59e8dd22))
- **storagetransfer:** update the API
([d6081de](d6081dea7d))
- **videointelligence:** update the API
([9d377f5](9d377f5e3e))
- **vmmigration:** update the API
([68a1d5f](68a1d5fede))
- **walletobjects:** update the API
([920ddc7](920ddc780c))
- **workflowexecutions:** update the API
([6553987](6553987f65))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-26 07:18:12 +00:00
Cats Juice
d272c4342d feat(core): replace emoji-mart with affine icon picker (#13644)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
  - Unified icon picker with consistent rendering across the app.
  - Picker can auto-close after selection.
  - “Remove” now clears the icon selection.

- Refactor
- Icon handling consolidated across editors, navigation, and document
titles for consistent behavior.
  - Picker now opens on the Emoji panel by default.

- Style
  - Adjusted line-height and selectors for icon picker visuals.

- Chores
  - Removed unused emoji-mart dependencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 06:41:29 +00:00
DarkSky
c540400496 feat(server): allow drop session (#13650)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Ensures deleted sessions and their messages are consistently cleaned
up, preventing lingering pinned or partially removed items.

* **Refactor**
* Streamlined session cleanup into a single bulk operation for improved
reliability and performance during deletions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 06:38:13 +00:00
EYHN
54498df247 feat(ios): upgrade button in setting (#13645)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Added a Subscription section in Mobile Settings (for signed-in users)
with plan info and an Upgrade button that opens the native paywall.
  - Supports showing “Pro” and “AI” paywalls.
  - Integrated native paywall provider on iOS.

- Style
- Introduced new styling for the subscription card, content, and button.

- Localization
- Added English strings for subscription title, description, and button.

- Chores
- Minor iOS project cleanup and internal wiring to enable the paywall
module.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 06:27:45 +00:00
DarkSky
3f9d9fef63 fix(server): rcat event sync (#13648)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Subscriptions now include an explicit "trial" flag so trialing users
are identified and treated correctly.

- Bug Fixes
  - More robust handling when webhook fields are missing or null.
- Improved family-sharing detection to avoid incorrect async processing.

- Refactor
- Status determination and store resolution simplified to rely on
subscription data rather than event payloads.

- Tests
- Test fixtures updated to include trial and store details for accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 19:00:48 +00:00
Lakr
7a90e1551c fix(ios): complete iap user interface (#13639)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- In-app purchases fully integrated for Pro and AI plans with restore,
live product loading, and StoreKit test configuration.

- Improvements
- Refreshed paywall: intro animation, delayed close button, smoother
horizontal paging, page dots interaction, per-item reveal animations,
and purchase-state UI (disabled/checked when owned).

- Changes
- "Believer" plan and related screens removed; Pro simplified to Monthly
and Annual offerings.

- Chores
- iOS project and build settings updated for newer toolchain and
StoreKit support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-25 04:50:12 +00:00
Peng Xiao
3c9d17c983 feat(core): insert artifact as code block (#13641)
#### PR Dependency Tree


* **PR #13641** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Insert HTML content directly into the document as a code block with
preview enabled.
* Default view changed from Code to Preview for faster content
inspection.
* New “Insert” action replaces the previous “Download” action to add
content into the document.
* Added a dedicated “Download HTML” button with an icon to save the HTML
file.
* Toast notifications confirm successful insertions; errors are reported
if insertion fails.
  * Updated button labeling to reflect the new workflow.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 08:29:03 +00:00
EYHN
2f118206cc feat(core): mcp server setting (#13630)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* MCP Server integration available in cloud workspaces with a dedicated
settings panel.
* Manage personal access tokens: generate/revoke tokens and view
revealed token.
  * One-click copy of a prefilled server configuration JSON.
  * New query to fetch revealed access tokens.

* **Improvements**
  * Integration list adapts to workspace type (cloud vs. local).
* More reliable token refresh with clearer loading, error and
revalidation states.

* **Localization**
* Added “Copied to clipboard” message and MCP Server name/description
translations.

* **Chores**
  * Updated icon dependency across many packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 08:02:47 +00:00
Cats Juice
ca9811792d feat(component): emoji and icon picker (#13638)
![CleanShot 2025-09-23 at 17 11
13](https://github.com/user-attachments/assets/0a4a9d09-1149-4042-bc73-e068a428f335)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Icon Picker added with Emoji and Icon panels, search/filtering, recent
selections, color selection, skin tone options, and smooth group
navigation.

- **Documentation**
  - Storybook example added to preview and test the Icon Picker.

- **Chores**
  - Bumped icon library dependency to a newer minor version.
  - Added emoji data dependency to support the Emoji Picker.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 07:14:54 +00:00
Jachin
812c2d86d4 feat(server): add Swagger API docs (#13455)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Interactive API documentation available at /api/docs when running in
development.

* **Chores**
* Added a development dependency to enable generation of the API
documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: DarkSky <darksky2048@gmail.com>
2025-09-23 10:31:16 +00:00
DarkSky
762b702e46 feat: sync rcat data (#13628)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* RevenueCat support: public webhook endpoint, webhook handler/service,
nightly reconciliation and per-user sync; subscriptions now expose
provider and iapStore; new user-facing error for App Store/Play-managed
subscriptions.
* **Chores**
* Multi-provider subscription schema (Provider, IapStore); Stripe
credentials moved into payment.stripe (top-level apiKey/webhookKey
deprecated); new payment.revenuecat config and defaults added.
* **Tests**
  * Comprehensive RevenueCat integration test suite and snapshots.
* **Documentation**
  * Admin config descriptions updated with deprecation guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 08:52:23 +00:00
Lakr
75a6c79b2c fix(ios): crash at swift runtime error (#13635)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Fetch copilot model options per prompt (default, optional, pro) with
generated GraphQL query and schema types.

* **Chores**
* Upgraded iOS deps: Apollo iOS 1.23.0, EventSource 0.1.5, Swift
Collections 1.2.1.
* Switched Intelligents to static linking and updated project
integration.
* Parameterized and standardized GraphQL codegen tooling; setup
automation now syncs versions and safely backs up/restores custom
scalars.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 05:56:08 +00:00
Wu Yue
b25759c264 feat(core): support gemini model switch in ai (#13631)
<img width="757" height="447" alt="截屏2025-09-22 17 49 34"
src="https://github.com/user-attachments/assets/bab96f45-112e-4d74-bc38-54429d8a54ab"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Subscription-aware AI model picker in chat: browse models with version
and category, see active selection, switch models, and receive
notifications when choosing pro models without a subscription.
Selections persist across sessions.
- Central AI model service wired into chat UI for consistent model
selection and availability.

- Changes
- Streamlined AI model availability: reduced to a curated set for a more
focused experience.
  - Context menu buttons can display supplemental info next to labels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 13:25:11 +00:00
renovate[bot]
da3e3eb3fa chore: bump up @faker-js/faker version to v10 (#13626)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [@faker-js/faker](https://fakerjs.dev)
([source](https://redirect.github.com/faker-js/faker)) | [`^9.6.0` ->
`^10.0.0`](https://renovatebot.com/diffs/npm/@faker-js%2ffaker/9.8.0/10.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@faker-js%2ffaker/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@faker-js%2ffaker/9.8.0/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@faker-js/faker](https://fakerjs.dev)
([source](https://redirect.github.com/faker-js/faker)) | [`^9.3.0` ->
`^10.0.0`](https://renovatebot.com/diffs/npm/@faker-js%2ffaker/9.8.0/10.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@faker-js%2ffaker/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@faker-js%2ffaker/9.8.0/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>faker-js/faker (@&#8203;faker-js/faker)</summary>

###
[`v10.0.0`](https://redirect.github.com/faker-js/faker/blob/HEAD/CHANGELOG.md#1000-2025-08-21)

[Compare
Source](https://redirect.github.com/faker-js/faker/compare/v9.9.0...v10.0.0)

##### New Locales

- **locale:** extended list of colors in Polish
([#&#8203;3586](https://redirect.github.com/faker-js/faker/issues/3586))
([9940d54](9940d54f75))

##### Features

- **locales:** add animal vocabulary(bear, bird, cat, rabbit, pet\_name)
in Korean
([#&#8203;3535](https://redirect.github.com/faker-js/faker/issues/3535))
([0d2143c](0d2143c75d))

##### Changed Locales

- **locale:** remove invalid credit card issuer patterns
([#&#8203;3568](https://redirect.github.com/faker-js/faker/issues/3568))
([9783d95](9783d95a8e))

###
[`v9.9.0`](https://redirect.github.com/faker-js/faker/blob/HEAD/CHANGELOG.md#990-2025-07-01)

[Compare
Source](https://redirect.github.com/faker-js/faker/compare/v9.8.0...v9.9.0)

##### New Locales

- **locale:** add word data to pt\_br and pt\_pt locales
([#&#8203;3531](https://redirect.github.com/faker-js/faker/issues/3531))
([a405ac8](a405ac8740))

##### Features

- **location:** simple coordinate methods
([#&#8203;3528](https://redirect.github.com/faker-js/faker/issues/3528))
([d07d96d](d07d96d018))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 12:18:23 +00:00
DarkSky
e3f3c8c4a8 feat: add config for mail server name (#13632)
fix #13627

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added configurable display names for primary and fallback SMTP
servers, improving email sender identification.
* Defaults to “AFFiNE Server,” with support for MAILER_SERVERNAME
environment variable for the primary SMTP.
* Exposed in admin settings for easy setup alongside existing SMTP
options.
* Names are now passed through to mail transport options for consistent
use across emails.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 11:52:15 +00:00
3720
7fe95f50f4 fix(editor): callout delete merge and slash menu (#13597)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Press Enter inside a callout splits the paragraph at the cursor into a
new focused paragraph.
- Clicking an empty callout inserts and focuses a new paragraph; emoji
menu behavior unchanged.
- New command to convert a callout paragraph to callout/selection flow
for Backspace handling.
  - New native API: ShareableContent.isUsingMicrophone(processId).

- Bug Fixes
- Backspace inside callout paragraphs now merges or deletes text
predictably and selects the callout when appropriate.

- Style
- Callout layout refined: top-aligned content and adjusted emoji
spacing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 11:29:18 +00:00
Cats Juice
195864fc88 feat(core): edit icon in navigation panel (#13595)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Rename dialog now edits per-item explorer icons (emoji or custom) and
can skip name-change callbacks. Doc icon picker added to the editor with
localized "Add icon" placeholder and readonly rendering. Icon editor
supports fallbacks, trigger variants, and improved input/test-id wiring.

- **Style**
- Updated icon picker and trigger sizing and placeholder visuals;
title/icon layout adjustments.

- **Chores**
- Explorer icon storage and module added to persist and serve icons
across the app.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 10:24:11 +00:00
dependabot[bot]
93554304e2 chore: bump dompurify from 3.1.6 to 3.2.7 (#13622)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.1.6 to
3.2.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.2.7</h2>
<ul>
<li>Added new attributes and elements to default allow-list, thanks <a
href="https://github.com/elrion018"><code>@​elrion018</code></a></li>
<li>Added <code>tagName</code> parameter to custom element
<code>attributeNameCheck</code>, thanks <a
href="https://github.com/nelstrom"><code>@​nelstrom</code></a></li>
<li>Added better check for animated <code>href</code> attributes, thanks
<a href="https://github.com/llamakko"><code>@​llamakko</code></a></li>
<li>Updated and improved the bundled types, thanks <a
href="https://github.com/ssi02014"><code>@​ssi02014</code></a></li>
<li>Updated several tests to better align with new browser encoding
behaviors</li>
<li>Improved the handling of potentially risky content inside CDATA
elements, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
<li>Improved the regular expression for raw-text elements to cover
textareas, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
</ul>
<h2>DOMPurify 3.2.6</h2>
<ul>
<li>Fixed several typos and removed clutter from our documentation,
thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Added <code>matrix:</code> as an allowed URI scheme, thanks <a
href="https://github.com/kleinesfilmroellchen"><code>@​kleinesfilmroellchen</code></a></li>
<li>Added better config hardening against prototype pollution, thanks <a
href="https://github.com/EffectRenan"><code>@​EffectRenan</code></a></li>
<li>Added better handling of attribute removal, thanks <a
href="https://github.com/michalnieruchalski-tiugo"><code>@​michalnieruchalski-tiugo</code></a></li>
<li>Added better configuration for aggressive mXSS scrubbing behavior,
thanks <a
href="https://github.com/BryanValverdeU"><code>@​BryanValverdeU</code></a></li>
<li>Removed the script that caused the fake entry <a
href="https://security.snyk.io/vuln/SNYK-JS-DOMPURIFY-10176060">CVE-2025-48050</a></li>
</ul>
<h2>DOMPurify 3.2.5</h2>
<ul>
<li>Added a check to the mXSS detection regex to be more strict, thanks
<a
href="https://github.com/masatokinugawa"><code>@​masatokinugawa</code></a></li>
<li>Added ESM type imports in source, removes patch function, thanks <a
href="https://github.com/donmccurdy"><code>@​donmccurdy</code></a></li>
<li>Added script to verify various TypeScript configurations, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Added more modern browsers to the Karma launchers list</li>
<li>Added Node 23.x to tested runtimes, removed Node 17.x</li>
<li>Fixed the generation of source maps, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed an unexpected behavior with <code>ALLOWED_URI_REGEXP</code>
using the 'g' flag, thanks <a
href="https://github.com/hhk-png"><code>@​hhk-png</code></a></li>
<li>Fixed a few typos in the README file</li>
</ul>
<h2>DOMPurify 3.2.4</h2>
<ul>
<li>Fixed a conditional and config dependent mXSS-style <a
href="https://nsysean.github.io/posts/dompurify-323-bypass/">bypass</a>
reported by <a
href="https://github.com/nsysean"><code>@​nsysean</code></a></li>
<li>Added a new feature to allow specific hook removal, thanks <a
href="https://github.com/davecardwell"><code>@​davecardwell</code></a></li>
<li>Added <em>purify.js</em> and <em>purify.min.js</em> to exports,
thanks <a
href="https://github.com/Aetherinox"><code>@​Aetherinox</code></a></li>
<li>Added better logic in case no window object is president, thanks <a
href="https://github.com/yehuya"><code>@​yehuya</code></a></li>
<li>Updated some dependencies called out by dependabot</li>
<li>Updated license files etc to show the correct year</li>
</ul>
<h2>DOMPurify 3.2.3</h2>
<ul>
<li>Fixed two conditional sanitizer bypasses discovered by <a
href="https://github.com/parrot409"><code>@​parrot409</code></a> and <a
href="https://x.com/slonser_"><code>@​Slonser</code></a></li>
<li>Updated the attribute clobbering checks to prevent future bypasses,
thanks <a
href="https://github.com/parrot409"><code>@​parrot409</code></a></li>
</ul>
<h2>DOMPurify 3.2.2</h2>
<ul>
<li>Fixed a possible bypass in case a rather specific config for custom
elements is set, thanks <a
href="https://github.com/yaniv-git"><code>@​yaniv-git</code></a></li>
<li>Fixed several minor issues with the type definitions, thanks again
<a href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed a minor issue with the types reference for trusted types,
thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed a minor problem with the template detection regex on some
systems, thanks <a
href="https://github.com/svdb99"><code>@​svdb99</code></a></li>
</ul>
<h2>DOMPurify 3.2.1</h2>
<ul>
<li>Fixed several minor issues with the type definitions, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a> <a
href="https://github.com/ghiscoding"><code>@​ghiscoding</code></a> <a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a> <a
href="https://github.com/MiniDigger"><code>@​MiniDigger</code></a></li>
<li>Fixed an issue with non-minified dist files and order of imports,
thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="eaa0bdb26a"><code>eaa0bdb</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1144">#1144</a>
from cure53/main</li>
<li><a
href="f712593118"><code>f712593</code></a>
fix: removed a possibly dossy regex</li>
<li><a
href="eb9b3b6874"><code>eb9b3b6</code></a>
Merge branch 'main' of github.com:cure53/DOMPurify</li>
<li><a
href="ce006f705c"><code>ce006f7</code></a>
chore: Preparing 3.2.7 release</li>
<li><a
href="ef0e0cb6eb"><code>ef0e0cb</code></a>
chore: Preparing 3.2.6 release</li>
<li><a
href="2f09cd3c8e"><code>2f09cd3</code></a>
Update README.md</li>
<li><a
href="6a795bcf3e"><code>6a795bc</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1142">#1142</a>
from cure53/dependabot/github_actions/actions/setup-...</li>
<li><a
href="2458bbdfca"><code>2458bbd</code></a>
build(deps): bump actions/setup-node from 4 to 5</li>
<li><a
href="e43d3f3548"><code>e43d3f3</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1136">#1136</a>
from cure53/dependabot/github_actions/actions/checko...</li>
<li><a
href="6f5be37ee0"><code>6f5be37</code></a>
build(deps): bump actions/checkout from 4 to 5</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.1.6...3.2.7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.1.6&new-version=3.2.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/toeverything/AFFiNE/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 19:05:12 +00:00
renovate[bot]
2f38953cf9 chore: bump up electron version to v35.7.5 [SECURITY] (#13561)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [electron](https://redirect.github.com/electron/electron) | [`35.5.1`
-> `35.7.5`](https://renovatebot.com/diffs/npm/electron/35.5.1/35.7.5) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/electron/35.7.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/electron/35.5.1/35.7.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

### GitHub Vulnerability Alerts

####
[CVE-2025-55305](https://redirect.github.com/electron/electron/security/advisories/GHSA-vmqv-hx8q-j7mg)

### Impact
This only impacts apps that have the `embeddedAsarIntegrityValidation`
and `onlyLoadAppFromAsar`
[fuses](https://www.electronjs.org/docs/latest/tutorial/fuses) enabled.
Apps without these fuses enabled are not impacted.

Specifically this issue can only be exploited if your app is launched
from a filesystem the attacker has write access too. i.e. the ability to
edit files inside the `resources` folder in your app installation on
Windows which these fuses are supposed to protect against.

### Workarounds
There are no app side workarounds, you must update to a patched version
of Electron.

### Fixed Versions
* `38.0.0-beta.6`
* `37.3.1`
* `36.8.1`
* `35.7.5`

### For more information
If you have any questions or comments about this advisory, email us at
[security@electronjs.org](mailto:security@electronjs.org)

---

### Release Notes

<details>
<summary>electron/electron (electron)</summary>

###
[`v35.7.5`](https://redirect.github.com/electron/electron/releases/tag/v35.7.5):
electron v35.7.5

[Compare
Source](https://redirect.github.com/electron/electron/compare/v35.7.4...v35.7.5)

##### Release Notes for v35.7.5

> \[!WARNING]
> Electron 35.x.y has reached end-of-support as per the project's
[support
policy](https://www.electronjs.org/docs/latest/tutorial/electron-timelines#version-support-policy).
Developers and applications are encouraged to upgrade to a newer version
of Electron.

##### Fixes

- Fixed an issue where `shell.openPath` was not non-blocking as
expected.
[#&#8203;48079](https://redirect.github.com/electron/electron/pull/48079)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/48088),
[37](https://redirect.github.com/electron/electron/pull/48088),
[38](https://redirect.github.com/electron/electron/pull/48088))</span>

###
[`v35.7.4`](https://redirect.github.com/electron/electron/releases/tag/v35.7.4):
electron v35.7.4

[Compare
Source](https://redirect.github.com/electron/electron/compare/v35.7.2...v35.7.4)

##### Release Notes for v35.7.4

- Fix ffmpeg generation on Windows non-x64

###
[`v35.7.2`](https://redirect.github.com/electron/electron/releases/tag/v35.7.2):
electron v35.7.2

[Compare
Source](https://redirect.github.com/electron/electron/compare/v35.7.0...v35.7.2)

##### Release Notes for v35.7.2

##### Fixes

- Fixed an issue where printing PDFs with `webContents.print({ silent:
true })` would fail.
[#&#8203;47645](https://redirect.github.com/electron/electron/pull/47645)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47624),
[37](https://redirect.github.com/electron/electron/pull/47397))</span>

###
[`v35.7.0`](https://redirect.github.com/electron/electron/releases/tag/v35.7.0):
electron v35.7.0

[Compare
Source](https://redirect.github.com/electron/electron/compare/v35.6.0...v35.7.0)

##### Release Notes for v35.7.0

##### Other Changes

- Updated Node.js to v22.16.0.
[#&#8203;47213](https://redirect.github.com/electron/electron/pull/47213)

###
[`v35.6.0`](https://redirect.github.com/electron/electron/releases/tag/v35.6.0):
electron v35.6.0

[Compare
Source](https://redirect.github.com/electron/electron/compare/v35.5.1...v35.6.0)

##### Release Notes for v35.6.0

##### Features

- Added support for `--no-experimental-global-navigator` flag.
[#&#8203;47416](https://redirect.github.com/electron/electron/pull/47416)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47417),
[37](https://redirect.github.com/electron/electron/pull/47418))</span>
- Added support for customizing system accent color and highlighting of
active window border.
[#&#8203;47539](https://redirect.github.com/electron/electron/pull/47539)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47538),
[37](https://redirect.github.com/electron/electron/pull/47537))</span>

##### Fixes

- Fixed a potential crash using `session.clearData` in some
circumstances.
[#&#8203;47410](https://redirect.github.com/electron/electron/pull/47410)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47411),
[37](https://redirect.github.com/electron/electron/pull/47412))</span>
- Fixed an error when importing `electron` for the first time from an
ESM module loaded by a CJS module in a packaged app.
[#&#8203;47344](https://redirect.github.com/electron/electron/pull/47344)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47343),
[37](https://redirect.github.com/electron/electron/pull/47342))</span>
- Fixed an issue where calling `Fetch.continueResponse` via debugger
with `WebContentsView` could cause a crash.
[#&#8203;47443](https://redirect.github.com/electron/electron/pull/47443)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47442),
[37](https://redirect.github.com/electron/electron/pull/47444))</span>
- Fixed an issue where utility processes could leak file handles.
[#&#8203;47542](https://redirect.github.com/electron/electron/pull/47542)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47541),
[37](https://redirect.github.com/electron/electron/pull/47543))</span>
- Partially fixes an issue with printing a PDF via `webContents.print()`
where the callback would not be called.
[#&#8203;47399](https://redirect.github.com/electron/electron/pull/47399)
<span style="font-size:small;">(Also in
[36](https://redirect.github.com/electron/electron/pull/47400),
[37](https://redirect.github.com/electron/electron/pull/47398))</span>

##### Other Changes

- Backported fix for
[`4206375`](https://redirect.github.com/electron/electron/commit/420637585).
[#&#8203;47369](https://redirect.github.com/electron/electron/pull/47369)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no
schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTcuMTAiLCJ0YXJnZXRCcmFuY2giOiJjYW5hcnkiLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-21 18:47:57 +00:00
renovate[bot]
ebf75e4d31 chore: bump up apollographql/apollo-ios version to v1.23.0 (#13623)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
|
[apollographql/apollo-ios](https://redirect.github.com/apollographql/apollo-ios)
| minor | `from: "1.22.0"` -> `from: "1.23.0"` |
|
[apollographql/apollo-ios](https://redirect.github.com/apollographql/apollo-ios)
| minor | `1.22.0` -> `1.23.0` |

---

### Release Notes

<details>
<summary>apollographql/apollo-ios (apollographql/apollo-ios)</summary>

###
[`v1.23.0`](https://redirect.github.com/apollographql/apollo-ios/blob/HEAD/CHANGELOG.md#v1230)

[Compare
Source](https://redirect.github.com/apollographql/apollo-ios/compare/1.22.0...1.23.0)

##### New

- **Added `requireNonOptionalMockFields` flag to
`ApolloCodegenConfiguration.OutputOptions`.
([#&#8203;669](https://redirect.github.com/apollographql/apollo-ios-dev/pull/669)):**
Added new flag to codegen output options to allow having non-optional
fields in the test mocks if desired. *Thank you to
[@&#8203;dwroth](https://redirect.github.com/dwroth) for the
contribution.*

##### Improvement

- **Added public initializer to `DatabaseRow`.
([#&#8203;664](https://redirect.github.com/apollographql/apollo-ios-dev/pull/664)):**
Not having a public initializer on `DatabasRow` was hindering the
ability to create custom `SQLiteDatabase` implementations. This solves
that by adding a public initializer to `DatabaseRow`.*Thank you to
[@&#8203;ChrisLaganiere](https://redirect.github.com/ChrisLaganiere) for
the contribution.*

##### Fixed

- **Unncessary deprecation warning in codegen options initializer.
([#&#8203;3563](https://redirect.github.com/apollographql/apollo-ios/issues/3563)):**
Added `@_disfavoredOverload` to the deprecated initialized in
`ApolloCodegenConfiguration` to prevent possible warnings caused by the
compiler selecting a deprecated initializer versus the new/current
initializer. See PR
[#&#8203;682](https://redirect.github.com/apollographql/apollo-ios-dev/pull/682).
*Thank you to
[@&#8203;CraigSiemens](https://redirect.github.com/CraigSiemens) for
raising the issue.*

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 18:35:24 +00:00
dependabot[bot]
2d0721a78f chore: bump axios from 1.9.0 to 1.12.2 (#13621)
Bumps [axios](https://github.com/axios/axios) from 1.9.0 to 1.12.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>Release v1.12.2</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> use current global fetch instead of cached
one when env fetch is not specified to keep MSW support; (<a
href="https://redirect.github.com/axios/axios/issues/7030">#7030</a>)
(<a
href="cf78825e12">cf78825</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+247/-16
([#7030](https://github.com/axios/axios/issues/7030)
[#7022](https://github.com/axios/axios/issues/7022)
[#7024](https://github.com/axios/axios/issues/7024) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/noritaka1166"
title="+2/-6 ([#7028](https://github.com/axios/axios/issues/7028)
[#7029](https://github.com/axios/axios/issues/7029) )">Noritaka
Kobayashi</a></li>
</ul>
<h2>Release v1.12.1</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>types:</strong> fixed env config types; (<a
href="https://redirect.github.com/axios/axios/issues/7020">#7020</a>)
(<a
href="b5f26b75bd">b5f26b7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+10/-4
([#7020](https://github.com/axios/axios/issues/7020) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h2>Release v1.12.0</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li>adding build artifacts (<a
href="9ec86de257">9ec86de</a>)</li>
<li>dont add dist on release (<a
href="a2edc3606a">a2edc36</a>)</li>
<li><strong>fetch-adapter:</strong> set correct Content-Type for Node
FormData (<a
href="https://redirect.github.com/axios/axios/issues/6998">#6998</a>)
(<a
href="a9f47afbf3">a9f47af</a>)</li>
<li><strong>node:</strong> enforce maxContentLength for data: URLs (<a
href="https://redirect.github.com/axios/axios/issues/7011">#7011</a>)
(<a
href="945435fc51">945435f</a>)</li>
<li>package exports (<a
href="https://redirect.github.com/axios/axios/issues/5627">#5627</a>)
(<a
href="aa78ac23fc">aa78ac2</a>)</li>
<li><strong>params:</strong> removing '[' and ']' from URL encode
exclude characters (<a
href="https://redirect.github.com/axios/axios/issues/3316">#3316</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/5715">#5715</a>)
(<a
href="6d84189349">6d84189</a>)</li>
<li>release pr run (<a
href="fd7f404488">fd7f404</a>)</li>
<li><strong>types:</strong> change the type guard on isCancel (<a
href="https://redirect.github.com/axios/axios/issues/5595">#5595</a>)
(<a
href="0dbb7fd4f6">0dbb7fd</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>adapter:</strong> surface low‑level network error details;
attach original error via cause (<a
href="https://redirect.github.com/axios/axios/issues/6982">#6982</a>)
(<a
href="78b290c57c">78b290c</a>)</li>
<li><strong>fetch:</strong> add fetch, Request, Response env config
variables for the adapter; (<a
href="https://redirect.github.com/axios/axios/issues/7003">#7003</a>)
(<a
href="c959ff2901">c959ff2</a>)</li>
<li>support reviver on JSON.parse (<a
href="https://redirect.github.com/axios/axios/issues/5926">#5926</a>)
(<a
href="2a9763426e">2a97634</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/5924">#5924</a></li>
<li><strong>types:</strong> extend AxiosResponse interface to include
custom headers type (<a
href="https://redirect.github.com/axios/axios/issues/6782">#6782</a>)
(<a
href="7960d34ede">7960d34</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/WillianAgostini" title="+132/-16760
([#7002](https://github.com/axios/axios/issues/7002)
[#5926](https://github.com/axios/axios/issues/5926)
[#6782](https://github.com/axios/axios/issues/6782) )">Willian
Agostini</a></li>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+4263/-293
([#7006](https://github.com/axios/axios/issues/7006)
[#7003](https://github.com/axios/axios/issues/7003) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/mkhani01"
title="+111/-15 ([#6982](https://github.com/axios/axios/issues/6982)
)">khani</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/AmeerAssadi"
title="+123/-0 ([#7011](https://github.com/axios/axios/issues/7011)
)">Ameer Assadi</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/emiedonmokumo"
title="+55/-35 ([#6998](https://github.com/axios/axios/issues/6998)
)">Emiedonmokumo Dick-Boro</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/opsysdebug"
title="+8/-8 ([#6980](https://github.com/axios/axios/issues/6980)
)">Zeroday BYTE</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/axios/axios/compare/v1.12.1...v1.12.2">1.12.2</a>
(2025-09-14)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>fetch:</strong> use current global fetch instead of cached
one when env fetch is not specified to keep MSW support; (<a
href="https://redirect.github.com/axios/axios/issues/7030">#7030</a>)
(<a
href="cf78825e12">cf78825</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+247/-16
([#7030](https://github.com/axios/axios/issues/7030)
[#7022](https://github.com/axios/axios/issues/7022)
[#7024](https://github.com/axios/axios/issues/7024) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/noritaka1166"
title="+2/-6 ([#7028](https://github.com/axios/axios/issues/7028)
[#7029](https://github.com/axios/axios/issues/7029) )">Noritaka
Kobayashi</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.12.0...v1.12.1">1.12.1</a>
(2025-09-12)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>types:</strong> fixed env config types; (<a
href="https://redirect.github.com/axios/axios/issues/7020">#7020</a>)
(<a
href="b5f26b75bd">b5f26b7</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+10/-4
([#7020](https://github.com/axios/axios/issues/7020) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h1><a
href="https://github.com/axios/axios/compare/v1.11.0...v1.12.0">1.12.0</a>
(2025-09-11)</h1>
<h3>Bug Fixes</h3>
<ul>
<li>adding build artifacts (<a
href="9ec86de257">9ec86de</a>)</li>
<li>dont add dist on release (<a
href="a2edc3606a">a2edc36</a>)</li>
<li><strong>fetch-adapter:</strong> set correct Content-Type for Node
FormData (<a
href="https://redirect.github.com/axios/axios/issues/6998">#6998</a>)
(<a
href="a9f47afbf3">a9f47af</a>)</li>
<li><strong>node:</strong> enforce maxContentLength for data: URLs (<a
href="https://redirect.github.com/axios/axios/issues/7011">#7011</a>)
(<a
href="945435fc51">945435f</a>)</li>
<li>package exports (<a
href="https://redirect.github.com/axios/axios/issues/5627">#5627</a>)
(<a
href="aa78ac23fc">aa78ac2</a>)</li>
<li><strong>params:</strong> removing '[' and ']' from URL encode
exclude characters (<a
href="https://redirect.github.com/axios/axios/issues/3316">#3316</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/5715">#5715</a>)
(<a
href="6d84189349">6d84189</a>)</li>
<li>release pr run (<a
href="fd7f404488">fd7f404</a>)</li>
<li><strong>types:</strong> change the type guard on isCancel (<a
href="https://redirect.github.com/axios/axios/issues/5595">#5595</a>)
(<a
href="0dbb7fd4f6">0dbb7fd</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>adapter:</strong> surface low‑level network error details;
attach original error via cause (<a
href="https://redirect.github.com/axios/axios/issues/6982">#6982</a>)
(<a
href="78b290c57c">78b290c</a>)</li>
<li><strong>fetch:</strong> add fetch, Request, Response env config
variables for the adapter; (<a
href="https://redirect.github.com/axios/axios/issues/7003">#7003</a>)
(<a
href="c959ff2901">c959ff2</a>)</li>
<li>support reviver on JSON.parse (<a
href="https://redirect.github.com/axios/axios/issues/5926">#5926</a>)
(<a
href="2a9763426e">2a97634</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/5924">#5924</a></li>
<li><strong>types:</strong> extend AxiosResponse interface to include
custom headers type (<a
href="https://redirect.github.com/axios/axios/issues/6782">#6782</a>)
(<a
href="7960d34ede">7960d34</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/WillianAgostini" title="+132/-16760
([#7002](https://github.com/axios/axios/issues/7002)
[#5926](https://github.com/axios/axios/issues/5926)
[#6782](https://github.com/axios/axios/issues/6782) )">Willian
Agostini</a></li>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+4263/-293
([#7006](https://github.com/axios/axios/issues/7006)
[#7003](https://github.com/axios/axios/issues/7003) )">Dmitriy
Mozgovoy</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/mkhani01"
title="+111/-15 ([#6982](https://github.com/axios/axios/issues/6982)
)">khani</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e5a33366d7"><code>e5a3336</code></a>
chore(release): v1.12.2 (<a
href="https://redirect.github.com/axios/axios/issues/7031">#7031</a>)</li>
<li><a
href="38726c7586"><code>38726c7</code></a>
refactor: change if in else to else if (<a
href="https://redirect.github.com/axios/axios/issues/7028">#7028</a>)</li>
<li><a
href="cf78825e12"><code>cf78825</code></a>
fix(fetch): use current global fetch instead of cached one when env
fetch is ...</li>
<li><a
href="c26d00f451"><code>c26d00f</code></a>
refactor: remove redundant assignment (<a
href="https://redirect.github.com/axios/axios/issues/7029">#7029</a>)</li>
<li><a
href="9fb41a8fcd"><code>9fb41a8</code></a>
chore(ci): add local HTTP server for Karma tests; (<a
href="https://redirect.github.com/axios/axios/issues/7022">#7022</a>)</li>
<li><a
href="19f9f36850"><code>19f9f36</code></a>
docs(readme): add custom fetch section; (<a
href="https://redirect.github.com/axios/axios/issues/7024">#7024</a>)</li>
<li><a
href="3cac78c2de"><code>3cac78c</code></a>
chore(release): v1.12.1 (<a
href="https://redirect.github.com/axios/axios/issues/7021">#7021</a>)</li>
<li><a
href="b5f26b75bd"><code>b5f26b7</code></a>
fix(types): fixed env config types; (<a
href="https://redirect.github.com/axios/axios/issues/7020">#7020</a>)</li>
<li><a
href="0d8ad6e1de"><code>0d8ad6e</code></a>
chore(release): v1.12.0 (<a
href="https://redirect.github.com/axios/axios/issues/7013">#7013</a>)</li>
<li><a
href="fd7f404488"><code>fd7f404</code></a>
fix: release pr run</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.9.0...v1.12.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.9.0&new-version=1.12.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/toeverything/AFFiNE/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-21 18:34:43 +00:00
Jachin
e08fc5ef06 feat(server): change the playground option to GraphiQL. (#13451)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* The GraphQL interactive UI is now available only in development
environments and will not be accessible in production. This change
affects only the availability of the interactive interface; public
exports and API context types remain unchanged. Users in development can
continue to use the tool as before, while production deployments will no
longer expose the interactive UI.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-21 16:08:30 +00:00
Finn Weigand
363f64ebfa feat: add dedicated sign-up config for oauth (#13610)
Currently, it is only possible to disable all registrations. However, it
would be helpful if you could disable normal registration but enable
OAuth registration.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a setting to enable/disable new user signups via OAuth (default:
enabled).
* Admin Settings (Authentication) now includes a toggle for OAuth
signups.
* OAuth signup flow now respects this setting, preventing new
registrations via OAuth when disabled.
  * Self-hosted configuration schema updated to include the new option.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Hudint Finn Weigand <dev@hudint.de>
Co-authored-by: DarkSky <darksky2048@gmail.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-21 15:38:25 +00:00
renovate[bot]
21bb8142b0 chore: bump up Recouse/EventSource version to from: "0.1.5" (#13620)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [Recouse/EventSource](https://redirect.github.com/Recouse/EventSource)
| patch | `from: "0.1.4"` -> `from: "0.1.5"` |

---

### Release Notes

<details>
<summary>Recouse/EventSource (Recouse/EventSource)</summary>

###
[`v0.1.5`](https://redirect.github.com/Recouse/EventSource/releases/tag/0.1.5)

[Compare
Source](https://redirect.github.com/Recouse/EventSource/compare/0.1.4...0.1.5)

#### What's Changed

- Fix potential data corruption by
[@&#8203;Recouse](https://redirect.github.com/Recouse) in
[#&#8203;30](https://redirect.github.com/Recouse/EventSource/pull/30)
- Concurrency improvements by
[@&#8203;Recouse](https://redirect.github.com/Recouse) in
[#&#8203;31](https://redirect.github.com/Recouse/EventSource/pull/31)
- Update EventParser.swift to Support CR LF by
[@&#8203;Lakr233](https://redirect.github.com/Lakr233) in
[#&#8203;28](https://redirect.github.com/Recouse/EventSource/pull/28)

#### New Contributors

- [@&#8203;Lakr233](https://redirect.github.com/Lakr233) made their
first contribution in
[#&#8203;28](https://redirect.github.com/Recouse/EventSource/pull/28)

**Full Changelog**:
<https://github.com/Recouse/EventSource/compare/0.1.4...0.1.5>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 14:54:00 +00:00
Bl4ckspell
750b008dc8 feat(android): add monochrome icon support (#13527)
Add missing themed icon support for android app icon.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Android app icon now supports a monochrome variant for adaptive icons,
enabling themed icons on compatible launchers.
* Improved icon consistency and visibility across system themes
(including dark mode).
  * Applied to both standard and round launcher icons.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-21 14:51:10 +00:00
renovate[bot]
d231b47f1f chore: bump up nestjs (#13614)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [@nestjs/bullmq](https://redirect.github.com/nestjs/bull) | [`11.0.2`
->
`11.0.3`](https://renovatebot.com/diffs/npm/@nestjs%2fbullmq/11.0.2/11.0.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fbullmq/11.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fbullmq/11.0.2/11.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/common](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common))
| [`11.1.5` ->
`11.1.6`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.5/11.1.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.1.5/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/core](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core))
| [`11.1.5` ->
`11.1.6`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.5/11.1.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.1.5/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/platform-express](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-express))
| [`11.1.5` ->
`11.1.6`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-express/11.1.5/11.1.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-express/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-express/11.1.5/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/platform-socket.io](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io))
| [`11.1.5` ->
`11.1.6`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-socket.io/11.1.5/11.1.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-socket.io/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-socket.io/11.1.5/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/schedule](https://redirect.github.com/nestjs/schedule) |
[`6.0.0` ->
`6.0.1`](https://renovatebot.com/diffs/npm/@nestjs%2fschedule/6.0.0/6.0.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fschedule/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fschedule/6.0.0/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@nestjs/websockets](https://redirect.github.com/nestjs/nest)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/websockets))
| [`11.1.5` ->
`11.1.6`](https://renovatebot.com/diffs/npm/@nestjs%2fwebsockets/11.1.5/11.1.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fwebsockets/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fwebsockets/11.1.5/11.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>nestjs/bull (@&#8203;nestjs/bullmq)</summary>

###
[`v11.0.3`](https://redirect.github.com/nestjs/bull/releases/tag/%40nestjs/bullmq%4011.0.3)

[Compare
Source](https://redirect.github.com/nestjs/bull/compare/@nestjs/bullmq@11.0.2...@nestjs/bullmq@11.0.3)

#### What's Changed

- feat(bullmq): add telemetry support for workers by
[@&#8203;noeljackson](https://redirect.github.com/noeljackson) in
[#&#8203;2585](https://redirect.github.com/nestjs/bull/pull/2585)

#### New Contributors

- [@&#8203;noeljackson](https://redirect.github.com/noeljackson) made
their first contribution in
[#&#8203;2585](https://redirect.github.com/nestjs/bull/pull/2585)

**Full Changelog**:
<https://github.com/nestjs/bull/compare/@nestjs/bull-shared@11.0.0...@&#8203;nestjs/bullmq@11.0.3>

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

###
[`v11.1.6`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.6)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.5...v11.1.6)

##### v11.1.6 (2025-08-07)

##### Bug fixes

- `core`
- [#&#8203;15504](https://redirect.github.com/nestjs/nest/pull/15504)
fix(core): fix race condition in class dependency resolution from
imported modules
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- [#&#8203;15469](https://redirect.github.com/nestjs/nest/pull/15469)
fix(core): attach root inquirer for nested transient providers
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- `microservices`
- [#&#8203;15508](https://redirect.github.com/nestjs/nest/pull/15508)
fix(microservices): report correct buffer length in exception
([@&#8203;kim-sung-jee](https://redirect.github.com/kim-sung-jee))
- [#&#8203;15492](https://redirect.github.com/nestjs/nest/pull/15492)
fix(microservices): fix kafka serilization of class instances
([@&#8203;LeonBiersch](https://redirect.github.com/LeonBiersch))

##### Dependencies

- `platform-fastify`
- [#&#8203;15493](https://redirect.github.com/nestjs/nest/pull/15493)
chore(deps): bump
[@&#8203;fastify/cors](https://redirect.github.com/fastify/cors) from
11.0.1 to 11.1.0
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

##### Committers: 6

- Jiri Hajek
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- Kamil Mysliwiec
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- Leon Biersch
([@&#8203;LeonBiersch](https://redirect.github.com/LeonBiersch))
- Seongjee Kim
([@&#8203;kim-sung-jee](https://redirect.github.com/kim-sung-jee))
- [@&#8203;premierbell](https://redirect.github.com/premierbell)
- pTr ([@&#8203;ptrgits](https://redirect.github.com/ptrgits))

</details>

<details>
<summary>nestjs/schedule (@&#8203;nestjs/schedule)</summary>

###
[`v6.0.1`](https://redirect.github.com/nestjs/schedule/releases/tag/6.0.1)

[Compare
Source](https://redirect.github.com/nestjs/schedule/compare/6.0.0...6.0.1)

#### What's Changed

- Add threshold to CronOptions by
[@&#8203;arjunatlightspeed](https://redirect.github.com/arjunatlightspeed)
in [#&#8203;2085](https://redirect.github.com/nestjs/schedule/pull/2085)
- refactor : clear jobs before application shutdown by
[@&#8203;spotlight21c](https://redirect.github.com/spotlight21c) in
[#&#8203;2053](https://redirect.github.com/nestjs/schedule/pull/2053)
- fix(deps): update dependency cron to v4.3.3 by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;2001](https://redirect.github.com/nestjs/schedule/pull/2001)

#### New Contributors

-
[@&#8203;arjunatlightspeed](https://redirect.github.com/arjunatlightspeed)
made their first contribution in
[#&#8203;2085](https://redirect.github.com/nestjs/schedule/pull/2085)
- [@&#8203;spotlight21c](https://redirect.github.com/spotlight21c) made
their first contribution in
[#&#8203;2053](https://redirect.github.com/nestjs/schedule/pull/2053)

**Full Changelog**:
<https://github.com/nestjs/schedule/compare/6.0.0...6.0.1>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 14:35:11 +00:00
Richard Lora
4efbb630fc fix(core): correct emoji extraction logic using regex (#12749)
https://github.com/user-attachments/assets/ef612f34-0388-49a2-bcad-0cac07a5f785

This PR solves the issue where a majority of emoji's are unable to
become the document or folders icon.

The regex used is below with the test string of a variety of emoji's:
https://regex101.com/r/0anB6Z/1

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-21 22:43:46 +08:00
renovate[bot]
19bd29e90c chore: bump up apple/swift-collections version to from: "1.2.1" (#13535)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
|
[apple/swift-collections](https://redirect.github.com/apple/swift-collections)
| patch | `from: "1.2.0"` -> `from: "1.2.1"` |

---

### Release Notes

<details>
<summary>apple/swift-collections (apple/swift-collections)</summary>

###
[`v1.2.1`](https://redirect.github.com/apple/swift-collections/releases/tag/1.2.1):
Swift Collections 1.2.1

[Compare
Source](https://redirect.github.com/apple/swift-collections/compare/1.2.0...1.2.1)

This is a patch release with the following minor improvements:

- `BigString` sometimes miscounted distances in its character view,
resulting in an invalid collection conformance. This is now fixed.
([#&#8203;485](https://redirect.github.com/apple/swift-collections/issues/485))
- `BigString`'s Unicode Scalar and character views now make better use
of known lengths of the text chunks stored in the tree, resulting in
significantly improved performance for their distance measurements.
([#&#8203;486](https://redirect.github.com/apple/swift-collections/issues/486))
- The Foundation-specific toolchain configuration was updated to include
the Deque type.
([#&#8203;496](https://redirect.github.com/apple/swift-collections/issues/496))

#### What's Changed

- \[BigString] Fix character indexing operations by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;485](https://redirect.github.com/apple/swift-collections/pull/485)
- \[BigString] Harvest some low-hanging performance fruit by
[@&#8203;lorentey](https://redirect.github.com/lorentey) in
[#&#8203;486](https://redirect.github.com/apple/swift-collections/pull/486)
- Include DequeModule in the Foundation toolchain build by
[@&#8203;cthielen](https://redirect.github.com/cthielen) in
[#&#8203;496](https://redirect.github.com/apple/swift-collections/pull/496)

**Full Changelog**:
<https://github.com/apple/swift-collections/compare/1.2.0...1.2.1>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/toeverything/AFFiNE).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuOTcuMTAiLCJ0YXJnZXRCcmFuY2giOiJjYW5hcnkiLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 14:22:59 +00:00
ShellWen | 颉文, Chen Chang
2a2793eada fix: Correct spacing in AI partner description (#13593)
Fixed spacing issue in AI partner description.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Improved readability by fixing a minor punctuation/spacing issue in
the project’s introductory text (added a space after a comma).
* Polished wording to better reflect professional tone without altering
meaning.
  * No changes to functionality, configuration, or user workflows.
  * No impact on APIs, interfaces, or compatibility.
* No additional steps required for users; purely a documentation
refinement.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-21 22:17:32 +08:00
Cats Juice
b6a3241451 chore(core): hide embedding status in chat (#13605)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Simplified the AI chat composer tip: removed the dynamic
embedding-status tooltip so only a single static caution remains — “AI
outputs can be misleading or wrong.”
* **Tests**
* One end-to-end test related to embedding status was commented out and
is no longer executed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 11:40:39 +00:00
Lakr
360c9545f4 feat(ios): [IAP] Paywall Initial Commit (#13609)
Requires https://github.com/toeverything/AFFiNE/pull/13606 to be merged.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Introduced an in-app Paywall with Pro, AI, and Believer plans, feature
previews, paging dots, and selectable pricing options.
- Added purchase and restore actions, plus a unified, polished UI using
new color/icon resources.

- Documentation
  - Added Swift Code Style Guidelines.

- Chores
- Updated dependencies (including MarkdownView 3.4.2), added new
resource packages, and removed an unused dependency.
  - Raised iOS deployment target to 16.5 and refreshed project settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
2025-09-19 11:01:46 +00:00
Lakr
1f228382c2 chore: fix building the app (#13606)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
  - Built-in Then-style DSL for fluent configuration.
- Centralized theming via a new resources library exposing standardized
colors and icons for SwiftUI and UIKit.

- Refactor
  - Migrated color and icon accessors to the new resources provider.
  - Removed redundant imports and streamlined UI configuration.

- Dependencies
  - Updated MarkdownView to 3.4.2.
- Removed the Then third-party dependency; updated package sources;
added resources package and assets.

- Documentation
  - Added iOS Swift code style and architecture guidelines.

- Chores
  - Updated Xcode project format and repository ignore rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 06:05:05 +00:00
DarkSky
ee77c548ca feat: get prompt model names (#13607)
fix AI-419

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- New API to fetch available models for a prompt, returning default,
optional, and pro models with human‑readable names.
- Added temperature and topP settings to prompt configuration for finer
control.
- Refactor
- When no model is chosen, the default model is used instead of
auto-picking a pro model.
- Model metadata across providers now includes readable names, improving
listings and selection UX.
- Tests
- Updated test snapshots and descriptions to reflect the new
default-model behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-18 12:56:54 +00:00
DarkSky
a0b73cdcec feat: improve model resolve (#13601)
fix AI-419
2025-09-18 10:51:12 +00:00
EYHN
89646869e4 feat(ios): create paywall api (#13602)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Introduced a new iOS Paywall plugin with a simple API to display a
paywall and receive a success response.
  - Added JavaScript wrapper and type definitions for easy integration.

- Refactor
  - Reorganized the iOS project structure for plugins.

- Chores
- Removed unused legacy iOS plugins to streamline the app and reduce
build complexity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-18 03:47:28 +00:00
390 changed files with 44821 additions and 1287 deletions

View File

@@ -148,6 +148,11 @@
"description": "Whether allow new registrations.\n@default true",
"default": true
},
"allowSignupForOauth": {
"type": "boolean",
"description": "Whether allow new registrations via configured oauth.\n@default true",
"default": true
},
"requireEmailDomainVerification": {
"type": "boolean",
"description": "Whether require email domain record verification before accessing restricted resources.\n@default false",
@@ -190,6 +195,11 @@
"type": "object",
"description": "Configuration for mailer module",
"properties": {
"SMTP.name": {
"type": "string",
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
"default": "AFFiNE Server"
},
"SMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
@@ -225,6 +235,11 @@
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.name": {
"type": "string",
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
"default": "AFFiNE Server"
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
@@ -669,12 +684,12 @@
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"gemini-2.5-flash\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "claude-sonnet-4@20250514",
"chat": "gemini-2.5-flash",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
@@ -1093,18 +1108,33 @@
},
"apiKey": {
"type": "string",
"description": "Stripe API key to enable payment service.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"description": "[Deprecated] Stripe API key. Use payment.stripe.apiKey instead.\n@default \"\"\n@environment `STRIPE_API_KEY`",
"default": ""
},
"webhookKey": {
"type": "string",
"description": "Stripe webhook key to enable payment service.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"description": "[Deprecated] Stripe webhook key. Use payment.stripe.webhookKey instead.\n@default \"\"\n@environment `STRIPE_WEBHOOK_KEY`",
"default": ""
},
"stripe": {
"type": "object",
"description": "Stripe sdk options\n@default {}\n@link https://docs.stripe.com/api",
"default": {}
"description": "Stripe sdk options and credentials\n@default {\"apiKey\":\"\",\"webhookKey\":\"\"}\n@link https://docs.stripe.com/api",
"default": {
"apiKey": "",
"webhookKey": ""
}
},
"revenuecat": {
"type": "object",
"description": "RevenueCat integration configs\n@default {\"enabled\":false,\"apiKey\":\"\",\"projectId\":\"\",\"webhookAuth\":\"\",\"environment\":\"production\",\"productMap\":{}}\n@link https://www.revenuecat.com/docs/",
"default": {
"enabled": false,
"apiKey": "",
"projectId": "",
"webhookAuth": "",
"environment": "production",
"productMap": {}
}
}
}
},

View File

@@ -81,7 +81,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Multimodal AI partner ready to kick in any work**
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination, just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
**Local-first & Real-time collaborative**

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emoji-mart/data": "^1.2.1",

View File

@@ -2,6 +2,7 @@ import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
import { type CalloutBlockModel } from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
@@ -22,14 +23,13 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
.affine-callout-block-container {
display: flex;
align-items: flex-start;
padding: 5px 10px;
border-radius: 8px;
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
}
.affine-callout-emoji-container {
margin-right: 10px;
margin-top: 14px;
user-select: none;
font-size: 1.2em;
width: 24px;
@@ -37,6 +37,9 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
margin-bottom: 10px;
flex-shrink: 0;
}
.affine-callout-emoji:hover {
cursor: pointer;
@@ -62,7 +65,7 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
createLitPortal({
template: html`<affine-emoji-menu
.theme=${theme}
.onEmojiSelect=${(data: any) => {
.onEmojiSelect=${(data: { native: string }) => {
this.model.props.emoji = data.native;
}}
></affine-emoji-menu>`,
@@ -81,6 +84,31 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
});
};
private readonly _handleBlockClick = (event: MouseEvent) => {
// Check if the click target is emoji related element
const target = event.target as HTMLElement;
if (
target.closest('.affine-callout-emoji-container') ||
target.classList.contains('affine-callout-emoji')
) {
return;
}
// Only handle clicks when there are no children
if (this.model.children.length > 0) {
return;
}
// Prevent event bubbling
event.stopPropagation();
// Create a new paragraph block
const paragraphId = this.store.addBlock('affine:paragraph', {}, this.model);
// Focus the new paragraph
focusTextModel(this.std, paragraphId);
};
get attributeRenderer() {
return this.inlineManager.getRenderer();
}
@@ -112,7 +140,10 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
override renderBlock() {
const emoji = this.model.props.emoji$.value;
return html`
<div class="affine-callout-block-container">
<div
class="affine-callout-block-container"
@click=${this._handleBlockClick}
>
<div
@click=${this._toggleEmojiMenu}
contenteditable="false"

View File

@@ -1,4 +1,7 @@
import { CalloutBlockModel } from '@blocksuite/affine-model';
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import {
BlockSelection,
@@ -6,13 +9,46 @@ import {
TextSelection,
} from '@blocksuite/std';
import { calloutToParagraphCommand } from './commands/callout-to-paragraph.js';
import { splitCalloutCommand } from './commands/split-callout.js';
export const CalloutKeymapExtension = KeymapExtension(std => {
return {
Enter: ctx => {
const text = std.selection.find(TextSelection);
if (!text) return false;
const currentBlock = std.store.getBlock(text.from.blockId);
if (!currentBlock) return false;
// Check if current block is a callout block
let calloutBlock = currentBlock;
if (!matchModels(currentBlock.model, [CalloutBlockModel])) {
// If not, check if the parent is a callout block
const parent = std.store.getParent(currentBlock.model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) {
return false;
}
const parentBlock = std.store.getBlock(parent.id);
if (!parentBlock) return false;
calloutBlock = parentBlock;
}
ctx.get('keyboardState').raw.preventDefault();
std.command
.chain()
.pipe(splitCalloutCommand, {
blockId: calloutBlock.model.id,
inlineIndex: text.from.index,
currentBlockId: text.from.blockId,
})
.run();
return true;
},
Backspace: ctx => {
const text = std.selection.find(TextSelection);
if (text && text.isCollapsed() && text.from.index === 0) {
const event = ctx.get('defaultState').event;
event.preventDefault();
const block = std.store.getBlock(text.from.blockId);
if (!block) return false;
@@ -20,6 +56,22 @@ export const CalloutKeymapExtension = KeymapExtension(std => {
if (!parent) return false;
if (!matchModels(parent, [CalloutBlockModel])) return false;
// Check if current block is a paragraph inside callout
if (matchModels(block.model, [ParagraphBlockModel])) {
event.preventDefault();
std.command
.chain()
.pipe(calloutToParagraphCommand, {
id: block.model.id,
})
.run();
return true;
}
// Fallback to selecting the callout block
event.preventDefault();
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,

View File

@@ -0,0 +1,86 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command } from '@blocksuite/std';
import { BlockSelection } from '@blocksuite/std';
import { Text } from '@blocksuite/store';
export const calloutToParagraphCommand: Command<
{
id: string;
stopCapturing?: boolean;
},
{
success: boolean;
}
> = (ctx, next) => {
const { id, stopCapturing = true } = ctx;
const std = ctx.std;
const doc = std.store;
const model = doc.getBlock(id)?.model;
if (!model || !matchModels(model, [ParagraphBlockModel])) return false;
const parent = doc.getParent(model);
if (!parent || !matchModels(parent, [CalloutBlockModel])) return false;
if (stopCapturing) std.store.captureSync();
// Get current block index in callout
const currentIndex = parent.children.indexOf(model);
const hasText = model.text && model.text.length > 0;
// Find previous paragraph block in callout
let previousBlock = null;
for (let i = currentIndex - 1; i >= 0; i--) {
const sibling = parent.children[i];
if (matchModels(sibling, [ParagraphBlockModel])) {
previousBlock = sibling;
break;
}
}
if (previousBlock && hasText) {
// Clone current text content before any operations to prevent data loss
const currentText = model.text || new Text();
// Get previous block text and merge index
const previousText = previousBlock.text || new Text();
const mergeIndex = previousText.length;
// Apply each delta from cloned current text to previous block to preserve formatting
previousText.join(currentText);
// Remove current block after text has been merged
doc.deleteBlock(model, {
deleteChildren: false,
});
// Focus at merge point in previous block
focusTextModel(std, previousBlock.id, mergeIndex);
} else if (previousBlock && !hasText) {
// Move cursor to end of previous block
doc.deleteBlock(model, {
deleteChildren: false,
});
const previousText = previousBlock.text || new Text();
focusTextModel(std, previousBlock.id, previousText.length);
} else {
// No previous block, select the entire callout
doc.deleteBlock(model, {
deleteChildren: false,
});
std.selection.setGroup('note', [
std.selection.create(BlockSelection, {
blockId: parent.id,
}),
]);
}
return next({ success: true });
};

View File

@@ -0,0 +1,85 @@
import {
CalloutBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { Command, EditorHost } from '@blocksuite/std';
export const splitCalloutCommand: Command<{
blockId: string;
inlineIndex: number;
currentBlockId: string;
}> = (ctx, next) => {
const { blockId, inlineIndex, currentBlockId, std } = ctx;
const host = std.host as EditorHost;
const doc = host.store;
const calloutModel = doc.getBlock(blockId)?.model;
if (!calloutModel || !matchModels(calloutModel, [CalloutBlockModel])) {
console.error(`block ${blockId} is not a callout block`);
return;
}
const currentModel = doc.getBlock(currentBlockId)?.model;
if (!currentModel) {
console.error(`current block ${currentBlockId} not found`);
return;
}
doc.captureSync();
if (matchModels(currentModel, [ParagraphBlockModel])) {
// User is in a paragraph within the callout's children
const afterText = currentModel.props.text.split(inlineIndex);
// Update the current paragraph's text to keep only the part before cursor
doc.transact(() => {
currentModel.props.text.delete(
inlineIndex,
currentModel.props.text.length - inlineIndex
);
});
// Create a new paragraph block after the current one
const parent = doc.getParent(currentModel);
if (parent) {
const currentIndex = parent.children.indexOf(currentModel);
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: afterText,
},
parent,
currentIndex + 1
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
} else {
// If current block is not a paragraph, create a new paragraph in callout
const newParagraphId = doc.addBlock(
'affine:paragraph',
{
text: new Text(),
},
calloutModel
);
if (newParagraphId) {
host.updateComplete
.then(() => {
focusTextModel(std, newParagraphId);
})
.catch(console.error);
}
}
next();
};

View File

@@ -1,24 +1,12 @@
import { CalloutBlockModel } from '@blocksuite/affine-model';
import { focusBlockEnd } from '@blocksuite/affine-shared/commands';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import {
findAncestorModel,
isInsideBlockByFlavour,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { FontIcon } from '@blocksuite/icons/lit';
import { calloutTooltip } from './tooltips';
export const calloutSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => {
return (
findAncestorModel(model, ancestor =>
matchModels(ancestor, [CalloutBlockModel])
) !== null
);
},
items: [
{
name: 'Callout',

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -1,4 +1,5 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import { updateBlockAlign } from '@blocksuite/affine-block-note';
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
@@ -12,6 +13,9 @@ import {
DeleteIcon,
DownloadIcon,
DuplicateIcon,
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
@@ -51,7 +55,55 @@ const builtinToolbarConfig = {
},
},
{
id: 'c.comment',
id: 'c.1.align-left',
tooltip: 'Align left',
icon: TextAlignLeftIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Left,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'c.2.align-center',
tooltip: 'Align center',
icon: TextAlignCenterIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Center,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'c.3.align-right',
tooltip: 'Align right',
icon: TextAlignRightIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.chain
.pipe(updateBlockAlign, {
textAlign: TextAlign.Right,
selectedBlocks: [block],
})
.run();
}
},
},
{
id: 'd.comment',
...blockCommentToolbarButton,
},
{

View File

@@ -143,6 +143,15 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
width: '100%',
});
const alignItemsStyleMap = styleMap({
alignItems:
this.model.props.textAlign$.value === 'left'
? 'flex-start'
: this.model.props.textAlign$.value === 'right'
? 'flex-end'
: undefined,
});
const resovledState = this.resourceController.resolveStateWith({
loadingIcon: LoadingIcon({
strokeColor: cssVarV2('button/pureWhiteText'),
@@ -162,6 +171,7 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
html`<affine-page-image
.block=${this}
.state=${resovledState}
style="${alignItemsStyleMap}"
></affine-page-image>`,
() =>
html`<affine-image-fallback-card

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -150,6 +150,10 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -161,7 +165,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
</div>`;
return html`
<div class=${'affine-list-block-container'}>
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
<div
class=${classMap({
'affine-list-rich-text-wrapper': true,

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -8,3 +8,4 @@ export { indentBlock } from './indent-block.js';
export { indentBlocks } from './indent-blocks.js';
export { selectBlock } from './select-block.js';
export { selectBlocksBetween } from './select-blocks-between.js';
export { updateBlockAlign } from './update-block-align.js';

View File

@@ -0,0 +1,53 @@
import type { TextAlign } from '@blocksuite/affine-model';
import {
getBlockSelectionsCommand,
getImageSelectionsCommand,
getSelectedBlocksCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import {
type BlockComponent,
type Command,
TextSelection,
} from '@blocksuite/std';
type UpdateBlockAlignConfig = {
textAlign: TextAlign;
selectedBlocks?: BlockComponent[];
};
export const updateBlockAlign: Command<UpdateBlockAlignConfig> = (
ctx,
next
) => {
let { std, textAlign, selectedBlocks } = ctx;
if (selectedBlocks === null) {
const [result, ctx] = std.command
.chain()
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
chain.pipe(getImageSelectionsCommand),
])
.pipe(getSelectedBlocksCommand, { types: ['text', 'block', 'image'] })
.run();
if (result) {
selectedBlocks = ctx.selectedBlocks;
}
}
if (!selectedBlocks || selectedBlocks.length === 0) return false;
selectedBlocks.forEach(block => {
std.store.updateBlock(block.model, { textAlign });
});
const selectionManager = std.host.selection;
const textSelection = selectionManager.find(TextSelection);
if (!textSelection) {
return false;
}
selectionManager.setGroup('note', [textSelection]);
return next();
};

View File

@@ -4,9 +4,15 @@ import {
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import {
type TextAlignConfig,
textAlignConfigs,
type TextConversionConfig,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import {
type SlashMenuActionItem,
@@ -17,7 +23,7 @@ import {
import { HeadingsIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { updateBlockType } from '../commands';
import { updateBlockAlign, updateBlockType } from '../commands';
import { tooltips } from './tooltips';
let basicIndex = 0;
@@ -60,6 +66,10 @@ const noteSlashMenuConfig: SlashMenuConfig = {
createConversionItem(config, `1_List@${index++}`)
),
...textAlignConfigs.map((config, index) =>
createAlignItem(config, `2_Align@${index++}`)
),
...textFormatConfigs
.filter(i => !['Code', 'Link'].includes(i.name))
.map((config, index) =>
@@ -89,6 +99,26 @@ function createConversionItem(
};
}
function createAlignItem(
config: TextAlignConfig,
group?: SlashMenuItem['group']
): SlashMenuActionItem {
const { textAlign, name, icon } = config;
return {
name,
group,
icon,
action: ({ std }) => {
std.command
.chain()
.pipe(getTextSelectionCommand)
.pipe(getSelectedModelsCommand, { types: ['text'] })
.pipe(updateBlockAlign, { textAlign })
.run();
},
};
}
function createTextFormatItem(
config: TextFormatConfig,
group?: SlashMenuItem['group']

View File

@@ -5,7 +5,10 @@ import {
NoteBlockSchema,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
focusBlockEnd,
focusBlockStart,
@@ -36,6 +39,7 @@ import {
indentBlocks,
selectBlock,
selectBlocksBetween,
updateBlockAlign,
updateBlockType,
} from './commands';
import { moveBlockConfigs } from './move-block';
@@ -157,6 +161,36 @@ class NoteKeymap {
);
};
private readonly _bindTextAlignHotKey = () => {
return textAlignConfigs.reduce(
(acc, item) => {
const keymap = item.hotkey!.reduce(
(acc, key) => {
return {
...acc,
[key]: ctx => {
ctx.get('defaultState').event.preventDefault();
const [result] = this._std.command
.chain()
.pipe(updateBlockAlign, { textAlign: item.textAlign })
.run();
return result;
},
};
},
{} as Record<string, UIEventHandler>
);
return {
...acc,
...keymap,
};
},
{} as Record<string, UIEventHandler>
);
};
private _focusBlock: BlockComponent | null = null;
private readonly _getClosestNoteByBlockId = (blockId: string) => {
@@ -568,6 +602,7 @@ class NoteKeymap {
...this._bindMoveBlockHotKey(),
...this._bindQuickActionHotKey(),
...this._bindTextConversionHotKey(),
...this._bindTextAlignHotKey(),
Tab: ctx => {
const [success] = this.std.command.exec(indentBlocks);

View File

@@ -264,6 +264,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
`;
}
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -288,6 +292,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
'affine-paragraph-block-container': true,
'highlight-comment': this.isCommentHighlighted,
})}
style="${textAlignStyle}"
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
>
<div

View File

@@ -38,7 +38,7 @@
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -8,7 +8,10 @@ import {
notifyDocCreated,
promptDocTitle,
} from '@blocksuite/affine-block-embed';
import { updateBlockType } from '@blocksuite/affine-block-note';
import {
updateBlockAlign,
updateBlockType,
} from '@blocksuite/affine-block-note';
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
import { toast } from '@blocksuite/affine-components/toast';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
@@ -23,8 +26,12 @@ import {
import {
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
type TextAlign,
} from '@blocksuite/affine-model';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
deleteSelectedModelsCommand,
@@ -46,6 +53,7 @@ import {
ActionPlacement,
blockCommentToolbarButton,
} from '@blocksuite/affine-shared/services';
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import {
CopyIcon,
@@ -130,6 +138,64 @@ const conversionsActionGroup = {
},
} as const satisfies ToolbarActionGenerator;
const alignActionGroup = {
id: 'b.align',
when: ({ chain }) => isFormatSupported(chain).run()[0],
generate({ chain }) {
const [ok, { selectedModels = [] }] = chain
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
])
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
.run();
if (!ok) return null;
const alignment =
textAlignConfigs.find(
({ textAlign }) =>
textAlign ===
getMostCommonValue(
selectedModels.map(
({ props }) => props as { textAlign?: TextAlign }
),
'textAlign'
)
) ?? textAlignConfigs[0];
const update = (textAlign: TextAlign) => {
chain.pipe(updateBlockAlign, { textAlign }).run();
};
return {
content: html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
${alignment.icon} ${EditorChevronDown}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${repeat(
textAlignConfigs,
item => item.name,
({ textAlign, name, icon }) => html`
<editor-menu-action
aria-label=${name}
@click=${() => update(textAlign)}
>
${icon}<span class="label">${name}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`,
};
},
} as const satisfies ToolbarActionGenerator;
const inlineTextActionGroup = {
id: 'b.inline-text',
when: ({ chain }) => isFormatSupported(chain).run()[0],
@@ -291,6 +357,7 @@ const turnIntoLinkedDoc = {
export const builtinToolbarConfig = {
actions: [
conversionsActionGroup,
alignActionGroup,
inlineTextActionGroup,
highlightActionGroup,
turnIntoDatabase,

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -20,7 +20,7 @@
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -144,6 +144,16 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
style=${styleMap({
paddingLeft: `${virtualPadding}px`,
paddingRight: `${virtualPadding}px`,
marginLeft:
!this.model.props.textAlign$.value ||
this.model.props.textAlign$?.value === 'left'
? undefined
: 'auto',
marginRight:
!this.model.props.textAlign$.value ||
this.model.props.textAlign$?.value === 'right'
? undefined
: 'auto',
width: 'max-content',
})}
>

View File

@@ -13,7 +13,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*",

View File

@@ -193,6 +193,7 @@ export const menuButtonItems = {
(config: {
name: string;
label?: () => TemplateResult;
info?: TemplateResult;
prefix?: TemplateResult;
postfix?: TemplateResult;
isSelected?: boolean;
@@ -211,7 +212,7 @@ export const menuButtonItems = {
return html`
${config.prefix}
<div class="affine-menu-action-text">
${config.label?.() ?? config.name}
${config.label?.() ?? config.name} ${config.info}
</div>
${config.postfix ?? (config.isSelected ? DoneIcon() : undefined)}
`;

View File

@@ -13,7 +13,7 @@
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -19,6 +19,7 @@ const DOC_BLOCK_CHILD_PADDING = 24;
export class DocTitle extends WithDisposable(ShadowlessElement) {
static override styles = css`
.doc-icon-container,
.doc-title-container {
box-sizing: border-box;
font-family: var(--affine-font-family);
@@ -49,6 +50,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
/* Extra small devices (phones, 640px and down) */
@container viewport (width <= 640px) {
.doc-icon-container,
.doc-title-container {
padding-left: ${DOC_BLOCK_CHILD_PADDING}px;
padding-right: ${DOC_BLOCK_CHILD_PADDING}px;

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -25,7 +25,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -19,7 +19,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -22,7 +22,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -9,6 +9,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types.js';
import { ImageBlockTransformer } from './image-transformer.js';
@@ -20,6 +21,7 @@ export type ImageBlockProps = {
rotate: number;
size?: number;
comments?: Record<string, boolean>;
textAlign?: TextAlign;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -34,6 +36,7 @@ const defaultImageProps: ImageBlockProps = {
rotate: 0,
size: -1,
comments: undefined,
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -5,6 +5,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
// `toggle` type has been deprecated, do not use it
@@ -13,6 +14,7 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
export type ListProps = {
type: ListType;
text: Text;
textAlign?: TextAlign;
checked: boolean;
collapsed: boolean;
order: number | null;
@@ -25,6 +27,7 @@ export const ListBlockSchema = defineBlockSchema({
({
type: 'bulleted',
text: internal.Text(),
textAlign: undefined,
checked: false,
collapsed: false,

View File

@@ -5,6 +5,7 @@ import {
type Text,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type ParagraphType =
@@ -19,6 +20,7 @@ export type ParagraphType =
export type ParagraphProps = {
type: ParagraphType;
textAlign?: TextAlign;
text: Text;
collapsed: boolean;
comments?: Record<string, boolean>;
@@ -29,6 +31,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
props: (internal): ParagraphProps => ({
type: 'text',
text: internal.Text(),
textAlign: undefined,
collapsed: false,
comments: undefined,
'meta:createdAt': undefined,

View File

@@ -5,6 +5,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type TableCell = {
@@ -30,6 +31,7 @@ export interface TableBlockProps extends BlockMeta {
// key = `${rowId}:${columnId}`
cells: Record<string, TableCell>;
comments?: Record<string, boolean>;
textAlign?: TextAlign;
}
export interface TableCellSerialized {
@@ -53,6 +55,7 @@ export const TableBlockSchema = defineBlockSchema({
columns: {},
cells: {},
comments: undefined,
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -14,7 +14,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -0,0 +1,35 @@
import { TextAlign } from '@blocksuite/affine-model';
import {
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import type { TemplateResult } from 'lit';
export interface TextAlignConfig {
textAlign: TextAlign;
name: string;
hotkey: string[] | null;
icon: TemplateResult<1>;
}
export const textAlignConfigs: TextAlignConfig[] = [
{
textAlign: TextAlign.Left,
name: 'Align left',
hotkey: [`Mod-Shift-L`],
icon: TextAlignLeftIcon(),
},
{
textAlign: TextAlign.Center,
name: 'Align center',
hotkey: [`Mod-Shift-E`],
icon: TextAlignCenterIcon(),
},
{
textAlign: TextAlign.Right,
name: 'Align right',
hotkey: [`Mod-Shift-R`],
icon: TextAlignRightIcon(),
},
];

View File

@@ -1,3 +1,4 @@
export { type TextAlignConfig, textAlignConfigs } from './align';
export { type TextConversionConfig, textConversionConfigs } from './conversion';
export {
asyncGetRichText,

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -21,7 +21,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -17,7 +17,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -33,7 +33,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",

View File

@@ -16,7 +16,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.16",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",

View File

@@ -18,7 +18,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -15,7 +15,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",

View File

@@ -14,7 +14,7 @@
"license": "MIT",
"dependencies": {
"@blocksuite/affine": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.3",
"@lottiefiles/dotlottie-wc": "^0.5.0",

View File

@@ -14,7 +14,7 @@
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/icons": "^2.2.17",
"@blocksuite/integration-test": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@shoelace-style/shoelace": "2.20.1",

View File

@@ -53,7 +53,7 @@
"@affine-tools/cli": "workspace:*",
"@capacitor/cli": "^7.0.0",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^9.3.0",
"@faker-js/faker": "^10.0.0",
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.6.1",
"@playwright/test": "=1.52.0",

View File

@@ -0,0 +1,12 @@
-- CreateEnum
CREATE TYPE "Provider" AS ENUM ('stripe', 'revenuecat');
-- CreateEnum
CREATE TYPE "IapStore" AS ENUM ('app_store', 'play_store');
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "iap_store" "IapStore",
ADD COLUMN "provider" "Provider" NOT NULL DEFAULT 'stripe',
ADD COLUMN "rc_entitlement" VARCHAR,
ADD COLUMN "rc_external_ref" VARCHAR,
ADD COLUMN "rc_product_id" VARCHAR;

View File

@@ -127,7 +127,8 @@
"@affine-tools/cli": "workspace:*",
"@affine-tools/utils": "workspace:*",
"@affine/graphql": "workspace:*",
"@faker-js/faker": "^9.6.0",
"@faker-js/faker": "^10.0.0",
"@nestjs/swagger": "^11.2.0",
"@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.1",

View File

@@ -749,6 +749,16 @@ model Subscription {
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
// stripe schedule id
stripeScheduleId String? @map("stripe_schedule_id") @db.VarChar
// subscription provider: stripe or revenuecat
provider Provider @default(stripe)
// iap store for revenuecat subscriptions
iapStore IapStore? @map("iap_store")
// revenuecat entitlement name like "Pro" / "AI"
rcEntitlement String? @map("rc_entitlement") @db.VarChar
// revenuecat product id like "app.affine.pro.Annual"
rcProductId String? @map("rc_product_id") @db.VarChar
// external reference, appstore originalTransactionId or play purchaseToken
rcExternalRef String? @map("rc_external_ref") @db.VarChar
// subscription.status, active/past_due/canceled/unpaid...
status String @db.VarChar(20)
// subscription.current_period_start
@@ -770,6 +780,16 @@ model Subscription {
@@map("subscriptions")
}
enum Provider {
stripe
revenuecat
}
enum IapStore {
app_store
play_store
}
model Invoice {
stripeInvoiceId String @id @map("stripe_invoice_id")
targetId String @map("target_id") @db.VarChar

View File

@@ -444,3 +444,37 @@ Generated by [AVA](https://avajs.dev).
},
],
}
## should resolve model correctly based on subscription status and prompt config
> should honor requested pro model
'gemini-2.5-pro'
> should fallback to default model
'gemini-2.5-flash'
> should fallback to default model when requesting pro model during trialing
'gemini-2.5-flash'
> should honor requested non-pro model during trialing
'gemini-2.5-flash'
> should pick default model when no requested model during trialing
'gemini-2.5-flash'
> should pick default model when no requested model during active
'gemini-2.5-flash'
> should honor requested pro model during active
'claude-sonnet-4@20250514'
> should fallback to default model when requesting non-optional model during active
'gemini-2.5-flash'

View File

@@ -60,6 +60,9 @@ import {
import { AutoRegisteredWorkflowExecutor } from '../plugins/copilot/workflow/executor/utils';
import { WorkflowGraphList } from '../plugins/copilot/workflow/graph';
import { CopilotWorkspaceService } from '../plugins/copilot/workspace';
import { PaymentModule } from '../plugins/payment';
import { SubscriptionService } from '../plugins/payment/service';
import { SubscriptionStatus } from '../plugins/payment/types';
import { MockCopilotProvider } from './mocks';
import { createTestingModule, TestingModule } from './utils';
import { WorkflowTestCases } from './utils/copilot';
@@ -82,6 +85,7 @@ type Context = {
storage: CopilotStorage;
workflow: CopilotWorkflowService;
cronJobs: CopilotCronJobs;
subscription: SubscriptionService;
executors: {
image: CopilotChatImageExecutor;
text: CopilotChatTextExecutor;
@@ -116,6 +120,7 @@ test.before(async t => {
},
},
}),
PaymentModule,
QuotaModule,
StorageModule,
CopilotModule,
@@ -124,6 +129,13 @@ test.before(async t => {
// use real JobQueue for testing
builder.overrideProvider(JobQueue).useClass(JobQueue);
builder.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider);
builder.overrideProvider(SubscriptionService).useClass(
class {
select() {
return { getSubscription: async () => undefined };
}
}
);
},
});
@@ -145,6 +157,7 @@ test.before(async t => {
const transcript = module.get(CopilotTranscriptionService);
const workspaceEmbedding = module.get(CopilotWorkspaceService);
const cronJobs = module.get(CopilotCronJobs);
const subscription = module.get(SubscriptionService);
t.context.module = module;
t.context.auth = auth;
@@ -163,6 +176,7 @@ test.before(async t => {
t.context.transcript = transcript;
t.context.workspaceEmbedding = workspaceEmbedding;
t.context.cronJobs = cronJobs;
t.context.subscription = subscription;
t.context.executors = {
image: module.get(CopilotChatImageExecutor),
@@ -2047,3 +2061,90 @@ test('should handle copilot cron jobs correctly', async t => {
toBeGenerateStub.restore();
jobAddStub.restore();
});
test('should resolve model correctly based on subscription status and prompt config', async t => {
const { db, session, subscription } = t.context;
// 1) Seed a prompt that has optionalModels and proModels in config
const promptName = 'resolve-model-test';
await db.aiPrompt.create({
data: {
name: promptName,
model: 'gemini-2.5-flash',
messages: {
create: [{ idx: 0, role: 'system', content: 'test' }],
},
config: { proModels: ['gemini-2.5-pro', 'claude-sonnet-4@20250514'] },
optionalModels: [
'gemini-2.5-flash',
'gemini-2.5-pro',
'claude-sonnet-4@20250514',
],
},
});
// 2) Create a chat session with this prompt
const sessionId = await session.create({
promptName,
docId: 'test',
workspaceId: 'test',
userId,
pinned: false,
});
const s = (await session.get(sessionId))!;
const mockStatus = (status?: SubscriptionStatus) => {
Sinon.restore();
Sinon.stub(subscription, 'select').callsFake(() => ({
// @ts-expect-error mock
getSubscription: async () => (status ? { status } : null),
}));
};
// payment disabled -> allow requested if in optional; pro not blocked
{
const model1 = await s.resolveModel(false, 'gemini-2.5-pro');
t.snapshot(model1, 'should honor requested pro model');
const model2 = await s.resolveModel(false, 'not-in-optional');
t.snapshot(model2, 'should fallback to default model');
}
// payment enabled + trialing: requesting pro should fallback to default
{
mockStatus(SubscriptionStatus.Trialing);
const model3 = await s.resolveModel(true, 'gemini-2.5-pro');
t.snapshot(
model3,
'should fallback to default model when requesting pro model during trialing'
);
const model4 = await s.resolveModel(true, 'gemini-2.5-flash');
t.snapshot(model4, 'should honor requested non-pro model during trialing');
const model5 = await s.resolveModel(true);
t.snapshot(
model5,
'should pick default model when no requested model during trialing'
);
}
// payment enabled + active: without requested -> default model; requested pro should be honored
{
mockStatus(SubscriptionStatus.Active);
const model6 = await s.resolveModel(true);
t.snapshot(
model6,
'should pick default model when no requested model during active'
);
const model7 = await s.resolveModel(true, 'claude-sonnet-4@20250514');
t.snapshot(model7, 'should honor requested pro model during active');
const model8 = await s.resolveModel(true, 'not-in-optional');
t.snapshot(
model8,
'should fallback to default model when requesting non-optional model during active'
);
}
});

View File

@@ -0,0 +1,253 @@
# Snapshot report for `src/__tests__/payment/revenuecat.spec.ts`
The actual snapshot is saved in `revenuecat.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should resolve product mapping consistently (whitelist, override, unknown)
> should map product for whitelist/override/unknown
{
override: {
customMonthly: {
plan: 'pro',
recurring: 'monthly',
},
},
unknown: null,
whitelist: {
aiAnnual: {
plan: 'ai',
recurring: 'yearly',
},
proAnnual: {
plan: 'pro',
recurring: 'yearly',
},
proMonthly: {
plan: 'pro',
recurring: 'monthly',
},
},
}
## should standardize RC subscriber response and upsert subscription with observability fields
> should standardize payload and have events
{
activatedCount: 1,
canceledCount: 0,
dbObservability: {
iapStore: 'app_store',
provider: 'revenuecat',
rcEntitlement: 'Pro',
rcExternalRef: 'orig-tx-1',
rcProductId: 'app.affine.pro.Annual',
},
lastActivated: {
plan: 'pro',
recurring: 'yearly',
},
subscriberCount: 1,
}
## should process expiration/refund by deleting subscription and emitting canceled
> should process expiration/refund and emit canceled
{
activatedCount: 0,
canceledCount: 1,
finalDBCount: 0,
lastCanceled: {
plan: 'pro',
recurring: 'yearly',
},
subscriberCount: 1,
}
## should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions
> should enqueue per-user RC reconciliation jobs (deduplicated by userId)
{
queued: [
{
name: 'nightly.revenuecat.syncUser',
opts: {
attempts: 3,
backoff: {
delay: 60000,
type: 'exponential',
},
jobId: 'nightly-rc-sync-u1',
},
payload: {
userId: 'u1',
},
},
{
name: 'nightly.revenuecat.syncUser',
opts: {
attempts: 3,
backoff: {
delay: 60000,
type: 'exponential',
},
jobId: 'nightly-rc-sync-u2',
},
payload: {
userId: 'u2',
},
},
],
uniqueJobCount: 2,
}
## should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)
> should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)
{
results: [
{
activatedCount: 1,
name: 'Pro monthly on iOS',
rec: {
iapStore: 'app_store',
plan: 'pro',
provider: 'revenuecat',
rcEntitlement: 'Pro',
rcExternalRef: 'orig-ios-1',
rcProductId: 'app.affine.pro.Monthly',
recurring: 'monthly',
status: 'active',
},
},
{
activatedCount: 1,
name: 'AI annual on Android',
rec: {
iapStore: 'play_store',
plan: 'ai',
provider: 'revenuecat',
rcEntitlement: 'AI',
rcExternalRef: 'token-android-1',
rcProductId: 'app.affine.pro.ai.Annual',
recurring: 'yearly',
status: 'active',
},
},
],
}
## should keep active and advance period dates when a trialing subscription renews
> should keep active after trial renewal
{
activatedCount: 2,
canceledCount: 0,
status: 'active',
}
## should remove or cancel the record and revoke entitlement when a trialing subscription expires
> should remove record
{
canceledCount: 1,
finalDBCount: 0,
}
## should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)
> should keep active until period end when will_renew is false
{
activatedCount: 1,
canceledCount: 0,
hasCanceledAt: true,
status: 'active',
}
## should retain record as past_due (inactive but not expired) and NOT emit canceled event
> should retain past_due record and NOT emit canceled event
{
canceledCount: 0,
status: 'past_due',
}
## should skip RC upsert when Stripe active already exists for same plan
> should skip RC upsert when Stripe active already exists
{
activatedCount: 0,
hasRCRecord: false,
}
## should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records
> should reconcile and fix missing or out-of-order states for revenuecat records
{
activatedCount: 1,
canceledCount: 0,
subscriberCount: 1,
}
## should treat refund as early expiration and revoke immediately
> should delete record and emit canceled on refund
{
canceledCount: 1,
finalDBCount: 0,
}
## should ignore non-whitelisted productId and not write to DB
> should ignore non-whitelisted productId and not write to DB
{
activatedCount: 0,
canceledCount: 0,
dbCount: 0,
}
## should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)
> should map via entitlement+duration fallback and ignore unsupported durations
{
aiViaFallback: {
plan: 'ai',
provider: 'revenuecat',
recurring: 'yearly',
},
eventsCounts: {
afterFirst: {
a: 1,
c: 0,
},
afterSecond: {
a: 2,
c: 0,
},
afterThird: {
a: 2,
c: 0,
},
},
proViaFallback: {
plan: 'pro',
provider: 'revenuecat',
recurring: 'monthly',
},
totalCount: 2,
}

View File

@@ -0,0 +1,929 @@
import { PrismaClient, User } from '@prisma/client';
import ava, { TestFn } from 'ava';
import { omit } from 'lodash-es';
import Sinon from 'sinon';
import {
EventBus,
ManagedByAppStoreOrPlay,
SubscriptionAlreadyExists,
} from '../../base';
import { ConfigModule } from '../../base/config';
import { FeatureService } from '../../core/features';
import { Models } from '../../models';
import { PaymentModule } from '../../plugins/payment';
import { SubscriptionCronJobs } from '../../plugins/payment/cron';
import { UserSubscriptionManager } from '../../plugins/payment/manager';
import {
RcEvent,
resolveProductMapping,
RevenueCatService,
RevenueCatWebhookController,
RevenueCatWebhookHandler,
type Subscription,
} from '../../plugins/payment/revenuecat';
import { SubscriptionService } from '../../plugins/payment/service';
import {
SubscriptionPlan,
SubscriptionRecurring,
} from '../../plugins/payment/types';
import { createTestingApp, TestingApp } from '../utils';
type Ctx = {
module: TestingApp;
db: PrismaClient;
models: Models;
event: Sinon.SinonStubbedInstance<EventBus>;
service: SubscriptionService;
rc: RevenueCatService;
webhook: RevenueCatWebhookHandler;
controller: RevenueCatWebhookController;
mockSub: (subs: Subscription[]) => Sinon.SinonStub;
mockSubSeq: (sequences: Subscription[][]) => Sinon.SinonStub;
triggerWebhook: (
userId: string,
event: Omit<RcEvent, 'app_id' | 'environment'>
) => Promise<void>;
collectEvents: () => {
activatedCount: number;
canceledCount: number;
events: Record<string, any[]>;
};
};
const test = ava as TestFn<Ctx>;
let user: User;
test.beforeEach(async t => {
const app = await createTestingApp({
imports: [
ConfigModule.override({
payment: {
revenuecat: {
enabled: true,
webhookAuth: '42',
},
},
}),
PaymentModule,
],
tapModule: m => {
m.overrideProvider(FeatureService).useValue(
Sinon.createStubInstance(FeatureService)
);
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
},
});
const db = app.get(PrismaClient);
const models = app.get(Models);
const event = app.get(EventBus) as Sinon.SinonStubbedInstance<EventBus>;
const service = app.get(SubscriptionService);
const rc = app.get(RevenueCatService);
const webhook = app.get(RevenueCatWebhookHandler);
const controller = app.get(RevenueCatWebhookController);
t.context.module = app;
t.context.db = db;
t.context.models = models;
t.context.event = event;
t.context.service = service;
t.context.rc = rc;
t.context.webhook = webhook;
t.context.controller = controller;
t.context.mockSub = subs => Sinon.stub(rc, 'getSubscriptions').resolves(subs);
t.context.mockSubSeq = sequences => {
const stub = Sinon.stub(rc, 'getSubscriptions');
sequences.forEach((seq, idx) => {
if (idx === 0) stub.onFirstCall().resolves(seq);
else if (idx === 1) stub.onSecondCall().resolves(seq);
else stub.onCall(idx).resolves(seq);
});
return stub;
};
t.context.triggerWebhook = async (appUserId, event) => {
await webhook.onWebhook({
appUserId,
event: {
...event,
app_id: 'app.affine.pro',
environment: 'SANDBOX',
} as RcEvent,
});
};
t.context.collectEvents = () => {
const events = event.emit.getCalls().reduce(
(acc, c) => {
const [key, value] = c.args;
acc[key] = acc[key] || [];
acc[key].push(value);
return acc;
},
{} as { [key: string]: any[] }
);
const activatedCount = events['user.subscription.activated']?.length || 0;
const canceledCount = events['user.subscription.canceled']?.length || 0;
return { activatedCount, canceledCount, events };
};
});
test.beforeEach(async t => {
await t.context.module.initTestingDB();
user = await t.context.models.user.create({
email: 'test@affine.pro',
});
});
test.afterEach.always(async t => {
Sinon.reset();
await t.context.module.close();
});
test('should resolve product mapping consistently (whitelist, override, unknown)', t => {
const override = {
'custom.sku.monthly': { plan: 'pro', recurring: 'monthly' },
} as Record<string, { plan: string; recurring: string }>;
const actual = {
whitelist: {
proMonthly: resolveProductMapping({
productId: 'app.affine.pro.Monthly',
}),
proAnnual: resolveProductMapping({ productId: 'app.affine.pro.Annual' }),
aiAnnual: resolveProductMapping({
productId: 'app.affine.pro.ai.Annual',
}),
},
override: {
customMonthly: resolveProductMapping(
{ productId: 'custom.sku.monthly' },
override
),
},
unknown: resolveProductMapping({ productId: 'unknown.sku' }),
};
t.snapshot(actual, 'should map product for whitelist/override/unknown');
});
test('should standardize RC subscriber response and upsert subscription with observability fields', async t => {
const { webhook, collectEvents, mockSub } = t.context;
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'),
expirationDate: new Date('2026-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await webhook.onWebhook({
appUserId: user.id,
event: {
id: 'evt_1',
environment: 'PRODUCTION',
app_id: 'app.affine.pro',
type: 'INITIAL_PURCHASE',
store: 'app_store',
original_transaction_id: 'orig-tx-1',
},
});
const { activatedCount, canceledCount, events } = collectEvents();
const record = await t.context.db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: {
provider: true,
iapStore: true,
rcEntitlement: true,
rcProductId: true,
rcExternalRef: true,
},
});
t.snapshot(
{
subscriberCount: subscriber.getCalls()?.length || 0,
activatedCount,
canceledCount,
lastActivated: omit(
events['user.subscription.activated']?.slice(-1)?.[0],
'userId'
),
dbObservability: record,
},
'should standardize payload and have events'
);
});
test('should process expiration/refund by deleting subscription and emitting canceled', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'annual',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2024-01-01T00:00:00.000Z'),
expirationDate: new Date('2024-02-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_2',
type: 'EXPIRATION',
store: 'app_store',
original_transaction_id: 'orig-tx-2',
});
const finalDBCount = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { activatedCount, canceledCount, events } = collectEvents();
t.snapshot(
{
finalDBCount,
subscriberCount: subscriber.getCalls()?.length || 0,
activatedCount,
canceledCount,
lastCanceled: omit(
events['user.subscription.canceled']?.slice(-1)?.[0],
'userId'
),
},
'should process expiration/refund and emit canceled'
);
});
test('should enqueue per-user reconciliation jobs for existing RC active/trialing/past_due subscriptions', async t => {
const { module, db } = t.context;
const cron = module.get(SubscriptionCronJobs);
const common = { provider: 'revenuecat', start: new Date() } as const;
await db.subscription.createMany({
data: [
{
targetId: 'u1',
plan: 'pro',
status: 'active',
recurring: 'monthly',
...common,
},
{
targetId: 'u2',
plan: 'ai',
status: 'trialing',
recurring: 'annual',
...common,
},
{
targetId: 'u1',
plan: 'ai',
status: 'past_due',
recurring: 'monthly',
...common,
},
],
});
await cron.reconcileRevenueCatSubscriptions();
const calls = module.queue.add.getCalls().map(c => ({
name: c.args[0],
payload: c.args[1],
opts: c.args[2],
}));
t.snapshot(
{
queued: calls,
uniqueJobCount: calls.filter(
c => c.name === 'nightly.revenuecat.syncUser'
).length,
},
'should enqueue per-user RC reconciliation jobs (deduplicated by userId)'
);
});
test('should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)', async t => {
const { db, event, collectEvents, mockSubSeq, triggerWebhook } = t.context;
const scenarios = [
{
name: 'Pro monthly on iOS',
stub: [
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-01-10T00:00:00.000Z'),
expirationDate: new Date('2025-02-10T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store' as const,
willRenew: true,
duration: null,
},
],
event: {
id: 'evt_ios_1',
type: 'INITIAL_PURCHASE',
store: 'app_store',
original_transaction_id: 'orig-ios-1',
},
expectedPlan: 'pro' as const,
},
{
name: 'AI annual on Android',
stub: [
{
identifier: 'AI',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'),
expirationDate: new Date('2026-03-01T00:00:00.000Z'),
productId: 'app.affine.pro.ai.Annual',
store: 'play_store' as const,
willRenew: true,
duration: null,
},
],
event: {
id: 'evt_android_1',
type: 'INITIAL_PURCHASE',
store: 'play_store',
purchase_token: 'token-android-1',
},
expectedPlan: 'ai' as const,
},
];
const results: any[] = [];
mockSubSeq(scenarios.map(s => s.stub));
for (const s of scenarios) {
// reset event history between scenarios for clean counts
event.emit.resetHistory?.();
await triggerWebhook(user.id, s.event);
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: s.expectedPlan } },
select: {
plan: true,
recurring: true,
status: true,
provider: true,
iapStore: true,
rcEntitlement: true,
rcProductId: true,
rcExternalRef: true,
},
});
const { activatedCount } = collectEvents();
results.push({ name: s.name, rec, activatedCount });
}
t.snapshot(
{ results },
'should activate subscriptions via webhook for whitelisted products across stores (iOS/Android)'
);
});
test('should keep active and advance period dates when a trialing subscription renews', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2025-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-08T00:00:00.000Z'),
expirationDate: new Date('2026-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
],
]);
await triggerWebhook(user.id, {
id: 'evt_trial',
type: 'INITIAL_PURCHASE',
period_type: 'trial',
store: 'app_store',
});
await triggerWebhook(user.id, {
id: 'evt_renew',
type: 'RENEWAL',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true, start: true, end: true },
});
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{ status: rec?.status, activatedCount, canceledCount },
'should keep active after trial renewal'
);
});
test('should remove or cancel the record and revoke entitlement when a trialing subscription expires', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2025-04-08T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-04-01T00:00:00.000Z'),
expirationDate: new Date('2024-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
],
]);
await triggerWebhook(user.id, {
id: 'evt_trial2',
type: 'INITIAL_PURCHASE',
period_type: 'trial',
store: 'app_store',
});
await triggerWebhook(user.id, {
id: 'evt_expire_trial',
type: 'EXPIRATION',
store: 'app_store',
});
const finalDBCount = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { canceledCount } = collectEvents();
t.snapshot({ finalDBCount, canceledCount }, 'should remove record');
});
test('should set canceledAt and keep active until expiration when will_renew is false (cancellation before period end)', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'),
expirationDate: new Date('2025-06-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_cancel_before_end',
type: 'CANCELLATION',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true, canceledAt: true },
});
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{
status: rec?.status,
hasCanceledAt: !!rec?.canceledAt,
activatedCount,
canceledCount,
},
'should keep active until period end when will_renew is false'
);
});
test('should retain record as past_due (inactive but not expired) and NOT emit canceled event', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-05-01T00:00:00.000Z'),
expirationDate: new Date('2999-01-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_pastdue',
type: 'BILLING_ISSUE',
store: 'app_store',
});
const rec = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { status: true },
});
const { canceledCount } = collectEvents();
t.snapshot(
{ status: rec?.status, canceledCount },
'should retain past_due record and NOT emit canceled event'
);
});
test('should block checkout when an existing subscription of the same plan is active', async t => {
const { module, db } = t.context;
const manager = module.get(UserSubscriptionManager);
{
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
await t.throwsAsync(
manager.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: null,
},
{
successCallbackLink: '/',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
},
{ user: { id: user.id, email: user.email } }
),
{ instanceOf: ManagedByAppStoreOrPlay }
);
}
{
await db.subscription.update({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
data: { provider: 'stripe' },
});
await t.throwsAsync(
() =>
manager.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: null,
},
{
successCallbackLink: '/',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
},
{ user: { id: user.id, email: user.email } }
),
{ instanceOf: SubscriptionAlreadyExists }
);
}
});
test('should skip RC upsert when Stripe active already exists for same plan', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'stripe',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-06-01T00:00:00.000Z'),
expirationDate: new Date('2025-07-01T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_conflict',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const rcRec = await db.subscription.findFirst({
where: { targetId: user.id, plan: 'pro', provider: 'revenuecat' },
});
const { activatedCount } = collectEvents();
t.snapshot(
{ hasRCRecord: !!rcRec, activatedCount },
'should skip RC upsert when Stripe active already exists'
);
});
test('should block read-write ops on revenuecat-managed record (cancel/resume/updateRecurring)', async t => {
const { db, service } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date(),
},
});
// local helper used multiple times within this test
const expectManaged = async (fn: () => Promise<any>) =>
t.throwsAsync(() => fn(), { instanceOf: ManagedByAppStoreOrPlay });
await expectManaged(() =>
service.cancelSubscription({ plan: SubscriptionPlan.Pro, userId: user.id })
);
await expectManaged(() =>
service.resumeSubscription({ plan: SubscriptionPlan.Pro, userId: user.id })
);
await expectManaged(() =>
service.updateSubscriptionRecurring(
{ plan: SubscriptionPlan.Pro, userId: user.id },
SubscriptionRecurring.Yearly
)
);
});
test('should reconcile and fix missing or out-of-order states for revenuecat Active/Trialing/PastDue records', async t => {
const { webhook, collectEvents, mockSub } = t.context;
const subscriber = mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-03-01T00:00:00.000Z'),
expirationDate: new Date('2026-03-01T00:00:00.000Z'),
productId: 'app.affine.pro.Annual',
store: 'play_store',
willRenew: true,
duration: null,
},
]);
await webhook.syncAppUser(user.id);
const { activatedCount, canceledCount } = collectEvents();
const subscriberCount = subscriber.getCalls()?.length || 0;
t.snapshot(
{ subscriberCount, activatedCount, canceledCount },
'should reconcile and fix missing or out-of-order states for revenuecat records'
);
});
test('should treat refund as early expiration and revoke immediately', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
await db.subscription.create({
data: {
targetId: user.id,
plan: 'pro',
status: 'active',
provider: 'revenuecat',
recurring: 'monthly',
start: new Date('2025-01-01T00:00:00.000Z'),
},
});
mockSub([
{
identifier: 'Pro',
isTrial: false,
isActive: false,
latestPurchaseDate: new Date('2025-01-01T00:00:00.000Z'),
expirationDate: new Date('2025-01-15T00:00:00.000Z'),
productId: 'app.affine.pro.Monthly',
store: 'app_store',
willRenew: false,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_refund',
type: 'CANCELLATION',
store: 'app_store',
});
const count = await db.subscription.count({
where: { targetId: user.id, plan: 'pro' },
});
const { canceledCount } = collectEvents();
t.snapshot(
{ finalDBCount: count, canceledCount },
'should delete record and emit canceled on refund'
);
});
test('should ignore non-whitelisted productId and not write to DB', async t => {
const { db, collectEvents, mockSub, triggerWebhook } = t.context;
mockSub([
{
identifier: 'Weird',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-07-01T00:00:00.000Z'),
expirationDate: new Date('2026-07-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: null,
},
]);
await triggerWebhook(user.id, {
id: 'evt_unknown',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const dbCount = await db.subscription.count({ where: { targetId: user.id } });
const { activatedCount, canceledCount } = collectEvents();
t.snapshot(
{ dbCount, activatedCount, canceledCount },
'should ignore non-whitelisted productId and not write to DB'
);
});
test('should map via entitlement+duration when productId not whitelisted (P1M/P1Y only)', async t => {
const { db, collectEvents, mockSubSeq, triggerWebhook } = t.context;
mockSubSeq([
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-08-01T00:00:00.000Z'),
expirationDate: new Date('2025-09-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: 'P1M',
},
],
[
{
identifier: 'AI',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-10-01T00:00:00.000Z'),
expirationDate: new Date('2026-10-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'play_store',
willRenew: true,
duration: 'P1Y',
},
],
[
{
identifier: 'Pro',
isTrial: false,
isActive: true,
latestPurchaseDate: new Date('2025-11-01T00:00:00.000Z'),
expirationDate: new Date('2026-02-01T00:00:00.000Z'),
productId: 'unknown.sku',
store: 'app_store',
willRenew: true,
duration: 'P3M', // not supported -> ignore
},
],
]);
// pro monthly via fallback
await triggerWebhook(user.id, {
id: 'evt_fb1',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const r1 = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'pro' } },
select: { plan: true, recurring: true, provider: true },
});
const s1 = collectEvents();
// ai yearly via fallback
await triggerWebhook(user.id, {
id: 'evt_fb2',
type: 'INITIAL_PURCHASE',
store: 'play_store',
});
const r2 = await db.subscription.findUnique({
where: { targetId_plan: { targetId: user.id, plan: 'ai' } },
select: { plan: true, recurring: true, provider: true },
});
const s2 = collectEvents();
// unsupported duration ignored
await triggerWebhook(user.id, {
id: 'evt_fb3',
type: 'INITIAL_PURCHASE',
store: 'app_store',
});
const count = await db.subscription.count({ where: { targetId: user.id } });
const s3 = collectEvents();
t.snapshot(
{
proViaFallback: r1,
aiViaFallback: r2,
totalCount: count,
eventsCounts: {
afterFirst: { a: s1.activatedCount, c: s1.canceledCount },
afterSecond: { a: s2.activatedCount, c: s2.canceledCount },
afterThird: { a: s3.activatedCount, c: s3.canceledCount },
},
},
'should map via entitlement+duration fallback and ignore unsupported durations'
);
});
test('should not dispatch webhook event when authorization header is missing or mismatched', async t => {
const { controller, event } = t.context;
const before = event.emitAsync.getCalls()?.length || 0;
const e = { id: '42', type: 'INITIAL_PURCHASE', app_user_id: user.id };
await controller.handleWebhook({ body: { event: e } } as any, undefined);
const after = event.emitAsync.getCalls()?.length || 0;
t.is(after - before, 0, 'should not emit event');
});

View File

@@ -192,8 +192,10 @@ test.before(async t => {
payment: {
enabled: true,
showLifetimePrice: true,
apiKey: '1',
webhookKey: '1',
stripe: {
apiKey: '1',
webhookKey: '1',
},
},
}),
AppModule,

View File

@@ -637,6 +637,11 @@ export const USER_FRIENDLY_ERRORS = {
type: 'invalid_input',
message: 'Workspace id is required to update team subscription.',
},
managed_by_app_store_or_play: {
type: 'action_forbidden',
message:
'This subscription is managed by App Store or Google Play. Please manage it in the corresponding store.',
},
// Copilot errors
copilot_session_not_found: {

View File

@@ -651,6 +651,12 @@ export class WorkspaceIdRequiredToUpdateTeamSubscription extends UserFriendlyErr
}
}
export class ManagedByAppStoreOrPlay extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'managed_by_app_store_or_play', message);
}
}
export class CopilotSessionNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'copilot_session_not_found', message);
@@ -1189,6 +1195,7 @@ export enum ErrorNames {
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION,
WORKSPACE_ID_REQUIRED_FOR_TEAM_SUBSCRIPTION,
WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION,
MANAGED_BY_APP_STORE_OR_PLAY,
COPILOT_SESSION_NOT_FOUND,
COPILOT_SESSION_INVALID_INPUT,
COPILOT_SESSION_DELETED,

View File

@@ -8,6 +8,7 @@ import { Global, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import type { Request, Response } from 'express';
import { NodeEnv } from '../../env';
import { Config } from '../config';
import { mapAnyError } from '../nestjs/exception';
import { GQLLoggerPlugin } from './logger-plugin';
@@ -30,7 +31,7 @@ export type GraphqlContext = {
numberScalarMode: 'integer',
},
useGlobalPrefix: true,
playground: true,
graphiql: env.NODE_ENV === NodeEnv.Development,
sortSchema: true,
autoSchemaFile: join(
env.projectRoot,

View File

@@ -50,6 +50,13 @@ export class AccessTokenResolver {
return await this.models.accessToken.list(user.id);
}
@Query(() => [RevealedAccessToken])
async revealedAccessTokens(
@CurrentUser() user: CurrentUser
): Promise<RevealedAccessToken[]> {
return await this.models.accessToken.list(user.id, true);
}
@Mutation(() => RevealedAccessToken)
async generateUserAccessToken(
@CurrentUser() user: CurrentUser,

View File

@@ -8,6 +8,7 @@ export interface AuthConfig {
ttr: number;
};
allowSignup: boolean;
allowSignupForOauth: boolean;
requireEmailDomainVerification: boolean;
requireEmailVerification: boolean;
passwordRequirements: ConfigItem<{
@@ -27,6 +28,10 @@ defineModuleConfig('auth', {
desc: 'Whether allow new registrations.',
default: true,
},
allowSignupForOauth: {
desc: 'Whether allow new registrations via configured oauth.',
default: true,
},
requireEmailDomainVerification: {
desc: 'Whether require email domain record verification before accessing restricted resources.',
default: false,

View File

@@ -6,6 +6,7 @@ declare global {
interface AppConfigSchema {
mailer: {
SMTP: {
name: string;
host: string;
port: number;
username: string;
@@ -16,6 +17,7 @@ declare global {
fallbackDomains: ConfigItem<string[]>;
fallbackSMTP: {
name: string;
host: string;
port: number;
username: string;
@@ -28,6 +30,11 @@ declare global {
}
defineModuleConfig('mailer', {
'SMTP.name': {
desc: 'Name of the email server (e.g. your domain name)',
default: 'AFFiNE Server',
env: 'MAILER_SERVERNAME',
},
'SMTP.host': {
desc: 'Host of the email server (e.g. smtp.gmail.com)',
default: '',
@@ -64,6 +71,10 @@ defineModuleConfig('mailer', {
default: [],
shape: z.array(z.string()),
},
'fallbackSMTP.name': {
desc: 'Name of the fallback email server (e.g. your domain name)',
default: 'AFFiNE Server',
},
'fallbackSMTP.host': {
desc: 'Host of the email server (e.g. smtp.gmail.com)',
default: '',

Some files were not shown because too many files have changed in this diff Show More