Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions lib/core/common/app_secrets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
class AppSecrets {
static String get macosAppGroupId => dotenv.env['MACOS_APP_GROUP'] ?? '';

static String get stripeTestPublishableKey =>
dotenv.env['STRIPE_TEST_PUBLISHABLE_KEY'] ?? '';

static String get stripePublishableKey =>
dotenv.env['STRIPE_PUBLISHABLE_KEY'] ?? '';

static String get windowsAppUserModelId =>
dotenv.env['WINDOWS_APP_USER_MODEL_ID'] ?? '';

Expand Down
6 changes: 6 additions & 0 deletions lib/core/extensions/plan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ extension PlanExtension on Plan {

String get formattedMonthlyPrice => _formatPriceMap(expectedMonthlyPrice);

/// The expected monthly price in cents — the amount quoted to Stripe.
/// The backend always sends USD as the expectedMonthlyPrice currency.
int get monthlyUsdCents => expectedMonthlyPrice.isEmpty
? 0
: _amountOf(expectedMonthlyPrice).round();

Comment thread
jigar-f marked this conversation as resolved.
/// The original (pre-discount) yearly price, taken directly from the
/// backend's `originalPrice` (no calculation). Shown as the strikethrough
/// price next to the discounted price when an affiliate code is applied.
Expand Down
63 changes: 47 additions & 16 deletions lib/core/models/plan_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ class PlansData {

PlansData({required this.providers, required this.plans});

/// The payment methods offered on the current platform. Matches the
/// selection in ChoosePaymentMethod: only Android uses the android list;
/// iOS pays via IAP and falls back to the desktop list like everyone else.
List<Android> get platformProviders =>
PlatformUtils.isAndroid ? providers.android : providers.desktop;

/// Sorts plans (best-value first, then by descending price) and orders the
/// platform's payment providers so subscription-capable ones come first.
/// Applied after fetching/attaching plans (including referral V2).
Expand All @@ -18,14 +24,22 @@ class PlansData {
return b.usdPrice.compareTo(a.usdPrice);
});

int bySubscription(Android a, Android b) =>
(b.providers.supportSubscription ? 1 : 0) -
(a.providers.supportSubscription ? 1 : 0);
if (PlatformUtils.isMobile) {
providers.android.sort(bySubscription);
} else {
providers.desktop.sort(bySubscription);
platformProviders.sort(
(a, b) =>
(b.providers.supportSubscription ? 1 : 0) -
(a.providers.supportSubscription ? 1 : 0),
);
}

/// Publishable key advertised by the Stripe provider for this platform,
/// or null when Stripe isn't offered or no key was sent.
String? get stripePubKey {
for (final method in platformProviders) {
if (method.providers.name == 'stripe') {
return method.providers.data.pubKey;
}
}
return null;
}

factory PlansData.fromJson(Map<String, dynamic> json) => PlansData(
Expand Down Expand Up @@ -153,32 +167,49 @@ class Android {

class Provider {
String name;
Map<String, dynamic>? data;
ProviderData data;
List<String> icons;
bool supportSubscription;

Provider({
required this.name,
required this.icons,
required this.supportSubscription,
this.data,
});
ProviderData? data,
}) : data = data ?? ProviderData();

factory Provider.fromJson(Map<String, dynamic> json) => Provider(
name: json["name"],
data: json["data"] == null
? {}
: (json["data"] as Map<String, dynamic>).map(
(key, value) => MapEntry(key, value),
),
data: ProviderData.fromJson(json["data"]),
icons: List<String>.from(json["icons"].map((x) => x)),
supportSubscription: json["supportsSubscription"] ?? false,
);

Map<String, dynamic> toJson() => {
"name": name,
"data": data,
"data": data.toJson(),
"icons": List<dynamic>.from(icons.map((x) => x)),
"supportsSubscription": supportSubscription,
};
}

/// Provider-specific payload. Its keys vary by provider (Stripe sends
/// `pubKey`, shepherd sends nothing), so the raw map is kept alongside the
/// typed accessors for keys the app understands.
class ProviderData {
final Map<String, dynamic> raw;

ProviderData({this.raw = const {}});

/// Stripe publishable key for this provider, or null if absent/empty.
String? get pubKey {
final value = raw['pubKey'];
return value is String && value.isNotEmpty ? value : null;
}

factory ProviderData.fromJson(dynamic json) => ProviderData(
raw: json is Map ? Map<String, dynamic>.from(json) : const {},
);

Map<String, dynamic> toJson() => raw;
}
24 changes: 11 additions & 13 deletions lib/core/services/injection_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,20 @@ Future<void> injectServices() async {
return service;
});

appLogger.debug('Initializing notification/Stripe services...');
if (PlatformUtils.isAndroid) {
// The publishable key arrives with the plans response (synced by the
// listenSelf listener in PlansNotifier.build), so no upfront
// initialization.
sl.registerSingleton<StripeService>(StripeService());
appLogger.debug('StripeService registered');
}

appLogger.debug('Initializing notification service...');
final notificationService = NotificationService();
try {
if (PlatformUtils.isAndroid) {
final stripeService = StripeService();
await Future.wait([
notificationService.init(),
stripeService.initialize(),
]);
sl.registerSingleton<StripeService>(stripeService);
appLogger.debug('StripeService initialized');
} else {
await notificationService.init();
}
await notificationService.init();
} catch (e, st) {
appLogger.error('Notification/Stripe init failed', e, st);
appLogger.error('Notification init failed', e, st);
}
sl.registerSingleton<NotificationService>(notificationService);
appLogger.debug('NotificationService initialized');
Expand Down
Loading
Loading