mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Require explicit AppCatalog relay acceptance before treating an app report as published
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:zapstore/services/notification_service.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
|
||||
const kMinimumReportDescriptionLength = 20;
|
||||
final _hexIdentifier = RegExp(r'^[0-9a-f]{64}$', caseSensitive: false);
|
||||
@@ -16,6 +17,25 @@ bool canSubmitAppReport({
|
||||
violationType != null &&
|
||||
description.trim().length >= kMinimumReportDescriptionLength;
|
||||
|
||||
bool wasAppReportAccepted(PublishResponse response, String reportEventId) =>
|
||||
response.results[reportEventId]?.any((result) => result.accepted) ?? false;
|
||||
|
||||
String appReportPublishFailure(PublishResponse response, String reportEventId) {
|
||||
final results = response.results[reportEventId];
|
||||
if (results == null || results.isEmpty) {
|
||||
return 'No relay responded to the report. Check your connection and retry.';
|
||||
}
|
||||
|
||||
final rejection = results.firstWhere((result) => !result.accepted);
|
||||
final reason = rejection.message?.trim();
|
||||
if (reason == null || reason.isEmpty) {
|
||||
return 'Relay ${rejection.relayUrl} explicitly rejected the report without '
|
||||
'providing a reason.';
|
||||
}
|
||||
|
||||
return 'Relay ${rejection.relayUrl} rejected the report: $reason';
|
||||
}
|
||||
|
||||
bool canReportApp(App app) =>
|
||||
reportableAppEventId(app) != null && _hexIdentifier.hasMatch(app.pubkey);
|
||||
|
||||
@@ -234,8 +254,14 @@ class AppReportSheet extends HookConsumerWidget {
|
||||
reason: description,
|
||||
).signWith(signer);
|
||||
|
||||
await report.save();
|
||||
await report.publish(relays: 'AppCatalog');
|
||||
await ref.storage.save({report});
|
||||
final response = await ref.storage.publish({
|
||||
report,
|
||||
}, relays: 'AppCatalog');
|
||||
if (!wasAppReportAccepted(response, report.id)) {
|
||||
submissionError.value = appReportPublishFailure(response, report.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# WORK-016 — Report Publish Confirmation
|
||||
|
||||
**Feature:** FEAT-009-nip56-app-reporting.md
|
||||
**Status:** Complete
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Treat a NIP-56 report as published only after an AppCatalog relay
|
||||
explicitly accepts its event.
|
||||
- Files: `lib/widgets/app_report_sheet.dart`
|
||||
- [x] 2. Preserve the report and expose a retryable failure when no relay
|
||||
accepts the event, including timeouts and rejections.
|
||||
- Files: `lib/widgets/app_report_sheet.dart`
|
||||
- [x] 3. Cover accepted, rejected, and absent relay responses.
|
||||
- Files: `test/widgets/app_report_sheet_test.dart`
|
||||
- [x] 4. Display the relay's rejection reason when it responds negatively.
|
||||
- Files: `lib/widgets/app_report_sheet.dart`,
|
||||
`test/widgets/app_report_sheet_test.dart`
|
||||
|
||||
## Test Coverage
|
||||
|
||||
| Scenario | Expected | Status |
|
||||
|----------|----------|--------|
|
||||
| Relay accepts report event | Success is reported | [x] |
|
||||
| Relay rejects or times out | Form stays open with retryable error | [x] |
|
||||
| No response for report event | Form stays open with retryable error | [x] |
|
||||
| Relay rejection includes a reason | User sees the relay's reason | [x] |
|
||||
| Relay rejection has no reason | User sees that the relay omitted it | [x] |
|
||||
|
||||
## Decisions
|
||||
|
||||
### 2026-07-10 — Explicit relay acceptance
|
||||
|
||||
**Decision:** The report flow waits for purplebase's publish response and
|
||||
considers publishing successful only when at least one AppCatalog relay accepts
|
||||
the signed report event.
|
||||
|
||||
**Rationale:** A timeout or relay rejection is a completed transport response,
|
||||
not a successful submission.
|
||||
@@ -48,4 +48,82 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('wasAppReportAccepted', () {
|
||||
test('accepts an explicit relay acceptance for the report event', () {
|
||||
final response = PublishResponse()
|
||||
..addEvent(
|
||||
'report-id',
|
||||
relayUrl: 'wss://relay.zapstore.dev',
|
||||
accepted: true,
|
||||
);
|
||||
|
||||
expect(wasAppReportAccepted(response, 'report-id'), isTrue);
|
||||
});
|
||||
|
||||
test('rejects a relay rejection or timeout', () {
|
||||
final response = PublishResponse()
|
||||
..addEvent(
|
||||
'report-id',
|
||||
relayUrl: 'wss://relay.zapstore.dev',
|
||||
accepted: false,
|
||||
message: 'Timeout',
|
||||
);
|
||||
|
||||
expect(wasAppReportAccepted(response, 'report-id'), isFalse);
|
||||
});
|
||||
|
||||
test('rejects a response without the report event', () {
|
||||
final response = PublishResponse()
|
||||
..addEvent(
|
||||
'another-event-id',
|
||||
relayUrl: 'wss://relay.zapstore.dev',
|
||||
accepted: true,
|
||||
);
|
||||
|
||||
expect(wasAppReportAccepted(response, 'report-id'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('appReportPublishFailure', () {
|
||||
test('includes the relay rejection reason', () {
|
||||
final response = PublishResponse()
|
||||
..addEvent(
|
||||
'report-id',
|
||||
relayUrl: 'wss://relay.zapstore.dev',
|
||||
accepted: false,
|
||||
message: 'rate-limited: slow down chief',
|
||||
);
|
||||
|
||||
expect(
|
||||
appReportPublishFailure(response, 'report-id'),
|
||||
'Relay wss://relay.zapstore.dev rejected the report: '
|
||||
'rate-limited: slow down chief',
|
||||
);
|
||||
});
|
||||
|
||||
test('explains when the report event has no relay response', () {
|
||||
final response = PublishResponse();
|
||||
|
||||
expect(
|
||||
appReportPublishFailure(response, 'report-id'),
|
||||
'No relay responded to the report. Check your connection and retry.',
|
||||
);
|
||||
});
|
||||
|
||||
test('explains when the relay rejects without a reason', () {
|
||||
final response = PublishResponse()
|
||||
..addEvent(
|
||||
'report-id',
|
||||
relayUrl: 'wss://relay.zapstore.dev',
|
||||
accepted: false,
|
||||
);
|
||||
|
||||
expect(
|
||||
appReportPublishFailure(response, 'report-id'),
|
||||
'Relay wss://relay.zapstore.dev explicitly rejected the report without '
|
||||
'providing a reason.',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user