Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
## Changelog

### v3.0.8
- Refined the edit profile flow and redesigned the profile completeness widget
- Applied minor profile UI polish and layout rework
- Additional Sentry fixes and test stability improvements
- Version and build number update for release

### v3.0.7
- Added user blocking, with follow-up fixes for notification auth, cooldowns, and review feedback
- Revamped the collections page, personalised feed editor, AI generation screen, streak UI, and bottom bar sizing
- Grouped notifications and improved in-app notification behaviour
- Added wallpaper reporting and migrated wall/setup view tracking from GitHub-backed data to Firestore
- Fixed Wall of the Day generation, profile refresh, and edit profile reliability
- Reworked the wallpaper detail sheet with liquid-glass styling, richer color handling, better author/date presentation, and tap animations
- Improved startup performance and added an email export utility
- iOS release stability fixes, including build/review compliance updates, Firebase/plugin updates, media host support, and app icon refresh
- Website refresh plus Kotlin, CI, pigeon, and formatting fixes for release stability

### v3.0.6
- Version and build number update for release

Expand Down
7 changes: 5 additions & 2 deletions lib/auth/google_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,13 @@ class GoogleAuth {

bool _isSignInCancelled(Object error) {
if (error is GoogleSignInException) {
return error.code == GoogleSignInExceptionCode.canceled;
return error.code == GoogleSignInExceptionCode.canceled || error.code == GoogleSignInExceptionCode.unknownError;
}
final String message = error.toString().toLowerCase();
return message.contains('user canceled') || message.contains('cancelled');
return message.contains('user canceled') ||
message.contains('cancelled') ||
message.contains('no credential') ||
message.contains('no credentials available');
Comment on lines +199 to +205
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For google_sign_in v7.2.0, what are the documented meanings of GoogleSignInExceptionCode.canceled vs GoogleSignInExceptionCode.unknownError, and is unknownError guaranteed to be user-initiated cancellation? Also, are "no credential" / "no credentials available" documented cancellation signals?

💡 Result:

  • GoogleSignInExceptionCode.canceled: “The operation was canceled by the user.” [1]
  • GoogleSignInExceptionCode.unknownError: “A catch-all … for implementations that need to return a code that does not have a corresponding known code”, and the exception description should contain details. [1]

Is unknownError guaranteed to be user-initiated cancellation?
No. It’s explicitly documented as a catch-all for uncategorized errors, not as a cancellation signal. [1]

Are “no credential” / “No credential(s) available” documented cancellation signals?
No. In the Android Credential Manager API, NoCredentialException means no viable credential is available, while GetCredentialCancellationException is the documented “user intentionally cancels” case. [2]
In google_sign_in_android, the “No credential available: …” message is produced for the noCredential failure type (mapped to unknownError in the button flow), not for a cancellation. [3]

Also note: even canceled itself is not guaranteed to be truly user-initiated on Android—google_sign_in_android documents that some configuration errors can surface as GoogleSignInExceptionCode.canceled, and the plugin can’t distinguish those from real user cancellation. [4]

Sources
[1] GoogleSignInExceptionCode enum docs (google_sign_in v7.2.0) (pub.dev)
[2] AndroidX Credential Manager GetCredentialException / subclasses docs (developer.android.com)
[3] google_sign_in_android implementation mapping noCredential → “No credential available …” (chromium.googlesource.com)
[4] google_sign_in_android troubleshooting docs about “canceled” sometimes being configuration errors (pub.dev)

Citations:


Do not classify unknownError or "no credential" messages as user cancellation.

Lines 199 and 202-205 misclassify non-cancellation errors as cancellations. Per the google_sign_in v7.2.0 documentation:

  • GoogleSignInExceptionCode.unknownError is a catch-all for uncategorized errors, not a cancellation signal.
  • "no credential" / "no credentials available" indicate missing credentials (not user cancellation); the Android Credential Manager distinguishes these from user-initiated cancellation via separate exception types.

Because this codebase suppresses errors for canceled sign-ins (session_repository_impl.dart:127-139, onboarding_v2_shell.dart:109-122, settings_screen.dart:394-407, signInPopUp.dart:207-220), misclassifying real failures as cancellation silently swallows error feedback to users.

Remove unknownError from line 199 and remove the 'no credential' and 'no credentials available' checks from lines 202-205. Retain only canceled and 'cancelled'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/auth/google_auth.dart` around lines 199 - 205, The cancellation-detection
logic currently treats GoogleSignInExceptionCode.unknownError and message
substrings 'no credential' / 'no credentials available' as user cancellations;
update the logic in the block that checks error.code and the message (the code
that sets final String message = error.toString().toLowerCase()) to only treat
GoogleSignInExceptionCode.canceled and the text 'cancelled' (and optionally
'user canceled') as cancellations—remove GoogleSignInExceptionCode.unknownError
from the error.code check and remove the message.contains checks for 'no
credential' and 'no credentials available' so real credential/misc errors are
not suppressed as cancellations.

}

Future<bool> signOutGoogle() async {
Expand Down
4 changes: 2 additions & 2 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import 'package:Prism/auth/userModel.dart';
const String defaultProfilePhotoUrl =
'https://firebasestorage.googleapis.com/v0/b/prism-wallpapers.appspot.com/o/Replacement%20Thumbnails%2Fpost%20bg.png?alt=media&token=d708b5e3-a7ee-421b-beae-3b10946678c4';

const String currentAppVersion = '3.0.7';
const String currentAppVersionCode = '330';
const String currentAppVersion = '3.0.8';
const String currentAppVersionCode = '331';
const String defaultObsoleteAppVersion = '2.6.0';

const String defaultBannerText = 'Join our Telegram';
Expand Down
94 changes: 56 additions & 38 deletions lib/core/firestore/firestore_tracked_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;

import 'package:Prism/core/firestore/firestore_client.dart';
import 'package:Prism/core/firestore/firestore_error.dart';
Expand All @@ -7,6 +8,10 @@ import 'package:Prism/core/firestore/firestore_telemetry.dart';
import 'package:Prism/logger/logger.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

const _transientFirestoreCodes = {'unavailable', 'deadline-exceeded', 'internal', 'resource-exhausted'};

bool _isTransientFirestoreError(FirestoreError e) => e.code != null && _transientFirestoreCodes.contains(e.code);

class _RawQueryDoc {
const _RawQueryDoc(this.id, this.data);

Expand Down Expand Up @@ -539,48 +544,61 @@ class FirestoreTrackedClient implements FirestoreClient {
required String sourceTag,
required String collection,
String? docId,
int maxRetries = 2,
}) async {
final Stopwatch sw = Stopwatch()..start();
try {
final T result = await _firestore.runTransaction<T>((Transaction transaction) {
final _FirestoreTransactionBridge bridge = _FirestoreTransactionBridge(_firestore, transaction);
return action(bridge);
});
await _emitTelemetry(
FirestoreTelemetryEvent(
timestamp: DateTime.now(),
sourceTag: sourceTag,
operation: FirestoreOperation.transaction,
collection: collection,
filtersHash: docId == null ? collection : '$collection:$docId',
durationMs: sw.elapsedMilliseconds,
docId: docId,
success: true,
),
);
return result;
} catch (error) {
final FirestoreError mapped = mapFirestoreError(error);
if (mapped.code == 'permission-denied') {
logger.w(
'[Firestore] permission-denied on transaction — collection: $collection, sourceTag: $sourceTag',
error: mapped,
int attempt = 0;
while (true) {
try {
final T result = await _firestore.runTransaction<T>((Transaction transaction) {
final _FirestoreTransactionBridge bridge = _FirestoreTransactionBridge(_firestore, transaction);
return action(bridge);
});
await _emitTelemetry(
FirestoreTelemetryEvent(
timestamp: DateTime.now(),
sourceTag: sourceTag,
operation: FirestoreOperation.transaction,
collection: collection,
filtersHash: docId == null ? collection : '$collection:$docId',
durationMs: sw.elapsedMilliseconds,
docId: docId,
success: true,
),
);
return result;
} catch (error) {
final FirestoreError mapped = mapFirestoreError(error);
if (_isTransientFirestoreError(mapped) && attempt < maxRetries) {
attempt++;
final int delayMs = (500 * math.pow(2, attempt - 1)).round();
logger.w(
'[Firestore] transient error (${mapped.code}) on transaction — retrying ($attempt/$maxRetries) after ${delayMs}ms, sourceTag: $sourceTag',
);
await Future<void>.delayed(Duration(milliseconds: delayMs));
continue;
}
if (mapped.code == 'permission-denied') {
logger.w(
'[Firestore] permission-denied on transaction — collection: $collection, sourceTag: $sourceTag',
error: mapped,
);
}
await _emitTelemetry(
FirestoreTelemetryEvent(
timestamp: DateTime.now(),
sourceTag: sourceTag,
operation: FirestoreOperation.transaction,
collection: collection,
filtersHash: docId == null ? collection : '$collection:$docId',
durationMs: sw.elapsedMilliseconds,
docId: docId,
success: false,
errorCode: mapped.code,
),
);
throw mapped;
}
await _emitTelemetry(
FirestoreTelemetryEvent(
timestamp: DateTime.now(),
sourceTag: sourceTag,
operation: FirestoreOperation.transaction,
collection: collection,
filtersHash: docId == null ? collection : '$collection:$docId',
durationMs: sw.elapsedMilliseconds,
docId: docId,
success: false,
errorCode: mapped.code,
),
);
throw mapped;
}
}

Expand Down
8 changes: 5 additions & 3 deletions lib/core/widgets/menuButton/favWallpaperButton.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ class _FavouriteWallpaperButtonState extends State<FavouriteWallpaperButton> {
}
context.favouriteWallsAdapter(listen: false).favCheck(wall).then((value) {
analytics.track(FavStatusChangedEvent(wallId: wall.id, provider: wall.source.legacyProviderString));
setState(() {
isLoading = false;
});
if (mounted) {
setState(() {
isLoading = false;
});
}
});
}
}
Loading
Loading