Skip to content

Commit 3545da3

Browse files
authored
feat: creator token is live dialog (#3008)
## Description - Display "Creator token is live" dialog when someone buy user token for the first time - Subscribes to 31175 in Global Subscription ## Task ID ION-4905, ION-5027 ## Type of Change - [x] New feature <!-- <img width="180" alt="image" src="image_url_here"> -->
1 parent 4485947 commit 3545da3

16 files changed

Lines changed: 299 additions & 3 deletions

File tree

lib/app/components/gradient_border_painter/gradient_border_painter.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,35 @@ class GradientBorderPainter extends CustomPainter {
77
required this.gradient,
88
this.strokeWidth = 2,
99
this.cornerRadius = 12,
10+
this.backgroundColor,
1011
});
1112

1213
final Gradient gradient;
1314
final double strokeWidth;
1415
final double cornerRadius;
16+
final Color? backgroundColor;
1517

1618
@override
1719
void paint(Canvas canvas, Size size) {
1820
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
19-
final paint = Paint()
21+
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius));
22+
23+
// Draw background if color is provided
24+
if (backgroundColor != null) {
25+
final backgroundPaint = Paint()
26+
..color = backgroundColor!
27+
..style = PaintingStyle.fill;
28+
29+
canvas.drawRRect(rrect, backgroundPaint);
30+
}
31+
32+
// Draw gradient border
33+
final borderPaint = Paint()
2034
..shader = gradient.createShader(rect)
2135
..style = PaintingStyle.stroke
2236
..strokeWidth = strokeWidth;
2337

24-
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius));
25-
canvas.drawRRect(rrect, paint);
38+
canvas.drawRRect(rrect, borderPaint);
2639
}
2740

2841
@override

lib/app/features/ion_connect/providers/events_management_service.r.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:ion/app/features/feed/notifications/providers/notifications/repo
1616
import 'package:ion/app/features/feed/notifications/providers/notifications/token_launch_notification_handler.r.dart';
1717
import 'package:ion/app/features/ion_connect/ion_connect.dart';
1818
import 'package:ion/app/features/ion_connect/model/global_subscription_event_handler.dart';
19+
import 'package:ion/app/features/tokenized_communities/providers/community_token_definition_handler.r.dart';
1920
import 'package:ion/app/features/user/providers/badge_award_handler.r.dart';
2021
import 'package:ion/app/features/user/providers/user_delegation_handler.r.dart';
2122
import 'package:ion/app/services/logger/logger.dart';
@@ -37,6 +38,7 @@ Future<EventsManagementService> eventsManagementService(Ref ref) async {
3738
ref.watch(badgeAwardHandlerProvider),
3839
ref.watch(userDelegationHandlerProvider),
3940
ref.watch(tokenLaunchNotificationHandlerProvider),
41+
ref.watch(communityTokenDefinitionHandlerProvider),
4042
];
4143

4244
final manager = EventsManagementService(handlers);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: ice License 1.0
2+
3+
import 'dart:async';
4+
5+
import 'package:hooks_riverpod/hooks_riverpod.dart';
6+
import 'package:ion/app/features/auth/providers/auth_provider.m.dart';
7+
import 'package:ion/app/features/ion_connect/ion_connect.dart';
8+
import 'package:ion/app/features/ion_connect/model/event_reference.f.dart';
9+
import 'package:ion/app/features/ion_connect/model/global_subscription_event_handler.dart';
10+
import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.r.dart';
11+
import 'package:ion/app/features/tokenized_communities/models/entities/community_token_definition.f.dart';
12+
import 'package:ion/app/features/tokenized_communities/views/creator_token_is_live_dialog.dart';
13+
import 'package:ion/app/features/user/model/user_metadata.f.dart';
14+
import 'package:ion/app/services/storage/local_storage.r.dart';
15+
import 'package:ion/app/services/ui_event_queue/ui_event_queue_notifier.r.dart';
16+
import 'package:riverpod_annotation/riverpod_annotation.dart';
17+
18+
part 'community_token_definition_handler.r.g.dart';
19+
20+
class CommunityTokenDefinitionHandler extends GlobalSubscriptionEventHandler {
21+
CommunityTokenDefinitionHandler({
22+
required this.localStorage,
23+
required this.ionConnectCache,
24+
required this.uiEventQueueCallback,
25+
required this.currentUserMasterPubkey,
26+
});
27+
28+
final LocalStorage localStorage;
29+
final IonConnectCache ionConnectCache;
30+
final String? currentUserMasterPubkey;
31+
final void Function(ReplaceableEventReference tokenDefinitionEventReference) uiEventQueueCallback;
32+
33+
String get localStorageKey => 'creator_token_is_live_dialog_shown_$currentUserMasterPubkey';
34+
35+
@override
36+
bool canHandle(EventMessage eventMessage) {
37+
return eventMessage.kind == CommunityTokenDefinitionEntity.kind;
38+
}
39+
40+
@override
41+
Future<void> handle(EventMessage eventMessage) async {
42+
final entity = CommunityTokenDefinitionEntity.fromEventMessage(eventMessage);
43+
44+
if (entity
45+
case CommunityTokenDefinitionEntity(
46+
data: CommunityTokenDefinitionIon(
47+
eventReference: ReplaceableEventReference(
48+
masterPubkey: final originalEventMasterPubkey,
49+
kind: UserMetadataEntity.kind
50+
),
51+
type: CommunityTokenDefinitionIonType.firstBuyAction
52+
)
53+
)
54+
when originalEventMasterPubkey == currentUserMasterPubkey &&
55+
!(localStorage.getBool(localStorageKey) ?? false)) {
56+
uiEventQueueCallback(entity.toEventReference());
57+
await localStorage.setBool(key: localStorageKey, value: true);
58+
}
59+
60+
await ionConnectCache.cache(entity);
61+
}
62+
}
63+
64+
@riverpod
65+
CommunityTokenDefinitionHandler communityTokenDefinitionHandler(Ref ref) {
66+
final localStorage = ref.watch(localStorageProvider);
67+
final cache = ref.watch(ionConnectCacheProvider.notifier);
68+
final currentUserMasterPubkey = ref.watch(currentPubkeySelectorProvider);
69+
70+
void uiEventQueueCallback(ReplaceableEventReference tokenDefinitionEventReference) {
71+
ref
72+
.read(uiEventQueueNotifierProvider.notifier)
73+
.emit(CreatorTokenIsLiveDialogEvent(tokenDefinitionEventReference));
74+
}
75+
76+
return CommunityTokenDefinitionHandler(
77+
ionConnectCache: cache,
78+
localStorage: localStorage,
79+
uiEventQueueCallback: uiEventQueueCallback,
80+
currentUserMasterPubkey: currentUserMasterPubkey,
81+
);
82+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// SPDX-License-Identifier: ice License 1.0
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_hooks/flutter_hooks.dart';
5+
import 'package:go_router/go_router.dart';
6+
import 'package:hooks_riverpod/hooks_riverpod.dart';
7+
import 'package:ion/app/components/avatar/avatar.dart';
8+
import 'package:ion/app/components/button/button.dart';
9+
import 'package:ion/app/components/gradient_border_painter/gradient_border_painter.dart';
10+
import 'package:ion/app/components/progress_bar/ion_loading_indicator.dart';
11+
import 'package:ion/app/components/screen_offset/screen_bottom_offset.dart';
12+
import 'package:ion/app/components/screen_offset/screen_side_offset.dart';
13+
import 'package:ion/app/extensions/extensions.dart';
14+
import 'package:ion/app/features/auth/providers/auth_provider.m.dart';
15+
import 'package:ion/app/features/feed/views/pages/feed_page/components/stories/mock.dart';
16+
import 'package:ion/app/features/ion_connect/model/event_reference.f.dart';
17+
import 'package:ion/app/features/tokenized_communities/providers/token_market_info_provider.r.dart';
18+
import 'package:ion/app/features/user/model/profile_mode.dart';
19+
import 'package:ion/app/features/user/pages/profile_page/components/profile_background.dart';
20+
import 'package:ion/app/features/user/pages/profile_page/components/profile_details/user_name_tile/user_name_tile.dart';
21+
import 'package:ion/app/features/user/providers/user_metadata_provider.r.dart';
22+
import 'package:ion/app/hooks/use_avatar_colors.dart';
23+
import 'package:ion/app/router/app_routes.gr.dart';
24+
import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart';
25+
import 'package:ion/app/router/utils/show_simple_bottom_sheet.dart';
26+
import 'package:ion/app/services/logger/logger.dart';
27+
import 'package:ion/app/services/ui_event_queue/ui_event_queue_notifier.r.dart';
28+
import 'package:ion/generated/assets.gen.dart';
29+
30+
class CreatorTokenIsLiveDialogEvent extends UiEvent {
31+
CreatorTokenIsLiveDialogEvent(this.tokenDefinitionEventReference);
32+
33+
static bool shown = false;
34+
final ReplaceableEventReference tokenDefinitionEventReference;
35+
36+
@override
37+
void performAction(BuildContext context) {
38+
if (!shown) {
39+
shown = true;
40+
showSimpleBottomSheet<void>(
41+
context: context,
42+
backgroundColor: context.theme.appColors.forest,
43+
child:
44+
CreatorTokenIsLiveDialog(tokenDefinitionEventReference: tokenDefinitionEventReference),
45+
).whenComplete(() => shown = false);
46+
}
47+
}
48+
}
49+
50+
class CreatorTokenIsLiveDialog extends HookConsumerWidget {
51+
const CreatorTokenIsLiveDialog({required this.tokenDefinitionEventReference, super.key});
52+
53+
final ReplaceableEventReference tokenDefinitionEventReference;
54+
55+
@override
56+
Widget build(BuildContext context, WidgetRef ref) {
57+
final avatarUrl = ref.watch(currentUserMetadataProvider).value?.data.avatarUrl;
58+
final imageColors = useImageColors(avatarUrl);
59+
60+
return ProfileGradientBackground(
61+
colors: imageColors ?? useAvatarFallbackColors,
62+
disableDarkGradient: false,
63+
child: _ContentState(tokenDefinitionEventReference),
64+
);
65+
}
66+
}
67+
68+
class _ContentState extends HookConsumerWidget {
69+
const _ContentState(this.tokenDefinitionEventReference);
70+
71+
final ReplaceableEventReference tokenDefinitionEventReference;
72+
73+
@override
74+
Widget build(BuildContext context, WidgetRef ref) {
75+
final isLoading = useState(false);
76+
final currentUserMasterPubkey = ref.watch(currentPubkeySelectorProvider) ?? '';
77+
final userMetadata = ref.watch(currentUserMetadataProvider).valueOrNull;
78+
final avatarUrl = userMetadata?.data.avatarUrl ?? '';
79+
80+
final eventReferenceString = userMetadata?.toEventReference().toString();
81+
82+
final token = eventReferenceString != null
83+
? ref.watch(tokenMarketInfoProvider(eventReferenceString)).valueOrNull
84+
: null;
85+
86+
return Stack(
87+
children: [
88+
PositionedDirectional(
89+
bottom: 0,
90+
start: 0,
91+
end: 0,
92+
child: Assets.images.tokenizedCommunities.creatorMonetizationLiveRays
93+
.iconWithDimensions(width: 461.s, height: 461.s),
94+
),
95+
PositionedDirectional(
96+
end: 8,
97+
child: NavigationCloseButton(color: context.theme.appColors.onPrimaryAccent),
98+
),
99+
ScreenSideOffset.medium(
100+
child: Column(
101+
children: [
102+
SizedBox(height: 30.0.s),
103+
CustomPaint(
104+
painter: GradientBorderPainter(
105+
strokeWidth: 2.0.s,
106+
cornerRadius: 26.0.s,
107+
gradient: storyBorderGradients[3],
108+
backgroundColor: context.theme.appColors.forest.withAlpha(125),
109+
),
110+
child: Padding(
111+
padding: EdgeInsets.all(18.0.s),
112+
child: Avatar(
113+
size: 64.0.s,
114+
fit: BoxFit.cover,
115+
imageUrl: avatarUrl,
116+
borderRadius: BorderRadius.all(Radius.circular(16.0.s)),
117+
),
118+
),
119+
),
120+
SizedBox(height: 8.0.s),
121+
UserNameTile(
122+
showProfileTokenPrice: true,
123+
profileMode: ProfileMode.dark,
124+
pubkey: currentUserMasterPubkey,
125+
priceUsd: token?.marketData.priceUSD,
126+
),
127+
SizedBox(height: 18.0.s),
128+
Text(
129+
context.i18n.tokenized_community_creator_token_live_title,
130+
textAlign: TextAlign.center,
131+
style: context.theme.appTextThemes.title
132+
.copyWith(color: context.theme.appColors.onPrimaryAccent),
133+
),
134+
SizedBox(height: 8.0.s),
135+
Text(
136+
context.i18n.tokenized_community_creator_token_live_subtitle,
137+
textAlign: TextAlign.center,
138+
style: context.theme.appTextThemes.body2
139+
.copyWith(color: context.theme.appColors.secondaryBackground),
140+
),
141+
SizedBox(height: 24.0.s),
142+
Button(
143+
disabled: isLoading.value,
144+
label: Text(context.i18n.button_share),
145+
minimumSize: Size(double.infinity, 56.0.s),
146+
trailingIcon:
147+
isLoading.value ? const IONLoadingIndicator() : const SizedBox.shrink(),
148+
onPressed: token != null
149+
? () async {
150+
isLoading.value = true;
151+
try {
152+
if (context.mounted) {
153+
context.pop();
154+
155+
await ShareViaMessageModalRoute(
156+
eventReference: tokenDefinitionEventReference.encode(),
157+
).push<void>(context);
158+
}
159+
} catch (e, st) {
160+
Logger.error(e, stackTrace: st);
161+
} finally {
162+
isLoading.value = false;
163+
}
164+
}
165+
: null,
166+
),
167+
ScreenBottomOffset(),
168+
],
169+
),
170+
),
171+
],
172+
);
173+
}
174+
}

lib/l10n/app_ar.arb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,9 @@
928928
"tokenized_communities_trending_tokens": "التوكنات الرائجة",
929929
"tokenized_community_bonding_curve": "منحنى الترابط",
930930
"tokenized_community_chart_tab": "المخطط",
931+
"tokenized_community_creator_token_live_title": "توكن المنشئ مباشر الآن!",
932+
"tokenized_community_creator_token_live_subtitle": "تهانينا، توكن المنشئ الخاص بك مباشر الآن ومتاح للجميع للتداول",
933+
"tokenized_community_comments_tab": "التعليقات",
931934
"tokenized_community_comments_empty": "كن أول من ينضم إلى المحادثة",
932935
"tokenized_community_comments_tab": "التعليقات",
933936
"tokenized_community_holders_tab": "الحائزون",

lib/l10n/app_bg.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,8 @@
917917
"tokenized_community_chart_tab": "Графика",
918918
"tokenized_community_comments_empty": "Бъдете първият, който се присъединява към разговора",
919919
"tokenized_community_comments_tab": "Коментари",
920+
"tokenized_community_creator_token_live_title": "Токенът на създателя е АКТИВЕН!",
921+
"tokenized_community_creator_token_live_subtitle": "Поздравления, вашият токен на създателя вече е активен и достъпен за търговия от всички",
920922
"tokenized_community_holders_tab": "Притежатели",
921923
"token_comment_holders_only": "Коментарите са достъпни само за притежатели на токени.",
922924
"tokenized_community_not_available_description": "Създаването на токени не е налично за вече създадени публикации.",

lib/l10n/app_de.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,8 @@
917917
"tokenized_community_chart_tab": "Chart",
918918
"tokenized_community_comments_empty": "Sei der Erste, der sich an der Unterhaltung beteiligt",
919919
"tokenized_community_comments_tab": "Kommentare",
920+
"tokenized_community_creator_token_live_title": "Creator-Token ist LIVE!",
921+
"tokenized_community_creator_token_live_subtitle": "Herzlichen Glückwunsch, Ihr Creator-Token ist jetzt live und für jeden handelbar",
920922
"tokenized_community_holders_tab": "Inhaber",
921923
"token_comment_holders_only": "Kommentare sind nur für Token-Inhaber verfügbar.",
922924
"tokenized_community_not_available_description": "Die Tokenerstellung ist für zuvor erstellte Beiträge nicht verfügbar.",

lib/l10n/app_en.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,8 @@
925925
"tokenized_community_token_twitter": "X token",
926926
"tokenized_community_token_content": "Content token",
927927
"tokenized_community_trades_tab": "Trades",
928+
"tokenized_community_creator_token_live_title": "Creator token is LIVE!",
929+
"tokenized_community_creator_token_live_subtitle": "Congratulations, your creator token is now live and available for everyone to trade it",
928930
"toolbar_link_placeholder": "https://example.com",
929931
"toolbar_link_title": "Add a link",
930932
"top_holders_empty": "Become the first holder",

lib/l10n/app_es.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,8 @@
917917
"tokenized_community_chart_tab": "Gráfico",
918918
"tokenized_community_comments_empty": "Sé el primero en unirse a la conversación",
919919
"tokenized_community_comments_tab": "Comentarios",
920+
"tokenized_community_creator_token_live_title": "¡El token del creador está EN VIVO!",
921+
"tokenized_community_creator_token_live_subtitle": "Felicidades, tu token de creador ya está en vivo y disponible para que todos lo intercambien",
920922
"tokenized_community_holders_tab": "Tenedores",
921923
"token_comment_holders_only": "Los comentarios están disponibles solo para los tenedores de tokens.",
922924
"tokenized_community_not_available_description": "La creación de tokens no está disponible para publicaciones creadas previamente.",

lib/l10n/app_fr.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,8 @@
917917
"tokenized_community_chart_tab": "Graphique",
918918
"tokenized_community_comments_empty": "Soyez le premier à rejoindre la conversation",
919919
"tokenized_community_comments_tab": "Commentaires",
920+
"tokenized_community_creator_token_live_title": "Le token du créateur est EN DIRECT !",
921+
"tokenized_community_creator_token_live_subtitle": "Félicitations, votre token de créateur est maintenant en direct et disponible pour que tout le monde puisse l'échanger",
920922
"tokenized_community_holders_tab": "Détenteurs",
921923
"token_comment_holders_only": "Les commentaires sont disponibles uniquement pour les détenteurs de tokens.",
922924
"tokenized_community_not_available_description": "La création de tokens n'est pas disponible pour les publications déjà créées.",

0 commit comments

Comments
 (0)