feat: boost post#2564
Conversation
771ae3e to
6744a6b
Compare
2e18727 to
090df13
Compare
090df13 to
fc0aa7f
Compare
1c8fa57 to
492d10f
Compare
|
|
||
| @override | ||
| Widget build(BuildContext context, WidgetRef ref) { | ||
| final encodedEventRef = eventReference.encode(); |
There was a problem hiding this comment.
Why encoded reference is used in this case? It takes resources to encode / decode it every time. Maybe use EventReference directly as in all other providers?
| // Calculate remaining cost: totalBudget * durationLeft / duration | ||
| final (balance, totalCost) = boostDataAsync.maybeWhen( | ||
| data: (data) { | ||
| if (data == null) return (0.0, 0.0); |
There was a problem hiding this comment.
Is it ok to display 0 instead of loading?
| ); | ||
|
|
||
| // TODO: Get real totalViews from backend API | ||
| const totalViews = 148632; |
There was a problem hiding this comment.
Don't we need a loader for that since it won't be available right away?
| label: context.i18n.boost_cost_title, | ||
| value: boostDataAsync.maybeWhen( | ||
| data: (_) => formatUSD(totalCost), | ||
| loading: () => '...', |
492d10f to
f8b191b
Compare
| // Cache for loaded SVG pictures | ||
| static ui.Picture? _cachedOuterPicture; | ||
| static ui.Picture? _cachedBgPicture; | ||
| static final Map<String, ui.Picture> _cachedInnerPictures = {}; |
There was a problem hiding this comment.
I wonder why do we need to load the SVGs manually and cache those for this slider? We don't do that anywhere else in the project.
| static Future<void> _testInAppPurchase(BuildContext context, WidgetRef ref) async { | ||
| try { | ||
| // 1. Check store availability | ||
| final available = await InAppPurchase.instance.isAvailable(); |
There was a problem hiding this comment.
Maybe we need to communicate only with our in-app-purchase service that encapsulates the [in_app_purchase.dart](package:in_app_purchase/in_app_purchase.dart) usage?
|
|
||
| final encoded = eventReference.encode(); | ||
| final boostedState = ref.watch(boostedPostsProvider); | ||
| final isBoosted = boostedState.maybeWhen( |
There was a problem hiding this comment.
Maybe it makes sense to have a provider -
final isBoosted = ref.watch(isBoostedProvider(eventReference));
?
f8b191b to
e9cbd26
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new “Boost post” entry point (behind a feature flag) and the initial UI/data scaffolding for a Boost flow, including local persistence of boost metadata and early in-app-purchase integration hooks.
Changes:
- Adds Boost + Tip Creator header buttons to the post bottom sheet menu (feature-flagged) and routes to a new Boost modal.
- Implements Boost modal UI (new/active states), slider components, and InfoModal support for Boost-related fields.
- Introduces local-storage persistence for boosted posts, plus adds
in_app_purchasedependency + platform plugin registration updates.
Reviewed changes
Copilot reviewed 49 out of 59 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| pubspec.yaml | Adds in_app_purchase dependency. |
| pubspec.lock | Locks in_app_purchase and transitive platform packages. |
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers StoreKit IAP plugin for macOS. |
| lib/l10n/app_zh.arb | Adds Boost/Tip Creator localized strings (ZH). |
| lib/l10n/app_tr.arb | Adds Boost/Tip Creator localized strings (TR). |
| lib/l10n/app_ru.arb | Adds Boost/Tip Creator localized strings (RU). |
| lib/l10n/app_ro.arb | Adds Boost/Tip Creator localized strings (RO). |
| lib/l10n/app_pt.arb | Adds Boost/Tip Creator localized strings (PT). |
| lib/l10n/app_pl.arb | Adds Boost/Tip Creator localized strings (PL). |
| lib/l10n/app_ko.arb | Adds Boost/Tip Creator localized strings (KO). |
| lib/l10n/app_it.arb | Adds Boost/Tip Creator localized strings (IT). |
| lib/l10n/app_fr.arb | Adds Boost/Tip Creator localized strings (FR). |
| lib/l10n/app_es.arb | Adds Boost/Tip Creator localized strings (ES). |
| lib/l10n/app_en.arb | Adds Boost/Tip Creator strings + placeholder metadata (EN). |
| lib/l10n/app_de.arb | Adds Boost/Tip Creator localized strings (DE). |
| lib/l10n/app_bg.arb | Adds Boost/Tip Creator localized strings (BG). |
| lib/l10n/app_ar.arb | Adds Boost/Tip Creator localized strings (AR). |
| lib/app/services/iap/iap_service.dart | Adds an IAP service abstraction for Boost products. |
| lib/app/services/iap/iap_provider.r.dart | Adds Riverpod provider to initialize/manage IAP service. |
| lib/app/services/iap/boost_post_products.dart | Defines Boost product ID generation + budget/duration sets. |
| lib/app/router/feed_routes.dart | Adds typed routes for new Boost modal screens. |
| lib/app/router/components/navigation_app_bar/navigation_back_button.dart | Adds optional back-button icon color override. |
| lib/app/router/components/navigation_app_bar/navigation_app_bar.dart | Sets back-button color for modal variant. |
| lib/app/router/app_routes.gr.dart | Imports Boost modal into generated routes. |
| lib/app/features/wallets/views/pages/coins_flow/coin_receive_modal/components/coin_address_tile/coin_address_tile.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/wallets/views/pages/coins_flow/coin_details/coin_details_page.dart | Updates InfoType import to shared components. |
| lib/app/features/wallets/views/components/share_address/receive_info_card.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/wallets/views/components/network_fee/network_fee.dart | Updates InfoType import to shared components. |
| lib/app/features/wallets/views/components/network_fee/list_item_network_fee.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/wallets/views/components/info_block_button.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/wallets/views/components/arrival_time/list_item_arrival_time.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/user/pages/profile_page/components/profile_details/profile_token_stats.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/feed/views/pages/boost_post_modal/components/new_post_boost_content.dart | Implements “new boost” UI + debug IAP test hook + local save. |
| lib/app/features/feed/views/pages/boost_post_modal/components/icon_slider.dart | Adds custom SVG-thumb slider used by Boost UI. |
| lib/app/features/feed/views/pages/boost_post_modal/components/boost_slider_row.dart | Adds labeled slider row wrapper for Boost settings. |
| lib/app/features/feed/views/pages/boost_post_modal/components/boost_info_row.dart | Adds labeled info row with info-icon modal trigger. |
| lib/app/features/feed/views/pages/boost_post_modal/components/active_post_boost_content.dart | Implements “active boost” UI (balance/cost + increase boost). |
| lib/app/features/feed/views/pages/boost_post_modal/boost_post_modal.dart | Adds Boost modal container switching active/new content. |
| lib/app/features/feed/views/components/community_token_live/components/feed_twitter_token.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/feed/views/components/community_token_live/components/feed_profile_token.dart | Updates InfoModal/InfoType imports to shared components. |
| lib/app/features/feed/views/components/bottom_sheet_menu/post_menu_bottom_sheet.dart | Adds header buttons (Boost / Tip Creator) behind feature flag. |
| lib/app/features/feed/providers/boosted_posts_provider.r.dart | Adds provider to store boosted post IDs + metadata (local for now). |
| lib/app/features/feed/data/repository/boosted_posts_repository.r.dart | Adds local-storage repository for boosted post IDs + metadata. |
| lib/app/features/feed/data/models/boosted_post_info.f.dart | Adds BoostPostData Freezed model for persisted metadata. |
| lib/app/features/core/providers/feature_flags_provider.r.dart | Adds default value for Boost feature flag. |
| lib/app/features/core/model/feature_flags.dart | Defines BoostPostFeatureFlag. |
| lib/app/components/message_notification/message_notification_wrapper.dart | Adjusts notification container layout constraints. |
| lib/app/components/info/info_type.dart | Adds Boost-related InfoType entries and icon/title/desc mappings. |
| lib/app/components/info/info_modal.dart | Updates InfoType import to shared components. |
| lib/app/components/bottom_sheet_menu/bottom_sheet_menu_header_button.dart | Adds reusable header button widget for bottom-sheet menus. |
| ios/Podfile.lock | Adds StoreKit IAP pod entry. |
| assets/svg/wallet_icon_cost.svg | Adds Boost “cost” icon asset. |
| assets/svg/wallet_icon_budget.svg | Adds Boost “budget” icon asset. |
| assets/svg/icon_sheet_boost.svg | Adds bottom-sheet Boost button icon asset. |
| assets/svg/icon_outer_slider.svg | Adds slider thumb outer SVG asset. |
| assets/svg/icon_inner_slider.svg | Adds slider thumb inner SVG asset. |
| assets/svg/action_promotion_taxes.svg | Adds Boost “taxes” info icon asset. |
| assets/svg/action_promotion_fee.svg | Adds Boost “Apple fee” info icon asset. |
| assets/svg/action_promotion_balance.svg | Adds Boost “balance” info icon asset. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| } else if (purchase.status == PurchaseStatus.purchased || | ||
| purchase.status == PurchaseStatus.restored) { | ||
| debugPrint('IAP: Purchase successful: ${purchase.productID}'); | ||
| _purchaseController.add(purchase); | ||
| // Complete the purchase after handling | ||
| _completePurchase(purchase); | ||
| } else if (purchase.status == PurchaseStatus.error) { |
| var headerButtons = <Widget>[]; | ||
| if (ref.watch(featureFlagsProvider.notifier).get(BoostPostFeatureFlag.boostPostEnabled)) { | ||
| headerButtons = <Widget>[ | ||
| BottomSheetMenuHeaderButton( | ||
| label: context.i18n.button_boost, | ||
| iconAsset: Assets.svg.iconSheetBoost, | ||
| onPressed: () { | ||
| Navigator.of(context).pop(); | ||
| if (isBoosted) { | ||
| ActiveBoostPostModalRoute(eventReference: encoded).push<void>(context); | ||
| } else { | ||
| NewBoostPostModalRoute(eventReference: encoded).push<void>(context); | ||
| } | ||
| }, |
| // Calculate remaining days | ||
| final durationLeft = endDate.isAfter(now) ? endDate.difference(now).inDays : 0; | ||
|
|
||
| // Calculate remaining cost | ||
| return (duration > 0 ? (totalBudget * durationLeft / duration) : 0.0, totalBudget); | ||
| }, |
| return GestureDetector( | ||
| onTap: onPressed, | ||
| child: Container( | ||
| padding: EdgeInsets.symmetric(vertical: 12.0.s, horizontal: 16.0.s), | ||
| decoration: BoxDecoration( | ||
| color: context.theme.appColors.primaryAccent, | ||
| borderRadius: BorderRadius.circular(16.0.s), | ||
| ), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| iconAsset.icon( | ||
| size: 20.0.s, | ||
| color: context.theme.appColors.onPrimaryAccent, | ||
| ), | ||
| SizedBox(height: 8.0.s), | ||
| Text( | ||
| label, | ||
| style: context.theme.appTextThemes.subtitle2.copyWith( | ||
| color: context.theme.appColors.onPrimaryAccent, | ||
| ), | ||
| ), | ||
| ], |
| import 'dart:async'; | ||
| import 'dart:io' show Platform; | ||
|
|
| class IAPService { | ||
| factory IAPService() => _instance; | ||
| IAPService._internal(); | ||
|
|
||
| static final IAPService _instance = IAPService._internal(); | ||
|
|
||
| final InAppPurchase _iap = InAppPurchase.instance; | ||
| StreamSubscription<List<PurchaseDetails>>? _subscription; | ||
| bool _isAvailable = false; |
| state = const AsyncValue.loading(); | ||
|
|
||
| state = await AsyncValue.guard(() async { | ||
| final currentSet = state.value ?? <String>{}; | ||
| final updatedSet = {...currentSet}..add(eventReference); |
| Future<BoostPostData?> boostedPostData( | ||
| Ref ref, | ||
| String eventReference, | ||
| ) async { |
| final outerRawSvg = await rootBundle.loadString(Assets.svg.iconOuterSlider); | ||
| final outerLoader = vg.SvgStringLoader(outerRawSvg); | ||
| final outerPictureInfo = await vg.vg.loadPicture(outerLoader, null); | ||
| _cachedOuterPicture = outerPictureInfo.picture; | ||
| } | ||
|
|
||
| // Load background SVG | ||
| if (_cachedBgPicture == null) { | ||
| final bgRawSvg = await rootBundle.loadString(Assets.svg.iconInnerSlider); | ||
| final bgLoader = vg.SvgStringLoader(bgRawSvg); | ||
| final bgPictureInfo = await vg.vg.loadPicture(bgLoader, null); | ||
| _cachedBgPicture = bgPictureInfo.picture; | ||
| } | ||
|
|
||
| // Load inner SVG (required) | ||
| if (!_cachedInnerPictures.containsKey(_innerIconPath)) { | ||
| final innerRawSvg = await rootBundle.loadString(_innerIconPath); | ||
| final innerLoader = vg.SvgStringLoader(innerRawSvg); | ||
| final innerPictureInfo = await vg.vg.loadPicture(innerLoader, null); | ||
| _cachedInnerPictures[_innerIconPath] = innerPictureInfo.picture; |
| "boost_approximately_views": "Approximately views", | ||
| "boost_balance_description": "The balance is the remaining funds in your advertising account, which is used to pay for ongoing advertising campaigns.", |
daad700 to
b471818
Compare
fa2ff72 to
1a14966
Compare
1a14966 to
324a4f7
Compare
Description
Add boost post button to bottomSheetMenu, add InAppPurchase integration
Additional Notes
Task ID
ION-3795
Type of Change
Screenshots (if applicable)
Video
https://github.com/user-attachments/assets/9c4709a0-e363-4066-9c66-cd3dfa2d8e75