Skip to content

feat: weekly attendance streak (fe + be)#448

Closed
sheeeuWu wants to merge 3 commits into
roshankumar0036singh:mainfrom
sheeeuWu:feat/weekly-attendance-streak
Closed

feat: weekly attendance streak (fe + be)#448
sheeeuWu wants to merge 3 commits into
roshankumar0036singh:mainfrom
sheeeuWu:feat/weekly-attendance-streak

Conversation

@sheeeuWu
Copy link
Copy Markdown
Contributor

@sheeeuWu sheeeuWu commented May 30, 2026

Description

Implements weekly attendance streak tracking for students. Every time a student checks in to an event, their streak is updated. Reaching a 4-week consecutive streak awards them a "Dedicated Student" certificate delivered via email.

Fixes #57

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

All 4 streak scenarios were verified locally using the Firebase emulator suite. To reproduce:

  1. Start emulators: cd cloud-functions && npm run serve
  2. Run test script: npx ts-node src/scripts/testStreak.ts
  • Test 1 — Fresh user first check-in: currentStreak: 1
  • Test 2 — 4 consecutive weekly check-ins: currentStreak: 4, certificate written to user document
  • Test 3 — Same week double check-in: streak unchanged
  • Test 4 — Missed 2+ weeks: currentStreak resets to 1, longestStreak preserved

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features

    • Added "My Streak" screen to track attendance streaks and view current/longest streak stats
    • Introduced streak tracking system that monitors weekly attendance patterns
    • Added dedicated student certificate awarded for maintaining a 4+ week streak
    • Added "Streak" menu option in Profile for quick access to streak information
    • Implemented visual progress indicators and milestone cards on the streak screen
  • Updates

    • Updated dependency versions for improved app performance

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR implements an attendance streak system rewarding consistent weekly event participation. Users see their current streak and 4-week milestone progress in a new dedicated screen. A Cloud Function computes streaks on check-in, and users reaching a 4-week streak receive a digital certificate.

Changes

Event Streaks Feature

Layer / File(s) Summary
Auth setup and app routing
.gitignore, app/App.js, app/src/lib/AuthContext.js
Firestore user documents initialize streak fields (currentStreak, longestStreak, lastAttendanceAt); App registers Streak navigation route; local scripts ignored.
Streak visualization in profiles and dedicated screen
app/src/screens/ProfileScreen.js, app/src/screens/StreakScreen.js
ProfileScreen adds "Streak" menu entry with lightning icon; StreakScreen displays current streak, longest streak, weekly milestones, progress bars, certificate history, and hints using real-time Firestore subscription.
Attendance streak computation trigger
cloud-functions/src/attendanceStreak.ts
Cloud Function fires on check-in document creation; computes streak from 7-day attendance windows; updates user streak/timestamp via transaction; conditionally awards certificate at 4-week milestone.
Dedicated Student certificate generation and delivery
cloud-functions/src/dedicatedStudentCertificate.ts
New function loads user, generates filled PDF from template, uploads to Firebase Storage, appends certificate record to user document, and sends email via Resend (with emulator mocking).
Email service refactoring and Cloud Functions updates
cloud-functions/src/certificateService.ts, cloud-functions/src/auditLog.ts, cloud-functions/src/index.ts
Resend client moved to dynamic getResendClient() helper; auditLog uses correct @google-cloud/firestore FieldValue import; index exports new attendanceStreak function.
Dependency version management
app/package.json
@shopify/react-native-skia updated from ^2.6.4 to ^1.5.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • roshankumar0036singh/Uni-Event#278: Both PRs refactor cloud-functions/src/certificateService.ts email-sending to use a dynamic Resend client and handle certificate delivery.
  • roshankumar0036singh/Uni-Event#123: The main PR removes the fraud-detection Cloud Function (analyzeAttendance and distance/fraud utils), while the retrieved PR re-introduces similar attendance analysis logic.
  • roshankumar0036singh/Uni-Event#429: Both PRs update Cloud Functions exports; main PR adds attendanceStreak and updates auditLog imports, while retrieved PR adds additional audit and event logging.

Suggested labels

type:feature, area:backend, area:frontend, quality:clean

Suggested reviewers

  • roshankumar0036singh

Poem

🐰 A streak so bright, a rabbit's delight!
Four weeks of coming back, earning the might
Of certificates golden, displayed with such pride,
As attendance rewards bloom from inside.
Now streaks shall compute when check-ins are made,
And milestones light paths where commitments parade! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes significant removals of cloud function modules (dailyDigest, eventNotifications, reminders, reputation, setRole, analyzeAttendance, certificateService, onEventCreate, server.js) that appear unrelated to the streak feature requirements. Clarify whether module removals (dailyDigest, eventNotifications, reminders, reputation, etc.) are intentional scope changes or should be handled separately. If intentional, document the deprecation rationale.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: weekly attendance streak (fe + be)' accurately describes the main change: implementing weekly attendance streak tracking across frontend and backend.
Linked Issues check ✅ Passed All key requirements from issue #57 are met: background service tracks attendance and updates currentStreak [#57], awards Dedicated Student certificate at 4-week streak [#57], and exposes streak visualization on user profile [#57].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/weekly-attendance-streak

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
app/src/screens/StreakScreen.js (1)

46-49: ⚡ Quick win

Clarify progress calculation logic.

The ternary on line 48 is correct but potentially confusing. The logic handles the case where currentStreak % 4 === 0 would return 0, but you want to display 4/4 when a milestone is reached. Consider adding a brief comment or renaming to make the intent clearer.

♻️ Proposed refactor with clarifying comment
-    // progress toward next milestone (every 4 weeks)
+    // Progress toward next milestone (every 4 weeks).
+    // At milestone (currentStreak % 4 === 0), show 4/4 instead of 0/4.
     const nextMilestone = Math.ceil((currentStreak + 1) / 4) * 4;
     const progress = currentStreak % 4 === 0 && currentStreak > 0 ? 4 : currentStreak % 4;
     const progressPercent = (progress / 4) * 100;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/screens/StreakScreen.js` around lines 46 - 49, The progress
calculation is correct but unclear: when currentStreak % 4 === 0 we want to show
a full 4/4 progress rather than 0. Update the logic around the progress variable
(used with nextMilestone and progressPercent) to make intent explicit—either
rename progress to something like progressInCycle or progressDisplay and add a
short inline comment explaining "treat cycle-complete (mod 0) as 4/4", or
replace the ternary with a small helper function
getProgressInCycle(currentStreak) that returns 4 when currentStreak % 4 === 0 &&
currentStreak > 0 otherwise currentStreak % 4; ensure progressPercent uses that
value.
app/src/screens/ProfileScreen.js (1)

69-73: 💤 Low value

Refactor icon rendering logic for better maintainability.

The hardcoded check for 'lightning-bolt-outline' creates tight coupling. If additional MaterialCommunityIcons are needed in the future, this pattern becomes unmaintainable. Consider passing an iconFamily prop or consolidating on a single icon library.

♻️ Proposed refactor to accept iconFamily prop
 const MenuItem = ({
     icon,
+    iconFamily = 'Ionicons',
     label,
     description,
     onPress,
     theme,
     styles,
     width = '50%',
     showChevron = true,
     rightElement,
 }) => (
     <TouchableOpacity onPress={onPress} style={[styles.bentoMenuItem, { width }]}>
         <View style={styles.bentoTop}>
             <View
                 style={[
                     styles.menuIconContainer,
                     {
                         backgroundColor: theme.colors.primary + '20',
                     },
                 ]}
             >
-                {icon === 'lightning-bolt-outline' ? (
-                    <MaterialCommunityIcons name={icon} size={20} color={theme.colors.primary} />
-                ) : (
-                    <Ionicons name={icon} size={20} color={theme.colors.primary} />
-                )}
+                {iconFamily === 'MaterialCommunityIcons' ? (
+                    <MaterialCommunityIcons name={icon} size={20} color={theme.colors.primary} />
+                ) : iconFamily === 'MaterialIcons' ? (
+                    <MaterialIcons name={icon} size={20} color={theme.colors.primary} />
+                ) : (
+                    <Ionicons name={icon} size={20} color={theme.colors.primary} />
+                )}
             </View>

Then update the Streak menu item:

 <MenuItem
     icon="lightning-bolt-outline"
+    iconFamily="MaterialCommunityIcons"
     label="Streak"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/screens/ProfileScreen.js` around lines 69 - 73, The icon rendering
currently checks icon === 'lightning-bolt-outline' and conditionally renders
MaterialCommunityIcons vs Ionicons; refactor the component (e.g., ProfileScreen
/ the Streak menu item) to accept an explicit iconFamily prop (or derive a
family from a lookup) and render based on that prop instead of a hardcoded icon
string; update the render logic to choose the correct component
(MaterialCommunityIcons, Ionicons, etc.) via a small map or switch and provide a
sensible default/fallback for unknown families so future icons can be added
without modifying this conditional.
cloud-functions/src/index.ts (1)

20-20: 💤 Low value

Add semicolon for consistency with other export statements.

Line 20 is missing a semicolon, while all other export statements (lines 6-19) end with semicolons.

✨ Proposed fix
-export * from './attendanceStreak'
+export * from './attendanceStreak';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/index.ts` at line 20, The export statement "export * from
'./attendanceStreak'" in index.ts is missing a trailing semicolon; update that
line to match the other export lines (lines 6-19) by adding a semicolon at the
end so it reads export * from './attendanceStreak'; ensuring consistent export
formatting across the file.
cloud-functions/src/attendanceStreak.ts (1)

70-77: ⚡ Quick win

Reuse existing Firestore instance and consider transaction-based certificate check.

Lines 71-72 create a new Firestore instance (db2) and re-read the user document outside the transaction. This is wasteful and introduces a potential race condition where another check-in could modify the streak between the transaction commit and this read.

♻️ Proposed refactor

Option 1: Reuse the existing db instance and accept the minor race (the duplicate check in awardDedicatedStudentCertificate mitigates most harm):

  //dedicated student certificate award
- const db2 = admin.firestore();
- const updatedSnap = await db2.collection("users").doc(userId).get();
+ const updatedSnap = await db.collection("users").doc(userId).get();
  const updatedStreak: number = updatedSnap.data()?.currentStreak || 0;

Option 2 (better): Check the streak inside the transaction and return a flag:

+ let shouldAwardCertificate = false;
+
  await db.runTransaction(async (tx) => {
    ...
    tx.update(userRef, {
      currentStreak: newStreak,
      longestStreak: newLongestStreak,
      lastAttendanceAt: now,
    });
+   
+   if (newStreak >= 4) {
+     shouldAwardCertificate = true;
+   }

    console.log("previous currentStreak:", currentStreak);
    ...
  });

  //dedicated student certificate award
- const db2 = admin.firestore();
- const updatedSnap = await db2.collection("users").doc(userId).get();
- const updatedStreak: number = updatedSnap.data()?.currentStreak || 0;
-
- if (updatedStreak >= 4) {
+ if (shouldAwardCertificate) {
    await awardDedicatedStudentCertificate(userId);
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/attendanceStreak.ts` around lines 70 - 77, Replace the
extra Firestore instance and post-transaction read (db2 and updatedSnap) by
performing the dedicated-student check as part of the same transaction (or reuse
the existing db if you accept the minor race); specifically, inside the
transaction that updates the user's streak (the code using db and
runTransaction), read the user doc there and compute updatedStreak and a boolean
flag, return that flag from the transaction scope, and only call
awardDedicatedStudentCertificate(userId) if the transaction returned true — this
removes the separate db2 read and prevents the race condition between commit and
the certificate check.
cloud-functions/src/certificateService.ts (1)

7-9: ⚡ Quick win

Consider reusing Resend client instance for better performance.

The getResendClient() helper creates a new Resend instance on every call (lines 146, 172). While not a critical issue, instantiating clients repeatedly can add unnecessary overhead. Consider creating a module-level singleton instance instead.

♻️ Proposed refactor to singleton pattern
+let resendClient: Resend | null = null;
+
 function getResendClient() {
-  return new Resend(process.env.RESEND_API_KEY || "re_test_placeholder");
+  if (!resendClient) {
+    resendClient = new Resend(process.env.RESEND_API_KEY || "re_test_placeholder");
+  }
+  return resendClient;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/certificateService.ts` around lines 7 - 9, The
getResendClient() helper creates a new Resend(...) on every call which adds
overhead; change it to a module-level singleton by instantiating Resend once
(using process.env.RESEND_API_KEY || "re_test_placeholder") and have
getResendClient() return that single instance (or remove getResendClient and
export the singleton directly). Update all callers that currently call
getResendClient() to use the shared instance so the Resend client is only
constructed once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/screens/StreakScreen.js`:
- Around line 208-212: The certificate date rendering in StreakScreen uses new
Date(cert.awardedAt) which will produce "Invalid Date" if awardedAt is missing;
update the JSX that renders the date to first check cert?.awardedAt (or test
Boolean(cert.awardedAt)) and, if present, format it with new
Date(...).toLocaleDateString('en-US', {year:'numeric', month:'long',
day:'numeric'}), otherwise render a safe fallback string like '—' or 'Date
unavailable' so the UI never shows "Invalid Date".

In `@cloud-functions/src/attendanceStreak.ts`:
- Around line 44-53: The boundary check wrongly treats diffMs == WEEK_MS * 2 as
a missed week; update the consecutive-week branch by changing the condition in
the else-if that references diffMs and WEEK_MS (the branch that sets newStreak =
currentStreak + 1) from a strict < to a <= so that diffMs == WEEK_MS * 2 is
counted as consecutive; keep the earlier diffMs < WEEK_MS check and the
newStreak/currentStreak logic unchanged.
- Around line 11-16: The code currently reads userId from
event.data?.data()?.userId which is fragile; change the extraction to use the
path parameter event.params.userId instead (replace the assignment to the userId
variable where it is set and remove the fallback to event.data?.data()?.userId),
and update the early-return log to reflect missing event.params.userId if
absent; ensure all subsequent uses of userId in attendanceStreak.ts refer to
this parameter-derived variable.

In `@cloud-functions/src/dedicatedStudentCertificate.ts`:
- Around line 121-128: The certificates array is storing awardedAt as an ISO
string; change it to store a Firestore Timestamp so queries stay consistent and
indexable: in the update call on db.collection("users").doc(userId").update(...)
where FieldValue.arrayUnion(...) is used to push the certificate object (type:
"dedicated_student", awardedAt: ... , certificateUrl: signedUrl), replace
now.toDate().toISOString() with the Firestore Timestamp instance (e.g., now or
Timestamp.now()) so the awardedAt field is saved as a Timestamp like other
fields such as lastAttendanceAt.
- Around line 113-118: The signed URL for the certificate is set to expire at
"2499-12-31", effectively permanent; update the call to file.getSignedUrl (and
the signedUrl assignment) to use a reasonable expiry (e.g., generate an expires
value programmatically for 5–10 years from now or read a config value) so the
URL can be invalidated later, or implement a URL refresh mechanism that issues
short-lived signed URLs on-demand; ensure you change the literal expires string
to a computed/managed expiry and document the chosen duration.

---

Nitpick comments:
In `@app/src/screens/ProfileScreen.js`:
- Around line 69-73: The icon rendering currently checks icon ===
'lightning-bolt-outline' and conditionally renders MaterialCommunityIcons vs
Ionicons; refactor the component (e.g., ProfileScreen / the Streak menu item) to
accept an explicit iconFamily prop (or derive a family from a lookup) and render
based on that prop instead of a hardcoded icon string; update the render logic
to choose the correct component (MaterialCommunityIcons, Ionicons, etc.) via a
small map or switch and provide a sensible default/fallback for unknown families
so future icons can be added without modifying this conditional.

In `@app/src/screens/StreakScreen.js`:
- Around line 46-49: The progress calculation is correct but unclear: when
currentStreak % 4 === 0 we want to show a full 4/4 progress rather than 0.
Update the logic around the progress variable (used with nextMilestone and
progressPercent) to make intent explicit—either rename progress to something
like progressInCycle or progressDisplay and add a short inline comment
explaining "treat cycle-complete (mod 0) as 4/4", or replace the ternary with a
small helper function getProgressInCycle(currentStreak) that returns 4 when
currentStreak % 4 === 0 && currentStreak > 0 otherwise currentStreak % 4; ensure
progressPercent uses that value.

In `@cloud-functions/src/attendanceStreak.ts`:
- Around line 70-77: Replace the extra Firestore instance and post-transaction
read (db2 and updatedSnap) by performing the dedicated-student check as part of
the same transaction (or reuse the existing db if you accept the minor race);
specifically, inside the transaction that updates the user's streak (the code
using db and runTransaction), read the user doc there and compute updatedStreak
and a boolean flag, return that flag from the transaction scope, and only call
awardDedicatedStudentCertificate(userId) if the transaction returned true — this
removes the separate db2 read and prevents the race condition between commit and
the certificate check.

In `@cloud-functions/src/certificateService.ts`:
- Around line 7-9: The getResendClient() helper creates a new Resend(...) on
every call which adds overhead; change it to a module-level singleton by
instantiating Resend once (using process.env.RESEND_API_KEY ||
"re_test_placeholder") and have getResendClient() return that single instance
(or remove getResendClient and export the singleton directly). Update all
callers that currently call getResendClient() to use the shared instance so the
Resend client is only constructed once.

In `@cloud-functions/src/index.ts`:
- Line 20: The export statement "export * from './attendanceStreak'" in index.ts
is missing a trailing semicolon; update that line to match the other export
lines (lines 6-19) by adding a semicolon at the end so it reads export * from
'./attendanceStreak'; ensuring consistent export formatting across the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f60cb11-6524-4796-a21b-5d54c0c0b391

📥 Commits

Reviewing files that changed from the base of the PR and between 2332e1b and ff3339d.

⛔ Files ignored due to path filters (16)
  • app/package-lock.json is excluded by !**/package-lock.json
  • cloud-functions/lib/analyzeAttendance.js.map is excluded by !**/*.map
  • cloud-functions/lib/certificateService.js.map is excluded by !**/*.map
  • cloud-functions/lib/dailyDigest.js.map is excluded by !**/*.map
  • cloud-functions/lib/eventNotifications.js.map is excluded by !**/*.map
  • cloud-functions/lib/inactiveUsers.js.map is excluded by !**/*.map
  • cloud-functions/lib/index.js.map is excluded by !**/*.map
  • cloud-functions/lib/lib/participants.js.map is excluded by !**/*.map
  • cloud-functions/lib/onEventCreate.js.map is excluded by !**/*.map
  • cloud-functions/lib/reminders.js.map is excluded by !**/*.map
  • cloud-functions/lib/reputation.js.map is excluded by !**/*.map
  • cloud-functions/lib/server.js.map is excluded by !**/*.map
  • cloud-functions/lib/setRole.js.map is excluded by !**/*.map
  • cloud-functions/lib/utils/distance.js.map is excluded by !**/*.map
  • cloud-functions/lib/utils/fraudScore.js.map is excluded by !**/*.map
  • cloud-functions/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • .gitignore
  • app/App.js
  • app/package.json
  • app/src/lib/AuthContext.js
  • app/src/screens/ProfileScreen.js
  • app/src/screens/StreakScreen.js
  • cloud-functions/lib/analyzeAttendance.js
  • cloud-functions/lib/certificateService.js
  • cloud-functions/lib/dailyDigest.js
  • cloud-functions/lib/eventNotifications.js
  • cloud-functions/lib/index.js
  • cloud-functions/lib/lib/participants.js
  • cloud-functions/lib/onEventCreate.js
  • cloud-functions/lib/reminders.js
  • cloud-functions/lib/reputation.js
  • cloud-functions/lib/server.js
  • cloud-functions/lib/setRole.js
  • cloud-functions/lib/utils/distance.js
  • cloud-functions/lib/utils/fraudScore.js
  • cloud-functions/src/attendanceStreak.ts
  • cloud-functions/src/auditLog.ts
  • cloud-functions/src/certificateService.ts
  • cloud-functions/src/dedicatedStudentCertificate.ts
  • cloud-functions/src/index.ts
💤 Files with no reviewable changes (13)
  • cloud-functions/lib/onEventCreate.js
  • cloud-functions/lib/reminders.js
  • cloud-functions/lib/setRole.js
  • cloud-functions/lib/analyzeAttendance.js
  • cloud-functions/lib/utils/fraudScore.js
  • cloud-functions/lib/index.js
  • cloud-functions/lib/eventNotifications.js
  • cloud-functions/lib/dailyDigest.js
  • cloud-functions/lib/utils/distance.js
  • cloud-functions/lib/server.js
  • cloud-functions/lib/lib/participants.js
  • cloud-functions/lib/certificateService.js
  • cloud-functions/lib/reputation.js

Comment on lines +208 to +212
{new Date(cert.awardedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add safety check for missing awardedAt field.

If a certificate record lacks the awardedAt field, new Date(cert.awardedAt) will create an invalid date, displaying "Invalid Date" to users. Add optional chaining or a fallback to handle missing data gracefully.

🛡️ Proposed fix
                                 <Text
                                     style={[styles.certDate, { color: theme.colors.textSecondary }]}
                                 >
-                                    Awarded{' '}
-                                    {new Date(cert.awardedAt).toLocaleDateString('en-US', {
-                                        year: 'numeric',
-                                        month: 'long',
-                                        day: 'numeric',
-                                    })}
+                                    {cert.awardedAt
+                                        ? `Awarded ${new Date(cert.awardedAt).toLocaleDateString('en-US', {
+                                              year: 'numeric',
+                                              month: 'long',
+                                              day: 'numeric',
+                                          })}`
+                                        : 'Awarded (date unknown)'}
                                 </Text>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/screens/StreakScreen.js` around lines 208 - 212, The certificate date
rendering in StreakScreen uses new Date(cert.awardedAt) which will produce
"Invalid Date" if awardedAt is missing; update the JSX that renders the date to
first check cert?.awardedAt (or test Boolean(cert.awardedAt)) and, if present,
format it with new Date(...).toLocaleDateString('en-US', {year:'numeric',
month:'long', day:'numeric'}), otherwise render a safe fallback string like '—'
or 'Date unavailable' so the UI never shows "Invalid Date".

Comment on lines +11 to +16
const userId = event.data?.data()?.userId;

if (!userId) {
console.error("No userId in checkIn document");
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Extract userId from path parameters, not document data.

The function is triggered on path "events/{eventId}/checkIns/{userId}", so userId is available in event.params.userId. Reading it from the document data (event.data?.data()?.userId) is fragile and could fail if the document structure changes or if userId is missing from the data.

🔧 Proposed fix
- const userId = event.data?.data()?.userId;
+ const userId = event.params.userId;
  
  if (!userId) {
-   console.error("No userId in checkIn document");
+   console.error("No userId in path parameters");
    return;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userId = event.data?.data()?.userId;
if (!userId) {
console.error("No userId in checkIn document");
return;
}
const userId = event.params.userId;
if (!userId) {
console.error("No userId in path parameters");
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/attendanceStreak.ts` around lines 11 - 16, The code
currently reads userId from event.data?.data()?.userId which is fragile; change
the extraction to use the path parameter event.params.userId instead (replace
the assignment to the userId variable where it is set and remove the fallback to
event.data?.data()?.userId), and update the early-return log to reflect missing
event.params.userId if absent; ensure all subsequent uses of userId in
attendanceStreak.ts refer to this parameter-derived variable.

Comment on lines +44 to +53
if (diffMs < WEEK_MS) {
//already attended within current 7-day window
return;
} else if (diffMs < WEEK_MS * 2) {
//attended during the next consecutive week
newStreak = currentStreak + 1;
} else {
//missed a week -> streak resets to 1
newStreak = 1;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix boundary condition in consecutive week detection.

Using strict inequality (<) at line 47 causes a missed-week reset when attendance is exactly 14 days apart. A user attending precisely one week apart (e.g., every Monday at the same time) will eventually hit exactly WEEK_MS * 2 milliseconds, incorrectly resetting their streak instead of incrementing it.

🔧 Proposed fix
  if (diffMs < WEEK_MS) {
    //already attended within current 7-day window
    return;
- } else if (diffMs < WEEK_MS * 2) {
+ } else if (diffMs <= WEEK_MS * 2) {
    //attended during the next consecutive week
    newStreak = currentStreak + 1;
  } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (diffMs < WEEK_MS) {
//already attended within current 7-day window
return;
} else if (diffMs < WEEK_MS * 2) {
//attended during the next consecutive week
newStreak = currentStreak + 1;
} else {
//missed a week -> streak resets to 1
newStreak = 1;
}
if (diffMs < WEEK_MS) {
//already attended within current 7-day window
return;
} else if (diffMs <= WEEK_MS * 2) {
//attended during the next consecutive week
newStreak = currentStreak + 1;
} else {
//missed a week -> streak resets to 1
newStreak = 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/attendanceStreak.ts` around lines 44 - 53, The boundary
check wrongly treats diffMs == WEEK_MS * 2 as a missed week; update the
consecutive-week branch by changing the condition in the else-if that references
diffMs and WEEK_MS (the branch that sets newStreak = currentStreak + 1) from a
strict < to a <= so that diffMs == WEEK_MS * 2 is counted as consecutive; keep
the earlier diffMs < WEEK_MS check and the newStreak/currentStreak logic
unchanged.

Comment on lines +113 to +118
const [url] = await file.getSignedUrl({
action: "read",
expires: "2499-12-31",
});
signedUrl = url;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Signed URL expiry date is excessively long, creating a permanent access link.

The expiry date "2499-12-31" (almost 500 years from now) effectively makes the certificate URL permanent. If a certificate needs to be revoked or if a user's privacy settings change, there's no way to invalidate the URL. Consider a shorter expiry (e.g., 5-10 years) or implement a URL-refresh mechanism.

🔒 Proposed fix with reasonable expiry
  const [url] = await file.getSignedUrl({
    action: "read",
-   expires: "2499-12-31",
+   expires: Date.now() + 10 * 365 * 24 * 60 * 60 * 1000, // 10 years
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [url] = await file.getSignedUrl({
action: "read",
expires: "2499-12-31",
});
signedUrl = url;
}
const [url] = await file.getSignedUrl({
action: "read",
expires: Date.now() + 10 * 365 * 24 * 60 * 60 * 1000, // 10 years
});
signedUrl = url;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/dedicatedStudentCertificate.ts` around lines 113 - 118,
The signed URL for the certificate is set to expire at "2499-12-31", effectively
permanent; update the call to file.getSignedUrl (and the signedUrl assignment)
to use a reasonable expiry (e.g., generate an expires value programmatically for
5–10 years from now or read a config value) so the URL can be invalidated later,
or implement a URL refresh mechanism that issues short-lived signed URLs
on-demand; ensure you change the literal expires string to a computed/managed
expiry and document the chosen duration.

Comment on lines +121 to +128
const now = Timestamp.fromDate(new Date());
await db.collection("users").doc(userId).update({
certificates: FieldValue.arrayUnion({
type: "dedicated_student",
awardedAt: now.toDate().toISOString(),
certificateUrl: signedUrl,
}),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Store awardedAt as Firestore Timestamp for consistency and queryability.

Line 125 converts the timestamp to an ISO string (now.toDate().toISOString()), losing Firestore's native Timestamp type. This makes date-based queries less efficient and inconsistent with other timestamp fields like lastAttendanceAt in the user document.

📅 Proposed fix
  const now = Timestamp.fromDate(new Date());
  await db.collection("users").doc(userId).update({
    certificates: FieldValue.arrayUnion({
      type: "dedicated_student",
-     awardedAt: now.toDate().toISOString(),
+     awardedAt: now,
      certificateUrl: signedUrl,
    }),
  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/dedicatedStudentCertificate.ts` around lines 121 - 128,
The certificates array is storing awardedAt as an ISO string; change it to store
a Firestore Timestamp so queries stay consistent and indexable: in the update
call on db.collection("users").doc(userId").update(...) where
FieldValue.arrayUnion(...) is used to push the certificate object (type:
"dedicated_student", awardedAt: ... , certificateUrl: signedUrl), replace
now.toDate().toISOString() with the Firestore Timestamp instance (e.g., now or
Timestamp.now()) so the awardedAt field is saved as a Timestamp like other
fields such as lastAttendanceAt.

@sheeeuWu sheeeuWu deleted the feat/weekly-attendance-streak branch May 31, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add "Event Streaks" for Consistent Attendance

2 participants