fix(content): double-pay is now impossible + purchases auto-file + Paid Files tab
Demo images / Build & push demo images (push) Failing after 32s
Demo images / Build & push demo images (push) Failing after 32s
The double-share/double-pay chain (user hit it live, 2026-07-22): ShareModal's already-shared lookup compared the slash-stripped stored path against the leading-slash filepath — never matched — so every re-share minted a NEW catalog entry with a new id, and the buyer's owned-guard (keyed by id) saw the duplicate as unowned and paid again. Three independent walls now: (1) ShareModal normalizes both sides so re-shares reuse the entry; (2) content_server::add_item dedupes by filename server-side (updates in place, keeps the id so buyers' owned records stay valid); (3) the buyer REFUSES to pay for content it already owns — matched by (onion, content_id) OR (onion, filename) — and serves the cached copy instead, before any ecash is minted. Purchases also auto-file into Photos/Music/Documents on the node (same buckets as the Cloud view, collision-safe naming) so bought files show up where files live on every device, and Cloud gains a Paid Files tab listing every purchase (name, size, sats paid, date) with in-app view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
df9b9905b6
commit
f339358109
@@ -145,6 +145,19 @@ const errorMsg = ref<string | null>(null)
|
||||
const successMsg = ref<string | null>(null)
|
||||
|
||||
// If we have an existing item, load its state
|
||||
|
||||
/** Catalog entries store the slash-stripped path; props carry a leading
|
||||
* slash (filepath) or just the basename (filename). Normalize both sides —
|
||||
* the old exact compare never matched, so every re-share created a brand
|
||||
* new priced entry and buyers could pay twice for one file (2026-07-22). */
|
||||
function matchesThisFile(catalogFilename: string): boolean {
|
||||
const strip = (v: string) => v.replace(/^\/+/, '')
|
||||
return (
|
||||
strip(catalogFilename) === strip(props.filepath || '') ||
|
||||
strip(catalogFilename) === strip(props.filename || '')
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await rpcClient.call<{ items: Array<{
|
||||
@@ -154,7 +167,7 @@ onMounted(async () => {
|
||||
availability: string | { allpeers?: unknown; nobody?: unknown }
|
||||
}> }>({ method: 'content.list-mine' })
|
||||
const match = res.items.find(
|
||||
(i) => i.filename === props.filename || i.filename === props.filepath
|
||||
(i) => matchesThisFile(i.filename)
|
||||
)
|
||||
if (match) {
|
||||
shared.value = true
|
||||
@@ -188,7 +201,7 @@ async function save() {
|
||||
method: 'content.list-mine',
|
||||
})
|
||||
const match = res.items.find(
|
||||
(i) => i.filename === props.filename || i.filename === props.filepath
|
||||
(i) => matchesThisFile(i.filename)
|
||||
)
|
||||
if (match) {
|
||||
await rpcClient.call({ method: 'content.remove', params: { id: match.id } })
|
||||
@@ -200,7 +213,7 @@ async function save() {
|
||||
method: 'content.list-mine',
|
||||
})
|
||||
let itemId = res.items.find(
|
||||
(i) => i.filename === props.filename || i.filename === props.filepath
|
||||
(i) => matchesThisFile(i.filename)
|
||||
)?.id
|
||||
|
||||
// Add if not in catalog
|
||||
|
||||
Reference in New Issue
Block a user