Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 4 additions & 0 deletions lib/core/models/app_setting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class AppSetting {
this.onboardingCompleted = false,
});

/// Whether the app points at the staging backend. The notifier writes
/// 'stage'; 'staging' is accepted for older stored settings.
bool get isStaging => environment == 'stage' || environment == 'staging';

AppSetting copyWith({
String? newLocale,
String? themeMode,
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