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:
parent
8491e2e874
commit
1de440156c
7 changed files with 227 additions and 44 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,7 +15,7 @@ prime
|
|||
*.secrets
|
||||
# Android keys file
|
||||
keys.json
|
||||
.env
|
||||
*.env
|
||||
!/public/.env
|
||||
*.env.local_choreo
|
||||
assets/.env.local_choreo
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
175
lib/pangea/analytics_practice/unsubscribed_practice_page.dart
Normal file
175
lib/pangea/analytics_practice/unsubscribed_practice_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
lib/pangea/common/widgets/shimmer_box.dart
Normal file
37
lib/pangea/common/widgets/shimmer_box.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue