flutter_in_store_app_version_checker 3.1.0-dev.1
flutter_in_store_app_version_checker: ^3.1.0-dev.1 copied to clipboard
A lightweight Flutter plugin to check app versions in Google Play, Apple App Store, ApkPure, RuStore, and AppGallery
import 'dart:async';
import 'dart:developer' as dev show log;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_in_store_app_version_checker/flutter_in_store_app_version_checker.dart';
/// A pair of store IDs for `Google Play` and `App Store`,
/// containing a current version of the app.
typedef StoreIDPair = ({
String googlePlayID,
String appleStoreID,
String currentVersion,
});
// TikTok
// bundle id: com.zhiliaoapp.musically, https://www.ntc.swiss/hubfs/NTC-security-analysis-tiktok-v1.0-en.pdf?utm_source=chatgpt.com
const StoreIDPair kTikTokStoreIDPair = (
googlePlayID: 'com.zhiliaoapp.musically',
appleStoreID: 'com.zhiliaoapp.musically',
currentVersion: '43.7.0',
);
// Roblox
// bundle id: com.roblox.robloxmobile, https://apptopia.com/ios/app/431946152/about?utm_source=chatgpt.com
const StoreIDPair kRobloxStoreIDPair = (
googlePlayID: 'com.roblox.client',
appleStoreID: 'com.roblox.robloxmobile',
currentVersion: '2.706.752',
);
// Free Fire (TH)
// bundle id: com.dts.freefireth, https://apptopia.com/ios/app/1300146617/about?utm_source=chatgpt.com
const StoreIDPair kFreefirethStoreIDPair = (
googlePlayID: 'com.dts.freefireth',
appleStoreID: 'com.dts.freefireth',
currentVersion: '1.120.1',
);
// Wildberries
// bundle id из AASA wildberries.ru, https://well-known.dev/resources/apple_app_site_association/sites/wildberries.ru?utm_source=chatgpt.com
const StoreIDPair kWildberriesStoreIDPair = (
googlePlayID: 'com.wildberries.ru',
appleStoreID: 'RU.WILDBERRIES.MOBILEAPP',
currentVersion: '',
);
// OZON
// bundle id (аналитика/ASO источники): ru.ozon.OzonStore, https://platform.foxdata.com/en/app-profile/407804998/BY/as?utm_source=chatgpt.com
const StoreIDPair kOzonStoreIDPair = (
googlePlayID: 'ru.ozon.app.android',
appleStoreID: 'ru.ozon.OzonStore',
currentVersion: '',
);
const _tetradkaPackageName = 'ru.beautybox.twa';
const _tetradkaAppGalleryStoreID = 'C107631977';
const _tetradkaCurrentVersion = '38.2.0';
void main() => runZonedGuarded<void>(
() => runApp(const App()),
(e, s) => dev.log('Top level exception: $e\n$s'),
);
/// {@template app}
/// App widget.
/// {@endtemplate}
class App extends StatelessWidget {
/// {@macro app}
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'In Store App Version Checker Example',
theme: ThemeData.dark(),
home: const Example(),
);
}
/// {@template example}
/// Example widget of using the `InStoreAppVersionChecker` plugin.
/// {@endtemplate}
class Example extends StatefulWidget {
/// {@macro example}
const Example({super.key});
@override
State<Example> createState() => _ExampleState();
}
/// State of the [Example] widget.
class _ExampleState extends State<Example> {
final ValueNotifier<bool> _updating = ValueNotifier<bool>(false);
final _checker = InStoreAppVersionChecker.instance;
Future<void>? _checkFuture;
InStoreAppVersionCheckerResponse? _wildberries,
_freefireth,
_roblox,
_tiktok,
_ozon,
_ruStore,
_appGallery,
_appGalleryNative;
/// Whether the app is running on Android.
bool get _isAndroid => defaultTargetPlatform == .android;
/// Whether the app is running on iOS.
bool get _isIOS => defaultTargetPlatform == .iOS; // ignore: unused_element
@override
void didChangeDependencies() {
super.didChangeDependencies();
_checkFuture ??= _checkVersion();
}
@override
void dispose() {
_updating.dispose();
super.dispose();
}
/// Create [InStoreAppVersionCheckerParams] for a given [StoreIDPair].
InStoreAppVersionCheckerParams _paramsFor(StoreIDPair storeIDPair) =>
InStoreAppVersionCheckerParams(
currentVersion: storeIDPair.currentVersion,
packageName: _isAndroid
? storeIDPair.googlePlayID
: storeIDPair.appleStoreID,
locale: View.of(context).platformDispatcher.locale.toLanguageTag(),
);
String get _locale =>
View.of(context).platformDispatcher.locale.toLanguageTag();
/// Check the current version of the app available in app stores
/// such as `AppStore`, `Google Play`, `ApkPure`, `RuStore`, and `AppGallery`,
/// comparing it with the installed version on the device.
Future<void> _checkVersion() async {
final stopwatch = Stopwatch()..start();
try {
await <Future<void>>[
_checker
.checkUpdate(_paramsFor(kOzonStoreIDPair))
.then((r) => _ozon = r),
_checker
.checkUpdate(_paramsFor(kRobloxStoreIDPair))
.then((r) => _roblox = r),
_checker
.checkUpdate(_paramsFor(kTikTokStoreIDPair))
.then((r) => _tiktok = r),
_checker
.checkUpdate(_paramsFor(kFreefirethStoreIDPair))
.then((r) => _freefireth = r),
_checker
.checkUpdate(_paramsFor(kWildberriesStoreIDPair))
.then((r) => _wildberries = r),
if (_isAndroid) ...<Future<void>>[
_checker
.checkUpdate(
InStoreAppVersionCheckerParams(
locale: _locale,
packageName: _tetradkaPackageName,
currentVersion: _tetradkaCurrentVersion,
androidStore:
InStoreAppVersionCheckerAndroidStoreType.ruStore,
),
)
.then((r) => _ruStore = r),
_checker
.checkUpdate(
InStoreAppVersionCheckerParams(
locale: _locale,
packageName: _tetradkaPackageName,
storeID: _tetradkaAppGalleryStoreID,
currentVersion: _tetradkaCurrentVersion,
androidStore:
InStoreAppVersionCheckerAndroidStoreType.appGallery,
),
)
.then((r) => _appGallery = r),
_checker
.checkUpdate(
InStoreAppVersionCheckerParams(
locale: _locale,
androidStore:
InStoreAppVersionCheckerAndroidStoreType.appGalleryNative,
),
)
.then((r) => _appGalleryNative = r),
],
].wait;
} on Object catch (e, _) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(
'Error checking for updates: $e',
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(color: Colors.white),
),
backgroundColor: CupertinoDynamicColor.resolve(
CupertinoColors.systemRed,
context,
),
),
);
} finally {
dev.log(
'${(stopwatch..stop()).elapsedMicroseconds / 10000} μs',
name: 'check_version',
level: 100,
);
}
}
Future<void> _refresh() async {
_updating.value = true;
final future = _checkVersion();
setState(() => _checkFuture = future);
await future;
if (mounted) _updating.value = false;
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
'In Store App Version Checker Example',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
actions: <Widget>[
IconButton(
iconSize: 24,
padding: .zero,
onPressed: _refresh,
icon: const Icon(CupertinoIcons.refresh),
),
],
),
body: SafeArea(
child: Padding(
padding: const .all(16),
child: ValueListenableBuilder(
valueListenable: _updating,
builder: (_, updating, _) => FutureBuilder(
future: _checkFuture,
builder: (context, snapshot) {
// --- Loading state --- //
if (updating || snapshot.connectionState == .waiting) {
return Center(
child: Column(
mainAxisAlignment: .center,
spacing: 8,
children: <Widget>[
const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
strokeCap: .round,
),
),
Text(updating ? 'Updating...' : 'Loading...'),
],
),
);
}
// --- Error state --- //
if (snapshot.hasError) {
return Center(
child: Text(
'Error: ${snapshot.error}',
style: TextStyle(
color: CupertinoDynamicColor.resolve(
CupertinoColors.systemRed,
context,
),
),
),
);
}
return SingleChildScrollView(
child: Column(
spacing: 16,
children: <Widget>[
StoreResultSection(title: 'Ozon', item: _ozon),
StoreResultSection(
title: 'Wildberries',
item: _wildberries,
),
StoreResultSection(title: 'Roblox', item: _roblox),
StoreResultSection(title: 'Tik Tok', item: _tiktok),
StoreResultSection(title: 'Freefeireth', item: _freefireth),
if (_isAndroid) ...<Widget>[
StoreResultSection(
title: 'Tetradka · RuStore',
item: _ruStore,
),
StoreResultSection(
title: 'Tetradka · AppGallery web',
item: _appGallery,
),
StoreResultSection(
title: 'Installed app · AppGallery native',
item: _appGalleryNative,
),
],
],
),
);
},
),
),
),
),
);
}
/// Displays a store-check result.
/// {@macro example}
class StoreResultSection extends StatelessWidget {
/// {@macro main}
const StoreResultSection({
required this.title,
required this.item,
super.key, // ignore: unused_element_parameter
});
final String title;
final InStoreAppVersionCheckerResponse? item;
@override
Widget build(BuildContext context) {
final response = item;
final (status, color) = switch (response) {
null => ('Not checked', CupertinoColors.systemGrey),
_ when response.isError => ('Error', CupertinoColors.systemRed),
_ when response.canUpdate => ('Can update', CupertinoColors.systemGreen),
_ => ('Up to date', CupertinoColors.systemBlue),
};
final resolvedColor = CupertinoDynamicColor.resolve(color, context);
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: .start,
spacing: 5,
children: <Widget>[
Row(
spacing: 10,
children: <Widget>[
Flexible(
child: Text(
title,
style: const TextStyle(fontWeight: .w600, fontSize: 17),
),
),
Badge(
label: Text(status),
textColor: resolvedColor,
backgroundColor: resolvedColor.withAlpha(25),
padding: const .symmetric(horizontal: 8, vertical: 3),
),
],
),
if (response == null)
const Text('No result yet.')
else if (response.isError)
Text(
response.errorMessage ?? 'Unknown store error.',
style: TextStyle(color: resolvedColor),
)
else ...<Widget>[
Text('Current version: ${response.currentVersion}'),
Text('Store version: ${response.newVersion ?? 'not reported'}'),
Text('App URL: ${response.appURL ?? 'not reported'}'),
],
],
),
);
}
}