feat: unsubscribed page in vocab practice (#5694)

* feat: unsubscribed page in vocab practice

* fix uncaught unsubscribed error

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
avashilling 2026-02-13 14:36:40 -05:00 committed by GitHub
parent 8491e2e874
commit 1de440156c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 227 additions and 44 deletions

2
.gitignore vendored
View file

@ -15,7 +15,7 @@ prime
*.secrets
# Android keys file
keys.json
.env
*.env
!/public/.env
*.env.local_choreo
assets/.env.local_choreo

View file

@ -5346,5 +5346,6 @@
"courseDescription": "Courses consist of 3-8 modules each with activities to encourage practicing words in different contexts",
"emailVerificationFailed": "Email verification failed. Please try again.",
"unlockLearningTools": "Unlock learning tools",
"unlockPracticeActivities": "Unlock practice activities",
"managementSnackbarMessage": "We launched subscription management in a new tab. If you didn't see the new tab, please check your popup blocker."
}

View file

@ -14,7 +14,9 @@ import 'package:fluffychat/pangea/analytics_practice/choice_cards/grammar_choice
import 'package:fluffychat/pangea/analytics_practice/choice_cards/meaning_choice_card.dart';
import 'package:fluffychat/pangea/analytics_practice/completed_activity_session_view.dart';
import 'package:fluffychat/pangea/analytics_practice/practice_timer_widget.dart';
import 'package:fluffychat/pangea/analytics_practice/unsubscribed_practice_page.dart';
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
@ -86,7 +88,11 @@ class AnalyticsPracticeView extends StatelessWidget {
builder: (context, state, _) {
return switch (state) {
AsyncError<AnalyticsPracticeSessionModel>(:final error) =>
ErrorIndicator(message: error.toLocalizedString(context)),
error is UnsubscribedException
? const UnsubscribedPracticePage()
: ErrorIndicator(
message: error.toLocalizedString(context),
),
AsyncLoaded<AnalyticsPracticeSessionModel>(:final value) =>
value.isComplete
? CompletedActivitySessionView(state.value, controller)

View file

@ -0,0 +1,175 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_box.dart';
import 'package:fluffychat/widgets/matrix.dart';
class _DecorativeStar extends StatelessWidget {
final double size;
final double rotation;
const _DecorativeStar({required this.size, this.rotation = 0});
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: rotation,
child: Opacity(
opacity: .25,
child: Text('', style: TextStyle(fontSize: size)),
),
);
}
}
class UnsubscribedPracticePage extends StatelessWidget {
const UnsubscribedPracticePage({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
final placeholderColor = isDarkMode
? Colors.white.withAlpha(50)
: Colors.black.withAlpha(50);
final primaryColor = theme.colorScheme.primary;
final exampleMessageColor = Color.alphaBlend(
ThemeData.dark().colorScheme.primary,
Colors.white,
).withAlpha(50);
final isColumnMode = FluffyThemes.isColumnMode(context);
return Column(
children: [
Expanded(
child: Stack(
children: [
Positioned.fill(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
const SizedBox(height: 16.0),
// Title
ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: 250,
height: 30,
),
const SizedBox(height: 8.0),
// Phonetic transcription
ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: 150,
height: 20,
),
const SizedBox(height: 24.0),
// Center content box (example message)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ShimmerBox(
baseColor: exampleMessageColor,
highlightColor: primaryColor,
width: double.infinity,
height: 80.0,
borderRadius: BorderRadius.circular(24),
),
),
const SizedBox(height: 24.0),
// Choice cards
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
spacing: 8.0,
children: List.generate(
4,
(index) => ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: double.infinity,
height: 60.0,
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
),
Positioned(
top: 20,
left: 20,
child: _DecorativeStar(
size: isColumnMode ? 80 : 35,
rotation: -math.pi / 8,
),
),
Positioned(
top: 30,
right: 30,
child: _DecorativeStar(
size: isColumnMode ? 90 : 40,
rotation: math.pi / 6,
),
),
Positioned(
top: 440,
left: -5,
child: _DecorativeStar(
size: isColumnMode ? 70 : 35,
rotation: math.pi / 4,
),
),
Positioned(
top: 450,
right: -5,
child: _DecorativeStar(
size: isColumnMode ? 75 : 35,
rotation: -math.pi / 5,
),
),
Center(child: Icon(Icons.lock, size: 80, color: primaryColor)),
],
),
),
Container(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: PressableButton(
borderRadius: BorderRadius.circular(36),
color: primaryColor,
onPressed: () {
MatrixState.pangeaController.subscriptionController.showPaywall(
context,
);
},
builder: (context, depressed, shadowColor) => Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: depressed ? shadowColor : primaryColor,
borderRadius: BorderRadius.circular(36),
),
child: Text(
L10n.of(context).unlockPracticeActivities,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.black : Colors.white,
),
),
),
),
),
),
],
);
}
}

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
/// A generic sealed class that represents the state of an asynchronous operation.
@ -72,8 +73,6 @@ abstract class AsyncLoader<T> {
T? get value => isLoaded ? (state.value as AsyncLoaded<T>).value : null;
Completer<T> completer = Completer<T>();
void dispose() {
_disposed = true;
state.dispose();
@ -93,14 +92,12 @@ abstract class AsyncLoader<T> {
final result = await fetch();
if (_disposed) return;
state.value = AsyncState.loaded(result);
completer.complete(result);
} catch (e, s) {
completer.completeError(e);
if (!_disposed) {
state.value = AsyncState.error(e);
}
if (e is! HttpException) {
if (e is! HttpException && e is! UnsubscribedException) {
ErrorHandler.logError(e: e, s: s, data: {});
}
}
@ -109,6 +106,5 @@ abstract class AsyncLoader<T> {
void reset() {
if (_disposed) return;
state.value = AsyncState.idle();
completer = Completer<T>();
}
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ShimmerBox extends StatelessWidget {
final Color baseColor;
final Color highlightColor;
final double width;
final double height;
final BorderRadius? borderRadius;
const ShimmerBox({
super.key,
required this.baseColor,
required this.highlightColor,
required this.width,
required this.height,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
loop: 1,
baseColor: baseColor,
highlightColor: highlightColor,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: baseColor,
borderRadius: borderRadius ?? BorderRadius.circular(8),
),
),
);
}
}

View file

@ -1,44 +1,12 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_box.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class _ShimmerBox extends StatelessWidget {
final Color baseColor;
final Color highlightColor;
final double width;
final double height;
const _ShimmerBox({
required this.baseColor,
required this.highlightColor,
required this.width,
required this.height,
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
loop: 1,
baseColor: baseColor,
highlightColor: highlightColor,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: baseColor,
borderRadius: BorderRadius.circular(8),
),
),
);
}
}
class MessageUnsubscribedCard extends StatelessWidget {
final PangeaTokenText token;
final VoidCallback? onClose;
@ -66,7 +34,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ShimmerBox(
ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: 200,
@ -79,7 +47,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
4,
(index) => Padding(
padding: EdgeInsets.only(left: index == 0 ? 0 : 8),
child: _ShimmerBox(
child: ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: 65,
@ -89,7 +57,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
),
),
const SizedBox(height: 12),
_ShimmerBox(
ShimmerBox(
baseColor: placeholderColor,
highlightColor: primaryColor,
width: 250,