mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
1. main.dart: Add database migration from Documents to Support directory - Migrates existing zapstore.db to new location on first run - Preserves existing users' data after directory change 2. installed_packages_snapshot.dart: Use atomic file replace - Remove explicit delete before rename (rename atomically replaces) - Add try/catch to preserve temp file on rename failure 3. polls_section.dart: Fix useEffect cleanup for dynamic controllers - Capture controller snapshot in useEffect - Properly dispose controllers added via "Add Option" 4. polls_section.dart: Fix filter to include votes at exact end time - Changed from isBefore to !isAfter for inclusive filtering 5. polls_utils.dart: Extract shared utility functions - canCreatePoll, filterValidResponses, calculateVoteCounts - Marked @visibleForTesting for test access 6. FEAT-001-package-manager.md: Add markdown language specifiers - Fix MD040 lint warnings for code blocks 7. polls_section_test.dart: Use production utils instead of duplicates - Import and test actual canCreatePoll from polls_utils - Add test for votes at exact poll end time Signed-off-by: alltheseas <alltheseas@users.noreply.github.com> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
85 lines
2.9 KiB
Dart
85 lines
2.9 KiB
Dart
import 'package:flutter/foundation.dart' show visibleForTesting;
|
|
import 'package:models/models.dart';
|
|
|
|
/// Zapstore team npubs authorized to create polls on any app
|
|
const zapstoreTeamNpubs = {
|
|
'npub10r8xl2njyepcw2zwv3a6dyufj4e4ajx86hz6v4ehu4gnpupxxp7stjt2p8',
|
|
'npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9',
|
|
'npub1zafcms4xya5ap9zr7xxr0jlrtrattwlesytn2s42030lzu0dwlzqpd26k5',
|
|
};
|
|
|
|
/// Convert zapstore team npubs to hex pubkeys
|
|
@visibleForTesting
|
|
Set<String> get zapstoreTeamHex => zapstoreTeamNpubs.map((npub) {
|
|
try {
|
|
return Utils.decodeShareableToString(npub);
|
|
} catch (_) {
|
|
return npub;
|
|
}
|
|
}).toSet();
|
|
|
|
/// Check if a pubkey is authorized to create polls on an app
|
|
///
|
|
/// Returns true if:
|
|
/// - signedInPubkey matches appPubkey (app developer)
|
|
/// - signedInPubkey is a zapstore team member
|
|
@visibleForTesting
|
|
bool canCreatePoll(String? signedInPubkey, String? appPubkey) {
|
|
if (signedInPubkey == null) return false;
|
|
// App developer can create polls on their own app
|
|
if (signedInPubkey == appPubkey) return true;
|
|
// Zapstore team can create polls on any app
|
|
return zapstoreTeamHex.contains(signedInPubkey);
|
|
}
|
|
|
|
/// Filter out votes created after poll expiry (per NIP-88)
|
|
/// Votes at exactly the end time are included
|
|
@visibleForTesting
|
|
List<T> filterValidResponses<T>({
|
|
required List<T> responses,
|
|
required DateTime? pollEndsAt,
|
|
required DateTime Function(T) getCreatedAt,
|
|
}) {
|
|
if (pollEndsAt == null) return responses;
|
|
// !isAfter means "at or before" - includes votes at exact end time
|
|
return responses.where((r) => !getCreatedAt(r).isAfter(pollEndsAt)).toList();
|
|
}
|
|
|
|
/// Calculate vote counts per option (NIP-88 compliant)
|
|
///
|
|
/// For singlechoice: only count first response tag
|
|
/// For multiplechoice: count all response tags
|
|
/// Returns (voteCounts, validVoterCount) - validVoterCount excludes invalid votes
|
|
@visibleForTesting
|
|
({Map<String, int> counts, int validVoterCount}) calculateVoteCounts<T>({
|
|
required Set<String> validOptionIds,
|
|
required List<T> responses,
|
|
required bool isSingleChoice,
|
|
required String? Function(T) getFirstOptionId,
|
|
required Iterable<String> Function(T) getAllOptionIds,
|
|
}) {
|
|
final voteCounts = <String, int>{};
|
|
for (final optionId in validOptionIds) {
|
|
voteCounts[optionId] = 0;
|
|
}
|
|
|
|
int validVoterCount = 0;
|
|
for (final response in responses) {
|
|
// For singlechoice: only first option counts (per NIP-88)
|
|
final optionIds = isSingleChoice
|
|
? [getFirstOptionId(response)].whereType<String>()
|
|
: getAllOptionIds(response);
|
|
|
|
bool hasValidVote = false;
|
|
for (final optionId in optionIds) {
|
|
if (validOptionIds.contains(optionId)) {
|
|
voteCounts[optionId] = (voteCounts[optionId] ?? 0) + 1;
|
|
hasValidVote = true;
|
|
}
|
|
}
|
|
if (hasValidVote) validVoterCount++;
|
|
}
|
|
|
|
return (counts: voteCounts, validVoterCount: validVoterCount);
|
|
}
|