diff --git a/mobile-app/lib/features/components/raid_submission_action_sheet.dart b/mobile-app/lib/features/components/raid_submission_action_sheet.dart index 733b3c04..6e8f9b86 100644 --- a/mobile-app/lib/features/components/raid_submission_action_sheet.dart +++ b/mobile-app/lib/features/components/raid_submission_action_sheet.dart @@ -25,13 +25,11 @@ class RaidSubmissionActionSheet extends ConsumerStatefulWidget { class _RaidSubmissionActionSheetState extends ConsumerState { final _taskmasterService = TaskmasterService(); - final _targetTweetController = TextEditingController(); final _replyTweetController = TextEditingController(); bool _isSubmitting = false; bool _isDisabled = true; - String? _targetErrorMsg; String? _replyErrorMsg; String? _errorMsg; @@ -39,23 +37,19 @@ class _RaidSubmissionActionSheetState extends ConsumerState _handleSubmit(String targetLink, String replyLink) async { + Future _handleSubmit(String replyLink) async { setState(() { _isSubmitting = true; }); try { - await _taskmasterService.addRaidSubmission(targetLink, replyLink); + await _taskmasterService.addRaidSubmission(replyLink); if (mounted) { context.showSuccessSnackbar(title: 'Success submitted!', message: 'Success adding raid submission!'); } @@ -90,7 +84,7 @@ class _RaidSubmissionActionSheetState extends ConsumerState createState() => _KingOfTheRaiderScreenState(); -} - -class _KingOfTheRaiderScreenState extends ConsumerState with WidgetsBindingObserver { - final _taskmasterService = TaskmasterService(); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - void refreshRaiderSubmissions() { - ref.invalidate(raiderSubmissionsProvider); - } - - String? extractXStatusId(String url) { - final uri = Uri.tryParse(url); - if (uri == null) return null; - - // Expected path: /{username}/status/{id} - final segments = uri.pathSegments; - - if (segments.length >= 3 && segments[1] == 'status') { - final id = segments[2]; - return RegExp(r'^\d+$').hasMatch(id) ? id : null; - } - - return null; - } - - Future _handleRemoveSubmission(String id) async { - try { - await _taskmasterService.removeRaidSubmission(id); - if (mounted) { - context.showSuccessSnackbar(title: 'Success removed!', message: 'Success removing raid submission!'); - } - ref.invalidate(raiderSubmissionsProvider); - } catch (e) { - print('Failed removing raid submission: $e'); - - if (mounted) { - context.showErrorSnackbar(title: 'Failed removing!', message: e.toString()); - } - } - } - - @override - Widget build(BuildContext context) { - final raiderSubmissionsAsync = ref.watch(raiderSubmissionsProvider); - final effectiveSpacing = context.isSmallHeight ? 24.0 : 36.0; - - return ScaffoldBase.refreshable( - appBar: WalletAppBar(title: 'King of The Raider'), - onRefresh: () async { - refreshRaiderSubmissions(); - }, - scrollController: _scrollController, - decorations: [ - const Positioned(top: 180, right: -34, child: Sphere(variant: 2, size: 194)), - const Positioned(left: -60, bottom: 0, child: Sphere(variant: 7, size: 240.68)), - ], - slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 11), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 17, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 96), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [OptinPositionStatus()], - ), - SizedBox(height: effectiveSpacing), - const AccountAssociationsStatus(), - SizedBox(height: effectiveSpacing), - ...raiderSubmissionsAsync.when( - loading: () => [ - Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader)), - ], - error: (error, stackTrace) => [ - Text( - 'Error fetching raider submissions.', - style: context.themeText.detail?.copyWith(color: context.themeColors.textError), - ), - const SizedBox(height: 12), - Button( - variant: ButtonVariant.neutral, - label: 'Try again', - onPressed: refreshRaiderSubmissions, - ), - ], - data: (state) { - switch (state) { - case RaiderSubmissionsOk(): - return [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - BasicCard( - child: Row( - children: [ - Text('Active Raid: ', style: context.themeText.smallTitle), - Text('Alpha', style: context.themeText.smallTitle), - ], - ), - ), - LinkText( - label: 'Learn more about QQ', - url: AppConstants.raidQuestsPageUrl, - textStyle: context.themeText.smallParagraph, - ), - ], - ), - SizedBox(height: effectiveSpacing), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Raid Submissions', style: context.themeText.smallTitle), - const SizedBox(width: 12), - InkWell( - child: Container( - decoration: ShapeDecoration( - color: context.themeColors.buttonNeutral, - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusGeometry.circular(8), - ), - ), - child: const Icon(Icons.add, color: Colors.black), - ), - onTap: () { - showRaidSubmissionActionSheet(context); - }, - ), - ], - ), - const SizedBox(height: 8), - if (state.submissions.isNotEmpty) - Column( - spacing: 4, - children: state.submissions.asMap().entries.map((entry) { - final index = entry.key + 1; - final value = entry.value; - final label = extractXStatusId(value) ?? 'Unknown'; - - return Row( - children: [ - Text('$index. '), - LinkText( - label: label, - url: value, - textStyle: context.themeText.smallParagraph, - ), - InkWell( - child: Icon(Icons.delete, color: context.themeColors.buttonDanger), - onTap: () { - _handleRemoveSubmission(label); - }, - ), - ], - ); - }).toList(), - ) - else - Text( - "You haven't submitted anything yet", - style: context.themeText.smallParagraph, - ), - ]; - - case NoActiveRaid(): - return [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - BasicCard( - child: Row( - children: [ - Text('No Active Raid: ', style: context.themeText.smallTitle), - ], - ), - ), - LinkText( - label: 'Learn more about QQ', - url: AppConstants.raidQuestsPageUrl, - textStyle: context.themeText.smallParagraph, - ), - ], - ), - ]; - - case NoTwitterLinked(): - return [Text('Please link your X account', style: context.themeText.smallTitle)]; - } - }, - ), - ], - ), - ), - ], - ), - SizedBox(height: context.isSmallHeight ? 18 : 40), - ], - ), - ), - const QuestTitle(), - ], - ), - ), - ], - ); - } -} diff --git a/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart b/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart deleted file mode 100644 index 8a7e067e..00000000 --- a/mobile-app/lib/features/main/screens/quests/king_of_the_shill_screen.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/basic_card.dart'; -import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/components/link_text.dart'; -import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; -import 'package:resonance_network_wallet/features/components/skeleton.dart'; -import 'package:resonance_network_wallet/features/components/sphere.dart'; -import 'package:resonance_network_wallet/features/components/wallet_app_bar.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/account_associations_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/optin_position_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/quest_title.dart'; -import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; -import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; -import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; -import 'package:resonance_network_wallet/providers/account_stats_providers.dart'; -import 'package:resonance_network_wallet/services/referral_service.dart'; -import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; -import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; -import 'package:share_plus/share_plus.dart'; - -class KingOfTheShillScreen extends ConsumerStatefulWidget { - const KingOfTheShillScreen({super.key}); - - @override - ConsumerState createState() => _KingOfTheShillScreenState(); -} - -class _KingOfTheShillScreenState extends ConsumerState with WidgetsBindingObserver { - final ReferralService _referralService = ReferralService(); - final ScrollController _scrollController = ScrollController(); - - String? _referralCode; - - Future _loadReferralCode() async { - try { - final myReferralCode = await _referralService.getMyInviteCode(); - setState(() { - _referralCode = myReferralCode; - }); - } catch (e) { - debugPrint('Error loading account data: $e'); - if (mounted) { - setState(() {}); - } - } - } - - Future _shareReferralLink() async { - final params = await _referralService.getShareLinkParameters(context.sharePositionRect()); - SharePlus.instance.share(params); - } - - void _copyReferralCode() { - if (_referralCode != null) { - ClipboardExtensions.copyTextWithSnackbar(context, _referralCode!, message: 'Referral code copied to clipboard'); - } - } - - @override - void initState() { - super.initState(); - _loadReferralCode(); - } - - @override - void dispose() { - super.dispose(); - } - - void refreshStatsData() { - ref.invalidate(accountsStatsProvider); - } - - void refreshAssociationsData() { - ref.invalidate(accountAssociationsProvider); - } - - @override - Widget build(BuildContext context) { - final statsAsync = ref.watch(accountsStatsProvider); - - return ScaffoldBase.refreshable( - appBar: WalletAppBar(title: 'King of The Shill'), - padding: const EdgeInsetsGeometry.all(0), - onRefresh: () async { - refreshStatsData(); - refreshAssociationsData(); - }, - scrollController: _scrollController, - decorations: [ - const Positioned(top: 180, right: -34, child: Sphere(variant: 2, size: 194)), - const Positioned(left: -60, bottom: 0, child: Sphere(variant: 7, size: 240.68)), - ], - slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 11), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(right: 44), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 17, - children: [ - _buildDecoration(), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 96), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [OptinPositionStatus(), SizedBox(width: 71)], - ), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), - const AccountAssociationsStatus(), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), - ..._buildAccountStats(context, statsAsync), - const SizedBox(height: 16), - LinkText( - label: 'Learn more about QQ', - url: AppConstants.shillQuestsPageUrl, - textStyle: context.themeText.smallParagraph, - ), - ], - ), - ), - ], - ), - ), - SizedBox(height: context.isSmallHeight ? 18 : 40), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: InkWell( - onTap: _copyReferralCode, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11), - decoration: ShapeDecoration( - color: context.themeColors.background, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - ), - child: Row( - spacing: 12.0, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _referralCode ?? 'Loading...', - style: context.themeText.smallParagraph, - textAlign: TextAlign.center, - ), - Icon(Icons.copy, color: Colors.white, size: context.isTablet ? 26 : 22), - ], - ), - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Button( - variant: ButtonVariant.glassOutline, - label: 'Share Referral Link', - onPressed: _shareReferralLink, - ), - ), - ], - ), - ), - const QuestTitle(), - ], - ), - ), - ], - ); - } - - List _buildAccountStats(BuildContext context, AsyncValue statsAsync) { - return statsAsync.when( - data: (stats) => [ - _buildStatCard(context, 'Referrals:', stats.referralCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Sends:', stats.sendCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Reversals:', stats.reversalCount), - const SizedBox(height: 9), - _buildStatCard(context, 'Mining:', stats.miningCount), - ], - loading: () => [ - _buildStatCard(context, 'Referrals:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Sends:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Reversals:', null), - const SizedBox(height: 9), - _buildStatCard(context, 'Mining:', null), - ], - error: (error, stack) => [ - Text( - 'Error fetching account stats.', - style: context.themeText.detail?.copyWith(color: context.themeColors.textError), - ), - const SizedBox(height: 12), - Button(variant: ButtonVariant.neutral, label: 'Try again', onPressed: refreshStatsData), - ], - ); - } - - Widget _buildStatCard(BuildContext context, String title, int? stat) { - final isLoading = stat == null; - - return BasicCard( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: context.themeText.smallTitle), - isLoading ? const Skeleton(width: 40, height: 16) : Text('$stat', style: context.themeText.smallTitle), - ], - ), - ); - } - - Widget _buildDecoration() { - return Container( - width: 85, - height: context.isSmallHeight ? 415 : 480, - decoration: const ShapeDecoration( - gradient: LinearGradient( - begin: Alignment(0.03, -1.00), - end: Alignment(-0.03, 1), - colors: [Color(0xFF0000FF), Color(0xFFED4CCE), Color(0xFFFFE91F)], - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only(topRight: Radius.circular(10), bottomRight: Radius.circular(10)), - ), - ), - ); - } -} diff --git a/mobile-app/lib/features/main/screens/quests/quests_screen.dart b/mobile-app/lib/features/main/screens/quests/quests_screen.dart index 1889db83..63d6c678 100644 --- a/mobile-app/lib/features/main/screens/quests/quests_screen.dart +++ b/mobile-app/lib/features/main/screens/quests/quests_screen.dart @@ -1,24 +1,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/basic_card.dart'; import 'package:resonance_network_wallet/features/components/button.dart'; -import 'package:resonance_network_wallet/features/components/list_item.dart'; +import 'package:resonance_network_wallet/features/components/link_text.dart'; import 'package:resonance_network_wallet/features/components/loading_text_animation.dart'; import 'package:resonance_network_wallet/features/components/quests_promo_video.dart'; +import 'package:resonance_network_wallet/features/components/raid_submission_action_sheet.dart'; import 'package:resonance_network_wallet/features/components/scaffold_base.dart'; import 'package:resonance_network_wallet/features/components/sphere.dart'; import 'package:resonance_network_wallet/features/main/screens/navbar.dart'; import 'package:resonance_network_wallet/features/main/screens/quests/account_associations_status.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/king_of_the_raider_screen.dart'; -import 'package:resonance_network_wallet/features/main/screens/quests/king_of_the_shill_screen.dart'; import 'package:resonance_network_wallet/features/main/screens/quests/optin_position_status.dart'; import 'package:resonance_network_wallet/features/main/screens/quests/quest_title.dart'; +import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; import 'package:resonance_network_wallet/providers/account_stats_providers.dart'; import 'package:resonance_network_wallet/providers/opt_in_position_providers.dart'; +import 'package:resonance_network_wallet/providers/raider_quest_providers.dart'; import 'package:resonance_network_wallet/services/referral_service.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; +import 'package:resonance_network_wallet/shared/extensions/snackbar_extensions.dart'; class QuestsScreen extends ConsumerStatefulWidget { const QuestsScreen({super.key}); @@ -28,6 +31,7 @@ class QuestsScreen extends ConsumerStatefulWidget { } class _QuestsScreenState extends ConsumerState { + final TaskmasterService _taskmasterService = TaskmasterService(); final ReferralService _referralService = ReferralService(); final ScrollController _scrollController = ScrollController(); @@ -71,6 +75,10 @@ class _QuestsScreenState extends ConsumerState { ref.invalidate(accountAssociationsProvider); } + void refreshRaiderSubmissions() { + ref.invalidate(raiderSubmissionsProvider); + } + void setVideoVisibility(bool isVisible) { if (mounted) { setState(() { @@ -116,6 +124,37 @@ class _QuestsScreenState extends ConsumerState { } } + String? extractXStatusId(String url) { + final uri = Uri.tryParse(url); + if (uri == null) return null; + + // Expected path: /{username}/status/{id} + final segments = uri.pathSegments; + + if (segments.length >= 3 && segments[1] == 'status') { + final id = segments[2]; + return RegExp(r'^\d+$').hasMatch(id) ? id : null; + } + + return null; + } + + Future _handleRemoveSubmission(String id) async { + try { + await _taskmasterService.removeRaidSubmission(id); + if (mounted) { + context.showSuccessSnackbar(title: 'Success removed!', message: 'Success removing raid submission!'); + } + ref.invalidate(raiderSubmissionsProvider); + } catch (e) { + print('Failed removing raid submission: $e'); + + if (mounted) { + context.showErrorSnackbar(title: 'Failed removing!', message: e.toString()); + } + } + } + @override Widget build(BuildContext context) { // Show videos for users who haven't opted in to the reward program @@ -181,6 +220,9 @@ class _QuestsScreenState extends ConsumerState { ); } + final raiderSubmissionsAsync = ref.watch(raiderSubmissionsProvider); + final effectiveSpacing = context.isSmallHeight ? 24.0 : 36.0; + return ScaffoldBase.refreshable( onRefresh: () async { refreshStatsData(); @@ -195,10 +237,11 @@ class _QuestsScreenState extends ConsumerState { SliverToBoxAdapter( child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ const QuestTitle(), - const OptinPositionStatus(), - SizedBox(height: context.isSmallHeight ? 18 : 37.0), + const Center(child: OptinPositionStatus()), + SizedBox(height: effectiveSpacing), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -208,30 +251,107 @@ class _QuestsScreenState extends ConsumerState { const AccountAssociationsStatus(), ], ), + SizedBox(height: effectiveSpacing), + ...raiderSubmissionsAsync.when( + loading: () => [Center(child: CircularProgressIndicator(color: context.themeColors.circularLoader))], + error: (error, stackTrace) => [ + Text( + 'Error fetching raider submissions.', + style: context.themeText.detail?.copyWith(color: context.themeColors.textError), + ), + const SizedBox(height: 12), + Button(variant: ButtonVariant.neutral, label: 'Try again', onPressed: refreshRaiderSubmissions), + ], + data: (state) { + switch (state) { + case RaiderSubmissionsOk(): + return [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text('Active Raid', style: context.themeText.smallParagraph, textAlign: TextAlign.start), + BasicCard(child: Text('Alpha', style: context.themeText.smallTitle)), + LinkText( + label: 'Learn more about QQ', + url: AppConstants.raidQuestsPageUrl, + textStyle: context.themeText.smallParagraph, + ), + ], + ), + SizedBox(height: effectiveSpacing), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Raid Submissions', style: context.themeText.smallTitle), + const SizedBox(width: 12), + InkWell( + child: Container( + decoration: ShapeDecoration( + color: context.themeColors.buttonNeutral, + shape: RoundedRectangleBorder(borderRadius: BorderRadiusGeometry.circular(8)), + ), + child: const Icon(Icons.add, color: Colors.black), + ), + onTap: () { + showRaidSubmissionActionSheet(context); + }, + ), + ], + ), + const SizedBox(height: 8), + if (state.submissions.isNotEmpty) + Column( + spacing: 4, + children: state.submissions.asMap().entries.map((entry) { + final index = entry.key + 1; + final value = entry.value; + final label = extractXStatusId(value) ?? 'Unknown'; + + return Row( + children: [ + Text('$index. '), + LinkText(label: label, url: value, textStyle: context.themeText.smallParagraph), + InkWell( + child: Icon(Icons.delete, color: context.themeColors.buttonDanger), + onTap: () { + _handleRemoveSubmission(label); + }, + ), + ], + ); + }).toList(), + ) + else + Text("You haven't submitted anything yet", style: context.themeText.smallParagraph), + ]; + + case NoActiveRaid(): + return [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + BasicCard( + child: Row(children: [Text('No Active Raid: ', style: context.themeText.smallTitle)]), + ), + LinkText( + label: 'Learn more about QQ', + url: AppConstants.raidQuestsPageUrl, + textStyle: context.themeText.smallParagraph, + ), + ], + ), + ]; + + case NoTwitterLinked(): + return [Text('Please link your X account', style: context.themeText.smallTitle)]; + } + }, + ), ], ), ), - - SliverToBoxAdapter(child: SizedBox(height: context.isSmallHeight ? 18 : 37.0)), - SliverToBoxAdapter(child: Text('Quests', style: context.themeText.smallParagraph)), - const SliverToBoxAdapter(child: SizedBox(height: 4)), - SliverList( - delegate: SliverChildListDelegate([ - ListItem( - title: 'King of The Raider', - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const KingOfTheRaiderScreen())); - }, - ), - const SizedBox(height: 12), - ListItem( - title: 'King of The Shill', - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const KingOfTheShillScreen())); - }, - ), - ]), - ), ], ); } diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 53fc6dda..64adbafc 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -245,11 +245,11 @@ class TaskmasterService { } } - Future addRaidSubmission(String targetTweetLink, String replyTweetLink) async { - print('add raid submission $targetTweetLink and $replyTweetLink'); + Future addRaidSubmission(String replyTweetLink) async { + print('add raid submission $replyTweetLink'); final raiderSubmissionsEndpoint = Uri.parse('${AppConstants.taskMasterEndpoint}/raid-quests/submissions'); - final Map requestBody = {'target_tweet_link': targetTweetLink, 'tweet_reply_link': replyTweetLink}; + final Map requestBody = {'tweet_reply_link': replyTweetLink}; final http.Response response = await _authenticatedHttpClient.post( raiderSubmissionsEndpoint,