Skip to content

Self-hosted tipping#671

Open
dkildar wants to merge 4 commits intodevelopfrom
feature/tipping
Open

Self-hosted tipping#671
dkildar wants to merge 4 commits intodevelopfrom
feature/tipping

Conversation

@dkildar
Copy link
Collaborator

@dkildar dkildar commented Feb 22, 2026

Summary by CodeRabbit

  • New Features

    • Tipping system: tip buttons on posts and sidebar, multi-step flow to pick amount and currency (HIVE, HBD, POINTS), wallet selection, QR code for external addresses, and preset/custom amounts.
    • Tip execution: sends transactions for supported assets.
  • Configuration

    • New tipping options for general and post-level enablement, button labels, and preset amounts.
  • Internationalization

    • Added localized tip-related strings and messages.

@dkildar dkildar requested a review from feruzm February 22, 2026 11:58
@dkildar dkildar self-assigned this Feb 22, 2026
# Conflicts:
#	apps/self-hosted/package.json
#	apps/self-hosted/src/core/i18n.ts
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Adds a new tipping feature to the self-hosted app: configuration schema, types, tip execution utilities, UI components (button, two-step popover, currency cards, QR), i18n keys, build/dependency updates, and integration points in blog sidebar and post footer.

Changes

Cohort / File(s) Summary
Config & Loader
apps/self-hosted/config.template.json, apps/self-hosted/src/core/configuration-loader.ts
Add features.tipping schema with general, post, and amounts; extend InstanceConfig typing for tipping.
Build & Shim
apps/self-hosted/package.json, apps/self-hosted/rsbuild.config.ts, apps/self-hosted/src/shim-bip39.ts
Add runtime deps (@floating-ui/*, qrcode), dev polyfill plugin; add bip39 resolution and shim to provide default + named exports; enable node polyfills in rsbuild.
Internationalization
apps/self-hosted/src/core/i18n.ts
Add multiple tip_* and cancel translation keys and expand TranslationKey union; formatting changes in locale blocks.
Auth / Broadcast
apps/self-hosted/src/features/auth/auth-actions.ts
Add exported BroadcastAuthorityType and extend broadcast(operations, options?) to accept authorityType (default Posting); forwards authority for keychain flows.
Tipping Types & Exports
apps/self-hosted/src/features/tipping/types.ts, apps/self-hosted/src/features/tipping/index.ts
Introduce TippingVariant/TippingAsset types, asset constants, guards (isTipableAsset, isExternalWalletAsset), and barrel exports (TipButton, useTippingConfig, types).
Tipping Config Hook
apps/self-hosted/src/features/tipping/hooks/use-tipping-config.ts
Add useTippingConfig(variant) hook returning enabled, buttonLabel, and presetAmounts with sensible defaults and memoization.
Tip Transaction Utility
apps/self-hosted/src/features/tipping/utils/tip-transaction.ts
Add executeTip to build and broadcast HIVE/HBD/POINTS operations using price lookup; exports related types/constants.
Tipping UI Components
apps/self-hosted/src/features/tipping/components/...
tip-button.tsx, tipping-popover.tsx, tipping-step-amount.tsx, tipping-step-currency.tsx, tipping-currency-card.tsx, tipping-currency-cards.tsx, tipping-wallet-qr.tsx
Add TipButton with floating-ui positioning, two-step popover (amount + currency), preset/custom amount UI, asset selection cards, wallet QR generation, queries for wallet data, and accessibility attributes.
Integration Points
apps/self-hosted/src/features/blog/layout/blog-sidebar.tsx, apps/self-hosted/src/features/blog/components/blog-post-footer.tsx, apps/self-hosted/src/features/floating-menu/config-fields.ts
Conditionally render TipButton in sidebar and post footer based on config; add tipping section to floating-menu config fields.

Sequence Diagram

sequenceDiagram
    actor User
    participant TipButton
    participant TippingPopover
    participant TippingStepAmount
    participant TippingStepCurrency
    participant executeTip
    participant broadcast
    participant Blockchain

    User->>TipButton: click
    TipButton->>TippingPopover: open (positioned via floating-ui)
    TippingPopover->>TippingStepAmount: show amount UI
    User->>TippingStepAmount: select/enter amount
    TippingStepAmount-->>TippingPopover: amount
    TippingPopover->>TippingStepCurrency: show currency UI
    User->>TippingStepCurrency: select asset (or external wallet)
    TippingStepCurrency-->>TippingPopover: asset, memo
    TippingPopover->>executeTip: executeTip(from,to,amount,asset,memo)
    executeTip->>executeTip: fetch asset price / compute real amount
    executeTip->>broadcast: build ops and broadcast(authorityType)
    broadcast->>Blockchain: submit tx
    Blockchain-->>broadcast: confirmation
    broadcast-->>executeTip: success
    executeTip-->>TippingPopover: success -> close
    TippingPopover-->>User: confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped to the code with a tiny request,
Buttons and QR codes all dressed in their best.
Coins hop and they tumble, a soft little clatter,
Send a small joy — may the networks all pitter-patter!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: adding tipping functionality to the self-hosted application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tipping

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/self-hosted/package.json (1)

14-65: ⚠️ Potential issue | 🟠 Major

Align dependency versions with web app and remove extraneous packages

Three dependencies have version mismatches with the web app that break architectural parity:

  • @floating-ui/dom: self-hosted has ^1.7.4, web app has ^1.6.13
  • @floating-ui/react-dom: self-hosted has ^2.1.6, web app has ^2.1.2
  • qrcode: self-hosted has ^1.5.4, web app has ^1.5.3

Additionally, @rsbuild/plugin-node-polyfill is present in self-hosted but absent from the web app, and should be removed unless there is a specific architectural reason for it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/package.json` around lines 14 - 65, Update the dependency
versions in package.json to match the web app: change "@floating-ui/dom" to
"^1.6.13", "@floating-ui/react-dom" to "^2.1.2", and "qrcode" to "^1.5.3"; also
remove the devDependency "@rsbuild/plugin-node-polyfill" unless you have a
documented reason to keep it so the self-hosted app remains architecturally
aligned with the web app.
apps/self-hosted/src/features/auth/auth-actions.ts (1)

119-133: ⚠️ Potential issue | 🟠 Major

authorityType silently ignored for HiveAuth and Hivesigner — active-key tips will fail with opaque errors.

Tipping with HIVE or HBD requires active authority (ASSETS_REQUIRING_KEY in types.ts). When broadcast() is called with authorityType: "Active" and the user is logged in via hiveauth or hivesigner, the authority type is silently dropped. The downstream blockchain error ("missing required active authority") will surface as a raw chain error rather than a user-friendly message.

Consider throwing an early, actionable error for these auth methods when an active-key operation is attempted:

🛡️ Proposed guard
  const authorityType = options?.authorityType ?? "Posting";

  switch (user.loginType) {
    case "keychain":
      return keychainBroadcast(user.username, operations, authorityType);

    case "hivesigner":
      if (!user.accessToken) {
        throw new Error("No access token available");
      }
+     if (authorityType === "Active") {
+       // Hivesigner token scope was set at login; active-key ops succeed only
+       // if the token was granted active scope. No early validation is possible here,
+       // but callers should be aware of this limitation.
+     }
      return broadcastWithHivesigner(user.accessToken, operations);

    case "hiveauth":
      if (!session) {
        throw new Error("No HiveAuth session available");
      }
+     if (authorityType === "Active") {
+       throw new Error(
+         "HiveAuth sessions use posting authority. Switch to Keychain for transfers."
+       );
+     }
      return broadcastWithHiveAuth(session, operations);

At a minimum, document the limitation for each non-keychain path so callers can gate the tipping UI accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/auth/auth-actions.ts` around lines 119 - 133,
The switch in broadcast (auth-actions.ts) drops authorityType for non-keychain
logins; update the branches that call broadcastWithHivesigner and
broadcastWithHiveAuth to validate authorityType and throw a clear, actionable
error when authorityType === "Active" (or ASSETS_REQUIRING_KEY) instead of
proceeding (so callers get a user-friendly message rather than a raw chain
error). Specifically, inside the "hivesigner" branch (before calling
broadcastWithHivesigner) and the "hiveauth" branch (before calling
broadcastWithHiveAuth) check the passed authorityType/session and throw a
descriptive Error like "Active key required for this operation with
hivesigner/hiveauth" if an active-level operation is attempted; also add a short
comment documenting the limitation so callers can gate tipping UI accordingly.
🧹 Nitpick comments (9)
apps/self-hosted/package.json (1)

14-15: Use @floating-ui/react for cleaner interaction handling instead of manual state management

The current implementation manually handles interactions (click-to-open via useState and outside-click dismiss via mousedown event listeners). While this approach works, @floating-ui/react provides dedicated interaction hooks (useClick, useDismiss, useInteractions) that would simplify this logic and reduce boilerplate in tip-button.tsx and tipping-popover.tsx.

Consider replacing @floating-ui/react-dom with @floating-ui/react to leverage these built-in interaction primitives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/package.json` around lines 14 - 15, Replace the manual
open-state and mousedown outside-click logic in tip-button.tsx and
tipping-popover.tsx with Floating UI's interaction hooks: change the dependency
from "@floating-ui/react-dom" to "@floating-ui/react" in package.json, import
useFloating plus useClick, useDismiss and useInteractions from
'@floating-ui/react', and wire them into the components (useClick to toggle
open, useDismiss to handle outside clicks/escape, useInteractions to combine
handlers). Remove the manual useState-based toggle and the document 'mousedown'
listener/cleanup in tip-button.tsx and ensure the floating context/props from
useFloating are passed through to tipping-popover.tsx so the built-in dismissal
and click behavior replace the existing custom logic.
apps/self-hosted/rsbuild.config.ts (2)

20-22: Duplicate bip39 alias entries across both resolution layers.

bip39 and bip39-original are defined identically in both resolve.alias (rsbuild-level, lines 21–22) and tools.rspack.resolve.alias (rspack-level, lines 34–35). rsbuild merges resolve.alias into the underlying rspack config, so the entries in tools.rspack.resolve.alias are redundant. The existing React aliases were only in the rspack layer, so this introduces an inconsistent pattern. Consider placing bip39 aliases in one layer only, consistent with where the React aliases live.

♻️ Proposed cleanup
   resolve: {
     alias: {
       '@': './src',
-      // bip39 has only named exports; wallets uses default import — shim provides default
-      'bip39': path.resolve(__dirname, 'src/shim-bip39.ts'),
-      'bip39-original': bip39Resolved,
     },
   },
   tools: {
     rspack: {
       resolve: {
         alias: {
           react: path.resolve(__dirname, 'node_modules/react'),
           'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
           '@ecency/ui': path.resolve(__dirname, '../../packages/ui/dist/index.js'),
+          // bip39 has only named exports; wallets uses default import — shim provides default
           'bip39': path.resolve(__dirname, 'src/shim-bip39.ts'),
           'bip39-original': bip39Resolved,
         },
       },

Also applies to: 34-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/rsbuild.config.ts` around lines 20 - 22, The bip39 aliases
are duplicated between resolve.alias and tools.rspack.resolve.alias (the keys
'bip39' and 'bip39-original'); remove the redundant pair from one layer so
aliases live only in the same layer as the React aliases for consistency—either
keep them in resolve.alias (rsbuild-level) and delete them from
tools.rspack.resolve.alias, or vice versa; update only the entries named 'bip39'
and 'bip39-original' and ensure the shim path (src/shim-bip39.ts) and
bip39Resolved symbol remain referenced in the surviving alias.

16-16: Consider scoping pluginNodePolyfill to only the modules bip39 actually needs.

The plugin supports an include option to specify which modules should be polyfilled; if set, only those modules are injected. Without it, the plugin polyfills a large set of built-ins: assert, buffer, crypto, http, https, stream, os, path, and many more. bip39 only requires crypto and buffer, so the current unconstrained call inflates the bundle with unnecessary polyfills.

♻️ Proposed scoping
-  plugins: [pluginReact(), pluginNodePolyfill()],
+  plugins: [pluginReact(), pluginNodePolyfill({ include: ['buffer', 'crypto'] })],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/rsbuild.config.ts` at line 16, The pluginNodePolyfill
invocation in the plugins array is currently unscoped and injects many
unnecessary polyfills; update the plugins entry that references
pluginNodePolyfill (in the plugins array) to pass an include option limited to
only the built-ins bip39 needs (e.g., "crypto" and "buffer") so only those
modules are polyfilled and the bundle size is reduced.
apps/self-hosted/src/shim-bip39.ts (1)

8-13: Consider export * from 'bip39-original' for maintainability.

The shim currently re-exports 6 of bip39's 9 public APIs; it omits wordlists, setDefaultWordlist, and getDefaultWordlist. While these are not imported anywhere in the current codebase, using export * from 'bip39-original' would automatically forward the full API surface and reduce maintenance burden if usage patterns expand in the future.

🔄 Suggested refactor
 import * as bip39 from 'bip39-original';
 
 export default bip39;
-export const generateMnemonic = bip39.generateMnemonic;
-export const mnemonicToSeedSync = bip39.mnemonicToSeedSync;
-export const mnemonicToSeed = bip39.mnemonicToSeed;
-export const entropyToMnemonic = bip39.entropyToMnemonic;
-export const mnemonicToEntropy = bip39.mnemonicToEntropy;
-export const validateMnemonic = bip39.validateMnemonic;
+export * from 'bip39-original';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/shim-bip39.ts` around lines 8 - 13, Replace the
five/fewer explicit re-exports (generateMnemonic, mnemonicToSeedSync,
mnemonicToSeed, entropyToMnemonic, mnemonicToEntropy, validateMnemonic) with a
single module re-export so the shim forwards the entire bip39-original public
API (including wordlists, setDefaultWordlist, getDefaultWordlist); update the
export statement to use a wildcard export from 'bip39-original' so future
additions to that module are automatically exposed without changing this file.
apps/self-hosted/src/features/tipping/types.ts (1)

3-9: TippingAsset and TIPABLE_ASSETS can silently diverge — derive one from the other.

Both the union type and the runtime array list the same three values. Adding a fourth asset to the type without updating the array (or vice-versa) will cause isTipableAsset to behave incorrectly at runtime with no compile-time error.

♻️ Proposed refactor — single source of truth
-export type TippingAsset = "HIVE" | "HBD" | "POINTS";
-
-const TIPABLE_ASSETS: TippingAsset[] = ["HIVE", "HBD", "POINTS"];
+const TIPABLE_ASSETS = ["HIVE", "HBD", "POINTS"] as const;
+export type TippingAsset = (typeof TIPABLE_ASSETS)[number];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/types.ts` around lines 3 - 9, Replace
the duplicated runtime and type definitions by making TIPABLE_ASSETS the single
source of truth (declare const TIPABLE_ASSETS = ["HIVE","HBD","POINTS"] as
const) and derive the TippingAsset union from it (type TippingAsset = typeof
TIPABLE_ASSETS[number]); then update isTipableAsset to use
TIPABLE_ASSETS.includes(asset as TippingAsset) exactly as before (keeping the
type predicate signature isTipableAsset(asset: string): asset is TippingAsset)
so compile-time and runtime values cannot diverge; ensure you remove the old
explicit union declaration and export the derived types/names unchanged.
apps/self-hosted/src/features/tipping/components/tipping-step-amount.tsx (1)

21-30: Duplicate values in presetAmounts produce duplicate React keys and identical buttons.

If an operator configures [1, 1, 5], React will warn about duplicate keys and two $1 buttons appear. A simple deduplicate or a runtime guard would prevent this:

🛡️ Defensive deduplicate
-{presetAmounts.map((val) => (
+{[...new Set(presetAmounts)].map((val) => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/components/tipping-step-amount.tsx`
around lines 21 - 30, presetAmounts can contain duplicates which leads to
duplicate React keys and identical buttons; before rendering in
tipping-step-amount.tsx, produce a deduplicated list (e.g., derive uniqueAmounts
from presetAmounts using a Set or filter by indexOf) and map over uniqueAmounts
when creating the <button> elements (referencing presetAmounts, uniqueAmounts,
onSelect and the button key) so each rendered button has a unique key and
duplicate values are not shown.
apps/self-hosted/src/features/tipping/components/tip-button.tsx (1)

3-4: Consolidate Floating UI imports to a single package.

autoUpdate and offset are imported from @floating-ui/dom, while flip, shift, and useFloating come from @floating-ui/react-dom. The official Floating UI docs show importing autoUpdate from @floating-ui/react (or @floating-ui/react-dom) when using it with useFloating's whileElementsMounted option. offset is also importable directly from @floating-ui/react-dom.

♻️ Proposed consolidation
-import { autoUpdate, offset } from "@floating-ui/dom";
-import { flip, shift, useFloating } from "@floating-ui/react-dom";
+import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react-dom";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/components/tip-button.tsx` around lines
3 - 4, Consolidate the Floating UI imports by replacing the split imports with a
single import from "@floating-ui/react-dom" that includes autoUpdate, offset,
flip, shift, and useFloating so that the whileElementsMounted option receives
the correct autoUpdate implementation; update the import statement to import all
those symbols (autoUpdate, offset, flip, shift, useFloating) from
"@floating-ui/react-dom".
apps/self-hosted/src/features/tipping/hooks/use-tipping-config.ts (1)

9-22: InstanceConfigManager.getConfig() result is absent from useMemo deps — config changes won't trigger recomputation.

Since InstanceConfigManager.getConfig() is called inside the memo but isn't listed as a dependency, any runtime config update won't cause the hook to recompute. If InstanceConfigManager is truly a static, load-once store this is intentional. If it can update at runtime (e.g., hot-reload or dynamic config), the stale value would silently persist until variant changes.

Additionally, DEFAULT_PRESETS (and potentially tipping.amounts) is returned by reference. If a consumer mutates the returned presetAmounts, the shared constant would be affected. Consider returning a copy:

♻️ Proposed defensive copy
-    const presetAmounts = Array.isArray(tipping.amounts) && tipping.amounts.length > 0
-      ? tipping.amounts
-      : DEFAULT_PRESETS;
+    const presetAmounts = Array.isArray(tipping.amounts) && tipping.amounts.length > 0
+      ? [...tipping.amounts]
+      : [...DEFAULT_PRESETS];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/hooks/use-tipping-config.ts` around
lines 9 - 22, The hook calls InstanceConfigManager.getConfig() inside useMemo
but doesn’t include the config in the dependency array and also returns preset
arrays by reference; fix by reading and including the live config value (result
of InstanceConfigManager.getConfig()) in the useMemo deps (or compute config
before useMemo and include that variable in deps) so changes retrigger
recomputation, and ensure returned presetAmounts and DEFAULT_PRESETS are
defensive copies (e.g., copy tipping.amounts or DEFAULT_PRESETS before
returning) to avoid exposing shared mutable arrays; update references to
InstanceConfigManager.getConfig, useMemo, DEFAULT_PRESETS, tipping.amounts and
variant accordingly.
apps/self-hosted/src/features/tipping/components/tipping-popover.tsx (1)

99-106: Redundant unsupported-asset check in handleSubmit.

By the time execution reaches line 99, canSubmit (which requires !isExternalAsset) has already passed. The only scenario this block could fire is if an arbitrary non-TippingAsset string is stored in selectedAsset — which is only possible because of the string | undefined type. Once selectedAsset is narrowed to TippingAsset | undefined (see comment above), this guard becomes dead code and can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/components/tipping-popover.tsx` around
lines 99 - 106, Remove the redundant asset-check in handleSubmit: since
canSubmit already ensures !isExternalAsset and selectedAsset should be narrowed
to TippingAsset | undefined, delete the block that checks selectedAsset against
"HIVE"/"HBD"/"POINTS" and the setError("This asset is not supported for tipping
yet") return; instead rely on the existing canSubmit/isExternalAsset narrowing
(and fix the selectedAsset type to TippingAsset | undefined where noted) so
handleSubmit no longer contains dead-code guarding unsupported assets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/self-hosted/src/core/configuration-loader.ts`:
- Around line 71-76: The top-level optional property tipping.enabled is an
orphan and should be removed: delete the enabled?: boolean from the tipping type
in configuration-loader.ts so the shape only contains general, post and amounts
(with general.enabled and post.enabled as the feature toggles); update the
corresponding schema/template (config.template.json) and the config-fields.ts to
remove references to tipping.enabled so runtime validation and UI fields align
with the type.

In `@apps/self-hosted/src/features/tipping/components/tipping-currency-card.tsx`:
- Around line 18-20: The useQuery call that passes
getAccountWalletAssetInfoQueryOptions(username, asset) is firing when username
may be an empty string; update the useQuery invocation in tipping-currency-card
(where getAccountWalletAssetInfoQueryOptions is used) to include an enabled:
!!username option so the query only runs when useInstanceConfig().username is
truthy; ensure you keep the same query options object shape and only add the
enabled guard to prevent API calls with an invalid/empty username.
- Around line 22-34: Remove the redundant key prop on the button (it's a no-op
here because the parent TippingCurrencyCards already assigns key={asset}) and
update the button label to fall back to the raw asset symbol when data?.title is
undefined; specifically, change the button rendering in the component that uses
selectedAsset, onAssetSelect and data?.title to display data?.title || asset so
the button isn't blank while the query is loading.

In `@apps/self-hosted/src/features/tipping/components/tipping-currency-cards.tsx`:
- Around line 19-27: The query for walletList fires even when username is empty;
update the useQuery call that uses getAccountWalletListQueryOptions(username,
"usd") so it includes an enabled: !!username guard (same pattern used in
BlogSidebarContent and TippingCurrencyCard) to prevent running the request with
an invalid identifier; modify the options passed into useQuery (or merge into
getAccountWalletListQueryOptions result) so the query is only enabled when
username is truthy.

In `@apps/self-hosted/src/features/tipping/components/tipping-popover.tsx`:
- Around line 126-133: The dialog element in tipping-popover (the div using
refs.setFloating with role="dialog") needs aria-modal="true" and proper focus
management; add aria-modal="true" to that div and implement focus trapping: on
open move focus into the dialog (e.g., focus the first tabbable element or a
dialog container) and on close return focus to the trigger element (the element
that called refs.setFloating), or use a focus-trap utility/hook; ensure focus is
restored on unmount and keyboard/escape handling still closes the popover.
- Around line 43-45: Change the selectedAsset state to use the TippingAsset type
(useState<TippingAsset | undefined>) instead of string so consumers get correct
typing; update the selectedAsset declaration (selectedAsset, setSelectedAsset)
to use that generic type, remove the unsafe cast "selectedAsset as TippingAsset"
where it's used (e.g. in the submit flow), and delete the now-unnecessary
explicit `"HIVE" | "HBD" | "POINTS"` runtime guard inside handleSubmit since the
typed state makes it unreachable.

In `@apps/self-hosted/src/features/tipping/components/tipping-step-amount.tsx`:
- Around line 28-29: The preset amount buttons in the TippingStepAmount
component render a hardcoded "$" (the `${val}` button text) which can mislead
users into thinking the payment is in USD; update the button label to explicitly
indicate these are USD equivalents (e.g. change the visible text from `$${val}`
to `$${val} USD` or `$${val} USD equivalent`) and also update the button's
aria-label/tooltip in the same component to mention conversion to HIVE/HBD so
screen readers and hover help are accurate (adjust any occurrences of `val`
rendering in TippingStepAmount and related tooltip/aria props accordingly).

In `@apps/self-hosted/src/features/tipping/components/tipping-step-currency.tsx`:
- Around line 117-118: The button currently renders both messages because the
ternary is evaluated after the &&, so when user?.username is falsy you still hit
the ternary false branch; fix the JSX in the tipping-step-currency component by
making the intent explicit: replace the two separate expressions with a single
conditional that first checks user?.username and returns t("tip_login_to_send")
when missing, otherwise returns loading ? t("tip_sending") : t("tip_send");
locate the fragment using user and loading in tipping-step-currency.tsx and
update that expression (or add parentheses to enforce (user?.username ? (loading
? ... : ...) : ...)).

In `@apps/self-hosted/src/features/tipping/components/tipping-wallet-qr.tsx`:
- Around line 30-32: In the QR generation .catch handler in
tipping-wallet-qr.tsx replace the use of the raw library error message with the
translated user-facing string: always call setError(t("tip_qr_failed")) (instead
of setError(err instanceof Error ? err.message : t("tip_qr_failed"))), so the
error shown to users comes from t("tip_qr_failed") regardless of the thrown err
in the .catch block that follows the qrcode call.
- Around line 50-56: Replace the hardcoded English alt text in the <img> inside
the tipping-wallet-qr component with a localized string; use the existing i18n
key "tip_wallet_address" (or add a new "tip_qr_alt" if preferred) and pass the
translated value as the alt prop on the <img> that renders dataUrl (refer to the
component/rendering of the <img> with props dataUrl, size, className) so the QR
image uses the locale-specific alt text.
- Around line 21-33: The effect that calls QRCode.toDataURL(address, { width:
size, margin: 2 }) can race: add a cancellation guard inside the useEffect to
ignore results from stale promises — e.g., create a local let cancelled = false
(or an incrementing requestId) at the top of the effect, check cancelled before
calling setDataUrl/setError in the promise resolution/rejection, and set
cancelled = true in the effect cleanup; ensure you still handle the
empty-address branch and clear data/error as currently done so only the latest
address/size updates the state via setDataUrl and setError.

In `@apps/self-hosted/src/features/tipping/utils/tip-transaction.ts`:
- Line 29: The variable formatted (const formatted = num.toFixed(3)) is unused
and realAmount is being embedded as a full-precision float into the Hive
transfer amount strings; replace those interpolations to use a 3-decimal string
(realAmount.toFixed(3)) for all Hive amount constructions, and either remove
formatted or repurpose it for the POINTS path once that conversion is fixed;
update the three places where amounts are built (the strings that currently
interpolate realAmount) to use realAmount.toFixed(3) and eliminate the dead
formatted variable if not reused.
- Line 7: ASSETS_WITH_USD_PRICE should be exported and used as a guard so USD
price conversion only runs for assets that need it: change the declaration to
export const ASSETS_WITH_USD_PRICE = ["HIVE", "HBD"]; then wrap the
ensureQueryData(...) call and the amount conversion (currently doing num /
price) in a conditional that checks if the asset is included in
ASSETS_WITH_USD_PRICE (e.g., if (!ASSETS_WITH_USD_PRICE.includes(asset)) skip
fetching prices and leave the numeric amount unchanged); update ensureQueryData
usage and the division logic inside the function handling tip conversion so
POINTS bypasses price lookup and conversion.
- Around line 16-20: Update the JSDoc for the tip execution block to reference
the correct helper: replace the non-existent getTokenPriceQueryOptions with
getAccountWalletAssetInfoQueryOptions so the comment matches the actual call in
this file (referencing getAccountWalletAssetInfoQueryOptions and the tip
execution behavior that fetches token price/info for conversion).
- Line 37: The division uses "realAmount = num / (info?.price ?? 0)" which can
divide by zero; before performing this division check that info exists and
info.price is a positive non-zero number (e.g., info?.price > 0). If the price
is missing or zero, handle it explicitly: throw or return a meaningful error,
log and abort the transfer, or set realAmount to a safe default and skip
creating the transfer string. Update the code around the realAmount calculation
(the realAmount assignment and the subsequent transfer amount string
construction) to validate info?.price and implement the chosen error/abort path
to avoid producing "Infinity HIVE".

---

Outside diff comments:
In `@apps/self-hosted/package.json`:
- Around line 14-65: Update the dependency versions in package.json to match the
web app: change "@floating-ui/dom" to "^1.6.13", "@floating-ui/react-dom" to
"^2.1.2", and "qrcode" to "^1.5.3"; also remove the devDependency
"@rsbuild/plugin-node-polyfill" unless you have a documented reason to keep it
so the self-hosted app remains architecturally aligned with the web app.

In `@apps/self-hosted/src/features/auth/auth-actions.ts`:
- Around line 119-133: The switch in broadcast (auth-actions.ts) drops
authorityType for non-keychain logins; update the branches that call
broadcastWithHivesigner and broadcastWithHiveAuth to validate authorityType and
throw a clear, actionable error when authorityType === "Active" (or
ASSETS_REQUIRING_KEY) instead of proceeding (so callers get a user-friendly
message rather than a raw chain error). Specifically, inside the "hivesigner"
branch (before calling broadcastWithHivesigner) and the "hiveauth" branch
(before calling broadcastWithHiveAuth) check the passed authorityType/session
and throw a descriptive Error like "Active key required for this operation with
hivesigner/hiveauth" if an active-level operation is attempted; also add a short
comment documenting the limitation so callers can gate tipping UI accordingly.

---

Nitpick comments:
In `@apps/self-hosted/package.json`:
- Around line 14-15: Replace the manual open-state and mousedown outside-click
logic in tip-button.tsx and tipping-popover.tsx with Floating UI's interaction
hooks: change the dependency from "@floating-ui/react-dom" to
"@floating-ui/react" in package.json, import useFloating plus useClick,
useDismiss and useInteractions from '@floating-ui/react', and wire them into the
components (useClick to toggle open, useDismiss to handle outside clicks/escape,
useInteractions to combine handlers). Remove the manual useState-based toggle
and the document 'mousedown' listener/cleanup in tip-button.tsx and ensure the
floating context/props from useFloating are passed through to
tipping-popover.tsx so the built-in dismissal and click behavior replace the
existing custom logic.

In `@apps/self-hosted/rsbuild.config.ts`:
- Around line 20-22: The bip39 aliases are duplicated between resolve.alias and
tools.rspack.resolve.alias (the keys 'bip39' and 'bip39-original'); remove the
redundant pair from one layer so aliases live only in the same layer as the
React aliases for consistency—either keep them in resolve.alias (rsbuild-level)
and delete them from tools.rspack.resolve.alias, or vice versa; update only the
entries named 'bip39' and 'bip39-original' and ensure the shim path
(src/shim-bip39.ts) and bip39Resolved symbol remain referenced in the surviving
alias.
- Line 16: The pluginNodePolyfill invocation in the plugins array is currently
unscoped and injects many unnecessary polyfills; update the plugins entry that
references pluginNodePolyfill (in the plugins array) to pass an include option
limited to only the built-ins bip39 needs (e.g., "crypto" and "buffer") so only
those modules are polyfilled and the bundle size is reduced.

In `@apps/self-hosted/src/features/tipping/components/tip-button.tsx`:
- Around line 3-4: Consolidate the Floating UI imports by replacing the split
imports with a single import from "@floating-ui/react-dom" that includes
autoUpdate, offset, flip, shift, and useFloating so that the
whileElementsMounted option receives the correct autoUpdate implementation;
update the import statement to import all those symbols (autoUpdate, offset,
flip, shift, useFloating) from "@floating-ui/react-dom".

In `@apps/self-hosted/src/features/tipping/components/tipping-popover.tsx`:
- Around line 99-106: Remove the redundant asset-check in handleSubmit: since
canSubmit already ensures !isExternalAsset and selectedAsset should be narrowed
to TippingAsset | undefined, delete the block that checks selectedAsset against
"HIVE"/"HBD"/"POINTS" and the setError("This asset is not supported for tipping
yet") return; instead rely on the existing canSubmit/isExternalAsset narrowing
(and fix the selectedAsset type to TippingAsset | undefined where noted) so
handleSubmit no longer contains dead-code guarding unsupported assets.

In `@apps/self-hosted/src/features/tipping/components/tipping-step-amount.tsx`:
- Around line 21-30: presetAmounts can contain duplicates which leads to
duplicate React keys and identical buttons; before rendering in
tipping-step-amount.tsx, produce a deduplicated list (e.g., derive uniqueAmounts
from presetAmounts using a Set or filter by indexOf) and map over uniqueAmounts
when creating the <button> elements (referencing presetAmounts, uniqueAmounts,
onSelect and the button key) so each rendered button has a unique key and
duplicate values are not shown.

In `@apps/self-hosted/src/features/tipping/hooks/use-tipping-config.ts`:
- Around line 9-22: The hook calls InstanceConfigManager.getConfig() inside
useMemo but doesn’t include the config in the dependency array and also returns
preset arrays by reference; fix by reading and including the live config value
(result of InstanceConfigManager.getConfig()) in the useMemo deps (or compute
config before useMemo and include that variable in deps) so changes retrigger
recomputation, and ensure returned presetAmounts and DEFAULT_PRESETS are
defensive copies (e.g., copy tipping.amounts or DEFAULT_PRESETS before
returning) to avoid exposing shared mutable arrays; update references to
InstanceConfigManager.getConfig, useMemo, DEFAULT_PRESETS, tipping.amounts and
variant accordingly.

In `@apps/self-hosted/src/features/tipping/types.ts`:
- Around line 3-9: Replace the duplicated runtime and type definitions by making
TIPABLE_ASSETS the single source of truth (declare const TIPABLE_ASSETS =
["HIVE","HBD","POINTS"] as const) and derive the TippingAsset union from it
(type TippingAsset = typeof TIPABLE_ASSETS[number]); then update isTipableAsset
to use TIPABLE_ASSETS.includes(asset as TippingAsset) exactly as before (keeping
the type predicate signature isTipableAsset(asset: string): asset is
TippingAsset) so compile-time and runtime values cannot diverge; ensure you
remove the old explicit union declaration and export the derived types/names
unchanged.

In `@apps/self-hosted/src/shim-bip39.ts`:
- Around line 8-13: Replace the five/fewer explicit re-exports
(generateMnemonic, mnemonicToSeedSync, mnemonicToSeed, entropyToMnemonic,
mnemonicToEntropy, validateMnemonic) with a single module re-export so the shim
forwards the entire bip39-original public API (including wordlists,
setDefaultWordlist, getDefaultWordlist); update the export statement to use a
wildcard export from 'bip39-original' so future additions to that module are
automatically exposed without changing this file.

Comment on lines +71 to +76
tipping?: {
enabled?: boolean;
general?: { enabled: boolean; buttonLabel?: string };
post?: { enabled: boolean; buttonLabel?: string };
amounts?: number[];
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Top-level tipping.enabled is a schema orphan.

The enabled?: boolean at the tipping root level is absent from both config.template.json and config-fields.ts. The per-variant general.enabled and post.enabled are the actual feature toggles, making the top-level field redundant and potentially confusing to anyone reading the type.

🔧 Suggested cleanup
 tipping?: {
-  enabled?: boolean;
   general?: { enabled: boolean; buttonLabel?: string };
   post?: { enabled: boolean; buttonLabel?: string };
   amounts?: number[];
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tipping?: {
enabled?: boolean;
general?: { enabled: boolean; buttonLabel?: string };
post?: { enabled: boolean; buttonLabel?: string };
amounts?: number[];
};
tipping?: {
general?: { enabled: boolean; buttonLabel?: string };
post?: { enabled: boolean; buttonLabel?: string };
amounts?: number[];
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/core/configuration-loader.ts` around lines 71 - 76, The
top-level optional property tipping.enabled is an orphan and should be removed:
delete the enabled?: boolean from the tipping type in configuration-loader.ts so
the shape only contains general, post and amounts (with general.enabled and
post.enabled as the feature toggles); update the corresponding schema/template
(config.template.json) and the config-fields.ts to remove references to
tipping.enabled so runtime validation and UI fields align with the type.

Comment on lines +22 to +34
<button
key={asset}
type="button"
className={clsx(
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm",
selectedAsset === asset
? "border-theme-accent bg-theme-tertiary text-theme-primary"
: "border-theme bg-theme-primary text-theme-primary hover:bg-theme-tertiary",
)}
onClick={() => onAssetSelect(asset)}
>
{data?.title}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Two minor issues: redundant key prop and empty loading state on the button.

  1. key={asset} on Line 23 is a no-op — key only has meaning when the element is placed directly in an array render (e.g. via .map()). The parent TippingCurrencyCards already applies key={asset} correctly at the call site.

  2. Line 33 renders {data?.title} which is undefined while the query is loading, resulting in a blank button. Consider showing the raw asset symbol as a fallback:

♻️ Proposed fix
  return (
    <button
-     key={asset}
      type="button"
      ...
    >
-     {data?.title}
+     {data?.title ?? asset}
    </button>
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/components/tipping-currency-card.tsx`
around lines 22 - 34, Remove the redundant key prop on the button (it's a no-op
here because the parent TippingCurrencyCards already assigns key={asset}) and
update the button label to fall back to the raw asset symbol when data?.title is
undefined; specifically, change the button rendering in the component that uses
selectedAsset, onAssetSelect and data?.title to display data?.title || asset so
the button isn't blank while the query is loading.

import type { Operation } from "@hiveio/dhive";
import type { TippingAsset } from "../types";

const ASSETS_WITH_USD_PRICE: TippingAsset[] = ["HIVE", "HBD"];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ASSETS_WITH_USD_PRICE is defined but never used as a guard — POINTS incorrectly goes through USD price conversion.

ASSETS_WITH_USD_PRICE = ["HIVE", "HBD"] clearly signals that POINTS should bypass USD-based price conversion, yet the ensureQueryData call and the num / price division at line 37 execute unconditionally for every asset, including POINTS. This means a user entering "100 POINTS" would send 100 / pointsUsdPrice points instead of 100.

The constant should be used as a branch condition, and the price-fetch block should only run for ASSETS_WITH_USD_PRICE assets. The constant also needs export to match its declared public role (the AI-generated summary lists it as an export, and the tipping UI will need it for display logic).

♻️ Proposed restructure
-const ASSETS_WITH_USD_PRICE: TippingAsset[] = ["HIVE", "HBD"];
+export const ASSETS_WITH_USD_PRICE: TippingAsset[] = ["HIVE", "HBD"];

  // ...inside executeTip...
- const queryClient = getQueryClient();
- const info = await queryClient.ensureQueryData(
-   getAccountWalletAssetInfoQueryOptions(to, asset),
- );
- realAmount = num / (info?.price ?? 0);

+ if ((ASSETS_WITH_USD_PRICE as TippingAsset[]).includes(asset)) {
+   const queryClient = getQueryClient();
+   const info = await queryClient.ensureQueryData(
+     getAccountWalletAssetInfoQueryOptions(to, asset),
+   );
+   const price = info?.price;
+   if (!price || price <= 0) {
+     throw new Error("Asset price unavailable");
+   }
+   realAmount = num / price;
+ } else {
+   realAmount = num; // POINTS: user enters the POINTS quantity directly
+ }

Also applies to: 34-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/utils/tip-transaction.ts` at line 7,
ASSETS_WITH_USD_PRICE should be exported and used as a guard so USD price
conversion only runs for assets that need it: change the declaration to export
const ASSETS_WITH_USD_PRICE = ["HIVE", "HBD"]; then wrap the
ensureQueryData(...) call and the amount conversion (currently doing num /
price) in a conditional that checks if the asset is included in
ASSETS_WITH_USD_PRICE (e.g., if (!ASSETS_WITH_USD_PRICE.includes(asset)) skip
fetching prices and leave the numeric amount unchanged); update ensureQueryData
usage and the division logic inside the function handling tip conversion so
POINTS bypasses price lookup and conversion.

if (!Number.isFinite(num) || num <= 0) {
throw new Error("Invalid amount");
}
const formatted = num.toFixed(3);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

formatted is dead code and realAmount is embedded as a raw float — Hive rejects non-3-decimal amounts.

const formatted = num.toFixed(3) (line 29) is never used anywhere. Meanwhile, the blockchain amount strings on lines 48, 60, and 74 interpolate the raw realAmount float (e.g., "1.4285714285714286 HIVE"). The Hive transfer operation requires the amount "to be a positive value with 3 decimals in order for the transaction to be completed." A full-precision float will be rejected by the node.

realAmount.toFixed(3) must be used in all three amount strings. The formatted variable should either be repurposed for the POINTS direct-input path (once the POINTS price-conversion issue below is resolved) or removed.

🐛 Proposed fix
- const formatted = num.toFixed(3);
  // ... price fetch ...
  if (asset === "HIVE") {
    operations = [[
      "transfer",
      {
        from, to,
-       amount: `${realAmount} HIVE`,
+       amount: `${realAmount.toFixed(3)} HIVE`,
        memo,
      },
    ]];
  } else if (asset === "HBD") {
    operations = [[
      "transfer",
      {
        from, to,
-       amount: `${realAmount} HBD`,
+       amount: `${realAmount.toFixed(3)} HBD`,
        memo,
      },
    ]];
  } else if (asset === "POINTS") {
    operations = [[
      "custom_json",
      {
        // ...
-       amount: `${realAmount} POINT`,
+       amount: `${num.toFixed(3)} POINT`,  // POINTS: use input directly (see ASSETS_WITH_USD_PRICE note)
        memo,
      },
    ]];
  }

Also applies to: 48-48, 60-60, 74-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/utils/tip-transaction.ts` at line 29,
The variable formatted (const formatted = num.toFixed(3)) is unused and
realAmount is being embedded as a full-precision float into the Hive transfer
amount strings; replace those interpolations to use a 3-decimal string
(realAmount.toFixed(3)) for all Hive amount constructions, and either remove
formatted or repurpose it for the POINTS path once that conversion is fixed;
update the three places where amounts are built (the strings that currently
interpolate realAmount) to use realAmount.toFixed(3) and eliminate the dead
formatted variable if not reused.

const info = await queryClient.ensureQueryData(
getAccountWalletAssetInfoQueryOptions(to, asset),
);
realAmount = num / (info?.price ?? 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Division by zero when price is unavailable produces Infinity as the transfer amount.

info?.price ?? 0 resolves to 0 whenever the query returns no data or a zero price, making realAmount = Infinity. This propagates directly into the blockchain operation amount string (e.g., "Infinity HIVE"), which will either be rejected by the node or exhibit undefined behaviour. A hard guard is required before dividing.

🐛 Proposed fix
- realAmount = num / (info?.price ?? 0);
+ const price = info?.price;
+ if (!price || price <= 0) {
+   throw new Error("Asset price unavailable");
+ }
+ realAmount = num / price;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
realAmount = num / (info?.price ?? 0);
const price = info?.price;
if (!price || price <= 0) {
throw new Error("Asset price unavailable");
}
realAmount = num / price;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/tipping/utils/tip-transaction.ts` at line 37,
The division uses "realAmount = num / (info?.price ?? 0)" which can divide by
zero; before performing this division check that info exists and info.price is a
positive non-zero number (e.g., info?.price > 0). If the price is missing or
zero, handle it explicitly: throw or return a meaningful error, log and abort
the transfer, or set realAmount to a safe default and skip creating the transfer
string. Update the code around the realAmount calculation (the realAmount
assignment and the subsequent transfer amount string construction) to validate
info?.price and implement the chosen error/abort path to avoid producing
"Infinity HIVE".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/self-hosted/src/core/i18n.ts (1)

158-218: ⚠️ Potential issue | 🔴 Critical

TypeScript compilation error: es, de, ko, ru locales are missing all 14 new translation keys.

Translations is Record<TranslationKey, string>, so every locale object must include every key in the union. The four locales es, de, ko, ru were not updated with the new tip_* and cancel keys, which will cause a TS compile-time type error. At runtime the t() fallback would silently serve English strings to non-English users, but the build should fail first.

All four locales need the following keys added (translations are placeholders — verify with a native speaker):

🐛 Proposed additions for `es`, `de`, `ko`, `ru`
   // es block — after community_not_found
+    tip_amount: "Cantidad",
+    tip_custom: "Personalizado",
+    tip_currency: "Moneda",
+    tip_private_key: "Clave activa",
+    tip_wallet_address: "Dirección de billetera",
+    tip_no_wallet_address: "El destinatario no ha configurado esta dirección.",
+    tip_send: "Propina",
+    tip_sending: "Enviando...",
+    tip_login_to_send: "Inicia sesión para enviar una propina",
+    tip_asset_not_supported: "Este activo aún no es compatible para propinas",
+    tip_transaction_failed: "Transacción fallida",
+    tip_qr_no_address: "Sin dirección",
+    tip_qr_failed: "Error al generar el QR",
+    cancel: "Cancelar",

   // de block — after community_not_found
+    tip_amount: "Betrag",
+    tip_custom: "Benutzerdefiniert",
+    tip_currency: "Währung",
+    tip_private_key: "Aktiver Schlüssel",
+    tip_wallet_address: "Wallet-Adresse",
+    tip_no_wallet_address: "Empfänger hat diese Wallet-Adresse nicht eingerichtet.",
+    tip_send: "Trinkgeld",
+    tip_sending: "Wird gesendet...",
+    tip_login_to_send: "Anmelden, um ein Trinkgeld zu senden",
+    tip_asset_not_supported: "Dieses Asset wird für Trinkgelder noch nicht unterstützt",
+    tip_transaction_failed: "Transaktion fehlgeschlagen",
+    tip_qr_no_address: "Keine Adresse",
+    tip_qr_failed: "QR-Generierung fehlgeschlagen",
+    cancel: "Abbrechen",

   // ko block — after community_not_found
+    tip_amount: "금액",
+    tip_custom: "사용자 정의",
+    tip_currency: "통화",
+    tip_private_key: "활성 키",
+    tip_wallet_address: "지갑 주소",
+    tip_no_wallet_address: "수신자가 이 지갑 주소를 설정하지 않았습니다.",
+    tip_send: "팁 보내기",
+    tip_sending: "전송 중...",
+    tip_login_to_send: "팁을 보내려면 로그인하세요",
+    tip_asset_not_supported: "이 자산은 아직 팁 기능을 지원하지 않습니다",
+    tip_transaction_failed: "트랜잭션 실패",
+    tip_qr_no_address: "주소 없음",
+    tip_qr_failed: "QR 생성 실패",
+    cancel: "취소",

   // ru block — after community_not_found
+    tip_amount: "Сумма",
+    tip_custom: "Произвольная",
+    tip_currency: "Валюта",
+    tip_private_key: "Активный ключ",
+    tip_wallet_address: "Адрес кошелька",
+    tip_no_wallet_address: "Получатель не настроил этот адрес кошелька.",
+    tip_send: "Чаевые",
+    tip_sending: "Отправка...",
+    tip_login_to_send: "Войдите, чтобы отправить чаевые",
+    tip_asset_not_supported: "Этот актив ещё не поддерживается для чаевых",
+    tip_transaction_failed: "Транзакция не удалась",
+    tip_qr_no_address: "Нет адреса",
+    tip_qr_failed: "Не удалось создать QR",
+    cancel: "Отмена",

Also applies to: 219-279, 356-416, 417-477

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/core/i18n.ts` around lines 158 - 218, The es, de, ko and
ru locale objects are missing the 14 new translation keys (the tip_* keys and
cancel) required by the Translations type, causing a TypeScript compile error;
update each locale object (es, de, ko, ru) inside the i18n export to include all
new keys (use placeholder strings if unsure) so each Record<TranslationKey,
string> contains every key in the union, ensuring functions like t() compile;
locate the locale objects named "es", "de", "ko", "ru" and add the missing tip_*
and cancel entries consistently wherever those locale blocks appear (e.g., the
blocks around the shown diff ranges).
🧹 Nitpick comments (2)
apps/self-hosted/src/core/i18n.ts (1)

137-156: Mixed quote style within the en block.

Lines 137–142 retain single-quoted strings (cant_reblog_own through community_not_found), while the new tip keys (lines 143–156) use double quotes. The same inconsistency exists in the fr block (lines 339–340 vs. 342–354).

♻️ Normalise to double quotes (matching the new keys)
-    cant_reblog_own: "You can't reblog your own post",
-    already_reblogged: 'Already reblogged',
-    reblog_to_followers: 'Reblog to your followers',
-    error_loading: 'Something went wrong. Please try again.',
-    retry: 'Retry',
-    community_not_found: 'Community not found.',
+    cant_reblog_own: "You can't reblog your own post",
+    already_reblogged: "Already reblogged",
+    reblog_to_followers: "Reblog to your followers",
+    error_loading: "Something went wrong. Please try again.",
+    retry: "Retry",
+    community_not_found: "Community not found.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/core/i18n.ts` around lines 137 - 156, Convert all string
literals in the en and fr locale blocks to use double quotes for consistency:
replace single-quoted values for keys like cant_reblog_own, already_reblogged,
reblog_to_followers, error_loading, retry, community_not_found with
double-quoted equivalents so they match the new tip_* keys (tip_amount,
tip_custom, tip_currency, tip_private_key, tip_wallet_address,
tip_no_wallet_address, tip_send, tip_sending, tip_login_to_send,
tip_asset_not_supported, tip_transaction_failed, tip_qr_no_address,
tip_qr_failed, cancel); do the same normalization in the fr block for the
corresponding translated keys to ensure consistent double-quote usage across
both locale objects.
apps/self-hosted/src/features/auth/auth-actions.ts (1)

102-102: JSDoc @param name doesn't match the actual parameter.

The comment uses @param authorityType but the function's second parameter is named options. This will produce incorrect IDE hover docs. Use the nested-property JSDoc form instead.

📝 Proposed fix
- * `@param` authorityType - For keychain: which key to use (e.g. "Active" for transfers).
+ * `@param` options - Optional broadcast settings.
+ * `@param` options.authorityType - For keychain: which key to use (e.g. "Active" for transfers). Ignored for hivesigner and hiveauth.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/auth/auth-actions.ts` at line 102, The JSDoc
`@param` currently names authorityType but the function actually takes an options
parameter; update the JSDoc to use the nested-property form (e.g. `@param`
options.authorityType - For keychain: which key to use (e.g. "Active" for
transfers).) and remove or replace the incorrect `@param` authorityType entry so
the docs match the function signature and IDE hover shows the correct
description for options.authorityType; ensure the JSDoc references the existing
parameter name "options" and the property "authorityType" to locate the doc for
the function in auth-actions.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/self-hosted/package.json`:
- Around line 14-15: The web app's package.json must be aligned to the
self-hosted package versions: update the web app package.json dependencies to
use "@floating-ui/dom": "^1.7.4", "@floating-ui/react-dom": "^2.1.6", and
"qrcode": "^1.5.4" (these are required because files like
apps/self-hosted/src/features/tipping/components/tip-button.tsx import
autoUpdate and offset from "@floating-ui/dom"), and either add the devDependency
"@rsbuild/plugin-node-polyfill" to the web app devDependencies or add a short
justification comment in the PR explaining why the web app should intentionally
keep a different set; ensure package.json keys are updated (not duplicate) and
run install to verify no resolution conflicts.

In `@apps/self-hosted/src/core/i18n.ts`:
- Line 143: The JSON/locales object has a stray tab before the "tip_amount"
entry in the English and French blocks causing inconsistent indentation; locate
the "tip_amount" key in the i18n exports (the entries under the "en" and "fr"
locale objects) and replace the leading tab character with the same number of
spaces used by surrounding lines so indentation matches the rest of the file
(update both occurrences referenced by the "tip_amount" symbol).

In `@apps/self-hosted/src/features/auth/auth-actions.ts`:
- Around line 114-118: Export the BroadcastAuthorityType from the auth module's
index.ts so callers can import it from the module root (add
BroadcastAuthorityType to the type exports alongside existing exports), and add
a validation in auth-actions.ts around the computed authorityType (variable
authorityType and the switch on user.loginType) that detects when authorityType
!== "Posting" but user.loginType is not "keychain" (e.g., hivesigner or
hiveauth) and either log a dev-time warning or return an error/early rejection
to prevent the passed authority being silently ignored; reference the
keychainBroadcast call and the switch handling of "hivesigner" / "hiveauth" when
inserting this guard.

---

Outside diff comments:
In `@apps/self-hosted/src/core/i18n.ts`:
- Around line 158-218: The es, de, ko and ru locale objects are missing the 14
new translation keys (the tip_* keys and cancel) required by the Translations
type, causing a TypeScript compile error; update each locale object (es, de, ko,
ru) inside the i18n export to include all new keys (use placeholder strings if
unsure) so each Record<TranslationKey, string> contains every key in the union,
ensuring functions like t() compile; locate the locale objects named "es", "de",
"ko", "ru" and add the missing tip_* and cancel entries consistently wherever
those locale blocks appear (e.g., the blocks around the shown diff ranges).

---

Nitpick comments:
In `@apps/self-hosted/src/core/i18n.ts`:
- Around line 137-156: Convert all string literals in the en and fr locale
blocks to use double quotes for consistency: replace single-quoted values for
keys like cant_reblog_own, already_reblogged, reblog_to_followers,
error_loading, retry, community_not_found with double-quoted equivalents so they
match the new tip_* keys (tip_amount, tip_custom, tip_currency, tip_private_key,
tip_wallet_address, tip_no_wallet_address, tip_send, tip_sending,
tip_login_to_send, tip_asset_not_supported, tip_transaction_failed,
tip_qr_no_address, tip_qr_failed, cancel); do the same normalization in the fr
block for the corresponding translated keys to ensure consistent double-quote
usage across both locale objects.

In `@apps/self-hosted/src/features/auth/auth-actions.ts`:
- Line 102: The JSDoc `@param` currently names authorityType but the function
actually takes an options parameter; update the JSDoc to use the nested-property
form (e.g. `@param` options.authorityType - For keychain: which key to use (e.g.
"Active" for transfers).) and remove or replace the incorrect `@param`
authorityType entry so the docs match the function signature and IDE hover shows
the correct description for options.authorityType; ensure the JSDoc references
the existing parameter name "options" and the property "authorityType" to locate
the doc for the function in auth-actions.ts.

Comment on lines +14 to +15
"@floating-ui/dom": "^1.7.4",
"@floating-ui/react-dom": "^2.1.6",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for direct imports of `@floating-ui/dom` in tipping code
rg -n --type ts --type tsx "from ['\"]@floating-ui/dom['\"]" || echo "No direct imports found"

Repository: ecency/vision-next

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Locate web app package.json (typically at apps/web or similar)
find . -name "package.json" -type f | grep -E "(web|app)" | head -10

Repository: ecency/vision-next

Length of output: 527


🏁 Script executed:

#!/bin/bash
# List all package.json files in the repository
find . -name "package.json" -type f | head -20

Repository: ecency/vision-next

Length of output: 1236


🏁 Script executed:

#!/bin/bash
# Read web app package.json dependencies
cat ./apps/web/package.json | grep -A 50 '"dependencies"' | head -80

Repository: ecency/vision-next

Length of output: 2028


🏁 Script executed:

#!/bin/bash
# Broader search for `@floating-ui/dom` usage with adjusted pattern
rg "@floating-ui/dom" --type ts -n || echo "No `@floating-ui/dom` references found"

Repository: ecency/vision-next

Length of output: 978


🏁 Script executed:

#!/bin/bash
# Check if `@floating-ui/dom`, qrcode, `@rsbuild/plugin-node-polyfill`, `@types/qrcode` are in web app
cat ./apps/web/package.json | grep -E '"(`@floating-ui`|qrcode|@rsbuild/plugin-node-polyfill|@types/qrcode)"'

Repository: ecency/vision-next

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Read web app devDependencies
cat ./apps/web/package.json | grep -A 100 '"devDependencies"' | grep -E '(`@rsbuild/plugin-node-polyfill`|@types/qrcode)'

Repository: ecency/vision-next

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Also check self-hosted versions to compare side-by-side
echo "=== Self-hosted versions ===" && \
cat ./apps/self-hosted/package.json | grep -E '"(`@floating-ui`|qrcode|@rsbuild|@types/qrcode)"' && \
echo "" && echo "=== Web app versions ===" && \
cat ./apps/web/package.json | grep -E '"(`@floating-ui`|qrcode|@rsbuild|@types/qrcode)"'

Repository: ecency/vision-next

Length of output: 226


Align dependency versions between self-hosted and web app packages.

The @floating-ui/dom dependency is not redundant—it's directly imported in apps/self-hosted/src/features/tipping/components/tip-button.tsx (e.g., import { autoUpdate, offset } from "@floating-ui/dom"), and multiple files in the web app also import directly from it.

However, the self-hosted package has version mismatches with the web app:

  • @floating-ui/dom: self-hosted uses ^1.7.4 while web app uses ^1.6.13
  • @floating-ui/react-dom: self-hosted uses ^2.1.6 while web app uses ^2.1.2
  • qrcode: self-hosted uses ^1.5.4 while web app uses ^1.5.3
  • @rsbuild/plugin-node-polyfill: added to self-hosted devDeps but not present in web app

Update the web app's package.json to match self-hosted versions, or justify version differences if intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/package.json` around lines 14 - 15, The web app's
package.json must be aligned to the self-hosted package versions: update the web
app package.json dependencies to use "@floating-ui/dom": "^1.7.4",
"@floating-ui/react-dom": "^2.1.6", and "qrcode": "^1.5.4" (these are required
because files like
apps/self-hosted/src/features/tipping/components/tip-button.tsx import
autoUpdate and offset from "@floating-ui/dom"), and either add the devDependency
"@rsbuild/plugin-node-polyfill" to the web app devDependencies or add a short
justification comment in the PR explaining why the web app should intentionally
keep a different set; ensure package.json keys are updated (not duplicate) and
run install to verify no resolution conflicts.

error_loading: 'Something went wrong. Please try again.',
retry: 'Retry',
community_not_found: 'Community not found.',
tip_amount: "Amount",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stray tab character causes inconsistent indentation.

Lines 143 and 341 (tip_amount in en and fr) are indented with a tab while every surrounding line uses spaces.

✏️ Proposed fix
-	tip_amount: "Amount",
+    tip_amount: "Amount",
-	tip_amount: "Montant",
+    tip_amount: "Montant",

Also applies to: 341-341

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/core/i18n.ts` at line 143, The JSON/locales object has a
stray tab before the "tip_amount" entry in the English and French blocks causing
inconsistent indentation; locate the "tip_amount" key in the i18n exports (the
entries under the "en" and "fr" locale objects) and replace the leading tab
character with the same number of spaces used by surrounding lines so
indentation matches the rest of the file (update both occurrences referenced by
the "tip_amount" symbol).

Comment on lines +114 to +118
const authorityType = options?.authorityType ?? "Posting";

switch (user.loginType) {
case "keychain":
return keychainBroadcast(user.username, operations);
return keychainBroadcast(user.username, operations, authorityType);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat apps/self-hosted/src/features/auth/index.ts

Repository: ecency/vision-next

Length of output: 1265


🏁 Script executed:

sed -n '110,150p' apps/self-hosted/src/features/auth/auth-actions.ts

Repository: ecency/vision-next

Length of output: 744


Add BroadcastAuthorityType to type exports in index.ts; add validation for non-keychain paths receiving non-default authorityType.

  1. BroadcastAuthorityType is not re-exported from the auth module's index, requiring callers to use a deep import. Add it to the type exports to maintain API consistency.

  2. The authorityType parameter (default: "Posting") is computed on line 114 but silently discarded for hivesigner and hiveauth login types (lines 124, 130). Consider adding a dev-time warning or early return guard when a non-default authorityType is passed with a non-keychain login type to prevent subtle bugs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/self-hosted/src/features/auth/auth-actions.ts` around lines 114 - 118,
Export the BroadcastAuthorityType from the auth module's index.ts so callers can
import it from the module root (add BroadcastAuthorityType to the type exports
alongside existing exports), and add a validation in auth-actions.ts around the
computed authorityType (variable authorityType and the switch on user.loginType)
that detects when authorityType !== "Posting" but user.loginType is not
"keychain" (e.g., hivesigner or hiveauth) and either log a dev-time warning or
return an error/early rejection to prevent the passed authority being silently
ignored; reference the keychainBroadcast call and the switch handling of
"hivesigner" / "hiveauth" when inserting this guard.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant