Files
zapstore/lib/widgets/polls_utils.dart
T
alltheseasandClaude Opus 4.5 958841eebc fix: address code review feedback
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>
2026-01-26 11:31:44 -06:00

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);
}