Skip to content

feat: boost post#2564

Open
ice-evander wants to merge 5 commits into
masterfrom
feature/boost-post
Open

feat: boost post#2564
ice-evander wants to merge 5 commits into
masterfrom
feature/boost-post

Conversation

@ice-evander

@ice-evander ice-evander commented Nov 21, 2025

Copy link
Copy Markdown
Contributor

Description

Add boost post button to bottomSheetMenu, add InAppPurchase integration

Additional Notes

  • hidden under feature flag
  • BE for storing boost data is not ready so temporary saving boost data to local storage.
  • InAppPurchase is in progress.
  • Tip creator functionality is not in scope so just put coming soon message on tap.
  • Views data is mocked

Task ID

ION-3795

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Documentation
  • Chore

Screenshots (if applicable)

Video
https://github.com/user-attachments/assets/9c4709a0-e363-4066-9c66-cd3dfa2d8e75

Simulator Screenshot - iPhone 16 Pro - 2025-11-21 at 16 22 44 Simulator Screenshot - iPhone 16 Pro - 2025-11-21 at 16 22 48 Simulator Screenshot - iPhone 16 Pro - 2025-11-21 at 16 23 31 Simulator Screenshot - iPhone 16 Pro - 2025-11-21 at 16 26 07 Simulator Screenshot - iPhone 16 Pro - 2025-11-21 at 16 26 02

@ice-evander ice-evander force-pushed the feature/boost-post branch 2 times, most recently from 2e18727 to 090df13 Compare March 16, 2026 14:17
@ice-evander ice-evander marked this pull request as ready for review March 16, 2026 16:35
@ice-morpheus ice-morpheus enabled auto-merge (squash) March 16, 2026 16:36
@ice-morpheus ice-morpheus force-pushed the feature/boost-post branch 4 times, most recently from 1c8fa57 to 492d10f Compare March 17, 2026 09:11

@override
Widget build(BuildContext context, WidgetRef ref) {
final encodedEventRef = eventReference.encode();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to display 0 instead of loading?

);

// TODO: Get real totalViews from backend API
const totalViews = 148632;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: () => '...',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it by design?

// Cache for loaded SVG pictures
static ui.Picture? _cachedOuterPicture;
static ui.Picture? _cachedBgPicture;
static final Map<String, ui.Picture> _cachedInnerPictures = {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to have a provider -

final isBoosted = ref.watch(isBoostedProvider(eventReference));

?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_purchase dependency + 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.

Comment on lines +145 to +151
} 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) {
Comment on lines +143 to +156
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);
}
},
Comment on lines +40 to +45
// 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);
},
Comment on lines +23 to +45
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,
),
),
],
Comment on lines +3 to +5
import 'dart:async';
import 'dart:io' show Platform;

Comment on lines +11 to +19
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;
Comment on lines +77 to +81
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 {
Comment on lines +73 to +92
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;
Comment thread lib/l10n/app_en.arb
Comment on lines +60 to +61
"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.",
@ice-morpheus ice-morpheus force-pushed the feature/boost-post branch 2 times, most recently from daad700 to b471818 Compare March 17, 2026 10:09
@ice-morpheus ice-morpheus force-pushed the feature/boost-post branch 9 times, most recently from fa2ff72 to 1a14966 Compare March 18, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants