Skip to content

Commit 256e227

Browse files
committed
Daily streak: StreakService, claim-here button, streak on Home
Made-with: Cursor
1 parent 7674688 commit 256e227

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

lib/claim.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:flutter/services.dart';
66
import 'package:flutter_svg/flutter_svg.dart';
77
import 'package:geolocator/geolocator.dart';
8+
import 'package:postbox_game/streak_service.dart';
89
import 'package:postbox_game/theme.dart';
910

1011
enum ClaimStage { initial, searching, results, empty, claimed }
@@ -80,6 +81,7 @@ class ClaimState extends State<Claim> with SingleTickerProviderStateMixin {
8081
FirebaseFunctions.instance.httpsCallable('nearbyPostboxes');
8182
final HttpsCallable _claimCallable =
8283
FirebaseFunctions.instance.httpsCallable('startScoring');
84+
final StreakService _streakService = StreakService();
8385

8486
Future<Position> _getPosition() async {
8587
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
@@ -148,6 +150,7 @@ class ClaimState extends State<Claim> with SingleTickerProviderStateMixin {
148150
currentStage = ClaimStage.claimed;
149151
});
150152
_successController.forward(from: 0);
153+
await _streakService.updateStreakAfterClaim();
151154
} on FirebaseFunctionsException catch (e) {
152155
debugPrint('Claim error: ${e.code} ${e.message}');
153156
_showErrorSnackBar(e.message ?? 'Could not claim postbox.');
@@ -196,6 +199,39 @@ class ClaimState extends State<Claim> with SingleTickerProviderStateMixin {
196199
child: Column(
197200
mainAxisAlignment: MainAxisAlignment.center,
198201
children: [
202+
StreamBuilder<int?>(
203+
stream: _streakService.streakStream(),
204+
builder: (context, snapshot) {
205+
final streak = snapshot.data;
206+
if (streak == null || streak == 0) return const SizedBox.shrink();
207+
return Padding(
208+
padding: const EdgeInsets.only(bottom: AppSpacing.md),
209+
child: Container(
210+
padding: const EdgeInsets.symmetric(
211+
horizontal: AppSpacing.md, vertical: AppSpacing.sm),
212+
decoration: BoxDecoration(
213+
color: postalRed.withValues(alpha: 0.08),
214+
borderRadius: BorderRadius.circular(20),
215+
border: Border.all(color: postalRed.withValues(alpha: 0.25)),
216+
),
217+
child: Row(
218+
mainAxisSize: MainAxisSize.min,
219+
children: [
220+
const Text('🔥', style: TextStyle(fontSize: 18)),
221+
const SizedBox(width: AppSpacing.xs),
222+
Text(
223+
'$streak-day streak',
224+
style: Theme.of(context).textTheme.labelLarge?.copyWith(
225+
color: postalRed,
226+
fontWeight: FontWeight.w600,
227+
),
228+
),
229+
],
230+
),
231+
),
232+
);
233+
},
234+
),
199235
SvgPicture.asset(
200236
'assets/postbox.svg',
201237
height: 100,

lib/streak_service.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:cloud_firestore/cloud_firestore.dart';
2+
import 'package:firebase_auth/firebase_auth.dart';
3+
4+
/// Updates and reads daily claim streak for the current user.
5+
class StreakService {
6+
StreakService({FirebaseFirestore? firestore, FirebaseAuth? auth})
7+
: _firestore = firestore ?? FirebaseFirestore.instance,
8+
_auth = auth ?? FirebaseAuth.instance;
9+
10+
final FirebaseFirestore _firestore;
11+
final FirebaseAuth _auth;
12+
13+
static String get _today => DateTime.now().toUtc().toIso8601String().split('T').first;
14+
15+
/// Call after user successfully claims (e.g. startScoring returned found with claims).
16+
Future<void> updateStreakAfterClaim() async {
17+
final uid = _auth.currentUser?.uid;
18+
if (uid == null) return;
19+
final ref = _firestore.collection('users').doc(uid);
20+
final today = _today;
21+
await _firestore.runTransaction((tx) async {
22+
final snap = await tx.get(ref);
23+
final data = snap.data();
24+
final last = data?['lastClaimDate'] as String?;
25+
final current = (data?['streak'] is int) ? data!['streak'] as int : 0;
26+
if (last == today) return; // already claimed today
27+
final yesterday = DateTime.now().toUtc().subtract(const Duration(days: 1)).toIso8601String().split('T').first;
28+
final newStreak = (last == yesterday) ? current + 1 : 1;
29+
tx.set(ref, {'lastClaimDate': today, 'streak': newStreak}, SetOptions(merge: true));
30+
});
31+
}
32+
33+
/// Stream of the current user's streak (for UI).
34+
Stream<int?> streakStream() {
35+
final uid = _auth.currentUser?.uid;
36+
if (uid == null) return Stream<int?>.value(null);
37+
return _firestore.collection('users').doc(uid).snapshots().map((s) {
38+
final d = s.data();
39+
if (d == null || d['streak'] is! int) return null;
40+
return d['streak'] as int;
41+
});
42+
}
43+
}

0 commit comments

Comments
 (0)